iOS web browser with a focus on security and privacy
at remove_ckhttpconnection 554 lines 18 kB view raw
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