JAHPAuthenticatingHTTPProtocol.m 41 KB

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