/* * Endless * Copyright (c) 2014-2015 joshua stein * * CKHTTP portions of this file are from Onion Browser * Copyright (c) 2012-2014 Mike Tigas * * See LICENSE file for redistribution terms. */ #import "AppDelegate.h" #import "HSTSCache.h" #import "HTTPSEverywhere.h" #import "LocalNetworkChecker.h" #import "SSLCertificate.h" #import "URLBlocker.h" #import "URLInterceptor.h" #import "WebViewTab.h" #import "NSData+CocoaDevUsersAdditions.h" @implementation URLInterceptor { WebViewTab *wvt; NSString *userAgent; } static AppDelegate *appDelegate; static BOOL sendDNT = true; static NSMutableArray *tmpAllowed; static NSCache *injectCache; #define INJECT_CACHE_SIZE 20 + (void)setup { [[NSNotificationCenter defaultCenter] addObserver:[URLInterceptor class] selector:@selector(clearInjectCache) name:HOST_SETTINGS_CHANGED object:nil]; } static NSString *_javascriptToInject; + (NSString *)javascriptToInject { if (!_javascriptToInject) { NSString *path = [[NSBundle mainBundle] pathForResource:@"injected" ofType:@"js"]; _javascriptToInject = [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:nil]; } return _javascriptToInject; } + (void)clearInjectCache { if (injectCache != nil) [injectCache removeAllObjects]; } + (NSString *)javascriptToInjectForURL:(NSURL *)url { if (injectCache == nil) { injectCache = [[NSCache alloc] init]; [injectCache setCountLimit:INJECT_CACHE_SIZE]; } NSString *c = [injectCache objectForKey:[url host]]; if (c != nil) return c; NSString *j = [self javascriptToInject]; HostSettings *hs = [HostSettings settingsOrDefaultsForHost:[url host]]; NSString *block_rtc = @"true"; if ([hs boolSettingOrDefault:HOST_SETTINGS_KEY_ALLOW_WEBRTC]) block_rtc = @"false"; j = [j stringByReplacingOccurrencesOfString:@"\"##BLOCK_WEBRTC##\"" withString:block_rtc]; [injectCache setObject:j forKey:[url host]]; return j; } + (void)setSendDNT:(BOOL)val { sendDNT = val; } + (void)temporarilyAllow:(NSURL *)url { if (!tmpAllowed) tmpAllowed = [[NSMutableArray alloc] initWithCapacity:1]; [tmpAllowed addObject:url]; } + (BOOL)isURLTemporarilyAllowed:(NSURL *)url { int found = -1; for (int i = 0; i < [tmpAllowed count]; i++) { if ([[tmpAllowed[i] absoluteString] isEqualToString:[url absoluteString]]) found = i; } if (found > -1) { NSLog(@"[URLInterceptor] temporarily allowing %@ from allowed list with no matching WebViewTab", url); [tmpAllowed removeObjectAtIndex:found]; } return (found > -1); } + (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request { return request; } /* * Start the show: WebView will ask NSURLConnection if it can handle this request, and will eventually hit this registered handler. * We will intercept all requests except for data: and file:// URLs. WebView will then call our initWithRequest. */ + (BOOL)canInitWithRequest:(NSURLRequest *)request { if ([NSURLProtocol propertyForKey:REWRITTEN_KEY inRequest:request] != nil) /* already mucked with this request */ return NO; NSString *scheme = [[[request URL] scheme] lowercaseString]; if ([scheme isEqualToString:@"data"] || [scheme isEqualToString:@"file"] || [scheme isEqualToString:@"mailto"]) /* can't do anything for these URLs */ return NO; if (appDelegate == nil) appDelegate = (AppDelegate *)[[UIApplication sharedApplication] delegate]; return YES; } + (NSString *)prependDirectivesIfExisting:(NSDictionary *)directives inCSPHeader:(NSString *)header { /* * CSP guide says apostrophe can't be in a bare string, so it should be safe to assume * splitting on ; will not catch any ; inside of an apostrophe-enclosed value, since those * can only be constant things like 'self', 'unsafe-inline', etc. * * https://www.w3.org/TR/CSP2/#source-list-parsing */ NSMutableDictionary *curDirectives = [[NSMutableDictionary alloc] init]; NSArray *td = [header componentsSeparatedByString:@";"]; for (int i = 0; i < [td count]; i++) { NSString *t = [(NSString *)[td objectAtIndex:i] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; NSRange r = [t rangeOfString:@" "]; if (r.length > 0) { NSString *dir = [[t substringToIndex:r.location] lowercaseString]; NSString *val = [[t substringFromIndex:r.location] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; [curDirectives setObject:val forKey:dir]; } } for (NSString *newDir in [directives allKeys]) { NSArray *newvals = [directives objectForKey:newDir]; NSString *curval = [curDirectives objectForKey:newDir]; if (curval) { NSString *newval = [newvals objectAtIndex:0]; /* * If none of the existing values for this directive have a nonce or hash, * then inserting our value with a nonce will cause the directive to become * strict, so "'nonce-abcd' 'self' 'unsafe-inline'" causes the browser to * ignore 'self' and 'unsafe-inline', requiring that all scripts have a * nonce or hash. Since the site would probably only ever have nonce values * in its ", [self cspNonce], [[self class] javascriptToInjectForURL:[[self actualRequest] mainDocumentURL]]] dataUsingEncoding:NSUTF8StringEncoding]]; [tData appendData:newData]; newData = tData; } firstChunk = NO; } /* clear our running buffer of data for this request */ _data = nil; } [self.client URLProtocol:self didLoadData:newData]; } - (void)HTTPConnectionDidFinishLoading:(CKHTTPConnection *)connection { [self.client URLProtocolDidFinishLoading:self]; [self setConnection:nil]; _data = nil; } - (void)HTTPConnection:(CKHTTPConnection *)connection didFailWithError:(NSError *)error { #ifdef TRACE NSLog(@"[URLInterceptor] [Tab %@] failed loading %@: %@", wvt.tabIndex, [[[self actualRequest] URL] absoluteString], error); #endif NSMutableDictionary *ui = [[NSMutableDictionary alloc] initWithDictionary:[error userInfo]]; [ui setObject:(self.isOrigin ? @YES : @NO) forKeyedSubscript:ORIGIN_KEY]; [self.client URLProtocol:self didFailWithError:[NSError errorWithDomain:[error domain] code:[error code] userInfo:ui]]; [self setConnection:nil]; _data = nil; } - (void)HTTPConnection:(CKHTTPConnection *)connection didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge { NSURLCredential *nsuc; /* if we have existing credentials for this realm, try it first */ if ([challenge previousFailureCount] == 0) { NSDictionary *d = [[NSURLCredentialStorage sharedCredentialStorage] credentialsForProtectionSpace:[challenge protectionSpace]]; if (d != nil) { for (id u in d) { nsuc = [d objectForKey:u]; break; } } } /* no credentials, prompt the user */ if (nsuc == nil) { dispatch_async(dispatch_get_main_queue(), ^{ UIAlertController *uiac = [UIAlertController alertControllerWithTitle:NSLocalizedString(@"Authentication Required", nil) message:@"" preferredStyle:UIAlertControllerStyleAlert]; if ([[challenge protectionSpace] realm] != nil && ![[[challenge protectionSpace] realm] isEqualToString:@""]) [uiac setMessage:[NSString stringWithFormat:@"%@: \"%@\"", [[challenge protectionSpace] host], [[challenge protectionSpace] realm]]]; else [uiac setMessage:[[challenge protectionSpace] host]]; [uiac addTextFieldWithConfigurationHandler:^(UITextField *textField) { textField.placeholder = NSLocalizedString(@"Log In", nil); }]; [uiac addTextFieldWithConfigurationHandler:^(UITextField *textField) { textField.placeholder = NSLocalizedString(@"Password", @"Password"); textField.secureTextEntry = YES; }]; [uiac addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"Cancel", nil) style:UIAlertActionStyleCancel handler:^(UIAlertAction *action) { [[challenge sender] cancelAuthenticationChallenge:challenge]; [self.client URLProtocol:self didFailWithError:[NSError errorWithDomain:NSCocoaErrorDomain code:NSUserCancelledError userInfo:@{ ORIGIN_KEY: @YES }]]; }]]; [uiac addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"Log In", nil) style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) { UITextField *login = uiac.textFields.firstObject; UITextField *password = uiac.textFields.lastObject; NSURLCredential *nsuc = [[NSURLCredential alloc] initWithUser:[login text] password:[password text] persistence:NSURLCredentialPersistenceForSession]; [[NSURLCredentialStorage sharedCredentialStorage] setCredential:nsuc forProtectionSpace:[challenge protectionSpace]]; [[challenge sender] useCredential:nsuc forAuthenticationChallenge:challenge]; }]]; [[appDelegate webViewController] presentViewController:uiac animated:YES completion:nil]; }); } else { [[NSURLCredentialStorage sharedCredentialStorage] setCredential:nsuc forProtectionSpace:[challenge protectionSpace]]; [[challenge sender] useCredential:nsuc forAuthenticationChallenge:challenge]; /* XXX: crashes in WebCore */ //[self.client URLProtocol:self didReceiveAuthenticationChallenge:challenge]; } } - (void)HTTPConnection:(CKHTTPConnection *)connection didCancelAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge { [self.client URLProtocol:self didCancelAuthenticationChallenge:challenge]; } - (void)HTTPConnection:(CKHTTPConnection *)connection willSendRequestForAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge { } - (NSString *)caseInsensitiveHeader:(NSString *)header inResponse:(NSHTTPURLResponse *)response { NSString *o; for (id h in [response allHeaderFields]) { if ([[h lowercaseString] isEqualToString:[header lowercaseString]]) { o = [[response allHeaderFields] objectForKey:h]; /* XXX: does webview always honor the first matching header or the last one? */ break; } } return o; } - (NSString *)cspNonce { if (!_cspNonce) { /* * from https://w3c.github.io/webappsec-csp/#security-nonces: * * "The generated value SHOULD be at least 128 bits long (before encoding), and SHOULD * "be generated via a cryptographically secure random number generator in order to * "ensure that the value is difficult for an attacker to predict. */ NSMutableData *data = [NSMutableData dataWithLength:16]; if (SecRandomCopyBytes(kSecRandomDefault, 16, data.mutableBytes) != 0) abort(); _cspNonce = [data base64EncodedStringWithOptions:0]; } return _cspNonce; } @end #ifdef USE_DUMMY_URLINTERCEPTOR /* * A simple NSURLProtocol handler to swap in for URLInterceptor, which does less mucking around. * Useful for troubleshooting. */ @implementation DummyURLInterceptor + (BOOL)canInitWithRequest:(NSURLRequest *)request { if ([NSURLProtocol propertyForKey:REWRITTEN_KEY inRequest:request] != nil) return NO; NSString *scheme = [[[request URL] scheme] lowercaseString]; if ([scheme isEqualToString:@"data"] || [scheme isEqualToString:@"file"]) return NO; return YES; } + (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request { return request; } - (void)startLoading { NSLog(@"[DummyURLInterceptor] [%lu] start loading %@ %@", self.hash, [self.request HTTPMethod], [self.request URL]); NSMutableURLRequest *newRequest = [self.request mutableCopy]; [NSURLProtocol setProperty:@YES forKey:REWRITTEN_KEY inRequest:newRequest]; self.connection = [NSURLConnection connectionWithRequest:newRequest delegate:self]; } - (void)stopLoading { [self.connection cancel]; } - (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data { NSLog(@"[DummyURLInterceptor] [%lu] got HTTP data with size %lu for %@", self.hash, [data length], [[connection originalRequest] URL]); [self.client URLProtocol:self didLoadData:data]; } - (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error { NSMutableDictionary *ui = [[NSMutableDictionary alloc] initWithDictionary:[error userInfo]]; [ui setObject:(self.isOrigin ? @YES : @NO) forKeyedSubscript:ORIGIN_KEY]; [self.client URLProtocol:self didFailWithError:[NSError errorWithDomain:[error domain] code:[error code] userInfo:ui]]; self.connection = nil; } - (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response { NSLog(@"[DummyURLInterceptor] [%lu] got HTTP response content-type %@, length %lld for %@", self.hash, [response MIMEType], [response expectedContentLength], [[connection originalRequest] URL]); [self.client URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageAllowed]; } - (void)connectionDidFinishLoading:(NSURLConnection *)connection { [self.client URLProtocolDidFinishLoading:self]; self.connection = nil; } @end #endif