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