iOS web browser with a focus on security and privacy
1/*
2 * Endless
3 * Copyright (c) 2014-2015 joshua stein <jcs@jcs.org>
4 *
5 * CKHTTP portions of this file are from Onion Browser
6 * Copyright (c) 2012-2014 Mike Tigas <mike@tig.as>
7 *
8 * See LICENSE file for redistribution terms.
9 */
10
11#import "AppDelegate.h"
12#import "HSTSCache.h"
13#import "HTTPSEverywhere.h"
14#import "LocalNetworkChecker.h"
15#import "SSLCertificate.h"
16#import "URLBlocker.h"
17#import "URLInterceptor.h"
18#import "WebViewTab.h"
19
20#import "NSData+CocoaDevUsersAdditions.h"
21
22@implementation URLInterceptor {
23 WebViewTab *wvt;
24 NSString *userAgent;
25}
26
27static AppDelegate *appDelegate;
28static BOOL sendDNT = true;
29static NSMutableArray *tmpAllowed;
30static NSCache *injectCache;
31
32#define INJECT_CACHE_SIZE 20
33
34+ (void)setup
35{
36 [[NSNotificationCenter defaultCenter] addObserver:[URLInterceptor class] selector:@selector(clearInjectCache) name:HOST_SETTINGS_CHANGED object:nil];
37}
38
39static NSString *_javascriptToInject;
40+ (NSString *)javascriptToInject
41{
42 if (!_javascriptToInject) {
43 NSString *path = [[NSBundle mainBundle] pathForResource:@"injected" ofType:@"js"];
44 _javascriptToInject = [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:nil];
45 }
46
47 return _javascriptToInject;
48}
49
50+ (void)clearInjectCache
51{
52 if (injectCache != nil)
53 [injectCache removeAllObjects];
54}
55
56+ (NSString *)javascriptToInjectForURL:(NSURL *)url
57{
58 if (injectCache == nil) {
59 injectCache = [[NSCache alloc] init];
60 [injectCache setCountLimit:INJECT_CACHE_SIZE];
61 }
62
63 NSString *c = [injectCache objectForKey:[url host]];
64 if (c != nil)
65 return c;
66
67 NSString *j = [self javascriptToInject];
68 HostSettings *hs = [HostSettings settingsOrDefaultsForHost:[url host]];
69
70 NSString *block_rtc = @"true";
71 if ([hs boolSettingOrDefault:HOST_SETTINGS_KEY_ALLOW_WEBRTC])
72 block_rtc = @"false";
73
74 j = [j stringByReplacingOccurrencesOfString:@"\"##BLOCK_WEBRTC##\"" withString:block_rtc];
75
76 [injectCache setObject:j forKey:[url host]];
77
78 return j;
79}
80
81+ (void)setSendDNT:(BOOL)val
82{
83 sendDNT = val;
84}
85
86+ (void)temporarilyAllow:(NSURL *)url
87{
88 if (!tmpAllowed)
89 tmpAllowed = [[NSMutableArray alloc] initWithCapacity:1];
90
91 [tmpAllowed addObject:url];
92}
93
94+ (BOOL)isURLTemporarilyAllowed:(NSURL *)url
95{
96 int found = -1;
97
98 for (int i = 0; i < [tmpAllowed count]; i++) {
99 if ([[tmpAllowed[i] absoluteString] isEqualToString:[url absoluteString]])
100 found = i;
101 }
102
103 if (found > -1) {
104 NSLog(@"[URLInterceptor] temporarily allowing %@ from allowed list with no matching WebViewTab", url);
105 [tmpAllowed removeObjectAtIndex:found];
106 }
107
108 return (found > -1);
109}
110
111+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request
112{
113 return request;
114}
115
116/*
117 * Start the show: WebView will ask NSURLConnection if it can handle this request, and will eventually hit this registered handler.
118 * We will intercept all requests except for data: and file:// URLs. WebView will then call our initWithRequest.
119 */
120+ (BOOL)canInitWithRequest:(NSURLRequest *)request
121{
122 if ([NSURLProtocol propertyForKey:REWRITTEN_KEY inRequest:request] != nil)
123 /* already mucked with this request */
124 return NO;
125
126 NSString *scheme = [[[request URL] scheme] lowercaseString];
127 if ([scheme isEqualToString:@"data"] || [scheme isEqualToString:@"file"] || [scheme isEqualToString:@"mailto"])
128 /* can't do anything for these URLs */
129 return NO;
130
131 if (appDelegate == nil)
132 appDelegate = (AppDelegate *)[[UIApplication sharedApplication] delegate];
133
134 return YES;
135}
136
137+ (NSString *)prependDirectivesIfExisting:(NSDictionary *)directives inCSPHeader:(NSString *)header
138{
139 /*
140 * CSP guide says apostrophe can't be in a bare string, so it should be safe to assume
141 * splitting on ; will not catch any ; inside of an apostrophe-enclosed value, since those
142 * can only be constant things like 'self', 'unsafe-inline', etc.
143 *
144 * https://www.w3.org/TR/CSP2/#source-list-parsing
145 */
146
147 NSMutableDictionary *curDirectives = [[NSMutableDictionary alloc] init];
148 NSArray *td = [header componentsSeparatedByString:@";"];
149 for (int i = 0; i < [td count]; i++) {
150 NSString *t = [(NSString *)[td objectAtIndex:i] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
151 NSRange r = [t rangeOfString:@" "];
152 if (r.length > 0) {
153 NSString *dir = [[t substringToIndex:r.location] lowercaseString];
154 NSString *val = [[t substringFromIndex:r.location] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
155 [curDirectives setObject:val forKey:dir];
156 }
157 }
158
159 for (NSString *newDir in [directives allKeys]) {
160 NSArray *newvals = [directives objectForKey:newDir];
161 NSString *curval = [curDirectives objectForKey:newDir];
162 if (curval) {
163 NSString *newval = [newvals objectAtIndex:0];
164
165 /*
166 * If none of the existing values for this directive have a nonce or hash,
167 * then inserting our value with a nonce will cause the directive to become
168 * strict, so "'nonce-abcd' 'self' 'unsafe-inline'" causes the browser to
169 * ignore 'self' and 'unsafe-inline', requiring that all scripts have a
170 * nonce or hash. Since the site would probably only ever have nonce values
171 * in its <script> tags if it was in the CSP policy, only include our nonce
172 * value if the CSP policy already has them.
173 */
174 if ([curval containsString:@"'nonce-"] || [curval containsString:@"'sha"])
175 newval = [newvals objectAtIndex:1];
176
177 if ([curval containsString:@"'none'"]) {
178 /*
179 * CSP spec says if 'none' is encountered to ignore anything else,
180 * so if 'none' is there, just replace it with newval rather than
181 * prepending.
182 */
183 } else {
184 if ([newval isEqualToString:@""])
185 newval = curval;
186 else
187 newval = [NSString stringWithFormat:@"%@ %@", newval, curval];
188 }
189
190 [curDirectives setObject:newval forKey:newDir];
191 }
192 }
193
194 NSMutableString *ret = [[NSMutableString alloc] init];
195 for (NSString *dir in [[curDirectives allKeys] sortedArrayUsingSelector:@selector(caseInsensitiveCompare:)])
196 [ret appendString:[NSString stringWithFormat:@"%@%@ %@;", ([ret length] > 0 ? @" " : @""), dir, [curDirectives objectForKey:dir]]];
197
198 return [NSString stringWithString:ret];
199}
200
201/*
202 * We said we can init a request for this URL, so allocate one.
203 * Take this opportunity to find out what tab this request came from based on its User-Agent.
204 */
205- (instancetype)initWithRequest:(NSURLRequest *)request cachedResponse:(NSCachedURLResponse *)cachedResponse client:(id<NSURLProtocolClient>)client
206{
207 self = [super initWithRequest:request cachedResponse:cachedResponse client:client];
208 wvt = nil;
209
210 /* extract tab hash from per-uiwebview user agent */
211 NSString *ua = [request valueForHTTPHeaderField:@"User-Agent"];
212 NSArray *uap = [ua componentsSeparatedByString:@"/"];
213 NSString *wvthash = uap[uap.count - 1];
214
215 /* store it for later without the hash */
216 userAgent = [[uap subarrayWithRange:NSMakeRange(0, uap.count - 1)] componentsJoinedByString:@"/"];
217
218 if ([NSURLProtocol propertyForKey:WVT_KEY inRequest:request])
219 wvthash = [NSString stringWithFormat:@"%lu", [(NSNumber *)[NSURLProtocol propertyForKey:WVT_KEY inRequest:request] longValue]];
220
221 if (wvthash != nil && ![wvthash isEqualToString:@""]) {
222 for (WebViewTab *_wvt in [[appDelegate webViewController] webViewTabs]) {
223 if ([[NSString stringWithFormat:@"%lu", (unsigned long)[_wvt hash]] isEqualToString:wvthash]) {
224 wvt = _wvt;
225 break;
226 }
227 }
228 }
229
230 if (wvt == nil && [[self class] isURLTemporarilyAllowed:[request URL]])
231 wvt = [[appDelegate webViewController] curWebViewTab];
232
233 /*
234 * Videos load without our modified User-Agent (which normally has a per-tab hash appended to it to be able to match
235 * it to the proper tab) but it does have its own UA which starts with "AppleCoreMedia/". Assume it came from the
236 * current tab and hope for the best.
237 */
238 if (wvt == nil && ([[[[request URL] scheme] lowercaseString] isEqualToString:@"http"] || [[[[request URL] scheme] lowercaseString] isEqualToString:@"https"]) && [[ua substringToIndex:15] isEqualToString:@"AppleCoreMedia/"]) {
239#ifdef TRACE
240 NSLog(@"[URLInterceptor] AppleCoreMedia request with no matching WebViewTab, binding to current tab: %@", [request URL]);
241#endif
242 wvt = [[appDelegate webViewController] curWebViewTab];
243 }
244
245 if (wvt == nil) {
246 NSLog(@"[URLInterceptor] request for %@ with no matching WebViewTab! (main URL %@, UA hash %@)", [request URL], [request mainDocumentURL], wvthash);
247
248 [client URLProtocol:self didFailWithError:[NSError errorWithDomain:NSCocoaErrorDomain code:NSUserCancelledError userInfo:@{ ORIGIN_KEY: @YES }]];
249
250 if (![[[[request URL] scheme] lowercaseString] isEqualToString:@"http"] && ![[[[request URL] scheme] lowercaseString] isEqualToString:@"https"]) {
251 /* iOS 10 blocks canOpenURL: requests, so we just have to assume these go somewhere */
252
253 /* about: URLs should just return nothing */
254 if ([[[request URL] scheme] isEqualToString:@"about"])
255 return nil;
256
257 UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"Open In External App" message:[NSString stringWithFormat:@"Allow URL to be opened by external app? This may compromise your privacy.\n\n%@", [request URL]] preferredStyle:UIAlertControllerStyleAlert];
258
259 UIAlertAction *okAction = [UIAlertAction actionWithTitle:NSLocalizedString(@"OK", @"OK action") style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) {
260#ifdef TRACE
261 NSLog(@"[URLInterceptor] opening in 3rd party app: %@", [request URL]);
262#endif
263 [[UIApplication sharedApplication] openURL:[request URL] options:@{} completionHandler:nil];
264 }];
265
266 UIAlertAction *cancelAction = [UIAlertAction actionWithTitle:NSLocalizedString(@"Cancel", @"Cancel action") style:UIAlertActionStyleCancel handler:nil];
267 [alertController addAction:cancelAction];
268 [alertController addAction:okAction];
269
270 [[appDelegate webViewController] presentViewController:alertController animated:YES completion:nil];
271 }
272
273 return nil;
274 }
275
276#ifdef TRACE
277 NSLog(@"[URLInterceptor] [Tab %@] initializing %@ to %@ (via %@)", wvt.tabIndex, [request HTTPMethod], [[request URL] absoluteString], [request mainDocumentURL]);
278#endif
279 return self;
280}
281
282- (NSMutableData *)data
283{
284 return _data;
285}
286
287- (void)appendData:(NSData *)newData
288{
289 if (_data == nil)
290 _data = [[NSMutableData alloc] initWithData:newData];
291 else
292 [_data appendData:newData];
293}
294
295/*
296 * We now have our request allocated and need to create a connection for it.
297 * Set our User-Agent back to a default (without its per-tab hash) and check our URL blocker to see if we should even bother with this request.
298 * If we proceed, pass it to CKHTTPConnection so we can do TLS options.
299 */
300- (void)startLoading
301{
302 NSMutableURLRequest *newRequest = [self.request mutableCopy];
303
304 [newRequest setHTTPShouldUsePipelining:YES];
305
306 [self setActualRequest:newRequest];
307
308 void (^cancelLoading)(void) = ^(void) {
309 /* need to continue the chain with a blank response so downstream knows we're done */
310 [self.client URLProtocol:self didReceiveResponse:[[NSURLResponse alloc] init] cacheStoragePolicy:NSURLCacheStorageNotAllowed];
311 [self.client URLProtocolDidFinishLoading:self];
312 };
313
314 if ([NSURLProtocol propertyForKey:ORIGIN_KEY inRequest:newRequest]) {
315 self.isOrigin = YES;
316 }
317 else if ([[newRequest URL] isEqual:[newRequest mainDocumentURL]]) {
318#ifdef TRACE
319 NSLog(@"[URLInterceptor] [Tab %@] considering as origin request: %@", wvt.tabIndex, [newRequest URL]);
320#endif
321 self.isOrigin = YES;
322 }
323
324 if (self.isOrigin) {
325 [LocalNetworkChecker clearCache];
326 }
327 else {
328 NSString *blocker = [URLBlocker blockingTargetForURL:[newRequest URL] fromMainDocumentURL:[newRequest mainDocumentURL]];
329 if (blocker != nil) {
330 [[wvt applicableURLBlockerTargets] setObject:@YES forKey:blocker];
331 cancelLoading();
332 return;
333 }
334 }
335
336 /* some rules act on the host we're connecting to, and some act on the origin host */
337 self.hostSettings = [HostSettings settingsOrDefaultsForHost:[[[self request] URL] host]];
338 NSString *oHost = [[[self request] mainDocumentURL] host];
339 if (oHost == nil || [oHost isEqualToString:@""] || [oHost isEqualToString:[[[self request] URL] host]])
340 self.originHostSettings = self.hostSettings;
341 else
342 self.originHostSettings = [HostSettings settingsOrDefaultsForHost:oHost];
343
344 /* set our proper UA, or use this host's version */
345 NSString *customUA = [self.originHostSettings settingOrDefault:HOST_SETTINGS_KEY_USER_AGENT];
346 if (customUA == nil || [customUA isEqualToString:@""])
347 [newRequest setValue:userAgent forHTTPHeaderField:@"User-Agent"];
348 else {
349#ifdef TRACE
350 NSLog(@"[URLInterceptor] [Tab %@] setting custom UA: %@", wvt.tabIndex, customUA);
351#endif
352 [newRequest setValue:customUA forHTTPHeaderField:@"User-Agent"];
353 }
354
355 /* check HSTS cache first to see if scheme needs upgrading */
356 [newRequest setURL:[[appDelegate hstsCache] rewrittenURI:[[self request] URL]]];
357
358 /* then check HTTPS Everywhere (must pass all URLs since some rules are not just scheme changes */
359 NSArray *HTErules = [HTTPSEverywhere potentiallyApplicableRulesForHost:[[[self request] URL] host]];
360 if (HTErules != nil && [HTErules count] > 0) {
361 [newRequest setURL:[HTTPSEverywhere rewrittenURI:[[self request] URL] withRules:HTErules]];
362
363 for (HTTPSEverywhereRule *HTErule in HTErules) {
364 [[wvt applicableHTTPSEverywhereRules] setObject:@YES forKey:[HTErule name]];
365 }
366 }
367
368 /* in case our URL changed/upgraded, send back to the webview so it knows what our protocol is for "//" assets */
369 if (self.isOrigin && ![[[newRequest URL] absoluteString] isEqualToString:[[self.request URL] absoluteString]]) {
370#ifdef TRACE_HOST_SETTINGS
371 NSLog(@"[URLInterceptor] [Tab %@] canceling origin request to redirect %@ rewritten to %@", wvt.tabIndex, [[self.request URL] absoluteString], [[newRequest URL] absoluteString]);
372#endif
373 [wvt loadURL:[newRequest URL]];
374 return;
375 }
376
377 if (!self.isOrigin) {
378 if ([wvt secureMode] > WebViewTabSecureModeInsecure && ![[[[newRequest URL] scheme] lowercaseString] isEqualToString:@"https"]) {
379 if ([self.originHostSettings settingOrDefault:HOST_SETTINGS_KEY_ALLOW_MIXED_MODE]) {
380#ifdef TRACE_HOST_SETTINGS
381 NSLog(@"[URLInterceptor] [Tab %@] allowing mixed-content request %@ from %@", wvt.tabIndex, [newRequest URL], [[newRequest mainDocumentURL] host]);
382#endif
383 }
384 else {
385 [wvt setSecureMode:WebViewTabSecureModeMixed];
386#ifdef TRACE_HOST_SETTINGS
387 NSLog(@"[URLInterceptor] [Tab %@] blocking mixed-content request %@ from %@", wvt.tabIndex, [newRequest URL], [[newRequest mainDocumentURL] host]);
388#endif
389 cancelLoading();
390 return;
391 }
392 }
393
394 if ([self.originHostSettings settingOrDefault:HOST_SETTINGS_KEY_BLOCK_LOCAL_NETS]) {
395 if (![LocalNetworkChecker isHostOnLocalNet:[[newRequest mainDocumentURL] host]] && [LocalNetworkChecker isHostOnLocalNet:[[newRequest URL] host]]) {
396#ifdef TRACE_HOST_SETTINGS
397 NSLog(@"[URLInterceptor] [Tab %@] blocking request from origin %@ to local net host %@", wvt.tabIndex, [newRequest mainDocumentURL], [newRequest URL]);
398#endif
399 cancelLoading();
400 return;
401 }
402 }
403 }
404
405 /* we're handling cookies ourself */
406 [newRequest setHTTPShouldHandleCookies:NO];
407 NSArray *cookies = [[appDelegate cookieJar] cookiesForURL:[newRequest URL] forTab:wvt.hash];
408 if (cookies != nil && [cookies count] > 0) {
409#ifdef TRACE_COOKIES
410 NSLog(@"[URLInterceptor] [Tab %@] sending %lu cookie(s) to %@", wvt.tabIndex, [cookies count], [newRequest URL]);
411#endif
412 NSDictionary *headers = [NSHTTPCookie requestHeaderFieldsWithCookies:cookies];
413 [newRequest setAllHTTPHeaderFields:headers];
414 }
415
416 /* add "do not track" header if it's enabled in the settings */
417 if (sendDNT)
418 [newRequest setValue:@"1" forHTTPHeaderField:@"DNT"];
419
420 /* if we're trying to bypass the cache (forced reload), kill the If-None-Match header that gets put here automatically */
421 if ([wvt forcingRefresh]) {
422#ifdef TRACE
423 NSLog(@"[URLInterceptor] [Tab %@] forcing refresh", wvt.tabIndex);
424#endif
425 [newRequest setValue:nil forHTTPHeaderField:@"If-None-Match"];
426 }
427
428 /* remember that we saw this to avoid a loop */
429 [NSURLProtocol setProperty:@YES forKey:REWRITTEN_KEY inRequest:newRequest];
430
431 [self setConnection:[CKHTTPConnection connectionWithRequest:newRequest delegate:self]];
432}
433
434- (void)stopLoading
435{
436 [self.connection cancel];
437}
438
439/*
440 * CKHTTPConnection has established a connection (possibly with our TLS options), sent our request, and gotten a response.
441 * Handle different types of content, inject JavaScript overrides, set fake CSP for WebView to process internally, etc.
442 * Note that at this point, [self request] may be stale, so use [self actualRequest]
443 */
444- (void)HTTPConnection:(CKHTTPConnection *)connection didReceiveResponse:(NSHTTPURLResponse *)response
445{
446#ifdef TRACE
447 NSLog(@"[URLInterceptor] [Tab %@] got HTTP response %ld, content-type %@, length %lld for %@", wvt.tabIndex, (long)[response statusCode], [response MIMEType], [response expectedContentLength], [[[self actualRequest] URL] absoluteString]);
448#endif
449
450 encoding = 0;
451 _data = nil;
452 firstChunk = YES;
453
454 contentType = CONTENT_TYPE_OTHER;
455 NSString *ctype = [[self caseInsensitiveHeader:@"content-type" inResponse:response] lowercaseString];
456 if (ctype != nil && ([ctype hasPrefix:@"text/html"] || [ctype hasPrefix:@"application/html"] || [ctype hasPrefix:@"application/xhtml+xml"]))
457 contentType = CONTENT_TYPE_HTML;
458
459 /* rewrite or inject Content-Security-Policy (and X-Webkit-CSP just in case) headers */
460 NSString *CSPheader = nil;
461 NSString *CSPmode = [self.originHostSettings settingOrDefault:HOST_SETTINGS_KEY_CSP];
462
463 if ([CSPmode isEqualToString:HOST_SETTINGS_CSP_STRICT])
464 CSPheader = @"connect-src 'none'; default-src 'none'; font-src 'none'; media-src 'none'; object-src 'none'; sandbox allow-forms allow-top-navigation; script-src 'none'; style-src 'unsafe-inline' *; report-uri;";
465 else if ([CSPmode isEqualToString:HOST_SETTINGS_CSP_BLOCK_CONNECT])
466 CSPheader = @"connect-src 'none'; media-src 'none'; object-src 'none'; report-uri;";
467
468 NSString *curCSP = [self caseInsensitiveHeader:@"content-security-policy" inResponse:response];
469
470#ifdef TRACE_HOST_SETTINGS
471 NSLog(@"[HostSettings] [Tab %@] setting CSP for %@ to %@ (via %@) (currently %@)", wvt.tabIndex, [[[self actualRequest] URL] host], CSPmode, [[[self actualRequest] mainDocumentURL] host], curCSP);
472#endif
473
474 NSMutableDictionary *mHeaders = [[NSMutableDictionary alloc] initWithDictionary:[response allHeaderFields]];
475
476 /* don't bother rewriting with the header if we don't want a restrictive one (CSPheader) and the site doesn't have one (curCSP) */
477 if (CSPheader != nil || curCSP != nil) {
478 id foundCSP = nil;
479
480 /* directives and their values (normal and nonced versions) to prepend */
481 NSDictionary *wantedDirectives = @{
482 @"child-src": @[ @"endlessipc:", @"endlessipc:" ],
483 @"default-src" : @[ @"endlessipc:", [NSString stringWithFormat:@"'nonce-%@' endlessipc:", [self cspNonce]] ],
484 @"frame-src": @[ @"endlessipc:", @"endlessipc:" ],
485 @"script-src" : @[ @"", [NSString stringWithFormat:@"'nonce-%@'", [self cspNonce]] ],
486 };
487
488 for (id h in [mHeaders allKeys]) {
489 NSString *hv = (NSString *)[[response allHeaderFields] valueForKey:h];
490
491 if ([[h lowercaseString] isEqualToString:@"content-security-policy"] || [[h lowercaseString] isEqualToString:@"x-webkit-csp"]) {
492 if ([CSPmode isEqualToString:HOST_SETTINGS_CSP_STRICT])
493 /* disregard the existing policy since ours will be the most strict anyway */
494 hv = CSPheader;
495
496 /* merge in the things we require for any policy in case exiting policies would block them */
497 hv = [URLInterceptor prependDirectivesIfExisting:wantedDirectives inCSPHeader:hv];
498
499 [mHeaders setObject:hv forKey:h];
500 foundCSP = hv;
501 }
502 else
503 [mHeaders setObject:hv forKey:h];
504 }
505
506 if (!foundCSP && CSPheader) {
507 [mHeaders setObject:CSPheader forKey:@"Content-Security-Policy"];
508 [mHeaders setObject:CSPheader forKey:@"X-WebKit-CSP"];
509 foundCSP = CSPheader;
510 }
511
512#ifdef TRACE_HOST_SETTINGS
513 NSLog(@"[HostSettings] [Tab %@] CSP header is now %@", wvt.tabIndex, foundCSP);
514#endif
515 }
516
517 /* rebuild our response with any modified headers */
518 response = [[NSHTTPURLResponse alloc] initWithURL:[response URL] statusCode:[response statusCode] HTTPVersion:@"1.1" headerFields:mHeaders];
519
520 /* save any cookies we just received */
521 [[appDelegate cookieJar] setCookies:[NSHTTPCookie cookiesWithResponseHeaderFields:[response allHeaderFields] forURL:[[self actualRequest] URL]] forURL:[[self actualRequest] URL] mainDocumentURL:[wvt url] forTab:wvt.hash];
522
523 /* in case of localStorage */
524 [[appDelegate cookieJar] trackDataAccessForDomain:[[response URL] host] fromTab:wvt.hash];
525
526 if ([[[self.request URL] scheme] isEqualToString:@"https"]) {
527 NSString *hsts = [[(NSHTTPURLResponse *)response allHeaderFields] objectForKey:HSTS_HEADER];
528 if (hsts != nil && ![hsts isEqualToString:@""]) {
529 [[appDelegate hstsCache] parseHSTSHeader:hsts forHost:[[self.request URL] host]];
530 }
531 }
532
533 if ([wvt secureMode] > WebViewTabSecureModeInsecure && ![[[[[self actualRequest] URL] scheme] lowercaseString] isEqualToString:@"https"]) {
534 /* an element on the page was not sent over https but the initial request was, downgrade to mixed */
535 if ([wvt secureMode] > WebViewTabSecureModeInsecure) {
536 [wvt setSecureMode:WebViewTabSecureModeMixed];
537 }
538 }
539
540 /* handle HTTP-level redirects */
541 if (response.statusCode == 301 || response.statusCode == 302 || response.statusCode == 303 || response.statusCode == 307) {
542 NSString *newURL = [self caseInsensitiveHeader:@"location" inResponse:response];
543 if (newURL == nil || [newURL isEqualToString:@""])
544 NSLog(@"[URLInterceptor] [Tab %@] got %ld redirect at %@ but no location header", wvt.tabIndex, (long)response.statusCode, [[self actualRequest] URL]);
545 else {
546 NSMutableURLRequest *newRequest = [[NSMutableURLRequest alloc] init];
547
548 /* 307 redirects are supposed to retain the method when redirecting but others should go back to GET */
549 if (response.statusCode == 307)
550 [newRequest setHTTPMethod:[[self actualRequest] HTTPMethod]];
551 else
552 [newRequest setHTTPMethod:@"GET"];
553
554 /* if the new URL is not absolute, try to build one relative to the current URL */
555 NSURL *tURL = [NSURL URLWithString:newURL relativeToURL:[[self actualRequest] URL]];
556
557 /* but if that failed, the new URL is probably absolute already */
558 if (tURL == nil)
559 tURL = [NSURL URLWithString:newURL];
560
561 if (tURL == nil) {
562 NSLog(@"[URLInterceptor] [Tab %@] failed building URL from %ld redirect to %@", wvt.tabIndex, (long)response.statusCode, newURL);
563 [[self connection] cancel];
564 [[self client] URLProtocol:self didFailWithError:[NSError errorWithDomain:NSCocoaErrorDomain code:NSUserCancelledError userInfo:@{ ORIGIN_KEY: (self.isOrigin ? @YES : @NO )}]];
565 return;
566 }
567
568 /* strangely, if we pass [NSURL URLWithString:/ relativeToURL:[NSURL https://blah/asdf/]] as the URL for the new request, it treats it as just "/" with no domain information so we have to build the relative URL, turn it into a string, then back to a URL */
569 NSURLComponents *newURLC = [[NSURLComponents alloc] initWithString:[tURL absoluteString]];
570
571 /* if we have no anchor in the new location, but the original request did, we need to preserve it */
572 if ([newURLC fragment] == nil || [[newURLC fragment] isEqualToString:@""]) {
573 if ([[[self actualRequest] URL] fragment] != nil && ![[[[self actualRequest] URL] fragment] isEqualToString:@""])
574 [newURLC setFragment:[[[self actualRequest] URL] fragment]];
575 }
576
577 [newRequest setURL:[newURLC URL]];
578
579#ifdef TRACE
580 NSLog(@"[URLInterceptor] [Tab %@] got %ld redirect from %@ to %@", wvt.tabIndex, (long)response.statusCode, [[[self actualRequest] URL] absoluteString], [[newRequest URL] absoluteURL]);
581#endif
582 [newRequest setMainDocumentURL:[[self actualRequest] mainDocumentURL]];
583
584 [NSURLProtocol setProperty:[NSNumber numberWithLong:wvt.hash] forKey:WVT_KEY inRequest:newRequest];
585
586 /* if we're being redirected from secure back to insecure, we might be stuck in a loop from an HTTPSEverywhere rule */
587 if ([[[[self actualRequest] URL] scheme] isEqualToString:@"https"] && [[[newRequest URL] scheme] isEqualToString:@"http"])
588 [HTTPSEverywhere noteInsecureRedirectionForURL:[[self actualRequest] URL] toURL:[newRequest URL]];
589
590 /* process it all over again */
591 [NSURLProtocol removePropertyForKey:REWRITTEN_KEY inRequest:newRequest];
592 [[self client] URLProtocol:self wasRedirectedToRequest:newRequest redirectResponse:response];
593 }
594
595 [[self connection] cancel];
596 [[self client] URLProtocol:self didFailWithError:[NSError errorWithDomain:NSCocoaErrorDomain code:NSUserCancelledError userInfo:@{ ORIGIN_KEY: (self.isOrigin ? @YES : @NO )}]];
597 return;
598 }
599
600 NSString *content_encoding = [self caseInsensitiveHeader:@"content-encoding" inResponse:response];
601 if (content_encoding != nil) {
602 if ([content_encoding isEqualToString:@"deflate"])
603 encoding = ENCODING_DEFLATE;
604 else if ([content_encoding isEqualToString:@"gzip"])
605 encoding = ENCODING_GZIP;
606 else
607 NSLog(@"[URLInterceptor] [Tab %@] unknown content encoding \"%@\"", wvt.tabIndex, content_encoding);
608 }
609
610 [self.client URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageAllowedInMemoryOnly];
611}
612
613- (void)HTTPConnection:(CKHTTPConnection *)connection didReceiveSecTrust:(SecTrustRef)secTrustRef certificate:(SSLCertificate *)certificate
614{
615 if (self.isOrigin)
616 [wvt setSSLCertificate:certificate];
617}
618
619- (void)HTTPConnection:(CKHTTPConnection *)connection didReceiveData:(NSData *)data
620{
621 [self appendData:data];
622
623 NSData *newData;
624 if (encoding) {
625 /*
626 * Try to un-gzip the data we've received so far. If we get nil (it's incomplete gzip data),
627 * continue to buffer it before passing it along. If we *can* ungzip it, pass the ugzip'd data
628 * along and reset the buffer.
629 */
630 if (encoding == ENCODING_DEFLATE)
631 newData = [_data zlibInflate];
632 else if (encoding == ENCODING_GZIP)
633 newData = [_data gzipInflate];
634 }
635 else
636 newData = [[NSData alloc] initWithBytes:[data bytes] length:[data length]];
637
638 if (newData != nil) {
639 if (firstChunk) {
640 /* we only need to do injection for top-level docs */
641 if (self.isOrigin) {
642 NSMutableData *tData = [[NSMutableData alloc] init];
643 if (contentType == CONTENT_TYPE_HTML)
644 /* prepend a doctype to force into standards mode and throw in any javascript overrides */
645 [tData appendData:[[NSString stringWithFormat:@"<!DOCTYPE html><script type=\"text/javascript\" nonce=\"%@\">%@</script>", [self cspNonce], [[self class] javascriptToInjectForURL:[[self actualRequest] mainDocumentURL]]] dataUsingEncoding:NSUTF8StringEncoding]];
646
647 [tData appendData:newData];
648 newData = tData;
649 }
650
651 firstChunk = NO;
652 }
653
654 /* clear our running buffer of data for this request */
655 _data = nil;
656 }
657 [self.client URLProtocol:self didLoadData:newData];
658}
659
660- (void)HTTPConnectionDidFinishLoading:(CKHTTPConnection *)connection {
661 [self.client URLProtocolDidFinishLoading:self];
662 [self setConnection:nil];
663 _data = nil;
664}
665
666- (void)HTTPConnection:(CKHTTPConnection *)connection didFailWithError:(NSError *)error {
667#ifdef TRACE
668 NSLog(@"[URLInterceptor] [Tab %@] failed loading %@: %@", wvt.tabIndex, [[[self actualRequest] URL] absoluteString], error);
669#endif
670
671 NSMutableDictionary *ui = [[NSMutableDictionary alloc] initWithDictionary:[error userInfo]];
672 [ui setObject:(self.isOrigin ? @YES : @NO) forKeyedSubscript:ORIGIN_KEY];
673
674 [self.client URLProtocol:self didFailWithError:[NSError errorWithDomain:[error domain] code:[error code] userInfo:ui]];
675 [self setConnection:nil];
676 _data = nil;
677}
678
679- (void)HTTPConnection:(CKHTTPConnection *)connection didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge
680{
681 NSURLCredential *nsuc;
682
683 /* if we have existing credentials for this realm, try it first */
684 if ([challenge previousFailureCount] == 0) {
685 NSDictionary *d = [[NSURLCredentialStorage sharedCredentialStorage] credentialsForProtectionSpace:[challenge protectionSpace]];
686 if (d != nil) {
687 for (id u in d) {
688 nsuc = [d objectForKey:u];
689 break;
690 }
691 }
692 }
693
694 /* no credentials, prompt the user */
695 if (nsuc == nil) {
696 dispatch_async(dispatch_get_main_queue(), ^{
697 UIAlertController *uiac = [UIAlertController alertControllerWithTitle:NSLocalizedString(@"Authentication Required", nil) message:@"" preferredStyle:UIAlertControllerStyleAlert];
698
699 if ([[challenge protectionSpace] realm] != nil && ![[[challenge protectionSpace] realm] isEqualToString:@""])
700 [uiac setMessage:[NSString stringWithFormat:@"%@: \"%@\"", [[challenge protectionSpace] host], [[challenge protectionSpace] realm]]];
701 else
702 [uiac setMessage:[[challenge protectionSpace] host]];
703
704 [uiac addTextFieldWithConfigurationHandler:^(UITextField *textField) {
705 textField.placeholder = NSLocalizedString(@"Log In", nil);
706 }];
707
708 [uiac addTextFieldWithConfigurationHandler:^(UITextField *textField) {
709 textField.placeholder = NSLocalizedString(@"Password", @"Password");
710 textField.secureTextEntry = YES;
711 }];
712
713 [uiac addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"Cancel", nil) style:UIAlertActionStyleCancel handler:^(UIAlertAction *action) {
714 [[challenge sender] cancelAuthenticationChallenge:challenge];
715 [self.client URLProtocol:self didFailWithError:[NSError errorWithDomain:NSCocoaErrorDomain code:NSUserCancelledError userInfo:@{ ORIGIN_KEY: @YES }]];
716 }]];
717
718 [uiac addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"Log In", nil) style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) {
719 UITextField *login = uiac.textFields.firstObject;
720 UITextField *password = uiac.textFields.lastObject;
721
722 NSURLCredential *nsuc = [[NSURLCredential alloc] initWithUser:[login text] password:[password text] persistence:NSURLCredentialPersistenceForSession];
723 [[NSURLCredentialStorage sharedCredentialStorage] setCredential:nsuc forProtectionSpace:[challenge protectionSpace]];
724
725 [[challenge sender] useCredential:nsuc forAuthenticationChallenge:challenge];
726 }]];
727
728 [[appDelegate webViewController] presentViewController:uiac animated:YES completion:nil];
729 });
730 }
731 else {
732 [[NSURLCredentialStorage sharedCredentialStorage] setCredential:nsuc forProtectionSpace:[challenge protectionSpace]];
733 [[challenge sender] useCredential:nsuc forAuthenticationChallenge:challenge];
734
735 /* XXX: crashes in WebCore */
736 //[self.client URLProtocol:self didReceiveAuthenticationChallenge:challenge];
737 }
738}
739
740- (void)HTTPConnection:(CKHTTPConnection *)connection didCancelAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge
741{
742 [self.client URLProtocol:self didCancelAuthenticationChallenge:challenge];
743}
744
745- (void)HTTPConnection:(CKHTTPConnection *)connection willSendRequestForAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge
746{
747}
748
749- (NSString *)caseInsensitiveHeader:(NSString *)header inResponse:(NSHTTPURLResponse *)response
750{
751 NSString *o;
752 for (id h in [response allHeaderFields]) {
753 if ([[h lowercaseString] isEqualToString:[header lowercaseString]]) {
754 o = [[response allHeaderFields] objectForKey:h];
755
756 /* XXX: does webview always honor the first matching header or the last one? */
757 break;
758 }
759 }
760
761 return o;
762}
763
764- (NSString *)cspNonce
765{
766 if (!_cspNonce) {
767 /*
768 * from https://w3c.github.io/webappsec-csp/#security-nonces:
769 *
770 * "The generated value SHOULD be at least 128 bits long (before encoding), and SHOULD
771 * "be generated via a cryptographically secure random number generator in order to
772 * "ensure that the value is difficult for an attacker to predict.
773 */
774
775 NSMutableData *data = [NSMutableData dataWithLength:16];
776 if (SecRandomCopyBytes(kSecRandomDefault, 16, data.mutableBytes) != 0)
777 abort();
778
779 _cspNonce = [data base64EncodedStringWithOptions:0];
780 }
781
782 return _cspNonce;
783}
784
785@end
786
787
788#ifdef USE_DUMMY_URLINTERCEPTOR
789
790/*
791 * A simple NSURLProtocol handler to swap in for URLInterceptor, which does less mucking around.
792 * Useful for troubleshooting.
793 */
794
795@implementation DummyURLInterceptor
796
797+ (BOOL)canInitWithRequest:(NSURLRequest *)request
798{
799 if ([NSURLProtocol propertyForKey:REWRITTEN_KEY inRequest:request] != nil)
800 return NO;
801
802 NSString *scheme = [[[request URL] scheme] lowercaseString];
803 if ([scheme isEqualToString:@"data"] || [scheme isEqualToString:@"file"])
804 return NO;
805
806 return YES;
807}
808
809+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request
810{
811 return request;
812}
813
814- (void)startLoading
815{
816 NSLog(@"[DummyURLInterceptor] [%lu] start loading %@ %@", self.hash, [self.request HTTPMethod], [self.request URL]);
817
818 NSMutableURLRequest *newRequest = [self.request mutableCopy];
819 [NSURLProtocol setProperty:@YES forKey:REWRITTEN_KEY inRequest:newRequest];
820 self.connection = [NSURLConnection connectionWithRequest:newRequest delegate:self];
821}
822
823- (void)stopLoading
824{
825 [self.connection cancel];
826}
827
828- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
829{
830 NSLog(@"[DummyURLInterceptor] [%lu] got HTTP data with size %lu for %@", self.hash, [data length], [[connection originalRequest] URL]);
831 [self.client URLProtocol:self didLoadData:data];
832}
833
834- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error
835{
836 NSMutableDictionary *ui = [[NSMutableDictionary alloc] initWithDictionary:[error userInfo]];
837 [ui setObject:(self.isOrigin ? @YES : @NO) forKeyedSubscript:ORIGIN_KEY];
838
839 [self.client URLProtocol:self didFailWithError:[NSError errorWithDomain:[error domain] code:[error code] userInfo:ui]];
840 self.connection = nil;
841}
842
843- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response
844{
845 NSLog(@"[DummyURLInterceptor] [%lu] got HTTP response content-type %@, length %lld for %@", self.hash, [response MIMEType], [response expectedContentLength], [[connection originalRequest] URL]);
846 [self.client URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageAllowed];
847}
848
849- (void)connectionDidFinishLoading:(NSURLConnection *)connection
850{
851 [self.client URLProtocolDidFinishLoading:self];
852 self.connection = nil;
853}
854
855@end
856
857#endif