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