iOS web browser with a focus on security and privacy
at master 857 lines 34 kB view raw
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