iOS web browser with a focus on security and privacy
at remove_ckhttpconnection 695 lines 28 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; 30 31static NSString *_javascriptToInject; 32+ (NSString *)javascriptToInject 33{ 34 if (!_javascriptToInject) { 35 NSString *path = [[NSBundle mainBundle] pathForResource:@"injected" ofType:@"js"]; 36 _javascriptToInject = [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:nil]; 37 } 38 39 return _javascriptToInject; 40} 41 42+ (void)setSendDNT:(BOOL)val 43{ 44 sendDNT = val; 45} 46 47+ (void)temporarilyAllow:(NSURL *)url 48{ 49 if (!tmpAllowed) 50 tmpAllowed = [[NSMutableArray alloc] initWithCapacity:1]; 51 52 [tmpAllowed addObject:url]; 53} 54 55+ (BOOL)isURLTemporarilyAllowed:(NSURL *)url 56{ 57 int found = -1; 58 59 for (int i = 0; i < [tmpAllowed count]; i++) { 60 if ([[tmpAllowed[i] absoluteString] isEqualToString:[url absoluteString]]) 61 found = i; 62 } 63 64 if (found > -1) { 65 NSLog(@"[URLInterceptor] temporarily allowing %@ from allowed list with no matching WebViewTab", url); 66 [tmpAllowed removeObjectAtIndex:found]; 67 } 68 69 return (found > -1); 70} 71 72+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request 73{ 74 return request; 75} 76 77/* 78 * Start the show: WebView will ask NSURLConnection if it can handle this request, and will eventually hit this registered handler. 79 * We will intercept all requests except for data: and file:// URLs. WebView will then call our initWithRequest. 80 */ 81+ (BOOL)canInitWithRequest:(NSURLRequest *)request 82{ 83 if ([NSURLProtocol propertyForKey:REWRITTEN_KEY inRequest:request] != nil) 84 /* already mucked with this request */ 85 return NO; 86 87 NSString *scheme = [[[request URL] scheme] lowercaseString]; 88 if ([scheme isEqualToString:@"data"] || [scheme isEqualToString:@"file"]) 89 /* can't do anything for these URLs */ 90 return NO; 91 92 if (appDelegate == nil) 93 appDelegate = (AppDelegate *)[[UIApplication sharedApplication] delegate]; 94 95 return YES; 96} 97 98+ (NSString *)prependDirectivesIfExisting:(NSDictionary *)directives inCSPHeader:(NSString *)header 99{ 100 /* 101 * CSP guide says apostrophe can't be in a bare string, so it should be safe to assume 102 * splitting on ; will not catch any ; inside of an apostrophe-enclosed value, since those 103 * can only be constant things like 'self', 'unsafe-inline', etc. 104 * 105 * https://www.w3.org/TR/CSP2/#source-list-parsing 106 */ 107 108 NSMutableDictionary *curDirectives = [[NSMutableDictionary alloc] init]; 109 NSArray *td = [header componentsSeparatedByString:@";"]; 110 for (int i = 0; i < [td count]; i++) { 111 NSString *t = [(NSString *)[td objectAtIndex:i] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; 112 NSRange r = [t rangeOfString:@" "]; 113 if (r.length > 0) { 114 NSString *dir = [[t substringToIndex:r.location] lowercaseString]; 115 NSString *val = [[t substringFromIndex:r.location] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; 116 [curDirectives setObject:val forKey:dir]; 117 } 118 } 119 120 for (NSString *newDir in [directives allKeys]) { 121 NSArray *newvals = [directives objectForKey:newDir]; 122 NSString *curval = [curDirectives objectForKey:newDir]; 123 if (curval) { 124 NSString *newval = [newvals objectAtIndex:0]; 125 126 /* 127 * If none of the existing values for this directive have a nonce or hash, 128 * then inserting our value with a nonce will cause the directive to become 129 * strict, so "'nonce-abcd' 'self' 'unsafe-inline'" causes the browser to 130 * ignore 'self' and 'unsafe-inline', requiring that all scripts have a 131 * nonce or hash. Since the site would probably only ever have nonce values 132 * in its <script> tags if it was in the CSP policy, only include our nonce 133 * value if the CSP policy already has them. 134 */ 135 if ([curval containsString:@"'nonce-"] || [curval containsString:@"'sha"]) 136 newval = [newvals objectAtIndex:1]; 137 138 if ([curval containsString:@"'none'"]) { 139 /* 140 * CSP spec says if 'none' is encountered to ignore anything else, 141 * so if 'none' is there, just replace it with newval rather than 142 * prepending. 143 */ 144 } else { 145 if ([newval isEqualToString:@""]) 146 newval = curval; 147 else 148 newval = [NSString stringWithFormat:@"%@ %@", newval, curval]; 149 } 150 151 [curDirectives setObject:newval forKey:newDir]; 152 } 153 } 154 155 NSMutableString *ret = [[NSMutableString alloc] init]; 156 for (NSString *dir in [[curDirectives allKeys] sortedArrayUsingSelector:@selector(caseInsensitiveCompare:)]) 157 [ret appendString:[NSString stringWithFormat:@"%@%@ %@;", ([ret length] > 0 ? @" " : @""), dir, [curDirectives objectForKey:dir]]]; 158 159 return [NSString stringWithString:ret]; 160} 161 162/* 163 * We said we can init a request for this URL, so allocate one. 164 * Take this opportunity to find out what tab this request came from based on its User-Agent. 165 */ 166- (instancetype)initWithRequest:(NSURLRequest *)request cachedResponse:(NSCachedURLResponse *)cachedResponse client:(id<NSURLProtocolClient>)client 167{ 168 self = [super initWithRequest:request cachedResponse:cachedResponse client:client]; 169 wvt = nil; 170 171 /* extract tab hash from per-uiwebview user agent */ 172 NSString *ua = [request valueForHTTPHeaderField:@"User-Agent"]; 173 NSArray *uap = [ua componentsSeparatedByString:@"/"]; 174 NSString *wvthash = uap[uap.count - 1]; 175 176 /* store it for later without the hash */ 177 userAgent = [[uap subarrayWithRange:NSMakeRange(0, uap.count - 1)] componentsJoinedByString:@"/"]; 178 179 if ([NSURLProtocol propertyForKey:WVT_KEY inRequest:request]) 180 wvthash = [NSString stringWithFormat:@"%lu", [(NSNumber *)[NSURLProtocol propertyForKey:WVT_KEY inRequest:request] longValue]]; 181 182 if (wvthash != nil && ![wvthash isEqualToString:@""]) { 183 for (WebViewTab *_wvt in [[appDelegate webViewController] webViewTabs]) { 184 if ([[NSString stringWithFormat:@"%lu", (unsigned long)[_wvt hash]] isEqualToString:wvthash]) { 185 wvt = _wvt; 186 break; 187 } 188 } 189 } 190 191 if (wvt == nil && [[self class] isURLTemporarilyAllowed:[request URL]]) 192 wvt = [[[appDelegate webViewController] webViewTabs] firstObject]; 193 194 if (wvt == nil) { 195 NSLog(@"[URLInterceptor] request for %@ with no matching WebViewTab! (main URL %@, UA hash %@)", [request URL], [request mainDocumentURL], wvthash); 196 197 [client URLProtocol:self didFailWithError:[NSError errorWithDomain:NSCocoaErrorDomain code:NSUserCancelledError userInfo:@{ ORIGIN_KEY: @YES }]]; 198 199 if (![[[[request URL] scheme] lowercaseString] isEqualToString:@"http"] && ![[[[request URL] scheme] lowercaseString] isEqualToString:@"https"]) { 200 if ([[UIApplication sharedApplication] canOpenURL:[request URL]]) { 201 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]; 202 203 UIAlertAction *okAction = [UIAlertAction actionWithTitle:NSLocalizedString(@"OK", @"OK action") style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) { 204#ifdef TRACE 205 NSLog(@"[URLInterceptor] opening in 3rd party app: %@", [request URL]); 206#endif 207 [[UIApplication sharedApplication] openURL:[request URL] options:@{} completionHandler:nil]; 208 }]; 209 210 UIAlertAction *cancelAction = [UIAlertAction actionWithTitle:NSLocalizedString(@"Cancel", @"Cancel action") style:UIAlertActionStyleCancel handler:nil]; 211 [alertController addAction:cancelAction]; 212 [alertController addAction:okAction]; 213 214 [[appDelegate webViewController] presentViewController:alertController animated:YES completion:nil]; 215 } 216 } 217 218 return nil; 219 } 220 221#ifdef TRACE 222 NSLog(@"[URLInterceptor] [Tab %@] initializing %@ to %@ (via %@)", wvt.tabIndex, [request HTTPMethod], [[request URL] absoluteString], [request mainDocumentURL]); 223#endif 224 return self; 225} 226 227/* 228 * We now have our request allocated and need to create a connection for it. 229 * 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. 230 * If we proceed, pass it to CKHTTPConnection so we can do TLS options. 231 */ 232- (void)startLoading 233{ 234 NSMutableURLRequest *newRequest = [self.request mutableCopy]; 235 [newRequest setValue:userAgent forHTTPHeaderField:@"User-Agent"]; 236 [newRequest setHTTPShouldUsePipelining:YES]; 237 238 [self setActualRequest:newRequest]; 239 240 void (^cancelLoading)(void) = ^(void) { 241 /* need to continue the chain with a blank response so downstream knows we're done */ 242 [self.client URLProtocol:self didReceiveResponse:[[NSURLResponse alloc] init] cacheStoragePolicy:NSURLCacheStorageNotAllowed]; 243 [self.client URLProtocolDidFinishLoading:self]; 244 }; 245 246 if ([NSURLProtocol propertyForKey:ORIGIN_KEY inRequest:newRequest]) { 247 self.isOrigin = YES; 248 } 249 else if ([[newRequest URL] isEqual:[newRequest mainDocumentURL]]) { 250#ifdef TRACE 251 NSLog(@"[URLInterceptor] [Tab %@] considering as origin request: %@", wvt.tabIndex, [newRequest URL]); 252#endif 253 self.isOrigin = YES; 254 } 255 256 if (self.isOrigin) { 257 [LocalNetworkChecker clearCache]; 258 } 259 else if ([URLBlocker shouldBlockURL:[newRequest URL] fromMainDocumentURL:[newRequest mainDocumentURL]]) { 260 cancelLoading(); 261 return; 262 } 263 264 /* some rules act on the host we're connecting to, and some act on the origin host */ 265 self.hostSettings = [HostSettings settingsOrDefaultsForHost:[[[self request] URL] host]]; 266 NSString *oHost = [[[self request] mainDocumentURL] host]; 267 if (oHost == nil || [oHost isEqualToString:@""] || [oHost isEqualToString:[[[self request] URL] host]]) 268 self.originHostSettings = self.hostSettings; 269 else 270 self.originHostSettings = [HostSettings settingsOrDefaultsForHost:oHost]; 271 272 /* check HSTS cache first to see if scheme needs upgrading */ 273 [newRequest setURL:[[appDelegate hstsCache] rewrittenURI:[[self request] URL]]]; 274 275 /* then check HTTPS Everywhere (must pass all URLs since some rules are not just scheme changes */ 276 NSArray *HTErules = [HTTPSEverywhere potentiallyApplicableRulesForHost:[[[self request] URL] host]]; 277 if (HTErules != nil && [HTErules count] > 0) { 278 [newRequest setURL:[HTTPSEverywhere rewrittenURI:[[self request] URL] withRules:HTErules]]; 279 280 for (HTTPSEverywhereRule *HTErule in HTErules) { 281 [[wvt applicableHTTPSEverywhereRules] setObject:@YES forKey:[HTErule name]]; 282 } 283 } 284 285 /* in case our URL changed/upgraded, send back to the webview so it knows what our protocol is for "//" assets */ 286 if (self.isOrigin && ![[[newRequest URL] absoluteString] isEqualToString:[[self.request URL] absoluteString]]) { 287#ifdef TRACE_HOST_SETTINGS 288 NSLog(@"[URLInterceptor] [Tab %@] canceling origin request to redirect %@ rewritten to %@", wvt.tabIndex, [[self.request URL] absoluteString], [[newRequest URL] absoluteString]); 289#endif 290 [wvt loadURL:[newRequest URL]]; 291 return; 292 } 293 294 if (!self.isOrigin) { 295 if ([wvt secureMode] > WebViewTabSecureModeInsecure && ![[[[newRequest URL] scheme] lowercaseString] isEqualToString:@"https"]) { 296 if ([self.originHostSettings settingOrDefault:HOST_SETTINGS_KEY_ALLOW_MIXED_MODE]) { 297#ifdef TRACE_HOST_SETTINGS 298 NSLog(@"[URLInterceptor] [Tab %@] allowing mixed-content request %@ from %@", wvt.tabIndex, [newRequest URL], [[newRequest mainDocumentURL] host]); 299#endif 300 } 301 else { 302 [wvt setSecureMode:WebViewTabSecureModeMixed]; 303#ifdef TRACE_HOST_SETTINGS 304 NSLog(@"[URLInterceptor] [Tab %@] blocking mixed-content request %@ from %@", wvt.tabIndex, [newRequest URL], [[newRequest mainDocumentURL] host]); 305#endif 306 cancelLoading(); 307 return; 308 } 309 } 310 311 if ([self.originHostSettings settingOrDefault:HOST_SETTINGS_KEY_BLOCK_LOCAL_NETS]) { 312 if (![LocalNetworkChecker isHostOnLocalNet:[[newRequest mainDocumentURL] host]] && [LocalNetworkChecker isHostOnLocalNet:[[newRequest URL] host]]) { 313#ifdef TRACE_HOST_SETTINGS 314 NSLog(@"[URLInterceptor] [Tab %@] blocking request from origin %@ to local net host %@", wvt.tabIndex, [newRequest mainDocumentURL], [newRequest URL]); 315#endif 316 cancelLoading(); 317 return; 318 } 319 } 320 } 321 322 /* we're handling cookies ourself */ 323 [newRequest setHTTPShouldHandleCookies:NO]; 324 NSArray *cookies = [[appDelegate cookieJar] cookiesForURL:[newRequest URL] forTab:wvt.hash]; 325 if (cookies != nil && [cookies count] > 0) { 326#ifdef TRACE_COOKIES 327 NSLog(@"[URLInterceptor] [Tab %@] sending %lu cookie(s) to %@", wvt.tabIndex, [cookies count], [newRequest URL]); 328#endif 329 NSDictionary *headers = [NSHTTPCookie requestHeaderFieldsWithCookies:cookies]; 330 [newRequest setAllHTTPHeaderFields:headers]; 331 } 332 333 /* add "do not track" header if it's enabled in the settings */ 334 if (sendDNT) 335 [newRequest setValue:@"1" forHTTPHeaderField:@"DNT"]; 336 337 /* remember that we saw this to avoid a loop */ 338 [NSURLProtocol setProperty:@YES forKey:REWRITTEN_KEY inRequest:newRequest]; 339 340 [self setConnection:[NSURLConnection connectionWithRequest:newRequest delegate:self]]; 341} 342 343- (void)stopLoading 344{ 345 [self.connection cancel]; 346} 347 348- (void)connection:(NSURLConnection *)connection willSendRequestForAuthenticationChallenge:(nonnull NSURLAuthenticationChallenge *)challenge 349{ 350 if (![[[[[self request] URL] scheme] lowercaseString] isEqualToString:@"https"]) 351 return; 352 353 SecTrustRef trust = [[challenge protectionSpace] serverTrust]; 354 if (trust != nil) { 355 SSLCertificate *cert = [[SSLCertificate alloc] initWithSecTrustRef:trust]; 356#if 0 357 SSLContextRef sslContext = (__bridge SSLContextRef)[theStream propertyForKey:(__bridge NSString *)kCFStreamPropertySSLContext]; 358 SSLProtocol proto; 359 SSLGetNegotiatedProtocolVersion(sslContext, &proto); 360 [cert setNegotiatedProtocol:proto]; 361 362 SSLCipherSuite cipher; 363 SSLGetNegotiatedCipher(sslContext, &cipher); 364 [cert setNegotiatedCipher:cipher]; 365#endif 366 367 if (self.isOrigin) 368 [wvt setSSLCertificate:cert]; 369 } 370 371 [[challenge sender] performDefaultHandlingForAuthenticationChallenge:challenge]; 372} 373 374- (NSURLRequest *)connection:(NSURLConnection *)connection willSendRequest:(nonnull NSURLRequest *)request redirectResponse:(nullable NSURLResponse *)response 375{ 376 /* pass along internal rewrites, we'll see them again when the actual request is being made */ 377 if (response == nil) 378 return request; 379 380 NSMutableURLRequest *finalDestination = [request mutableCopy]; 381 [NSURLProtocol removePropertyForKey:REWRITTEN_KEY inRequest:finalDestination]; 382 383 long statusCode = [(NSHTTPURLResponse *)response statusCode]; 384 385#ifdef TRACE 386 NSLog(@"[URLInterceptor] [Tab %@] got HTTP redirect response %ld, content-type %@, length %lld for %@ -> %@", wvt.tabIndex, statusCode, [response MIMEType], [response expectedContentLength], [[response URL] absoluteString], [[finalDestination URL] absoluteString]); 387#endif 388 389 /* save any cookies we just received */ 390 [[appDelegate cookieJar] setCookies:[NSHTTPCookie cookiesWithResponseHeaderFields:[(NSHTTPURLResponse *)response allHeaderFields] forURL:[[self actualRequest] URL]] forURL:[[self actualRequest] URL] mainDocumentURL:[wvt url] forTab:wvt.hash]; 391 392 /* in case of localStorage */ 393 [[appDelegate cookieJar] trackDataAccessForDomain:[[response URL] host] fromTab:wvt.hash]; 394 395 if ([[[self.request URL] scheme] isEqualToString:@"https"]) { 396 NSString *hsts = [[(NSHTTPURLResponse *)response allHeaderFields] objectForKey:HSTS_HEADER]; 397 if (hsts != nil && ![hsts isEqualToString:@""]) { 398 [[appDelegate hstsCache] parseHSTSHeader:hsts forHost:[[self.request URL] host]]; 399 } 400 } 401 402 if ([wvt secureMode] > WebViewTabSecureModeInsecure && ![[[[[self actualRequest] URL] scheme] lowercaseString] isEqualToString:@"https"]) { 403 /* an element on the page was not sent over https but the initial request was, downgrade to mixed */ 404 if ([wvt secureMode] > WebViewTabSecureModeInsecure) { 405 [wvt setSecureMode:WebViewTabSecureModeMixed]; 406 } 407 } 408 409 /* if we're being redirected from secure back to insecure, we might be stuck in a loop from an HTTPSEverywhere rule */ 410 if ([[[[[self actualRequest] URL] scheme] lowercaseString] isEqualToString:@"https"] && ![[[[finalDestination URL] scheme] lowercaseString] isEqualToString:@"https"]) 411 [HTTPSEverywhere noteInsecureRedirectionForURL:[[self actualRequest] URL]]; 412 413 [[self connection] cancel]; 414 [[self client] URLProtocol:self wasRedirectedToRequest:finalDestination redirectResponse:response]; 415 416 return nil; 417} 418 419- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response 420{ 421 long statusCode = [(NSHTTPURLResponse *)response statusCode]; 422 encoding = 0; 423 _data = nil; 424 425#ifdef TRACE 426 NSLog(@"[URLInterceptor] [Tab %@] got HTTP response %ld, content-type %@, length %lld for %@", wvt.tabIndex, statusCode, [response MIMEType], [response expectedContentLength], [[[self actualRequest] URL] absoluteString]); 427#endif 428 429 contentType = CONTENT_TYPE_OTHER; 430 NSString *ctype = [[self caseInsensitiveHeader:@"content-type" inResponse:(NSHTTPURLResponse *)response] lowercaseString]; 431 if (ctype != nil) { 432 if ([ctype hasPrefix:@"text/html"] || [ctype hasPrefix:@"application/html"] || [ctype hasPrefix:@"application/xhtml+xml"]) 433 contentType = CONTENT_TYPE_HTML; 434 else if ([ctype hasPrefix:@"application/javascript"] || [ctype hasPrefix:@"text/javascript"] || [ctype hasPrefix:@"application/x-javascript"] || [ctype hasPrefix:@"text/x-javascript"]) 435 contentType = CONTENT_TYPE_JAVASCRIPT; 436 else if ([ctype hasPrefix:@"image/"]) 437 contentType = CONTENT_TYPE_IMAGE; 438 } 439 440 /* rewrite or inject Content-Security-Policy (and X-Webkit-CSP just in case) headers */ 441 NSString *CSPheader = nil; 442 NSString *CSPmode = [self.originHostSettings setting:HOST_SETTINGS_KEY_CSP]; 443 444 if ([CSPmode isEqualToString:HOST_SETTINGS_CSP_STRICT]) 445 CSPheader = @"media-src 'none'; object-src 'none'; connect-src 'none'; font-src 'none'; sandbox allow-forms allow-top-navigation; style-src 'unsafe-inline' *; report-uri;"; 446 else if ([CSPmode isEqualToString:HOST_SETTINGS_CSP_BLOCK_CONNECT]) 447 CSPheader = @"connect-src 'none'; media-src 'none'; object-src 'none'; report-uri;"; 448 449 NSString *curCSP = [self caseInsensitiveHeader:@"content-security-policy" inResponse:(NSHTTPURLResponse *)response]; 450#ifdef TRACE_HOST_SETTINGS 451 NSLog(@"[HostSettings] [Tab %@] setting CSP for %@ to %@ (via %@) (currently %@)", wvt.tabIndex, [[[self actualRequest] URL] host], CSPmode, [[[self actualRequest] mainDocumentURL] host], curCSP); 452#endif 453 NSMutableDictionary *mHeaders = [[NSMutableDictionary alloc] initWithDictionary:[(NSHTTPURLResponse *)response allHeaderFields]]; 454 455 /* don't bother rewriting with the header if we don't want a restrictive one (CSPheader) and the site doesn't have one (curCSP) */ 456 if (CSPheader != nil || curCSP != nil) { 457 id foundCSP = nil; 458 459 /* directives and their values (normal and nonced versions) to prepend */ 460 NSDictionary *wantedDirectives = @{ 461 @"child-src": @[ @"endlessipc:", @"endlessipc:" ], 462 @"default-src" : @[ @"endlessipc:", [NSString stringWithFormat:@"'nonce-%@' endlessipc:", [self cspNonce]] ], 463 @"frame-src": @[ @"endlessipc:", @"endlessipc:" ], 464 @"script-src" : @[ @"", [NSString stringWithFormat:@"'nonce-%@'", [self cspNonce]] ], 465 }; 466 467 for (id h in [mHeaders allKeys]) { 468 NSString *hv = (NSString *)[[(NSHTTPURLResponse *)response allHeaderFields] valueForKey:h]; 469 470 if ([[h lowercaseString] isEqualToString:@"content-security-policy"] || [[h lowercaseString] isEqualToString:@"x-webkit-csp"]) { 471 if ([CSPmode isEqualToString:HOST_SETTINGS_CSP_STRICT]) 472 /* disregard the existing policy since ours will be the most strict anyway */ 473 hv = CSPheader; 474 475 /* merge in the things we require for any policy in case exiting policies would block them */ 476 hv = [URLInterceptor prependDirectivesIfExisting:wantedDirectives inCSPHeader:hv]; 477 478 [mHeaders setObject:hv forKey:h]; 479 foundCSP = hv; 480 } 481 else if ([[h lowercaseString] isEqualToString:@"cache-control"]) { 482 /* ignore */ 483 } 484 else 485 [mHeaders setObject:hv forKey:h]; 486 } 487 488 if (!foundCSP && CSPheader) { 489 [mHeaders setObject:CSPheader forKey:@"Content-Security-Policy"]; 490 [mHeaders setObject:CSPheader forKey:@"X-WebKit-CSP"]; 491 foundCSP = CSPheader; 492 } 493 494#ifdef TRACE_HOST_SETTINGS 495 NSLog(@"[HostSettings] [Tab %@] CSP header is now %@", wvt.tabIndex, foundCSP); 496#endif 497 } 498 499 response = [[NSHTTPURLResponse alloc] initWithURL:[response URL] statusCode:[(NSHTTPURLResponse *)response statusCode] HTTPVersion:@"1.1" headerFields:mHeaders]; 500 501 /* save any cookies we just received */ 502 [[appDelegate cookieJar] setCookies:[NSHTTPCookie cookiesWithResponseHeaderFields:[(NSHTTPURLResponse *)response allHeaderFields] forURL:[[self actualRequest] URL]] forURL:[[self actualRequest] URL] mainDocumentURL:[wvt url] forTab:wvt.hash]; 503 504 /* in case of localStorage */ 505 [[appDelegate cookieJar] trackDataAccessForDomain:[[response URL] host] fromTab:wvt.hash]; 506 507 if ([[[self.request URL] scheme] isEqualToString:@"https"]) { 508 NSString *hsts = [[(NSHTTPURLResponse *)response allHeaderFields] objectForKey:HSTS_HEADER]; 509 if (hsts != nil && ![hsts isEqualToString:@""]) { 510 [[appDelegate hstsCache] parseHSTSHeader:hsts forHost:[[self.request URL] host]]; 511 } 512 } 513 514 if ([wvt secureMode] > WebViewTabSecureModeInsecure && ![[[[[self actualRequest] URL] scheme] lowercaseString] isEqualToString:@"https"]) { 515 /* an element on the page was not sent over https but the initial request was, downgrade to mixed */ 516 if ([wvt secureMode] > WebViewTabSecureModeInsecure) { 517 [wvt setSecureMode:WebViewTabSecureModeMixed]; 518 } 519 } 520 521 [self.client URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageAllowedInMemoryOnly]; 522} 523 524- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data 525{ 526 NSLog(@"[URLInterceptor] [%lu] got HTTP data with size %lu for %@", self.hash, [data length], [[connection originalRequest] URL]); 527 528 if (!firstChunk) { 529 /* we only need to do injection for top-level docs */ 530 if (self.isOrigin) { 531 NSMutableData *tData = [[NSMutableData alloc] init]; 532 if (contentType == CONTENT_TYPE_HTML) 533 /* prepend a doctype to force into standards mode and throw in any javascript overrides */ 534 [tData appendData:[[NSString stringWithFormat:@"<!DOCTYPE html><script type=\"text/javascript\" nonce=\"%@\">%@</script>", [self cspNonce], [[self class] javascriptToInject]] dataUsingEncoding:NSUTF8StringEncoding]]; 535 536 [tData appendData:data]; 537 data = tData; 538 } 539 540 firstChunk = YES; 541 } 542 543 [self.client URLProtocol:self didLoadData:data]; 544} 545 546- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error 547{ 548#ifdef TRACE 549 NSLog(@"[URLInterceptor] [Tab %@] failed loading %@: %@", wvt.tabIndex, [[[self actualRequest] URL] absoluteString], error); 550#endif 551 552 NSMutableDictionary *ui = [[NSMutableDictionary alloc] initWithDictionary:[error userInfo]]; 553 [ui setObject:(self.isOrigin ? @YES : @NO) forKeyedSubscript:ORIGIN_KEY]; 554 555 [self.client URLProtocol:self didFailWithError:[NSError errorWithDomain:[error domain] code:[error code] userInfo:ui]]; 556 [self setConnection:nil]; 557} 558 559- (void)connectionDidFinishLoading:(NSURLConnection *)connection 560{ 561 [self.client URLProtocolDidFinishLoading:self]; 562 self.connection = nil; 563} 564 565- (NSString *)caseInsensitiveHeader:(NSString *)header inResponse:(NSHTTPURLResponse *)response 566{ 567 NSString *o; 568 for (id h in [response allHeaderFields]) { 569 if ([[h lowercaseString] isEqualToString:[header lowercaseString]]) { 570 o = [[response allHeaderFields] objectForKey:h]; 571 572 /* XXX: does webview always honor the first matching header or the last one? */ 573 break; 574 } 575 } 576 577 return o; 578} 579 580- (NSString *)cspNonce 581{ 582 if (!_cspNonce) { 583 /* 584 * from https://w3c.github.io/webappsec-csp/#security-nonces: 585 * 586 * "The generated value SHOULD be at least 128 bits long (before encoding), and SHOULD 587 * "be generated via a cryptographically secure random number generator in order to 588 * "ensure that the value is difficult for an attacker to predict. 589 */ 590 591 NSMutableData *data = [NSMutableData dataWithLength:16]; 592 if (SecRandomCopyBytes(kSecRandomDefault, 16, data.mutableBytes) != 0) 593 abort(); 594 595 _cspNonce = [data base64EncodedStringWithOptions:0]; 596 } 597 598 return _cspNonce; 599} 600 601@end 602 603 604#ifdef USE_DUMMY_URLINTERCEPTOR 605 606/* 607 * A simple NSURLProtocol handler to swap in for URLInterceptor, which does less mucking around. 608 * Useful for troubleshooting. 609 */ 610 611@implementation DummyURLInterceptor 612 613static AppDelegate *appDelegate; 614 615+ (BOOL)canInitWithRequest:(NSURLRequest *)request 616{ 617 if ([NSURLProtocol propertyForKey:REWRITTEN_KEY inRequest:request] != nil) 618 return NO; 619 620 NSString *scheme = [[[request URL] scheme] lowercaseString]; 621 if ([scheme isEqualToString:@"data"] || [scheme isEqualToString:@"file"]) 622 return NO; 623 624 return YES; 625} 626 627+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request 628{ 629 return request; 630} 631 632- (void)startLoading 633{ 634 NSLog(@"[DummyURLInterceptor] [%lu] start loading %@ %@", self.hash, [self.request HTTPMethod], [self.request URL]); 635 636 appDelegate = (AppDelegate *)[[UIApplication sharedApplication] delegate]; 637 638 NSMutableURLRequest *newRequest = [self.request mutableCopy]; 639 [NSURLProtocol setProperty:@YES forKey:REWRITTEN_KEY inRequest:newRequest]; 640 641#if 0 642 [newRequest setHTTPShouldHandleCookies:NO]; 643 644 NSArray *cookies = [[appDelegate cookieJar] cookiesForURL:[newRequest URL] forTab:self.hash]; 645 if (cookies != nil && [cookies count] > 0) { 646#ifdef TRACE_COOKIES 647 NSLog(@"[DummyURLInterceptor] [Tab %lu] sending %lu cookie(s) to %@", (unsigned long)self.hash, [cookies count], [newRequest URL]); 648#endif 649 NSDictionary *headers = [NSHTTPCookie requestHeaderFieldsWithCookies:cookies]; 650 [newRequest setAllHTTPHeaderFields:headers]; 651 } 652#endif 653 654 self.connection = [NSURLConnection connectionWithRequest:newRequest delegate:self]; 655} 656 657- (void)stopLoading 658{ 659 [self.connection cancel]; 660} 661 662- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data 663{ 664 NSLog(@"[DummyURLInterceptor] [%lu] got HTTP data with size %lu for %@", self.hash, [data length], [[connection originalRequest] URL]); 665 [self.client URLProtocol:self didLoadData:data]; 666} 667 668- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error 669{ 670 NSMutableDictionary *ui = [[NSMutableDictionary alloc] initWithDictionary:[error userInfo]]; 671 [ui setObject:(self.isOrigin ? @YES : @NO) forKeyedSubscript:ORIGIN_KEY]; 672 673 [self.client URLProtocol:self didFailWithError:[NSError errorWithDomain:[error domain] code:[error code] userInfo:ui]]; 674 self.connection = nil; 675} 676 677- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response 678{ 679 NSLog(@"[DummyURLInterceptor] [%lu] got HTTP response %ld, content-type %@, length %lld for %@", self.hash, [(NSHTTPURLResponse *)response statusCode], [response MIMEType], [response expectedContentLength], [[connection originalRequest] URL]); 680 681 /* save any cookies we just received */ 682 [[appDelegate cookieJar] setCookies:[NSHTTPCookie cookiesWithResponseHeaderFields:[(NSHTTPURLResponse *)response allHeaderFields] forURL:[[self request] URL]] forURL:[[self request] URL] mainDocumentURL:[[self request] mainDocumentURL] forTab:self.hash]; 683 684 [self.client URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageAllowed]; 685} 686 687- (void)connectionDidFinishLoading:(NSURLConnection *)connection 688{ 689 [self.client URLProtocolDidFinishLoading:self]; 690 self.connection = nil; 691} 692 693@end 694 695#endif