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 * Originally created by Mike Abdullah on 17/03/2009.
6 * Copyright 2009 Karelia Software. All rights reserved.
7 *
8 * Originally from ConnectionKit 2.0 branch; source at:
9 * http://www.opensource.utr-software.com/source/connection/branches/2.0/CKHTTPConnection.m
10 * (CKHTTPConnection.m last updated rev 1242, 2009-06-16 09:40:21 -0700, by mabdullah)
11 *
12 * Under Modified BSD License, as per description at
13 * http://www.opensource.utr-software.com/
14 */
15
16#import "CKHTTPConnection.h"
17#import "HostSettings.h"
18#import "SSLCertificate.h"
19
20@interface CKHTTPConnection ()
21- (CFHTTPMessageRef)HTTPRequest;
22- (NSInputStream *)HTTPStream;
23- (void)start;
24- (id <CKHTTPConnectionDelegate>)delegate;
25@end
26
27@interface CKHTTPConnection (Authentication) <NSURLAuthenticationChallengeSender>
28- (CKHTTPAuthenticationChallenge *)currentAuthenticationChallenge;
29@end
30
31@interface CKHTTPAuthenticationChallenge : NSURLAuthenticationChallenge
32{
33 CFHTTPAuthenticationRef _HTTPAuthentication;
34}
35
36- (id)initWithResponse:(CFHTTPMessageRef)response proposedCredential:(NSURLCredential *)credential previousFailureCount:(NSInteger)failureCount failureResponse:(NSHTTPURLResponse *)URLResponse sender:(id <NSURLAuthenticationChallengeSender>)sender;
37- (CFHTTPAuthenticationRef)CFHTTPAuthentication;
38
39@end
40
41
42@implementation CKHTTPConnection
43
44+ (CKHTTPConnection *)connectionWithRequest:(NSURLRequest *)request delegate:(id <CKHTTPConnectionDelegate>)delegate
45{
46 return [[self alloc] initWithRequest:request delegate:delegate];
47}
48
49- (id)initWithRequest:(NSURLRequest *)request delegate:(id <CKHTTPConnectionDelegate>)delegate;
50{
51 NSParameterAssert(request);
52
53 if (!(self = [super init]))
54 return nil;
55
56 _delegate = delegate;
57 _HTTPRequest = [request makeHTTPMessage];
58 _HTTPBodyStream = [request HTTPBodyStream];
59
60 [self start];
61
62 return self;
63}
64
65- (void)dealloc
66{
67 CFRelease(_HTTPRequest);
68}
69
70#pragma mark Accessors
71
72- (CFHTTPMessageRef)HTTPRequest {
73 return _HTTPRequest;
74}
75
76- (NSInputStream *)HTTPStream {
77 return _HTTPStream;
78}
79
80- (NSInputStream *)stream {
81 return (NSInputStream *)[self HTTPStream];
82}
83
84- (id <CKHTTPConnectionDelegate>)delegate {
85 return _delegate;
86}
87
88#pragma mark Status handling
89
90- (void)start
91{
92 NSAssert(!_HTTPStream, @"Connection already started");
93 HostSettings *hs;
94
95 if (_HTTPBodyStream)
96 _HTTPStream = (__bridge_transfer NSInputStream *)(CFReadStreamCreateForStreamedHTTPRequest(NULL, [self HTTPRequest], (__bridge CFReadStreamRef)_HTTPBodyStream));
97 else
98 _HTTPStream = (__bridge_transfer NSInputStream *)CFReadStreamCreateForHTTPRequest(NULL, [self HTTPRequest]);
99
100 /* we're handling redirects ourselves */
101 CFReadStreamSetProperty((__bridge CFReadStreamRef)(_HTTPStream), kCFStreamPropertyHTTPShouldAutoredirect, kCFBooleanFalse);
102
103 NSString *method = (__bridge_transfer NSString *)CFHTTPMessageCopyRequestMethod([self HTTPRequest]);
104 if ([[method uppercaseString] isEqualToString:@"GET"])
105 CFReadStreamSetProperty((__bridge CFReadStreamRef)(_HTTPStream), kCFStreamPropertyHTTPAttemptPersistentConnection, kCFBooleanTrue);
106 else
107 CFReadStreamSetProperty((__bridge CFReadStreamRef)(_HTTPStream), kCFStreamPropertyHTTPAttemptPersistentConnection, kCFBooleanFalse);
108
109 /* set SSL protocol version enforcement before opening, when using kCFStreamSSLLevel */
110 NSURL *url = (__bridge_transfer NSURL *)(CFHTTPMessageCopyRequestURL([self HTTPRequest]));
111 if ([[[url scheme] lowercaseString] isEqualToString:@"https"]) {
112 hs = [HostSettings settingsOrDefaultsForHost:[url host]];
113
114 CFMutableDictionaryRef sslOptions = CFDictionaryCreateMutable(kCFAllocatorDefault, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
115
116 BOOL setOptions = NO;
117
118 if ([hs boolSettingOrDefault:HOST_SETTINGS_KEY_IGNORE_TLS_ERRORS]
119 && [[NSUserDefaults standardUserDefaults] boolForKey:@"allow_tls_error_ignore"])
120 {
121 CFDictionarySetValue(sslOptions, kCFStreamSSLValidatesCertificateChain, kCFBooleanFalse);
122 setOptions = YES;
123 }
124
125 if ([[hs settingOrDefault:HOST_SETTINGS_KEY_TLS] isEqualToString:HOST_SETTINGS_TLS_12]) {
126 /* kTLSProtocol12 allows lower protocols, so use kCFStreamSSLLevel to force 1.2 */
127
128 CFDictionarySetValue(sslOptions, kCFStreamSSLLevel, CFSTR("kCFStreamSocketSecurityLevelTLSv1_2"));
129 setOptions = YES;
130
131#ifdef TRACE_HOST_SETTINGS
132 NSLog(@"[HostSettings] set TLS/SSL min level for %@ to TLS 1.2", [url host]);
133#endif
134 }
135
136 if (setOptions) {
137 CFReadStreamSetProperty((__bridge CFReadStreamRef)_HTTPStream, kCFStreamPropertySSLSettings, sslOptions);
138 }
139 }
140
141 [_HTTPStream setDelegate:(id<NSStreamDelegate>)self];
142 [_HTTPStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
143 [_HTTPStream open];
144
145#ifdef TRACE_RAW_HTTP
146 NSData *d = (__bridge NSData *)CFHTTPMessageCopySerializedMessage((__bridge CFHTTPMessageRef _Nonnull)([_HTTPStream propertyForKey:(NSString *)kCFStreamPropertyHTTPFinalRequest]));
147 NSLog(@"[CKHTTPConnection] final request for %@\n%@", url, [[[NSString alloc] initWithBytes:[d bytes] length:[d length] encoding:NSUTF8StringEncoding] stringByReplacingOccurrencesOfString:@"\r" withString:@""]);
148#endif
149
150 /* for other SSL options, these need an SSLContextRef which doesn't exist until the stream is opened */
151 if ([[[url scheme] lowercaseString] isEqualToString:@"https"]) {
152 SSLContextRef sslContext = (__bridge SSLContextRef)[_HTTPStream propertyForKey:(__bridge NSString *)kCFStreamPropertySSLContext];
153 if (sslContext != NULL) {
154 SSLSessionState sslState;
155 SSLGetSessionState(sslContext, &sslState);
156
157 /* if we're not idle, this is probably a persistent connection we already opened and negotiated */
158 if (sslState == kSSLIdle) {
159 if (![self disableWeakSSLCiphers:sslContext]) {
160 NSLog(@"[CKHTTPConnection] failed disabling weak ciphers, aborting connection");
161 [self _cancelStream];
162 return;
163 }
164 }
165 }
166 }
167}
168
169- (BOOL)disableWeakSSLCiphers:(SSLContextRef)sslContext
170{
171 OSStatus status;
172 size_t numSupported;
173 SSLCipherSuite *supported = NULL;
174 SSLCipherSuite *enabled = NULL;
175 int numEnabled = 0;
176
177 status = SSLGetNumberSupportedCiphers(sslContext, &numSupported);
178 if (status != noErr) {
179 NSLog(@"[CKHTTPConnection] failed getting number of supported ciphers");
180 return NO;
181 }
182
183 supported = (SSLCipherSuite *)malloc(numSupported * sizeof(SSLCipherSuite));
184 status = SSLGetSupportedCiphers(sslContext, supported, &numSupported);
185 if (status != noErr) {
186 NSLog(@"[CKHTTPConnection] failed getting supported ciphers");
187 free(supported);
188 return NO;
189 }
190
191 enabled = (SSLCipherSuite *)malloc(numSupported * sizeof(SSLCipherSuite));
192
193 for (int i = 0; i < numSupported; i++) {
194 switch (supported[i]) {
195 case TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256:
196 case TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256:
197 case TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384:
198 case TLS_DHE_RSA_WITH_AES_256_GCM_SHA384:
199 case TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384:
200 case TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256:
201 case TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256:
202 case TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA:
203 case TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA:
204 case TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA:
205 case TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA:
206 case TLS_RSA_WITH_AES_128_CBC_SHA:
207 case TLS_RSA_WITH_AES_256_CBC_SHA:
208 enabled[numEnabled++] = supported[i];
209 break;
210 default:
211 if (supported[i] > TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 &&
212 supported[i] < TLS_EMPTY_RENEGOTIATION_INFO_SCSV) {
213 /* assume anything newer than what we've allowed is more secure */
214 enabled[numEnabled++] = supported[i];
215 break;
216 }
217#ifdef TRACE
218 NSLog(@"[CKHTTPConnection] disabling weak SSL/TLS cipher 0x%d", supported[i]);
219#endif
220 }
221 }
222 free(supported);
223
224 status = SSLSetEnabledCiphers(sslContext, enabled, numEnabled);
225 free(enabled);
226 if (status != noErr) {
227 NSLog(@"[CKHTTPConnection] failed setting enabled ciphers on %@: %d", sslContext, (int)status);
228 return NO;
229 }
230
231 return YES;
232}
233
234- (void)_cancelStream
235{
236 // Support method to cancel the HTTP stream, but not change the delegate. Used for:
237 // A) Cancelling the connection
238 // B) Waiting to restart the connection while authentication takes place
239 // C) Restarting the connection after an HTTP redirect
240 [_HTTPStream close];
241 CFBridgingRelease((__bridge_retained CFTypeRef)(_HTTPStream));
242 //[_HTTPStream release];
243 _HTTPStream = nil;
244}
245
246- (void)cancel
247{
248 // Cancel the stream and stop the delegate receiving any more info
249 [self _cancelStream];
250 _delegate = nil;
251}
252
253- (void)stream:(NSInputStream *)theStream handleEvent:(NSStreamEvent)streamEvent
254{
255 NSParameterAssert(theStream == [self stream]);
256
257 NSURL *URL = [theStream propertyForKey:(NSString *)kCFStreamPropertyHTTPFinalURL];
258
259 if (!_haveReceivedResponse) {
260 CFHTTPMessageRef response = (__bridge CFHTTPMessageRef)[theStream propertyForKey:(NSString *)kCFStreamPropertyHTTPResponseHeader];
261 if (response && CFHTTPMessageIsHeaderComplete(response)) {
262 NSHTTPURLResponse *URLResponse = [[NSHTTPURLResponse alloc] initWithURL:URL statusCode:CFHTTPMessageGetResponseStatusCode(response) HTTPVersion:(__bridge NSString * _Nullable)(CFHTTPMessageCopyVersion(response)) headerFields:(__bridge NSDictionary<NSString *,NSString *> * _Nullable)(CFHTTPMessageCopyAllHeaderFields(response))];
263
264 NSData *d = (__bridge NSData *)CFHTTPMessageCopySerializedMessage((__bridge CFHTTPMessageRef _Nonnull)([_HTTPStream propertyForKey:(NSString *)kCFStreamPropertyHTTPResponseHeader]));
265
266 /* work around bug where CFHTTPMessageIsHeaderComplete reports true but there is no actual header data to be found */
267 if ([d length] < 5) {
268#ifdef TRACE
269 NSLog(@"[CKHTTPConnection] hit CFHTTPMessageIsHeaderComplete bug, waiting for more data");
270#endif
271 goto process;
272 }
273
274#ifdef TRACE_RAW_HTTP
275 NSLog(@"[CKHTTPConnection] final response for %@\n%@", URL, [[[NSString alloc] initWithBytes:[d bytes] length:[d length] encoding:NSUTF8StringEncoding] stringByReplacingOccurrencesOfString:@"\r" withString:@""]);
276#endif
277
278 // If the response was an authentication failure, try to request fresh credentials.
279 if ([URLResponse statusCode] == 401 || [URLResponse statusCode] == 407) {
280 // Cancel any further loading and ask the delegate for authentication
281 [self _cancelStream];
282
283 NSAssert(![self currentAuthenticationChallenge], @"Authentication challenge received while another is in progress");
284
285 _authenticationChallenge = [[CKHTTPAuthenticationChallenge alloc] initWithResponse:response proposedCredential:nil previousFailureCount:_authenticationAttempts failureResponse:URLResponse sender:self];
286
287 if ([self currentAuthenticationChallenge]) {
288 _authenticationAttempts++;
289 [[self delegate] HTTPConnection:self didReceiveAuthenticationChallenge:[self currentAuthenticationChallenge]];
290 return; // Stops the delegate being sent a response received message
291 }
292 }
293
294 // By reaching this point, the response was not a valid request for authentication,
295 // so go ahead and report it
296 _haveReceivedResponse = YES;
297 [[self delegate] HTTPConnection:self didReceiveResponse:URLResponse];
298 }
299 }
300
301process:
302 switch (streamEvent) {
303 case NSStreamEventOpenCompleted:
304 break;
305 case NSStreamEventHasSpaceAvailable:
306 socketReady = true;
307 break;
308 case NSStreamEventErrorOccurred:
309 if (!socketReady && !retriedSocket) {
310 /* probably a dead keep-alive socket from the get go */
311 retriedSocket = true;
312#ifdef TRACE
313 NSLog(@"[CKHTTPConnection] socket for %@ dead but never writable, retrying (%@)", [URL absoluteString], [theStream streamError]);
314#endif
315 [self _cancelStream];
316 [self start];
317 }
318 else
319 [[self delegate] HTTPConnection:self didFailWithError:[theStream streamError]];
320 break;
321
322 case NSStreamEventEndEncountered: // Report the end of the stream to the delegate
323 [[self delegate] HTTPConnectionDidFinishLoading:self];
324 break;
325
326 case NSStreamEventHasBytesAvailable: {
327 socketReady = true;
328
329 if ([[[URL scheme] lowercaseString] isEqualToString:@"https"]) {
330 SecTrustRef trust = (__bridge SecTrustRef)[theStream propertyForKey:(__bridge NSString *)kCFStreamPropertySSLPeerTrust];
331 if (trust != nil) {
332 SSLCertificate *cert = [[SSLCertificate alloc] initWithSecTrustRef:trust];
333
334 SSLContextRef sslContext = (__bridge SSLContextRef)[theStream propertyForKey:(__bridge NSString *)kCFStreamPropertySSLContext];
335 SSLProtocol proto;
336 SSLGetNegotiatedProtocolVersion(sslContext, &proto);
337 [cert setNegotiatedProtocol:proto];
338
339 SSLCipherSuite cipher;
340 SSLGetNegotiatedCipher(sslContext, &cipher);
341 [cert setNegotiatedCipher:cipher];
342
343 [[self delegate] HTTPConnection:self didReceiveSecTrust:trust certificate:cert];
344 }
345 }
346
347 NSMutableData *data = [[NSMutableData alloc] initWithCapacity:1024];
348 while ([theStream hasBytesAvailable]) {
349 uint8_t buf[1024];
350 NSUInteger len = [theStream read:buf maxLength:1024];
351 [data appendBytes:(const void *)buf length:len];
352 }
353
354 [[self delegate] HTTPConnection:self didReceiveData:data];
355
356 break;
357 }
358 default:
359 break;
360 }
361}
362
363@end
364
365
366#pragma mark -
367
368
369@implementation CKHTTPConnection (Authentication)
370
371- (CKHTTPAuthenticationChallenge *)currentAuthenticationChallenge {
372 return _authenticationChallenge;
373}
374
375- (void)_finishCurrentAuthenticationChallenge
376{
377 _authenticationChallenge = nil;
378}
379
380- (void)useCredential:(NSURLCredential *)credential forAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge
381{
382 NSParameterAssert(challenge == [self currentAuthenticationChallenge]);
383 [self _finishCurrentAuthenticationChallenge];
384
385 // Retry the request, this time with authentication
386 // TODO: What if this function fails?
387 CFHTTPAuthenticationRef HTTPAuthentication = [(CKHTTPAuthenticationChallenge *)challenge CFHTTPAuthentication];
388 CFHTTPMessageApplyCredentials([self HTTPRequest], HTTPAuthentication, (__bridge CFStringRef)[credential user], (__bridge CFStringRef)[credential password], NULL);
389 [self start];
390}
391
392- (void)continueWithoutCredentialForAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge
393{
394 NSParameterAssert(challenge == [self currentAuthenticationChallenge]);
395 [self _finishCurrentAuthenticationChallenge];
396
397 // Just return the authentication response to the delegate
398 [[self delegate] HTTPConnection:self didReceiveResponse:(NSHTTPURLResponse *)[challenge failureResponse]];
399 [[self delegate] HTTPConnectionDidFinishLoading:self];
400}
401
402- (void)cancelAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge
403{
404 NSParameterAssert(challenge == [self currentAuthenticationChallenge]);
405 [self _finishCurrentAuthenticationChallenge];
406
407 // Treat like a -cancel message
408 [self cancel];
409}
410
411@end
412
413
414#pragma mark -
415
416
417@implementation NSURLRequest (CKHTTPURLRequest)
418
419- (CFHTTPMessageRef)makeHTTPMessage
420{
421 CFHTTPMessageRef result = CFHTTPMessageCreateRequest(NULL, (__bridge CFStringRef)[self HTTPMethod], (__bridge CFURLRef)[self URL], kCFHTTPVersion1_1);
422
423 CFHTTPMessageSetHeaderFieldValue(result, (__bridge CFStringRef)@"Accept-Encoding", (__bridge CFStringRef)@"gzip, deflate");
424
425 if ([[[self HTTPMethod] uppercaseString] isEqualToString:@"GET"])
426 CFHTTPMessageSetHeaderFieldValue(result, (__bridge CFStringRef)@"Connection", (__bridge CFStringRef)@"keep-alive");
427 else
428 CFHTTPMessageSetHeaderFieldValue(result, (__bridge CFStringRef)@"Connection", (__bridge CFStringRef)@"close");
429
430 for (NSString *hf in [self allHTTPHeaderFields])
431 CFHTTPMessageSetHeaderFieldValue(result, (__bridge CFStringRef)hf, (__bridge CFStringRef)[[self allHTTPHeaderFields] objectForKey:hf]);
432
433 NSData *body = [self HTTPBody];
434 if (body)
435 CFHTTPMessageSetBody(result, (__bridge CFDataRef)body);
436
437 return result;
438}
439
440@end
441
442
443@implementation CKHTTPAuthenticationChallenge
444
445/* Returns nil if the ref is not suitable
446 */
447- (id)initWithResponse:(CFHTTPMessageRef)response
448 proposedCredential:(NSURLCredential *)credential
449 previousFailureCount:(NSInteger)failureCount
450 failureResponse:(NSHTTPURLResponse *)URLResponse
451 sender:(id <NSURLAuthenticationChallengeSender>)sender
452{
453 NSParameterAssert(response);
454
455#warning "Instance variable used while 'self' is not set to the result of [self init]"
456
457 // Try to create an authentication object from the response
458 _HTTPAuthentication = CFHTTPAuthenticationCreateFromResponse(NULL, response);
459 if (![self CFHTTPAuthentication])
460 return nil;
461
462 // NSURLAuthenticationChallenge only handles user and password
463 if (!CFHTTPAuthenticationIsValid([self CFHTTPAuthentication], NULL))
464 return nil;
465
466 if (!CFHTTPAuthenticationRequiresUserNameAndPassword([self CFHTTPAuthentication]))
467 return nil;
468
469 // Fail if we can't retrieve decent protection space info
470 CFArrayRef authenticationDomains = CFHTTPAuthenticationCopyDomains([self CFHTTPAuthentication]);
471 NSURL *URL = [(__bridge NSArray *)authenticationDomains lastObject];
472 CFRelease(authenticationDomains);
473
474 if (!URL || ![URL host])
475 return nil;
476
477 // Fail for an unsupported authentication method
478 CFStringRef authMethod = CFHTTPAuthenticationCopyMethod([self CFHTTPAuthentication]);
479 NSString *authenticationMethod;
480 if ([(__bridge NSString *)authMethod isEqualToString:(NSString *)kCFHTTPAuthenticationSchemeBasic])
481 authenticationMethod = NSURLAuthenticationMethodHTTPBasic;
482 else if ([(__bridge NSString *)authMethod isEqualToString:(NSString *)kCFHTTPAuthenticationSchemeDigest])
483 authenticationMethod = NSURLAuthenticationMethodHTTPDigest;
484 else {
485 CFRelease(authMethod);
486 // unsupported authentication scheme
487 return nil;
488 }
489 CFRelease(authMethod);
490
491 // Initialise
492 CFStringRef realm = CFHTTPAuthenticationCopyRealm([self CFHTTPAuthentication]);
493
494 NSURLProtectionSpace *protectionSpace = [[NSURLProtectionSpace alloc] initWithHost:[URL host]
495 port:([URL port] ? [[URL port] intValue] : 80)
496 protocol:[[URL scheme] lowercaseString]
497 realm:(__bridge NSString *)realm
498 authenticationMethod:authenticationMethod];
499 CFRelease(realm);
500
501 self = [self initWithProtectionSpace:protectionSpace
502 proposedCredential:credential
503 previousFailureCount:failureCount
504 failureResponse:URLResponse
505 error:nil
506 sender:sender];
507
508 return self;
509}
510
511- (void)dealloc
512{
513 CFRelease(_HTTPAuthentication);
514}
515
516- (CFHTTPAuthenticationRef)CFHTTPAuthentication {
517 return _HTTPAuthentication;
518}
519
520@end