// // Example // man // // Created by man 11/11/2018. // Copyright © 2020 man. All rights reserved. // #import "_CustomHTTPProtocol.h" #import "_CanonicalRequest.h" #import "_CacheStoragePolicy.h" #import "_QNSURLSessionDemux.h" //liman #import "_Swizzling.h" #import "_NetworkHelper.h" #import "_HttpDatasource.h" #import "NSObject+CocoaDebug.h" // https://stackoverflow.com/questions/27604052/nsurlsessiontask-authentication-challenge-completionhandler-and-nsurlauthenticat @interface CPURLSessionChallengeSender : NSObject - (instancetype)initWithSessionCompletionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler; @end @implementation CPURLSessionChallengeSender { void (^_sessionCompletionHandler)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential); } - (instancetype)initWithSessionCompletionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler { self = [super init]; if (self) { _sessionCompletionHandler = [completionHandler copy]; } return self; } - (void)useCredential:(NSURLCredential *)credential forAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge { _sessionCompletionHandler(NSURLSessionAuthChallengeUseCredential, credential); } - (void)continueWithoutCredentialForAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge { _sessionCompletionHandler(NSURLSessionAuthChallengeUseCredential, nil); } - (void)cancelAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge; { _sessionCompletionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, nil); } - (void)performDefaultHandlingForAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge { _sessionCompletionHandler(NSURLSessionAuthChallengePerformDefaultHandling, nil); } - (void)rejectProtectionSpaceAndContinueWithChallenge:(NSURLAuthenticationChallenge *)challenge { _sessionCompletionHandler(NSURLSessionAuthChallengeRejectProtectionSpace, nil); } @end //liman typedef NSURLSessionConfiguration *(*SessionConfigConstructor)(id,SEL); static SessionConfigConstructor orig_defaultSessionConfiguration; static SessionConfigConstructor orig_ephemeralSessionConfiguration; static NSURLSessionConfiguration *replaced_defaultSessionConfiguration(id self, SEL _cmd) { NSURLSessionConfiguration *config = orig_defaultSessionConfiguration(self,_cmd); if ([config respondsToSelector:@selector(protocolClasses)] && [config respondsToSelector:@selector(setProtocolClasses:)]) { NSMutableArray *urlProtocolClasses = [NSMutableArray arrayWithArray:config.protocolClasses]; Class protoCls = _CustomHTTPProtocol.class; if (![urlProtocolClasses containsObject:protoCls]) { [urlProtocolClasses insertObject:protoCls atIndex:0]; } config.protocolClasses = urlProtocolClasses; } return config; } static NSURLSessionConfiguration *replaced_ephemeralSessionConfiguration(id self, SEL _cmd) { NSURLSessionConfiguration *config = orig_ephemeralSessionConfiguration(self,_cmd); if ([config respondsToSelector:@selector(protocolClasses)] && [config respondsToSelector:@selector(setProtocolClasses:)]) { NSMutableArray *urlProtocolClasses = [NSMutableArray arrayWithArray:config.protocolClasses]; Class protoCls = _CustomHTTPProtocol.class; if (![urlProtocolClasses containsObject:protoCls]) { [urlProtocolClasses insertObject:protoCls atIndex:0]; } config.protocolClasses = urlProtocolClasses; } return config; } // I use the following typedef to keep myself sane in the face of the wacky // Objective-C block syntax. typedef void (^_ChallengeCompletionHandler)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential * credential); @interface _CustomHTTPProtocol () @property (atomic, strong, readwrite) NSThread * clientThread; ///< The thread on which we should call the client. /*! The run loop modes in which to call the client. * \details The concurrency control here is complex. It's set up on the client * thread in -startLoading and then never modified. It is, however, read by code * running on other threads (specifically the main thread), so we deallocate it in * -dealloc rather than in -stopLoading. We can be sure that it's not read before * it's set up because the main thread code that reads it can only be called after * -startLoading has started the connection running. */ @property (atomic, copy, readwrite) NSArray * modes; @property (atomic, assign, readwrite) NSTimeInterval startTime; ///< The start time of the request; written by client thread only; read by any thread. @property (atomic, strong, readwrite) NSURLSessionDataTask * task; ///< The NSURLSession task for that request; client thread only. @property (atomic, strong, readwrite) NSURLAuthenticationChallenge * pendingChallenge; @property (atomic, copy, readwrite) _ChallengeCompletionHandler pendingChallengeCompletionHandler; ///< The completion handler that matches pendingChallenge; main thread only. //liman @property (atomic, strong) NSURLResponse *response; @property (atomic, strong) NSMutableData *data; @property (atomic, strong) NSError *error; @end @implementation _CustomHTTPProtocol #pragma mark * Subclass specific additions /*! The backing store for the class delegate. This is protected by @synchronized on the class. */ static id<_CustomHTTPProtocolDelegate> sDelegate; + (void)start { [NSURLProtocol registerClass:self]; } //liman + (void)stop { [NSURLProtocol unregisterClass:self]; } + (id<_CustomHTTPProtocolDelegate>)delegate { id<_CustomHTTPProtocolDelegate> result; @synchronized (self) { result = sDelegate; } return result; } + (void)setDelegate:(id<_CustomHTTPProtocolDelegate>)newValue { @synchronized (self) { sDelegate = newValue; } } /*! Returns the session demux object used by all the protocol instances. * \details This object allows us to have a single NSURLSession, with a session delegate, * and have its delegate callbacks routed to the correct protocol instance on the correct * thread in the correct modes. Can be called on any thread. */ + (_QNSURLSessionDemux *)sharedDemux { static dispatch_once_t sOnceToken; static _QNSURLSessionDemux * sDemux; dispatch_once(&sOnceToken, ^{ NSURLSessionConfiguration * config; config = [NSURLSessionConfiguration defaultSessionConfiguration]; // You have to explicitly configure the session to use your own protocol subclass here // otherwise you don't see redirects . config.protocolClasses = @[ self ]; sDemux = [[_QNSURLSessionDemux alloc] initWithConfiguration:config]; }); return sDemux; } /*! Called by by both class code and instance code to log various bits of information. * Can be called on any thread. * \param protocol The protocol instance; nil if it's the class doing the logging. * \param format A standard NSString-style format string; will not be nil. */ #pragma mark * NSURLProtocol overrides /*! Used to mark our recursive requests so that we don't try to handle them (and thereby * suffer an infinite recursive death). */ static NSString * kOurRecursiveRequestFlagProperty = @"com.apple.dts.CustomHTTPProtocol"; //liman //+ (BOOL)canInitWithRequest:(NSURLRequest *)request //{ // BOOL shouldAccept; // NSURL * url; // NSString * scheme; // // // Check the basics. This routine is extremely defensive because experience has shown that // // it can be called with some very odd requests . // // shouldAccept = (request != nil); // if (shouldAccept) { // url = [request URL]; // shouldAccept = (url != nil); // } // if ( ! shouldAccept ) { // [self customHTTPProtocol:nil logWithFormat:@"decline request (malformed)"]; // } // // // Decline our recursive requests. // // if (shouldAccept) { // shouldAccept = ([self propertyForKey:kOurRecursiveRequestFlagProperty inRequest:request] == nil); // if ( ! shouldAccept ) { // [self customHTTPProtocol:nil logWithFormat:@"decline request %@ (recursive)", url]; // } // } // // // Get the scheme. // // if (shouldAccept) { // scheme = [[url scheme] lowercaseString]; // shouldAccept = (scheme != nil); // // if ( ! shouldAccept ) { // [self customHTTPProtocol:nil logWithFormat:@"decline request %@ (no scheme)", url]; // } // } // // // Look for "http" or "https". // // // // Flip either or both of the following to YESes to control which schemes go through this custom // // NSURLProtocol subclass. // // if (shouldAccept) { // shouldAccept = /* DISABLES CODE */ (NO) && [scheme isEqual:@"http"]; // if ( ! shouldAccept ) { // shouldAccept = YES && [scheme isEqual:@"https"]; // } // // if ( ! shouldAccept ) { // [self customHTTPProtocol:nil logWithFormat:@"decline request %@ (scheme mismatch)", url]; // } else { // [self customHTTPProtocol:nil logWithFormat:@"accept request %@", url]; // } // } // // return shouldAccept; //} //liman + (BOOL)canInitWithRequest:(NSURLRequest *)request { if (![request.URL.scheme isEqualToString:@"http"] && ![request.URL.scheme isEqualToString:@"https"]) { return NO; } if ([NSURLProtocol propertyForKey:kOurRecursiveRequestFlagProperty inRequest:request] ) { return NO; } if ([[_NetworkHelper shared] onlyURLs].count > 0) { NSString* url = [request.URL.absoluteString lowercaseString]; for (NSString* _url in [_NetworkHelper shared].onlyURLs) { if ([url rangeOfString:[_url lowercaseString]].location != NSNotFound) return YES; } return NO; } return YES; } + (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request { NSURLRequest * result; //assert(request != nil); // can be called on any thread // Canonicalising a request is quite complex, so all the heavy lifting has // been shuffled off to a separate module. result = CanonicalRequestForRequest(request); return result; } - (id)initWithRequest:(NSURLRequest *)request cachedResponse:(NSCachedURLResponse *)cachedResponse client:(id )client { //assert(request != nil); // cachedResponse may be nil //assert(client != nil); // can be called on any thread self = [super initWithRequest:request cachedResponse:cachedResponse client:client]; return self; } - (void)dealloc { // can be called on any thread //assert(self->_task == nil); // we should have cleared it by now //assert(self->_pendingChallenge == nil); // we should have cancelled it by now //assert(self->_pendingChallengeCompletionHandler == nil); // we should have cancelled it by now } - (void)startLoading { NSMutableURLRequest * recursiveRequest; NSMutableArray * calculatedModes; NSString * currentMode; // At this point we kick off the process of loading the URL via NSURLSession. // The thread that calls this method becomes the client thread. //assert(self.clientThread == nil); // you can't call -startLoading twice //assert(self.task == nil); // Calculate our effective run loop modes. In some circumstances (yes I'm looking at // you UIWebView!) we can be called from a non-standard thread which then runs a // non-standard run loop mode waiting for the request to finish. We detect this // non-standard mode and add it to the list of run loop modes we use when scheduling // our callbacks. Exciting huh? // // For debugging purposes the non-standard mode is "WebCoreSynchronousLoaderRunLoopMode" // but it's better not to hard-code that here. //assert(self.modes == nil); calculatedModes = [NSMutableArray array]; [calculatedModes addObject:NSDefaultRunLoopMode]; currentMode = [[NSRunLoop currentRunLoop] currentMode]; if ( (currentMode != nil) && ! [currentMode isEqual:NSDefaultRunLoopMode] ) { [calculatedModes addObject:currentMode]; } self.modes = calculatedModes; //assert([self.modes count] > 0); // Create new request that's a clone of the request we were initialised with, // except that it has our 'recursive request flag' property set on it. recursiveRequest = [[self request] mutableCopy]; //assert(recursiveRequest != nil); [[self class] setProperty:@YES forKey:kOurRecursiveRequestFlagProperty inRequest:recursiveRequest]; //liman self.startTime = [[NSDate date] timeIntervalSince1970]; self.data = [NSMutableData data]; // Latch the thread we were called on, primarily for debugging purposes. self.clientThread = [NSThread currentThread]; // Once everything is ready to go, create a data task with the new request. self.task = [[[self class] sharedDemux] dataTaskWithRequest:recursiveRequest delegate:self modes:self.modes]; //assert(self.task != nil); [self.task resume]; } - (void)stopLoading { // The implementation just cancels the current load (if it's still running). //assert(self.clientThread != nil); // someone must have called -startLoading // Check that we're being stopped on the same thread that we were started // on. Without this invariant things are going to go badly (for example, // run loop sources that got attached during -startLoading may not get // detached here). // // I originally had code here to bounce over to the client thread but that // actually gets complex when you consider run loop modes, so I've nixed it. // Rather, I rely on our client calling us on the right thread, which is what // the following //assert is about. //assert([NSThread currentThread] == self.clientThread); [self cancelPendingChallenge]; if (self.task != nil) { [self.task cancel]; self.task = nil; // The following ends up calling -URLSession:task:didCompleteWithError: with NSURLErrorDomain / NSURLErrorCancelled, // which specificallys traps and ignores the error. } // Don't nil out self.modes; see property declaration comments for a a discussion of this. //liman if (![_NetworkHelper shared].isNetworkEnable) { return; } _HttpModel* model = [[_HttpModel alloc] init]; model.url = self.request.URL; model.method = self.request.HTTPMethod; model.mineType = self.response.MIMEType; if (self.request.HTTPBody) { model.requestData = self.request.HTTPBody; } if (self.request.HTTPBodyStream) {//liman NSData* data = [NSData dataWithInputStream:self.request.HTTPBodyStream]; model.requestData = data; } NSHTTPURLResponse* httpResponse = (NSHTTPURLResponse*)self.response; model.statusCode = [NSString stringWithFormat:@"%d",(int)httpResponse.statusCode]; model.responseData = self.data; model.size = [[NSByteCountFormatter new] stringFromByteCount:self.data.length]; model.isImage = [self.response.MIMEType rangeOfString:@"image"].location != NSNotFound; //时间 NSTimeInterval startTimeDouble = self.startTime; NSTimeInterval endTimeDouble = [[NSDate date] timeIntervalSince1970]; NSTimeInterval durationDouble = fabs(endTimeDouble - startTimeDouble); model.startTime = [NSString stringWithFormat:@"%f", startTimeDouble]; model.endTime = [NSString stringWithFormat:@"%f", endTimeDouble]; model.totalDuration = [NSString stringWithFormat:@"%f (s)", durationDouble]; model.errorDescription = self.error.description; model.errorLocalizedDescription = self.error.localizedDescription; model.requestHeaderFields = self.request.allHTTPHeaderFields; if ([self.response isKindOfClass:[NSHTTPURLResponse class]]) { model.responseHeaderFields = ((NSHTTPURLResponse *)self.response).allHeaderFields; } if (self.response.MIMEType == nil) { model.isImage = NO; } if ([model.url.absoluteString length] > 4) { NSString *str = [model.url.absoluteString substringFromIndex: [model.url.absoluteString length] - 4]; if ([str isEqualToString:@".png"] || [str isEqualToString:@".PNG"] || [str isEqualToString:@".jpg"] || [str isEqualToString:@".JPG"] || [str isEqualToString:@".gif"] || [str isEqualToString:@".GIF"]) { model.isImage = YES; } } if ([model.url.absoluteString length] > 5) { NSString *str = [model.url.absoluteString substringFromIndex: [model.url.absoluteString length] - 5]; if ([str isEqualToString:@".jpeg"] || [str isEqualToString:@".JPEG"]) { model.isImage = YES; } } //Handling errors 404... model = [self handleError:self.error model:model]; if ([[_HttpDatasource shared] addHttpRequset:model]) { [[NSNotificationCenter defaultCenter] postNotificationName:@"reloadHttp_CocoaDebug" object:nil userInfo:@{@"statusCode":model.statusCode}]; } } #pragma mark * Authentication challenge handling /*! Performs the block on the specified thread in one of specified modes. * \param thread The thread to target; nil implies the main thread. * \param modes The modes to target; nil or an empty array gets you the default run loop mode. * \param block The block to run. */ - (void)performOnThread:(NSThread *)thread modes:(NSArray *)modes block:(dispatch_block_t)block { // thread may be nil // modes may be nil //assert(block != nil); if (thread == nil) { thread = [NSThread mainThread]; } if ([modes count] == 0) { modes = @[ NSDefaultRunLoopMode ]; } [self performSelector:@selector(onThreadPerformBlock:) onThread:thread withObject:[block copy] waitUntilDone:NO modes:modes]; } /*! A helper method used by -performOnThread:modes:block:. Runs in the specified context * and simply calls the block. * \param block The block to run. */ - (void)onThreadPerformBlock:(dispatch_block_t)block { //assert(block != nil); block(); } /*! Called by our NSURLSession delegate callback to pass the challenge to our delegate. * \description This simply passes the challenge over to the main thread. * We do this so that all accesses to pendingChallenge are done from the main thread, * which avoids the need for extra synchronisation. * * By the time this runes, the NSURLSession delegate callback has already confirmed with * the delegate that it wants the challenge. * * Note that we use the default run loop mode here, not the common modes. We don't want * an authorisation dialog showing up on top of an active menu (-: * * Also, we implement our own 'perform block' infrastructure because Cocoa doesn't have * one and CFRunLoopPerformBlock is inadequate for the * return case (where we need to pass in an array of modes; CFRunLoopPerformBlock only takes * one mode). * \param challenge The authentication challenge to process; must not be nil. * \param completionHandler The associated completion handler; must not be nil. */ - (void)didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(_ChallengeCompletionHandler)completionHandler { //assert(challenge != nil); //assert(completionHandler != nil); //assert([NSThread currentThread] == self.clientThread); [self performOnThread:nil modes:nil block:^{ [self mainThreadDidReceiveAuthenticationChallenge:challenge completionHandler:completionHandler]; }]; } /*! The main thread side of authentication challenge processing. * \details If there's already a pending challenge, something has gone wrong and * the routine simply cancels the new challenge. If our delegate doesn't implement * the -customHTTPProtocol:canAuthenticateAgainstProtectionSpace: delegate callback, * we also cancel the challenge. OTOH, if all goes well we simply call our delegate * with the challenge. * \param challenge The authentication challenge to process; must not be nil. * \param completionHandler The associated completion handler; must not be nil. */ - (void)mainThreadDidReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(_ChallengeCompletionHandler)completionHandler { //assert(challenge != nil); //assert(completionHandler != nil); //assert([NSThread isMainThread]); if (self.pendingChallenge != nil) { // Our delegate is not expecting a second authentication challenge before resolving the // first. Likewise, NSURLSession shouldn't send us a second authentication challenge // before we resolve the first. If this happens, //assert, log, and cancel the challenge. // // Note that we have to cancel the challenge on the thread on which we received it, // namely, the client thread. //assert(NO); [self clientThreadCancelAuthenticationChallenge:challenge completionHandler:completionHandler]; } else { id<_CustomHTTPProtocolDelegate> strongDelegate; strongDelegate = [[self class] delegate]; // Tell the delegate about it. It would be weird if the delegate didn't support this // selector (it did return YES from -customHTTPProtocol:canAuthenticateAgainstProtectionSpace: // after all), but if it doesn't then we just cancel the challenge ourselves (or the client // thread, of course). if ( ! [strongDelegate respondsToSelector:@selector(customHTTPProtocol:canAuthenticateAgainstProtectionSpace:)] ) { //assert(NO); [self clientThreadCancelAuthenticationChallenge:challenge completionHandler:completionHandler]; } else { // Remember that this challenge is in progress. self.pendingChallenge = challenge; self.pendingChallengeCompletionHandler = completionHandler; // Pass the challenge to the delegate. [strongDelegate customHTTPProtocol:self didReceiveAuthenticationChallenge:self.pendingChallenge]; } } } /*! Cancels an authentication challenge that hasn't made it to the pending challenge state. * \details This routine is called as part of various error cases in the challenge handling * code. It cancels a challenge that, for some reason, we've failed to pass to our delegate. * * The routine is always called on the main thread but bounces over to the client thread to * do the actual cancellation. * \param challenge The authentication challenge to cancel; must not be nil. * \param completionHandler The associated completion handler; must not be nil. */ - (void)clientThreadCancelAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(_ChallengeCompletionHandler)completionHandler { #pragma unused(challenge) //assert(challenge != nil); //assert(completionHandler != nil); //assert([NSThread isMainThread]); [self performOnThread:self.clientThread modes:self.modes block:^{ completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, nil); }]; } /*! Cancels an authentication challenge that /has/ made to the pending challenge state. * \details This routine is called by -stopLoading to cancel any challenge that might be * pending when the load is cancelled. It's always called on the client thread but * immediately bounces over to the main thread (because .pendingChallenge is a main * thread only value). */ - (void)cancelPendingChallenge { //assert([NSThread currentThread] == self.clientThread); // Just pass the work off to the main thread. We do this so that all accesses // to pendingChallenge are done from the main thread, which avoids the need for // extra synchronisation. [self performOnThread:nil modes:nil block:^{ if (self.pendingChallenge == nil) { // This is not only not unusual, it's actually very typical. It happens every time you shut down // the connection. Ideally I'd like to not even call -mainThreadCancelPendingChallenge when // there's no challenge outstanding, but the synchronisation issues are tricky. Rather than solve // those, I'm just not going to log in this case. // // [[self class] customHTTPProtocol:self logWithFormat:@"challenge not cancelled; no challenge pending"]; } else { id<_CustomHTTPProtocolDelegate> strongeDelegate; NSURLAuthenticationChallenge * challenge; strongeDelegate = [[self class] delegate]; challenge = self.pendingChallenge; self.pendingChallenge = nil; self.pendingChallengeCompletionHandler = nil; if ([strongeDelegate respondsToSelector:@selector(customHTTPProtocol:didCancelAuthenticationChallenge:)]) { [strongeDelegate customHTTPProtocol:self didCancelAuthenticationChallenge:challenge]; } else { // If we managed to send a challenge to the client but can't cancel it, that's bad. // There's nothing we can do at this point except log the problem. //assert(NO); } } }]; } - (void)resolveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge withCredential:(NSURLCredential *)credential { //assert(challenge == self.pendingChallenge); // credential may be nil //assert([NSThread isMainThread]); //assert(self.clientThread != nil); if (challenge != self.pendingChallenge) { // This should never happen, and we want to know if it does, at least in the debug build. //assert(NO); } else { _ChallengeCompletionHandler completionHandler; // We clear out our record of the pending challenge and then pass the real work // over to the client thread (which ensures that the challenge is resolved on // the same thread we received it on). completionHandler = self.pendingChallengeCompletionHandler; self.pendingChallenge = nil; self.pendingChallengeCompletionHandler = nil; [self performOnThread:self.clientThread modes:self.modes block:^{ if (credential == nil) { completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, nil); } else { completionHandler(NSURLSessionAuthChallengeUseCredential, credential); } }]; } } #pragma mark * NSURLSession delegate callbacks //liman //- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task willPerformHTTPRedirection:(NSHTTPURLResponse *)response newRequest:(NSURLRequest *)newRequest completionHandler:(void (^)(NSURLRequest *))completionHandler //{ // NSMutableURLRequest * redirectRequest; // // #pragma unused(session) // #pragma unused(task) // //assert(task == self.task); // //assert(response != nil); // //assert(newRequest != nil); // #pragma unused(completionHandler) // //assert(completionHandler != nil); // //assert([NSThread currentThread] == self.clientThread); // // // // The new request was copied from our old request, so it has our magic property. We actually // // have to remove that so that, when the client starts the new request, we see it. If we // // don't do this then we never see the new request and thus don't get a chance to change // // its caching behaviour. // // // // We also cancel our current connection because the client is going to start a new request for // // us anyway. // // //assert([[self class] propertyForKey:kOurRecursiveRequestFlagProperty inRequest:newRequest] != nil); // // redirectRequest = [newRequest mutableCopy]; // [[self class] removePropertyForKey:kOurRecursiveRequestFlagProperty inRequest:redirectRequest]; // // // Tell the client about the redirect. // // [[self client] URLProtocol:self wasRedirectedToRequest:redirectRequest redirectResponse:response]; // // // Stop our load. The CFNetwork infrastructure will create a new NSURLProtocol instance to run // // the load of the redirect. // // // The following ends up calling -URLSession:task:didCompleteWithError: with NSURLErrorDomain / NSURLErrorCancelled, // // which specificallys traps and ignores the error. // // [self.task cancel]; // // [[self client] URLProtocol:self didFailWithError:[NSError errorWithDomain:NSCocoaErrorDomain code:NSUserCancelledError userInfo:nil]]; //} //liman - (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task willPerformHTTPRedirection:(NSHTTPURLResponse *)response newRequest:(NSURLRequest *)request completionHandler:(void (^)(NSURLRequest * _Nullable))completionHandler { //Redirect: code >=300 && < 400 if (response && [response isKindOfClass:[NSHTTPURLResponse class]]) { NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response; NSInteger status = httpResponse.statusCode; if (status >= 300 && status < 400) { [[self client] URLProtocol:self wasRedirectedToRequest:request redirectResponse:response]; //Remember to set to nil, otherwise the normal request will be requested twice request = nil; } } completionHandler(request); } - (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, NSURLCredential *))completionHandler { BOOL result; id<_CustomHTTPProtocolDelegate> strongeDelegate; #pragma unused(session) #pragma unused(task) //assert(task == self.task); //assert(challenge != nil); //assert(completionHandler != nil); //assert([NSThread currentThread] == self.clientThread); // Ask our delegate whether it wants this challenge. We do this from this thread, not the main thread, // to avoid the overload of bouncing to the main thread for challenges that aren't going to be customised // anyway. strongeDelegate = [[self class] delegate]; result = NO; if ([strongeDelegate respondsToSelector:@selector(customHTTPProtocol:canAuthenticateAgainstProtectionSpace:)]) { result = [strongeDelegate customHTTPProtocol:self canAuthenticateAgainstProtectionSpace:[challenge protectionSpace]]; } // If the client wants the challenge, kick off that process. If not, resolve it by doing the default thing. if (result) { [self didReceiveAuthenticationChallenge:challenge completionHandler:completionHandler]; } else { // completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, nil); // Callback the original method NSURLAuthenticationChallenge* challengeWrapper = [[NSURLAuthenticationChallenge alloc] initWithAuthenticationChallenge:challenge sender:[[CPURLSessionChallengeSender alloc] initWithSessionCompletionHandler:completionHandler]]; [self.client URLProtocol:self didReceiveAuthenticationChallenge:challengeWrapper]; } } - (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(NSURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition))completionHandler { NSURLCacheStoragePolicy cacheStoragePolicy; NSInteger statusCode; #pragma unused(session) #pragma unused(dataTask) //assert(dataTask == self.task); //assert(response != nil); //assert(completionHandler != nil); //assert([NSThread currentThread] == self.clientThread); // Pass the call on to our client. The only tricky thing is that we have to decide on a // cache storage policy, which is based on the actual request we issued, not the request // we were given. if ([response isKindOfClass:[NSHTTPURLResponse class]]) { cacheStoragePolicy = CacheStoragePolicyForRequestAndResponse(self.task.originalRequest, (NSHTTPURLResponse *) response); statusCode = [((NSHTTPURLResponse *) response) statusCode]; } else { //assert(NO); cacheStoragePolicy = NSURLCacheStorageNotAllowed; statusCode = 42; } [[self client] URLProtocol:self didReceiveResponse:response cacheStoragePolicy:cacheStoragePolicy]; self.response = response;//liman completionHandler(NSURLSessionResponseAllow); } - (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data { #pragma unused(session) #pragma unused(dataTask) //assert(dataTask == self.task); //assert(data != nil); //assert([NSThread currentThread] == self.clientThread); // Just pass the call on to our client. [[self client] URLProtocol:self didLoadData:data]; [self.data appendData:data];//liman } - (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask willCacheResponse:(NSCachedURLResponse *)proposedResponse completionHandler:(void (^)(NSCachedURLResponse *))completionHandler { #pragma unused(session) #pragma unused(dataTask) //assert(dataTask == self.task); //assert(proposedResponse != nil); //assert(completionHandler != nil); //assert([NSThread currentThread] == self.clientThread); // We implement this delegate callback purely for the purposes of logging. completionHandler(proposedResponse); } //liman //- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error // // An NSURLSession delegate callback. We pass this on to the client. //{ // #pragma unused(session) // #pragma unused(task) // //assert( (self.task == nil) || (task == self.task) ); // can be nil in the 'cancel from -stopLoading' case // //assert([NSThread currentThread] == self.clientThread); // // // Just log and then, in most cases, pass the call on to our client. // // if (error == nil) { // // [[self client] URLProtocolDidFinishLoading:self]; // } else if ( [[error domain] isEqual:NSURLErrorDomain] && ([error code] == NSURLErrorCancelled) ) { // // Do nothing. This happens in two cases: // // // // o during a redirect, in which case the redirect code has already told the client about // // the failure // // // // o if the request is cancelled by a call to -stopLoading, in which case the client doesn't // // want to know about the failure // } else { // // [[self client] URLProtocol:self didFailWithError:error]; // } // // // We don't need to clean up the connection here; the system will call, or has already called, // // -stopLoading to do that. //} //liman - (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error { if (error) { [[self client] URLProtocol:self didFailWithError:error]; self.error = error; } else { [[self client] URLProtocolDidFinishLoading:self]; } } #pragma mark - //liman + (void)load { if (![[NSUserDefaults standardUserDefaults] boolForKey:@"disableNetworkMonitoring_CocoaDebug"]) { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ orig_defaultSessionConfiguration = (SessionConfigConstructor)replaceMethod(@selector(defaultSessionConfiguration), (IMP)replaced_defaultSessionConfiguration, [NSURLSessionConfiguration class], YES); orig_ephemeralSessionConfiguration = (SessionConfigConstructor)replaceMethod(@selector(ephemeralSessionConfiguration), (IMP)replaced_ephemeralSessionConfiguration, [NSURLSessionConfiguration class], YES); }); } } //Handling errors 404... - (_HttpModel *)handleError:(NSError *)error model:(_HttpModel *)model { if (!error) { //https://httpcodes.co/status/ switch (model.statusCode.integerValue) { case 100: model.errorDescription = @"Informational :\nClient should continue with request"; model.errorLocalizedDescription = @"Continue"; break; case 101: model.errorDescription = @"Informational :\nServer is switching protocols"; model.errorLocalizedDescription = @"Switching Protocols"; break; case 102: model.errorDescription = @"Informational :\nServer has received and is processing the request"; model.errorLocalizedDescription = @"Processing"; break; case 103: model.errorDescription = @"Informational :\nresume aborted PUT or POST requests"; model.errorLocalizedDescription = @"Checkpoint"; break; case 122: model.errorDescription = @"Informational :\nURI is longer than a maximum of 2083 characters"; model.errorLocalizedDescription = @"Request-URI too long"; break; case 300: model.errorDescription = @"Redirection :\nMultiple options for the resource delivered"; model.errorLocalizedDescription = @"Multiple Choices"; break; case 301: model.errorDescription = @"Redirection :\nThis and all future requests directed to the given URI"; model.errorLocalizedDescription = @"Moved Permanently"; break; case 302: model.errorDescription = @"Redirection :\nTemporary response to request found via alternative URI"; model.errorLocalizedDescription = @"Found"; break; case 303: model.errorDescription = @"Redirection :\nPermanent response to request found via alternative URI"; model.errorLocalizedDescription = @"See Other"; break; case 304: model.errorDescription = @"Redirection :\nResource has not been modified since last requested"; model.errorLocalizedDescription = @"Not Modified"; break; case 305: model.errorDescription = @"Redirection :\nContent located elsewhere, retrieve from there"; model.errorLocalizedDescription = @"Use Proxy"; break; case 306: model.errorDescription = @"Redirection :\nSubsequent requests should use the specified proxy"; model.errorLocalizedDescription = @"Switch Proxy"; break; case 307: model.errorDescription = @"Redirection :\nConnect again to different URI as provided"; model.errorLocalizedDescription = @"Temporary Redirect"; break; case 308: model.errorDescription = @"Redirection :\nConnect again to a different URI using the same method"; model.errorLocalizedDescription = @"Permanent Redirect"; break; case 400: model.errorDescription = @"Client Error :\nRequest cannot be fulfilled due to bad syntax"; model.errorLocalizedDescription = @"Bad Request"; break; case 401: model.errorDescription = @"Client Error :\nAuthentication is possible but has failed"; model.errorLocalizedDescription = @"Unauthorized"; break; case 402: model.errorDescription = @"Client Error :\nPayment required, reserved for future use"; model.errorLocalizedDescription = @"Payment Required"; break; case 403: model.errorDescription = @"Client Error :\nServer refuses to respond to request"; model.errorLocalizedDescription = @"Forbidden"; break; case 404: model.errorDescription = @"Client Error :\nRequested resource could not be found"; model.errorLocalizedDescription = @"Not Found"; break; case 405: model.errorDescription = @"Client Error :\nRequest method not supported by that resource"; model.errorLocalizedDescription = @"Method Not Allowed"; break; case 406: model.errorDescription = @"Client Error :\nContent not acceptable according to the Accept headers"; model.errorLocalizedDescription = @"Not Acceptable"; break; case 407: model.errorDescription = @"Client Error :\nClient must first authenticate itself with the proxy"; model.errorLocalizedDescription = @"Proxy Authentication Required"; break; case 408: model.errorDescription = @"Client Error :\nServer timed out waiting for the request"; model.errorLocalizedDescription = @"Request Timeout"; break; case 409: model.errorDescription = @"Client Error :\nRequest could not be processed because of conflict"; model.errorLocalizedDescription = @"Conflict"; break; case 410: model.errorDescription = @"Client Error :\nResource is no longer available and will not be available again"; model.errorLocalizedDescription = @"Gone"; break; case 411: model.errorDescription = @"Client Error :\nRequest did not specify the length of its content"; model.errorLocalizedDescription = @"Length Required"; break; case 412: model.errorDescription = @"Client Error :\nServer does not meet request preconditions"; model.errorLocalizedDescription = @"Precondition Failed"; break; case 413: model.errorDescription = @"Client Error :\nRequest is larger than the server is willing or able to process"; model.errorLocalizedDescription = @"Request Entity Too Large"; break; case 414: model.errorDescription = @"Client Error :\nURI provided was too long for the server to process"; model.errorLocalizedDescription = @"Request-URI Too Long"; break; case 415: model.errorDescription = @"Client Error :\nServer does not support media type"; model.errorLocalizedDescription = @"Unsupported Media Type"; break; case 416: model.errorDescription = @"Client Error :\nClient has asked for unprovidable portion of the file"; model.errorLocalizedDescription = @"Requested Range Not Satisfiable"; break; case 417: model.errorDescription = @"Client Error :\nServer cannot meet requirements of Expect request-header field"; model.errorLocalizedDescription = @"Expectation Failed"; break; case 418: model.errorDescription = @"Client Error :\nI'm a teapot"; model.errorLocalizedDescription = @"I'm a Teapot"; break; case 420: model.errorDescription = @"Client Error :\nTwitter rate limiting"; model.errorLocalizedDescription = @"Enhance Your Calm"; break; case 421: model.errorDescription = @"Client Error :\nMisdirected Request"; model.errorLocalizedDescription = @"Misdirected Request"; break; case 422: model.errorDescription = @"Client Error :\nRequest unable to be followed due to semantic errors"; model.errorLocalizedDescription = @"Unprocessable Entity"; break; case 423: model.errorDescription = @"Client Error :\nResource that is being accessed is locked"; model.errorLocalizedDescription = @"Locked"; break; case 424: model.errorDescription = @"Client Error :\nRequest failed due to failure of a previous request"; model.errorLocalizedDescription = @"Failed Dependency"; break; case 426: model.errorDescription = @"Client Error :\nClient should switch to a different protocol"; model.errorLocalizedDescription = @"Upgrade Required"; break; case 428: model.errorDescription = @"Client Error :\nOrigin server requires the request to be conditional"; model.errorLocalizedDescription = @"Precondition Required"; break; case 429: model.errorDescription = @"Client Error :\nUser has sent too many requests in a given amount of time"; model.errorLocalizedDescription = @"Too Many Requests"; break; case 431: model.errorDescription = @"Client Error :\nServer is unwilling to process the request"; model.errorLocalizedDescription = @"Request Header Fields Too Large"; break; case 444: model.errorDescription = @"Client Error :\nServer returns no information and closes the connection"; model.errorLocalizedDescription = @"No Response"; break; case 449: model.errorDescription = @"Client Error :\nRequest should be retried after performing action"; model.errorLocalizedDescription = @"Retry With"; break; case 450: model.errorDescription = @"Client Error :\nWindows Parental Controls blocking access to webpage"; model.errorLocalizedDescription = @"Blocked by Windows Parental Controls"; break; case 451: model.errorDescription = @"Client Error :\nThe server cannot reach the client's mailbox"; model.errorLocalizedDescription = @"Wrong Exchange server"; break; case 499: model.errorDescription = @"Client Error :\nConnection closed by client while HTTP server is processing"; model.errorLocalizedDescription = @"Client Closed Request"; break; case 500: model.errorDescription = @"Server Error :\ngeneric error message"; model.errorLocalizedDescription = @"Internal Server Error"; break; case 501: model.errorDescription = @"Server Error :\nserver does not recognise method or lacks ability to fulfill"; model.errorLocalizedDescription = @"Not Implemented"; break; case 502: model.errorDescription = @"Server Error :\nserver received an invalid response from upstream server"; model.errorLocalizedDescription = @"Bad Gateway"; break; case 503: model.errorDescription = @"Server Error :\nserver is currently unavailable"; model.errorLocalizedDescription = @"Service Unavailable"; break; case 504: model.errorDescription = @"Server Error :\ngateway did not receive response from upstream server"; model.errorLocalizedDescription = @"Gateway Timeout"; break; case 505: model.errorDescription = @"Server Error :\nserver does not support the HTTP protocol version"; model.errorLocalizedDescription = @"HTTP Version Not Supported"; break; case 506: model.errorDescription = @"Server Error :\ncontent negotiation for the request results in a circular reference"; model.errorLocalizedDescription = @"Variant Also Negotiates"; break; case 507: model.errorDescription = @"Server Error :\nserver is unable to store the representation"; model.errorLocalizedDescription = @"Insufficient Storage"; break; case 508: model.errorDescription = @"Server Error :\nserver detected an infinite loop while processing the request"; model.errorLocalizedDescription = @"Loop Detected"; break; case 509: model.errorDescription = @"Server Error :\nbandwidth limit exceeded"; model.errorLocalizedDescription = @"Bandwidth Limit Exceeded"; break; case 510: model.errorDescription = @"Server Error :\nfurther extensions to the request are required"; model.errorLocalizedDescription = @"Not Extended"; break; case 511: model.errorDescription = @"Server Error :\nclient needs to authenticate to gain network access"; model.errorLocalizedDescription = @"Network Authentication Required"; break; case 526: model.errorDescription = @"Server Error :\nThe origin web server does not have a valid SSL certificate"; model.errorLocalizedDescription = @"Invalid SSL certificate"; break; case 598: model.errorDescription = @"Server Error :\nnetwork read timeout behind the proxy"; model.errorLocalizedDescription = @"Network Read Timeout Error"; break; case 599: model.errorDescription = @"Server Error :\nnetwork connect timeout behind the proxy"; model.errorLocalizedDescription = @"Network Connect Timeout Error"; break; default: break; } } return model; } @end