/* * 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 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)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"]) /* 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] javascriptToInject]] dataUsingEncoding:NSUTF8StringEncoding]]; [tData appendData:data]; data = tData; } firstChunk = YES; } [self.client URLProtocol:self didLoadData:data]; } - (void)connection:(NSURLConnection *)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]; } - (void)connectionDidFinishLoading:(NSURLConnection *)connection { [self.client URLProtocolDidFinishLoading:self]; self.connection = nil; } - (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 static AppDelegate *appDelegate; + (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]); appDelegate = (AppDelegate *)[[UIApplication sharedApplication] delegate]; NSMutableURLRequest *newRequest = [self.request mutableCopy]; [NSURLProtocol setProperty:@YES forKey:REWRITTEN_KEY inRequest:newRequest]; #if 0 [newRequest setHTTPShouldHandleCookies:NO]; NSArray *cookies = [[appDelegate cookieJar] cookiesForURL:[newRequest URL] forTab:self.hash]; if (cookies != nil && [cookies count] > 0) { #ifdef TRACE_COOKIES NSLog(@"[DummyURLInterceptor] [Tab %lu] sending %lu cookie(s) to %@", (unsigned long)self.hash, [cookies count], [newRequest URL]); #endif NSDictionary *headers = [NSHTTPCookie requestHeaderFieldsWithCookies:cookies]; [newRequest setAllHTTPHeaderFields:headers]; } #endif 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 %ld, content-type %@, length %lld for %@", self.hash, [(NSHTTPURLResponse *)response statusCode], [response MIMEType], [response expectedContentLength], [[connection originalRequest] URL]); /* save any cookies we just received */ [[appDelegate cookieJar] setCookies:[NSHTTPCookie cookiesWithResponseHeaderFields:[(NSHTTPURLResponse *)response allHeaderFields] forURL:[[self request] URL]] forURL:[[self request] URL] mainDocumentURL:[[self request] mainDocumentURL] forTab:self.hash]; [self.client URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageAllowed]; } - (void)connectionDidFinishLoading:(NSURLConnection *)connection { [self.client URLProtocolDidFinishLoading:self]; self.connection = nil; } @end #endif