github.com/psiphon-Labs/psiphon-tunnel-core@v2.0.28+incompatible/MobileLibrary/iOS/SampleApps/TunneledWebView/External/JiveAuthenticatingHTTPProtocol/JAHPAuthenticatingHTTPProtocol.m (about) 1 /* 2 File: JAHPAuthenticatingHTTPProtocol.m 3 Abstract: An NSURLProtocol subclass that overrides the built-in HTTP/HTTPS protocol. 4 Version: 1.1 5 6 Disclaimer: IMPORTANT: This Apple software is supplied to you by Apple 7 Inc. ("Apple") in consideration of your agreement to the following 8 terms, and your use, installation, modification or redistribution of 9 this Apple software constitutes acceptance of these terms. If you do 10 not agree with these terms, please do not use, install, modify or 11 redistribute this Apple software. 12 13 In consideration of your agreement to abide by the following terms, and 14 subject to these terms, Apple grants you a personal, non-exclusive 15 license, under Apple's copyrights in this original Apple software (the 16 "Apple Software"), to use, reproduce, modify and redistribute the Apple 17 Software, with or without modifications, in source and/or binary forms; 18 provided that if you redistribute the Apple Software in its entirety and 19 without modifications, you must retain this notice and the following 20 text and disclaimers in all such redistributions of the Apple Software. 21 Neither the name, trademarks, service marks or logos of Apple Inc. may 22 be used to endorse or promote products derived from the Apple Software 23 without specific prior written permission from Apple. Except as 24 expressly stated in this notice, no other rights or licenses, express or 25 implied, are granted by Apple herein, including but not limited to any 26 patent rights that may be infringed by your derivative works or by other 27 works in which the Apple Software may be incorporated. 28 29 The Apple Software is provided by Apple on an "AS IS" basis. APPLE 30 MAKES NO WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION 31 THE IMPLIED WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS 32 FOR A PARTICULAR PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS USE AND 33 OPERATION ALONE OR IN COMBINATION WITH YOUR PRODUCTS. 34 35 IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL 36 OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 37 SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 38 INTERRUPTION) ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION, 39 MODIFICATION AND/OR DISTRIBUTION OF THE APPLE SOFTWARE, HOWEVER CAUSED 40 AND WHETHER UNDER THEORY OF CONTRACT, TORT (INCLUDING NEGLIGENCE), 41 STRICT LIABILITY OR OTHERWISE, EVEN IF APPLE HAS BEEN ADVISED OF THE 42 POSSIBILITY OF SUCH DAMAGE. 43 44 Copyright (C) 2014 Apple Inc. All Rights Reserved. 45 46 */ 47 48 #import "JAHPAuthenticatingHTTPProtocol.h" 49 50 #import "JAHPCanonicalRequest.h" 51 #import "JAHPCacheStoragePolicy.h" 52 #import "JAHPQNSURLSessionDemux.h" 53 54 #import "OCSPAuthURLSessionDelegate.h" 55 #import "TunneledWebView-Swift.h" 56 57 // I use the following typedef to keep myself sane in the face of the wacky 58 // Objective-C block syntax. 59 60 typedef void (^JAHPChallengeCompletionHandler)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential * credential); 61 62 @interface JAHPWeakDelegateHolder : NSObject 63 64 @property (nonatomic, weak) id<JAHPAuthenticatingHTTPProtocolDelegate> delegate; 65 66 @end 67 68 @interface JAHPAuthenticatingHTTPProtocol () <NSURLSessionDataDelegate> 69 70 @property (atomic, strong, readwrite) NSThread * clientThread; ///< The thread on which we should call the client. 71 72 /*! The run loop modes in which to call the client. 73 * \details The concurrency control here is complex. It's set up on the client 74 * thread in -startLoading and then never modified. It is, however, read by code 75 * running on other threads (specifically the main thread), so we deallocate it in 76 * -dealloc rather than in -stopLoading. We can be sure that it's not read before 77 * it's set up because the main thread code that reads it can only be called after 78 * -startLoading has started the connection running. 79 */ 80 81 @property (atomic, copy, readwrite) NSArray * modes; 82 @property (atomic, assign, readwrite) NSTimeInterval startTime; ///< The start time of the request; written by client thread only; read by any thread. 83 @property (atomic, strong, readwrite) NSURLSessionDataTask * task; ///< The NSURLSession task for that request; client thread only. 84 @property (atomic, strong, readwrite) NSURLAuthenticationChallenge * pendingChallenge; 85 @property (atomic, copy, readwrite) JAHPChallengeCompletionHandler pendingChallengeCompletionHandler; ///< The completion handler that matches pendingChallenge; main thread only. 86 @property (atomic, copy, readwrite) JAHPDidCancelAuthenticationChallengeHandler pendingDidCancelAuthenticationChallengeHandler; ///< The handler that runs when we cancel the pendingChallenge; main thread only. 87 88 @end 89 90 @implementation JAHPAuthenticatingHTTPProtocol 91 92 #pragma mark * Subclass specific additions 93 94 /*! The backing store for the class delegate. This is protected by @synchronized on the class. 95 */ 96 97 static JAHPWeakDelegateHolder* weakDelegateHolder; 98 99 100 /*! A token to append to all HTTP user agent headers. 101 */ 102 static NSString * sUserAgentToken; 103 104 + (void)start 105 { 106 [NSURLProtocol registerClass:self]; 107 } 108 109 + (void)stop { 110 [NSURLProtocol unregisterClass:self]; 111 } 112 113 + (id<JAHPAuthenticatingHTTPProtocolDelegate>)delegate 114 { 115 id<JAHPAuthenticatingHTTPProtocolDelegate> result; 116 117 @synchronized (self) { 118 if (!weakDelegateHolder) { 119 weakDelegateHolder = [JAHPWeakDelegateHolder new]; 120 } 121 result = weakDelegateHolder.delegate; 122 } 123 return result; 124 } 125 126 + (void)setDelegate:(id<JAHPAuthenticatingHTTPProtocolDelegate>)newValue 127 { 128 @synchronized (self) { 129 if (!weakDelegateHolder) { 130 weakDelegateHolder = [JAHPWeakDelegateHolder new]; 131 } 132 weakDelegateHolder.delegate = newValue; 133 } 134 } 135 136 + (NSString *)userAgentToken { 137 NSString *userAgentToken; 138 @synchronized(self) { 139 userAgentToken = sUserAgentToken; 140 } 141 return userAgentToken; 142 } 143 144 + (void)setUserAgentToken:(NSString *)userAgentToken { 145 @synchronized(self) { 146 sUserAgentToken = userAgentToken; 147 } 148 } 149 150 /*! Returns the session demux object used by all the protocol instances. 151 * \details This object allows us to have a single NSURLSession, with a session delegate, 152 * and have its delegate callbacks routed to the correct protocol instance on the correct 153 * thread in the correct modes. Can be called on any thread. 154 */ 155 156 static JAHPQNSURLSessionDemux *sharedDemuxInstance = nil; 157 158 + (JAHPQNSURLSessionDemux *)sharedDemux 159 { 160 @synchronized(self) { 161 if (sharedDemuxInstance == nil) { 162 NSURLSessionConfiguration * config; 163 164 config = [NSURLSessionConfiguration defaultSessionConfiguration]; 165 // You have to explicitly configure the session to use your own protocol subclass here 166 // otherwise you don't see redirects <rdar://problem/17384498>. 167 if (config.protocolClasses) { 168 config.protocolClasses = [config.protocolClasses arrayByAddingObject:self]; 169 } else { 170 config.protocolClasses = @[ self ]; 171 } 172 173 // Set proxy 174 NSString* proxyHost = @"localhost"; 175 176 NSNumber* socksProxyPort = [NSNumber numberWithInt: (int)[AppDelegate sharedDelegate].socksProxyPort]; 177 NSNumber* httpProxyPort = [NSNumber numberWithInt: (int)[AppDelegate sharedDelegate].httpProxyPort]; 178 179 NSDictionary *proxyDict = @{ 180 /* Disable SOCKS (to enable set to 1 and set "HTTPEnable" and "HTTPSEnable" to 0) */ 181 @"SOCKSEnable" : [NSNumber numberWithInt:0], 182 (NSString *)kCFStreamPropertySOCKSProxyHost : proxyHost, 183 (NSString *)kCFStreamPropertySOCKSProxyPort : socksProxyPort, 184 185 @"HTTPEnable" : [NSNumber numberWithInt:1], 186 (NSString *)kCFStreamPropertyHTTPProxyHost : proxyHost, 187 (NSString *)kCFStreamPropertyHTTPProxyPort : httpProxyPort, 188 189 @"HTTPSEnable" : [NSNumber numberWithInt:1], 190 (NSString *)kCFStreamPropertyHTTPSProxyHost : proxyHost, 191 (NSString *)kCFStreamPropertyHTTPSProxyPort : httpProxyPort, 192 }; 193 config.connectionProxyDictionary = proxyDict; 194 195 sharedDemuxInstance = [[JAHPQNSURLSessionDemux alloc] initWithConfiguration:config]; 196 } 197 } 198 return sharedDemuxInstance; 199 } 200 201 + (void)resetSharedDemux 202 { 203 @synchronized(self) { 204 sharedDemuxInstance = nil; 205 } 206 } 207 208 /*! Called by by both class code and instance code to log various bits of information. 209 * Can be called on any thread. 210 * \param protocol The protocol instance; nil if it's the class doing the logging. 211 * \param format A standard NSString-style format string; will not be nil. 212 */ 213 214 + (void)authenticatingHTTPProtocol:(JAHPAuthenticatingHTTPProtocol *)protocol logWithFormat:(NSString *)format, ... NS_FORMAT_FUNCTION(2, 3) 215 // All internal logging calls this routine, which routes the log message to the 216 // delegate. 217 { 218 // protocol may be nil 219 id<JAHPAuthenticatingHTTPProtocolDelegate> strongDelegate; 220 221 strongDelegate = [self delegate]; 222 if ([strongDelegate respondsToSelector:@selector(authenticatingHTTPProtocol:logWithFormat:arguments:)]) { 223 va_list arguments; 224 225 va_start(arguments, format); 226 [strongDelegate authenticatingHTTPProtocol:protocol logWithFormat:format arguments:arguments]; 227 va_end(arguments); 228 } 229 if ([strongDelegate respondsToSelector:@selector(authenticatingHTTPProtocol:logMessage:)]) { 230 va_list arguments; 231 232 va_start(arguments, format); 233 NSString *message = [[NSString alloc] initWithFormat:format arguments:arguments]; 234 va_end(arguments); 235 [strongDelegate authenticatingHTTPProtocol:protocol logMessage:message]; 236 } 237 } 238 239 #pragma mark * NSURLProtocol overrides 240 241 /*! Used to mark our recursive requests so that we don't try to handle them (and thereby 242 * suffer an infinite recursive death). 243 */ 244 245 static NSString * kJAHPRecursiveRequestFlagProperty = @"com.jivesoftware.JAHPAuthenticatingHTTPProtocol"; 246 247 + (BOOL)canInitWithRequest:(NSURLRequest *)request 248 { 249 BOOL shouldAccept; 250 NSURL * url; 251 NSString * scheme; 252 253 // Check the basics. This routine is extremely defensive because experience has shown that 254 // it can be called with some very odd requests <rdar://problem/15197355>. 255 256 shouldAccept = (request != nil); 257 if (shouldAccept) { 258 url = [request URL]; 259 shouldAccept = (url != nil); 260 } 261 if ( ! shouldAccept ) { 262 [self authenticatingHTTPProtocol:nil logWithFormat:@"decline request (malformed)"]; 263 } 264 265 // Decline our recursive requests. 266 267 if (shouldAccept) { 268 shouldAccept = ([self propertyForKey:kJAHPRecursiveRequestFlagProperty inRequest:request] == nil); 269 if ( ! shouldAccept ) { 270 [self authenticatingHTTPProtocol:nil logWithFormat:@"decline request %@ (recursive)", url]; 271 } 272 } 273 274 // Get the scheme. 275 276 if (shouldAccept) { 277 scheme = [[url scheme] lowercaseString]; 278 shouldAccept = (scheme != nil); 279 280 if ( ! shouldAccept ) { 281 [self authenticatingHTTPProtocol:nil logWithFormat:@"decline request %@ (no scheme)", url]; 282 } 283 } 284 285 // Do not try and handle requests to localhost 286 287 if (shouldAccept) { 288 shouldAccept = (![[url host] isEqualToString:@"127.0.0.1"]); 289 } 290 291 // Look for "http" or "https". 292 // 293 // Flip either or both of the following to YESes to control which schemes go through this custom 294 // NSURLProtocol subclass. 295 296 if (shouldAccept) { 297 shouldAccept = YES && [scheme isEqual:@"http"]; 298 if ( ! shouldAccept ) { 299 shouldAccept = YES && [scheme isEqual:@"https"]; 300 } 301 302 if ( ! shouldAccept ) { 303 [self authenticatingHTTPProtocol:nil logWithFormat:@"decline request %@ (scheme mismatch)", url]; 304 } else { 305 [self authenticatingHTTPProtocol:nil logWithFormat:@"accept request %@", url]; 306 } 307 } 308 309 return shouldAccept; 310 } 311 312 + (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request 313 { 314 NSURLRequest * result; 315 316 assert(request != nil); 317 // can be called on any thread 318 319 // Canonicalising a request is quite complex, so all the heavy lifting has 320 // been shuffled off to a separate module. 321 322 result = JAHPCanonicalRequestForRequest(request); 323 324 [self authenticatingHTTPProtocol:nil logWithFormat:@"canonicalized %@ to %@", [request URL], [result URL]]; 325 326 return result; 327 } 328 329 - (id)initWithRequest:(NSURLRequest *)request cachedResponse:(NSCachedURLResponse *)cachedResponse client:(id <NSURLProtocolClient>)client 330 { 331 assert(request != nil); 332 // cachedResponse may be nil 333 assert(client != nil); 334 // can be called on any thread 335 336 NSMutableURLRequest *mutableRequest = [request mutableCopy]; 337 NSArray *cookies = [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookiesForURL:mutableRequest.URL]; 338 NSString *cookieString = @""; 339 for (NSHTTPCookie *cookie in cookies) { 340 cookieString = [cookieString stringByAppendingString:[NSString stringWithFormat:@"%@=%@; ", cookie.name, cookie.value]]; 341 } 342 if ([cookieString length] > 0) { 343 cookieString = [cookieString substringToIndex:[cookieString length] - 2]; 344 NSUInteger cookieStringBytes = [cookieString lengthOfBytesUsingEncoding:NSUTF8StringEncoding]; 345 if (cookieStringBytes > 3999) { 346 [mutableRequest setValue:cookieString forHTTPHeaderField:@"Cookie"]; 347 348 } 349 } 350 351 NSString *userAgentToken = [[self class] userAgentToken]; 352 if ([userAgentToken length]) { 353 // use addValue:forHTTPHeaderField: instead of setValue:forHTTPHeaderField:. 354 // we want to append the userAgentToken to the existing user agent instead of 355 // replacing the existing user agent. 356 [mutableRequest addValue:userAgentToken forHTTPHeaderField:@"User-Agent"]; 357 } 358 359 self = [super initWithRequest:mutableRequest cachedResponse:cachedResponse client:client]; 360 if (self != nil) { 361 // All we do here is log the call. 362 [[self class] authenticatingHTTPProtocol:self logWithFormat:@"init for %@ from <%@ %p>", [request URL], [client class], client]; 363 } 364 return self; 365 } 366 367 - (void)dealloc 368 { 369 // can be called on any thread 370 [[self class] authenticatingHTTPProtocol:self logWithFormat:@"dealloc"]; 371 assert(self->_task == nil); // we should have cleared it by now 372 assert(self->_pendingChallenge == nil); // we should have cancelled it by now 373 assert(self->_pendingChallengeCompletionHandler == nil); // we should have cancelled it by now 374 } 375 376 - (void)startLoading 377 { 378 NSMutableURLRequest * recursiveRequest; 379 NSMutableArray * calculatedModes; 380 NSString * currentMode; 381 382 // At this point we kick off the process of loading the URL via NSURLSession. 383 // The thread that calls this method becomes the client thread. 384 385 assert(self.clientThread == nil); // you can't call -startLoading twice 386 assert(self.task == nil); 387 388 // Calculate our effective run loop modes. In some circumstances (yes I'm looking at 389 // you UIWebView!) we can be called from a non-standard thread which then runs a 390 // non-standard run loop mode waiting for the request to finish. We detect this 391 // non-standard mode and add it to the list of run loop modes we use when scheduling 392 // our callbacks. Exciting huh? 393 // 394 // For debugging purposes the non-standard mode is "WebCoreSynchronousLoaderRunLoopMode" 395 // but it's better not to hard-code that here. 396 397 assert(self.modes == nil); 398 calculatedModes = [NSMutableArray array]; 399 [calculatedModes addObject:NSDefaultRunLoopMode]; 400 currentMode = [[NSRunLoop currentRunLoop] currentMode]; 401 if ( (currentMode != nil) && ! [currentMode isEqual:NSDefaultRunLoopMode] ) { 402 [calculatedModes addObject:currentMode]; 403 } 404 self.modes = calculatedModes; 405 assert([self.modes count] > 0); 406 407 // Create new request that's a clone of the request we were initialised with, 408 // except that it has our 'recursive request flag' property set on it. 409 410 recursiveRequest = [[self request] mutableCopy]; 411 assert(recursiveRequest != nil); 412 413 [[self class] setProperty:@YES forKey:kJAHPRecursiveRequestFlagProperty inRequest:recursiveRequest]; 414 415 self.startTime = [NSDate timeIntervalSinceReferenceDate]; 416 if (currentMode == nil) { 417 [[self class] authenticatingHTTPProtocol:self logWithFormat:@"start %@", [recursiveRequest URL]]; 418 } else { 419 [[self class] authenticatingHTTPProtocol:self logWithFormat:@"start %@ (mode %@)", [recursiveRequest URL], currentMode]; 420 } 421 422 // Latch the thread we were called on, primarily for debugging purposes. 423 424 self.clientThread = [NSThread currentThread]; 425 426 // Once everything is ready to go, create a data task with the new request. 427 428 self.task = [[[self class] sharedDemux] dataTaskWithRequest:recursiveRequest delegate:self modes:self.modes]; 429 assert(self.task != nil); 430 431 [self.task resume]; 432 } 433 434 - (void)stopLoading 435 { 436 // The implementation just cancels the current load (if it's still running). 437 438 [[self class] authenticatingHTTPProtocol:self logWithFormat:@"stop (elapsed %.1f)", [NSDate timeIntervalSinceReferenceDate] - self.startTime]; 439 440 assert(self.clientThread != nil); // someone must have called -startLoading 441 442 // Check that we're being stopped on the same thread that we were started 443 // on. Without this invariant things are going to go badly (for example, 444 // run loop sources that got attached during -startLoading may not get 445 // detached here). 446 // 447 // I originally had code here to bounce over to the client thread but that 448 // actually gets complex when you consider run loop modes, so I've nixed it. 449 // Rather, I rely on our client calling us on the right thread, which is what 450 // the following assert is about. 451 452 assert([NSThread currentThread] == self.clientThread); 453 454 [self cancelPendingChallenge]; 455 if (self.task != nil) { 456 [self.task cancel]; 457 self.task = nil; 458 // The following ends up calling -URLSession:task:didCompleteWithError: with NSURLErrorDomain / NSURLErrorCancelled, 459 // which specificallys traps and ignores the error. 460 } 461 // Don't nil out self.modes; see property declaration comments for a a discussion of this. 462 } 463 464 #pragma mark * Authentication challenge handling 465 466 /*! Performs the block on the specified thread in one of specified modes. 467 * \param thread The thread to target; nil implies the main thread. 468 * \param modes The modes to target; nil or an empty array gets you the default run loop mode. 469 * \param block The block to run. 470 */ 471 472 - (void)performOnThread:(NSThread *)thread modes:(NSArray *)modes block:(dispatch_block_t)block 473 { 474 // thread may be nil 475 // modes may be nil 476 assert(block != nil); 477 478 if (thread == nil) { 479 thread = [NSThread mainThread]; 480 } 481 if ([modes count] == 0) { 482 modes = @[ NSDefaultRunLoopMode ]; 483 } 484 [self performSelector:@selector(onThreadPerformBlock:) onThread:thread withObject:[block copy] waitUntilDone:NO modes:modes]; 485 } 486 487 /*! A helper method used by -performOnThread:modes:block:. Runs in the specified context 488 * and simply calls the block. 489 * \param block The block to run. 490 */ 491 492 - (void)onThreadPerformBlock:(dispatch_block_t)block 493 { 494 assert(block != nil); 495 block(); 496 } 497 498 /*! Called by our NSURLSession delegate callback to pass the challenge to our delegate. 499 * \description This simply passes the challenge over to the main thread. 500 * We do this so that all accesses to pendingChallenge are done from the main thread, 501 * which avoids the need for extra synchronisation. 502 * 503 * By the time this runes, the NSURLSession delegate callback has already confirmed with 504 * the delegate that it wants the challenge. 505 * 506 * Note that we use the default run loop mode here, not the common modes. We don't want 507 * an authorisation dialog showing up on top of an active menu (-: 508 * 509 * Also, we implement our own 'perform block' infrastructure because Cocoa doesn't have 510 * one <rdar://problem/17232344> and CFRunLoopPerformBlock is inadequate for the 511 * return case (where we need to pass in an array of modes; CFRunLoopPerformBlock only takes 512 * one mode). 513 * \param challenge The authentication challenge to process; must not be nil. 514 * \param completionHandler The associated completion handler; must not be nil. 515 */ 516 517 - (void)didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(JAHPChallengeCompletionHandler)completionHandler 518 { 519 assert(challenge != nil); 520 assert(completionHandler != nil); 521 assert([NSThread currentThread] == self.clientThread); 522 523 [[self class] authenticatingHTTPProtocol:self logWithFormat:@"challenge %@ received", [[challenge protectionSpace] authenticationMethod]]; 524 525 [self performOnThread:nil modes:nil block:^{ 526 [self mainThreadDidReceiveAuthenticationChallenge:challenge completionHandler:completionHandler]; 527 }]; 528 } 529 530 /*! The main thread side of authentication challenge processing. 531 * \details If there's already a pending challenge, something has gone wrong and 532 * the routine simply cancels the new challenge. If our delegate doesn't implement 533 * the -authenticatingHTTPProtocol:canAuthenticateAgainstProtectionSpace: delegate callback, 534 * we also cancel the challenge. OTOH, if all goes well we simply call our delegate 535 * with the challenge. 536 * \param challenge The authentication challenge to process; must not be nil. 537 * \param completionHandler The associated completion handler; must not be nil. 538 */ 539 540 - (void)mainThreadDidReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(JAHPChallengeCompletionHandler)completionHandler 541 { 542 assert(challenge != nil); 543 assert(completionHandler != nil); 544 assert([NSThread isMainThread]); 545 546 if (self.pendingChallenge != nil) { 547 548 // Our delegate is not expecting a second authentication challenge before resolving the 549 // first. Likewise, NSURLSession shouldn't send us a second authentication challenge 550 // before we resolve the first. If this happens, assert, log, and cancel the challenge. 551 // 552 // Note that we have to cancel the challenge on the thread on which we received it, 553 // namely, the client thread. 554 555 [[self class] authenticatingHTTPProtocol:self logWithFormat:@"challenge %@ cancelled; other challenge pending", [[challenge protectionSpace] authenticationMethod]]; 556 assert(NO); 557 [self clientThreadCancelAuthenticationChallenge:challenge completionHandler:completionHandler]; 558 } else { 559 id<JAHPAuthenticatingHTTPProtocolDelegate> strongDelegate; 560 561 strongDelegate = [[self class] delegate]; 562 563 // Tell the delegate about it. It would be weird if the delegate didn't support this 564 // selector (it did return YES from -authenticatingHTTPProtocol:canAuthenticateAgainstProtectionSpace: 565 // after all), but if it doesn't then we just cancel the challenge ourselves (or the client 566 // thread, of course). 567 568 if ( ! [strongDelegate respondsToSelector:@selector(authenticatingHTTPProtocol:canAuthenticateAgainstProtectionSpace:)] ) { 569 [[self class] authenticatingHTTPProtocol:self logWithFormat:@"challenge %@ cancelled; no delegate method", [[challenge protectionSpace] authenticationMethod]]; 570 assert(NO); 571 [self clientThreadCancelAuthenticationChallenge:challenge completionHandler:completionHandler]; 572 } else { 573 574 // Remember that this challenge is in progress. 575 576 self.pendingChallenge = challenge; 577 self.pendingChallengeCompletionHandler = completionHandler; 578 579 // Pass the challenge to the delegate. 580 581 [[self class] authenticatingHTTPProtocol:self logWithFormat:@"challenge %@ passed to delegate", [[challenge protectionSpace] authenticationMethod]]; 582 self.pendingDidCancelAuthenticationChallengeHandler = [strongDelegate authenticatingHTTPProtocol:self didReceiveAuthenticationChallenge:self.pendingChallenge]; 583 } 584 } 585 } 586 587 /*! Cancels an authentication challenge that hasn't made it to the pending challenge state. 588 * \details This routine is called as part of various error cases in the challenge handling 589 * code. It cancels a challenge that, for some reason, we've failed to pass to our delegate. 590 * 591 * The routine is always called on the main thread but bounces over to the client thread to 592 * do the actual cancellation. 593 * \param challenge The authentication challenge to cancel; must not be nil. 594 * \param completionHandler The associated completion handler; must not be nil. 595 */ 596 597 - (void)clientThreadCancelAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(JAHPChallengeCompletionHandler)completionHandler 598 { 599 #pragma unused(challenge) 600 assert(challenge != nil); 601 assert(completionHandler != nil); 602 assert([NSThread isMainThread]); 603 604 [self performOnThread:self.clientThread modes:self.modes block:^{ 605 completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, nil); 606 }]; 607 } 608 609 /*! Cancels an authentication challenge that /has/ made to the pending challenge state. 610 * \details This routine is called by -stopLoading to cancel any challenge that might be 611 * pending when the load is cancelled. It's always called on the client thread but 612 * immediately bounces over to the main thread (because .pendingChallenge is a main 613 * thread only value). 614 */ 615 616 - (void)cancelPendingChallenge 617 { 618 assert([NSThread currentThread] == self.clientThread); 619 620 // Just pass the work off to the main thread. We do this so that all accesses 621 // to pendingChallenge are done from the main thread, which avoids the need for 622 // extra synchronisation. 623 624 [self performOnThread:nil modes:nil block:^{ 625 if (self.pendingChallenge == nil) { 626 // This is not only not unusual, it's actually very typical. It happens every time you shut down 627 // the connection. Ideally I'd like to not even call -mainThreadCancelPendingChallenge when 628 // there's no challenge outstanding, but the synchronisation issues are tricky. Rather than solve 629 // those, I'm just not going to log in this case. 630 // 631 // [[self class] authenticatingHTTPProtocol:self logWithFormat:@"challenge not cancelled; no challenge pending"]; 632 } else { 633 id<JAHPAuthenticatingHTTPProtocolDelegate> strongDelegate; 634 NSURLAuthenticationChallenge * challenge; 635 JAHPDidCancelAuthenticationChallengeHandler didCancelAuthenticationChallengeHandler; 636 637 strongDelegate = [[self class] delegate]; 638 639 challenge = self.pendingChallenge; 640 didCancelAuthenticationChallengeHandler = self.pendingDidCancelAuthenticationChallengeHandler; 641 self.pendingChallenge = nil; 642 self.pendingChallengeCompletionHandler = nil; 643 self.pendingDidCancelAuthenticationChallengeHandler = nil; 644 645 if ([strongDelegate respondsToSelector:@selector(authenticatingHTTPProtocol:didCancelAuthenticationChallenge:)]) { 646 [[self class] authenticatingHTTPProtocol:self logWithFormat:@"challenge %@ cancellation passed to delegate", [[challenge protectionSpace] authenticationMethod]]; 647 if (didCancelAuthenticationChallengeHandler) { 648 didCancelAuthenticationChallengeHandler(self, challenge); 649 } 650 [strongDelegate authenticatingHTTPProtocol:self didCancelAuthenticationChallenge:challenge]; 651 } else if (didCancelAuthenticationChallengeHandler) { 652 didCancelAuthenticationChallengeHandler(self, challenge); 653 } else { 654 [[self class] authenticatingHTTPProtocol:self logWithFormat:@"challenge %@ cancellation failed; no delegate method", [[challenge protectionSpace] authenticationMethod]]; 655 // If we managed to send a challenge to the client but can't cancel it, that's bad. 656 // There's nothing we can do at this point except log the problem. 657 assert(NO); 658 } 659 } 660 }]; 661 } 662 663 - (void)resolvePendingAuthenticationChallengeWithCredential:(NSURLCredential *)credential 664 { 665 // credential may be nil 666 assert([NSThread isMainThread]); 667 assert(self.clientThread != nil); 668 669 JAHPChallengeCompletionHandler completionHandler; 670 NSURLAuthenticationChallenge *challenge; 671 672 // We clear out our record of the pending challenge and then pass the real work 673 // over to the client thread (which ensures that the challenge is resolved on 674 // the same thread we received it on). 675 676 completionHandler = self.pendingChallengeCompletionHandler; 677 challenge = self.pendingChallenge; 678 self.pendingChallenge = nil; 679 self.pendingChallengeCompletionHandler = nil; 680 self.pendingDidCancelAuthenticationChallengeHandler = nil; 681 682 [self performOnThread:self.clientThread modes:self.modes block:^{ 683 if (credential == nil) { 684 [[self class] authenticatingHTTPProtocol:self logWithFormat:@"challenge %@ resolved without credential", [[challenge protectionSpace] authenticationMethod]]; 685 completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, nil); 686 } else { 687 [[self class] authenticatingHTTPProtocol:self logWithFormat:@"challenge %@ resolved with <%@ %p>", [[challenge protectionSpace] authenticationMethod], [credential class], credential]; 688 completionHandler(NSURLSessionAuthChallengeUseCredential, credential); 689 } 690 }]; 691 } 692 693 - (void)cancelPendingAuthenticationChallenge { 694 assert([NSThread isMainThread]); 695 assert(self.clientThread != nil); 696 697 JAHPChallengeCompletionHandler completionHandler; 698 NSURLAuthenticationChallenge *challenge; 699 700 // We clear out our record of the pending challenge and then pass the real work 701 // over to the client thread (which ensures that the challenge is resolved on 702 // the same thread we received it on). 703 704 completionHandler = self.pendingChallengeCompletionHandler; 705 challenge = self.pendingChallenge; 706 self.pendingChallenge = nil; 707 self.pendingChallengeCompletionHandler = nil; 708 self.pendingDidCancelAuthenticationChallengeHandler = nil; 709 710 [self performOnThread:self.clientThread modes:self.modes block:^{ 711 [[self class] authenticatingHTTPProtocol:self logWithFormat:@"challenge %@ was canceled", [[challenge protectionSpace] authenticationMethod]]; 712 713 completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, nil); 714 }]; 715 } 716 717 718 #pragma mark * NSURLSession delegate callbacks 719 720 - (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task willPerformHTTPRedirection:(NSHTTPURLResponse *)response newRequest:(NSURLRequest *)newRequest completionHandler:(void (^)(NSURLRequest *))completionHandler 721 { 722 // rdar://21484589 723 // this is called from JAHPQNSURLSessionDemuxTaskInfo, 724 // which is called from the NSURLSession delegateQueue, 725 // which is a different thread than self.clientThread. 726 // It is possible that -stopLoading was called on self.clientThread 727 // just before this method if so, ignore this callback 728 if (!self.task) { return; } 729 730 NSMutableURLRequest * redirectRequest; 731 732 #pragma unused(session) 733 #pragma unused(task) 734 assert(task == self.task); 735 assert(response != nil); 736 assert(newRequest != nil); 737 #pragma unused(completionHandler) 738 assert(completionHandler != nil); 739 assert([NSThread currentThread] == self.clientThread); 740 741 [[self class] authenticatingHTTPProtocol:self logWithFormat:@"will redirect from %@ to %@", [response URL], [newRequest URL]]; 742 743 // The new request was copied from our old request, so it has our magic property. We actually 744 // have to remove that so that, when the client starts the new request, we see it. If we 745 // don't do this then we never see the new request and thus don't get a chance to change 746 // its caching behaviour. 747 // 748 // We also cancel our current connection because the client is going to start a new request for 749 // us anyway. 750 751 assert([[self class] propertyForKey:kJAHPRecursiveRequestFlagProperty inRequest:newRequest] != nil); 752 753 redirectRequest = [newRequest mutableCopy]; 754 [[self class] removePropertyForKey:kJAHPRecursiveRequestFlagProperty inRequest:redirectRequest]; 755 756 // Tell the client about the redirect. 757 758 [[self client] URLProtocol:self wasRedirectedToRequest:redirectRequest redirectResponse:response]; 759 760 // Stop our load. The CFNetwork infrastructure will create a new NSURLProtocol instance to run 761 // the load of the redirect. 762 763 // The following ends up calling -URLSession:task:didCompleteWithError: with NSURLErrorDomain / NSURLErrorCancelled, 764 // which specificallys traps and ignores the error. 765 766 [self.task cancel]; 767 768 [[self client] URLProtocol:self didFailWithError:[NSError errorWithDomain:NSCocoaErrorDomain code:NSUserCancelledError userInfo:nil]]; 769 } 770 771 - (void)URLSession:(NSURLSession *)session 772 task:(NSURLSessionTask *)task 773 didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge 774 completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, NSURLCredential *))completionHandler 775 { 776 // rdar://21484589 777 // this is called from JAHPQNSURLSessionDemuxTaskInfo, 778 // which is called from the NSURLSession delegateQueue, 779 // which is a different thread than self.clientThread. 780 // It is possible that -stopLoading was called on self.clientThread 781 // just before this method if so, ignore this callback 782 if (!self.task) { return; } 783 784 BOOL result; 785 id<JAHPAuthenticatingHTTPProtocolDelegate> strongDelegate; 786 787 #pragma unused(session) 788 #pragma unused(task) 789 assert(task == self.task); 790 assert(challenge != nil); 791 assert(completionHandler != nil); 792 assert([NSThread currentThread] == self.clientThread); 793 794 // Resolve NSURLAuthenticationMethodServerTrust ourselves 795 if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) { 796 // Delegate for handling certificate validation. 797 // Makes OCSP requests through local HTTP proxy. 798 OCSPAuthURLSessionDelegate *authHandler = [[AppDelegate sharedDelegate] authURLSessionDelegate]; 799 800 assert(challenge.protectionSpace.serverTrust != nil); 801 802 BOOL evaluateSuccess = 803 [authHandler evaluateTrust:challenge.protectionSpace.serverTrust 804 modifyOCSPURLOverride:nil 805 sessionOverride:sharedDemuxInstance.session 806 completionHandler:completionHandler]; 807 808 [[self class] authenticatingHTTPProtocol:self 809 logWithFormat:@"Evaluate trust for %@ %@", 810 challenge.protectionSpace.host, 811 evaluateSuccess ? @"succeeded": @"failed"]; 812 813 return; 814 } 815 816 // Ask our delegate whether it wants this challenge. We do this from this thread, not the main thread, 817 // to avoid the overload of bouncing to the main thread for challenges that aren't going to be customised 818 // anyway. 819 820 strongDelegate = [[self class] delegate]; 821 822 result = NO; 823 if ([strongDelegate respondsToSelector:@selector(authenticatingHTTPProtocol:canAuthenticateAgainstProtectionSpace:)]) { 824 result = [strongDelegate authenticatingHTTPProtocol:self canAuthenticateAgainstProtectionSpace:[challenge protectionSpace]]; 825 } 826 827 // If the client wants the challenge, kick off that process. If not, resolve it by doing the default thing. 828 829 if (result) { 830 [[self class] authenticatingHTTPProtocol:self logWithFormat:@"can authenticate %@", [[challenge protectionSpace] authenticationMethod]]; 831 832 [self didReceiveAuthenticationChallenge:challenge completionHandler:completionHandler]; 833 } else { 834 [[self class] authenticatingHTTPProtocol:self logWithFormat:@"cannot authenticate %@", [[challenge protectionSpace] authenticationMethod]]; 835 836 completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, nil); 837 } 838 } 839 840 - (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(NSURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition))completionHandler 841 { 842 // rdar://21484589 843 // this is called from JAHPQNSURLSessionDemuxTaskInfo, 844 // which is called from the NSURLSession delegateQueue, 845 // which is a different thread than self.clientThread. 846 // It is possible that -stopLoading was called on self.clientThread 847 // just before this method if so, ignore this callback 848 if (!self.task) { return; } 849 850 NSURLCacheStoragePolicy cacheStoragePolicy; 851 NSInteger statusCode; 852 853 #pragma unused(session) 854 #pragma unused(dataTask) 855 assert(dataTask == self.task); 856 assert(response != nil); 857 assert(completionHandler != nil); 858 assert([NSThread currentThread] == self.clientThread); 859 860 // Pass the call on to our client. The only tricky thing is that we have to decide on a 861 // cache storage policy, which is based on the actual request we issued, not the request 862 // we were given. 863 864 if ([response isKindOfClass:[NSHTTPURLResponse class]]) { 865 cacheStoragePolicy = JAHPCacheStoragePolicyForRequestAndResponse(self.task.originalRequest, (NSHTTPURLResponse *) response); 866 statusCode = [((NSHTTPURLResponse *) response) statusCode]; 867 } else { 868 assert(NO); 869 cacheStoragePolicy = NSURLCacheStorageNotAllowed; 870 statusCode = 42; 871 } 872 873 [[self class] authenticatingHTTPProtocol:self logWithFormat:@"received response %zd / %@ with cache storage policy %zu", (ssize_t) statusCode, [response URL], (size_t) cacheStoragePolicy]; 874 875 [[self client] URLProtocol:self didReceiveResponse:response cacheStoragePolicy:cacheStoragePolicy]; 876 877 completionHandler(NSURLSessionResponseAllow); 878 } 879 880 - (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data 881 { 882 // rdar://21484589 883 // this is called from JAHPQNSURLSessionDemuxTaskInfo, 884 // which is called from the NSURLSession delegateQueue, 885 // which is a different thread than self.clientThread. 886 // It is possible that -stopLoading was called on self.clientThread 887 // just before this method if so, ignore this callback 888 if (!self.task) { return; } 889 890 #pragma unused(session) 891 #pragma unused(dataTask) 892 assert(dataTask == self.task); 893 assert(data != nil); 894 assert([NSThread currentThread] == self.clientThread); 895 896 // Just pass the call on to our client. 897 898 [[self class] authenticatingHTTPProtocol:self logWithFormat:@"received %zu bytes of data", (size_t) [data length]]; 899 900 [[self client] URLProtocol:self didLoadData:data]; 901 } 902 903 - (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask willCacheResponse:(NSCachedURLResponse *)proposedResponse completionHandler:(void (^)(NSCachedURLResponse *))completionHandler 904 { 905 // rdar://21484589 906 // this is called from JAHPQNSURLSessionDemuxTaskInfo, 907 // which is called from the NSURLSession delegateQueue, 908 // which is a different thread than self.clientThread. 909 // It is possible that -stopLoading was called on self.clientThread 910 // just before this method if so, ignore this callback 911 if (!self.task) { return; } 912 913 #pragma unused(session) 914 #pragma unused(dataTask) 915 assert(dataTask == self.task); 916 assert(proposedResponse != nil); 917 assert(completionHandler != nil); 918 assert([NSThread currentThread] == self.clientThread); 919 920 // We implement this delegate callback purely for the purposes of logging. 921 922 [[self class] authenticatingHTTPProtocol:self logWithFormat:@"will cache response"]; 923 924 completionHandler(proposedResponse); 925 } 926 927 - (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error 928 // An NSURLSession delegate callback. We pass this on to the client. 929 { 930 #pragma unused(session) 931 #pragma unused(task) 932 assert( (self.task == nil) || (task == self.task) ); // can be nil in the 'cancel from -stopLoading' case 933 assert([NSThread currentThread] == self.clientThread); 934 935 // Just log and then, in most cases, pass the call on to our client. 936 937 if (error == nil) { 938 [[self class] authenticatingHTTPProtocol:self logWithFormat:@"success"]; 939 940 [[self client] URLProtocolDidFinishLoading:self]; 941 } else if ( [[error domain] isEqual:NSURLErrorDomain] && ([error code] == NSURLErrorCancelled) ) { 942 // Do nothing. This happens in two cases: 943 // 944 // o during a redirect, in which case the redirect code has already told the client about 945 // the failure 946 // 947 // o if the request is cancelled by a call to -stopLoading, in which case the client doesn't 948 // want to know about the failure 949 } else { 950 [[self class] authenticatingHTTPProtocol:self logWithFormat:@"error %@ / %d", [error domain], (int) [error code]]; 951 952 [[self client] URLProtocol:self didFailWithError:error]; 953 } 954 955 // We don't need to clean up the connection here; the system will call, or has already called, 956 // -stopLoading to do that. 957 } 958 959 @end 960 961 @implementation JAHPWeakDelegateHolder 962 963 @end