JAHPAuthenticatingHTTPProtocol.m 40 KB

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