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;
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