iOS web browser with a focus on security and privacy
at master 520 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@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