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
60typedef 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
97static JAHPWeakDelegateHolder* weakDelegateHolder;
98
99
100/*! A token to append to all HTTP user agent headers.
101 */
102static 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
156static 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
245static 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
773didReceiveChallenge:(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
964