This commit is contained in:
DDIsFriend
2023-08-23 09:24:40 +08:00
parent 6bd037c5dd
commit 63ca919ed5
494 changed files with 35308 additions and 6623 deletions

View File

@@ -0,0 +1,22 @@
//
// Example
// man
//
// Created by man 11/11/2018.
// Copyright © 2020 man. All rights reserved.
//
@import Foundation;
/*! Determines the cache storage policy for a response.
* \details When we provide a response up to the client we need to tell the client whether
* the response is cacheable or not. The default HTTP/HTTPS protocol has a reasonable
* complex chunk of code to determine this, but we can't get at it. Thus, we have to
* reimplement it ourselves. This is split off into a separate file to emphasise that
* this is standard boilerplate that you probably don't need to look at.
* \param request The request that generated the response; must not be nil.
* \param response The response itself; must not be nil.
* \returns A cache storage policy to use.
*/
extern NSURLCacheStoragePolicy CacheStoragePolicyForRequestAndResponse(NSURLRequest * request, NSHTTPURLResponse * response);

View File

@@ -0,0 +1,85 @@
//
// Example
// man
//
// Created by man 11/11/2018.
// Copyright © 2020 man. All rights reserved.
//
#import "_CacheStoragePolicy.h"
extern NSURLCacheStoragePolicy CacheStoragePolicyForRequestAndResponse(NSURLRequest * request, NSHTTPURLResponse * response)
// See comment in header.
{
BOOL cacheable;
NSURLCacheStoragePolicy result;
//assert(request != NULL);
//assert(response != NULL);
// First determine if the request is cacheable based on its status code.
switch ([response statusCode]) {
case 200:
case 203:
case 206:
case 301:
case 304:
case 404:
case 410: {
cacheable = YES;
} break;
default: {
cacheable = NO;
} break;
}
// If the response might be cacheable, look at the "Cache-Control" header in
// the response.
// IMPORTANT: We can't rely on -rangeOfString: returning valid results if the target
// string is nil, so we have to explicitly test for nil in the following two cases.
if (cacheable) {
NSString * responseHeader;
responseHeader = [[response allHeaderFields][@"Cache-Control"] lowercaseString];
if ( (responseHeader != nil) && [responseHeader rangeOfString:@"no-store"].location != NSNotFound) {
cacheable = NO;
}
}
// If we still think it might be cacheable, look at the "Cache-Control" header in
// the request.
if (cacheable) {
NSString * requestHeader;
requestHeader = [[request allHTTPHeaderFields][@"Cache-Control"] lowercaseString];
if ( (requestHeader != nil)
&& ([requestHeader rangeOfString:@"no-store"].location != NSNotFound)
&& ([requestHeader rangeOfString:@"no-cache"].location != NSNotFound) ) {
cacheable = NO;
}
}
// Use the cacheable flag to determine the result.
if (cacheable) {
// This code only caches HTTPS data in memory. This is inline with earlier versions of
// iOS. Modern versions of iOS use file protection to protect the cache, and thus are
// happy to cache HTTPS on disk. I've not made the correspondencing change because
// it's nice to see all three cache policies in action.
if ([[[[request URL] scheme] lowercaseString] isEqual:@"https"]) {
result = NSURLCacheStorageAllowedInMemoryOnly;
} else {
result = NSURLCacheStorageAllowed;
}
} else {
result = NSURLCacheStorageNotAllowed;
}
return result;
}

View File

@@ -0,0 +1,25 @@
//
// Example
// man
//
// Created by man 11/11/2018.
// Copyright © 2020 man. All rights reserved.
//
@import Foundation;
/*! Returns a canonical form of the supplied request.
* \details The Foundation URL loading system needs to be able to canonicalize URL
* requests for various reasons (for example, to look for cache hits). The default
* HTTP/HTTPS protocol has a complex chunk of code to perform this function. Unfortunately
* there's no way for third party code to access this. Instead, we have to reimplement
* it all ourselves. This is split off into a separate file to emphasise that this
* is standard boilerplate that you probably don't need to look at.
*
* IMPORTANT: While you can take most of this code as read, you might want to tweak
* the handling of the "Accept-Language" in the CanonicaliseHeaders routine.
* \param request The request to canonicalise; must not be nil.
* \returns The canonical request; should never be nil.
*/
extern NSMutableURLRequest * CanonicalRequestForRequest(NSURLRequest *request);

View File

@@ -0,0 +1,397 @@
//
// Example
// man
//
// Created by man 11/11/2018.
// Copyright © 2020 man. All rights reserved.
//
#import "_CanonicalRequest.h"
#include <xlocale.h>
#pragma mark * URL canonicalization steps
/*! A step in the canonicalisation process.
* \details The canonicalisation process is made up of a sequence of steps, each of which is
* implemented by a function that matches this function pointer. The function gets a URL
* and a mutable buffer holding that URL as bytes. The function can mutate the buffer as it
* sees fit. It typically does this by calling CFURLGetByteRangeForComponent to find the range
* of interest in the buffer. In that case bytesInserted is the amount to adjust that range,
* and the function should modify that to account for any bytes it inserts or deletes. If
* the function modifies the buffer too much, it can return kCFNotFound to force the system
* to re-create the URL from the buffer.
* \param url The original URL to work on.
* \param urlData The URL as a mutable buffer; the routine modifies this.
* \param bytesInserted The number of bytes that have been inserted so far the mutable buffer.
* \returns An updated value of bytesInserted or kCFNotFound if the URL must be reparsed.
*/
typedef CFIndex (*CanonicalRequestStepFunction)(NSURL *url, NSMutableData *urlData, CFIndex bytesInserted);
/*! The post-scheme separate should be "://"; if that's not the case, fix it.
* \param url The original URL to work on.
* \param urlData The URL as a mutable buffer; the routine modifies this.
* \param bytesInserted The number of bytes that have been inserted so far the mutable buffer.
* \returns An updated value of bytesInserted or kCFNotFound if the URL must be reparsed.
*/
static CFIndex FixPostSchemeSeparator(NSURL *url, NSMutableData *urlData, CFIndex bytesInserted)
{
CFRange range;
uint8_t * urlDataBytes;
NSUInteger urlDataLength;
NSUInteger cursor;
NSUInteger separatorLength;
NSUInteger expectedSeparatorLength;
//assert(url != nil);
//assert(urlData != nil);
//assert(bytesInserted >= 0);
range = CFURLGetByteRangeForComponent( (CFURLRef) url, kCFURLComponentScheme, NULL);
if (range.location != kCFNotFound) {
//assert(range.location >= 0);
//assert(range.length >= 0);
urlDataBytes = [urlData mutableBytes];
urlDataLength = [urlData length];
separatorLength = 0;
cursor = (NSUInteger) range.location + (NSUInteger) bytesInserted + (NSUInteger) range.length;
if ( (cursor < urlDataLength) && (urlDataBytes[cursor] == ':') ) {
cursor += 1;
separatorLength += 1;
if ( (cursor < urlDataLength) && (urlDataBytes[cursor] == '/') ) {
cursor += 1;
separatorLength += 1;
if ( (cursor < urlDataLength) && (urlDataBytes[cursor] == '/') ) {
cursor += 1;
separatorLength += 1;
}
}
}
#pragma unused(cursor) // quietens an analyser warning
expectedSeparatorLength = strlen("://");
if (separatorLength != expectedSeparatorLength) {
[urlData replaceBytesInRange:NSMakeRange((NSUInteger) range.location + (NSUInteger) bytesInserted + (NSUInteger) range.length, separatorLength) withBytes:"://" length:expectedSeparatorLength];
bytesInserted = kCFNotFound; // have to build everything now
}
}
return bytesInserted;
}
/*! The scheme should be lower case; if it's not, make it so.
* \param url The original URL to work on.
* \param urlData The URL as a mutable buffer; the routine modifies this.
* \param bytesInserted The number of bytes that have been inserted so far the mutable buffer.
* \returns An updated value of bytesInserted or kCFNotFound if the URL must be reparsed.
*/
static CFIndex LowercaseScheme(NSURL *url, NSMutableData *urlData, CFIndex bytesInserted)
{
CFRange range;
uint8_t * urlDataBytes;
CFIndex i;
//assert(url != nil);
//assert(urlData != nil);
//assert(bytesInserted >= 0);
range = CFURLGetByteRangeForComponent( (CFURLRef) url, kCFURLComponentScheme, NULL);
if (range.location != kCFNotFound) {
//assert(range.location >= 0);
//assert(range.length >= 0);
urlDataBytes = [urlData mutableBytes];
for (i = range.location + bytesInserted; i < (range.location + bytesInserted + range.length); i++) {
urlDataBytes[i] = (uint8_t) tolower_l(urlDataBytes[i], NULL);
}
}
return bytesInserted;
}
/*! The host should be lower case; if it's not, make it so.
* \param url The original URL to work on.
* \param urlData The URL as a mutable buffer; the routine modifies this.
* \param bytesInserted The number of bytes that have been inserted so far the mutable buffer.
* \returns An updated value of bytesInserted or kCFNotFound if the URL must be reparsed.
*/
static CFIndex LowercaseHost(NSURL *url, NSMutableData *urlData, CFIndex bytesInserted)
// The host should be lower case; if it's not, make it so.
{
CFRange range;
uint8_t * urlDataBytes;
CFIndex i;
//assert(url != nil);
//assert(urlData != nil);
//assert(bytesInserted >= 0);
range = CFURLGetByteRangeForComponent( (CFURLRef) url, kCFURLComponentHost, NULL);
if (range.location != kCFNotFound) {
//assert(range.location >= 0);
//assert(range.length >= 0);
urlDataBytes = [urlData mutableBytes];
for (i = range.location + bytesInserted; i < (range.location + bytesInserted + range.length); i++) {
urlDataBytes[i] = (uint8_t) tolower_l(urlDataBytes[i], NULL);
}
}
return bytesInserted;
}
/*! An empty host should be treated as "localhost" case; if it's not, make it so.
* \param url The original URL to work on.
* \param urlData The URL as a mutable buffer; the routine modifies this.
* \param bytesInserted The number of bytes that have been inserted so far the mutable buffer.
* \returns An updated value of bytesInserted or kCFNotFound if the URL must be reparsed.
*/
static CFIndex FixEmptyHost(NSURL *url, NSMutableData *urlData, CFIndex bytesInserted)
{
CFRange range;
CFRange rangeWithSeparator;
//assert(url != nil);
//assert(urlData != nil);
//assert(bytesInserted >= 0);
range = CFURLGetByteRangeForComponent( (CFURLRef) url, kCFURLComponentHost, &rangeWithSeparator);
if (range.length == 0) {
NSUInteger localhostLength;
//assert(range.location >= 0);
//assert(range.length >= 0);
localhostLength = strlen("localhost");
if (range.location != kCFNotFound) {
[urlData replaceBytesInRange:NSMakeRange( (NSUInteger) range.location + (NSUInteger) bytesInserted, 0) withBytes:"localhost" length:localhostLength];
bytesInserted += localhostLength;
} else if ( (rangeWithSeparator.location != kCFNotFound) && (rangeWithSeparator.length == 0) ) {
[urlData replaceBytesInRange:NSMakeRange((NSUInteger) rangeWithSeparator.location + (NSUInteger) bytesInserted, 0) withBytes:"localhost" length:localhostLength];
bytesInserted += localhostLength;
}
}
return bytesInserted;
}
/*! Transform an empty URL path to "/". For example, "http://www.apple.com" becomes "http://www.apple.com/".
* \param url The original URL to work on.
* \param urlData The URL as a mutable buffer; the routine modifies this.
* \param bytesInserted The number of bytes that have been inserted so far the mutable buffer.
* \returns An updated value of bytesInserted or kCFNotFound if the URL must be reparsed.
*/
static CFIndex FixEmptyPath(NSURL *url, NSMutableData *urlData, CFIndex bytesInserted)
{
CFRange range;
CFRange rangeWithSeparator;
//assert(url != nil);
//assert(urlData != nil);
//assert(bytesInserted >= 0);
range = CFURLGetByteRangeForComponent( (CFURLRef) url, kCFURLComponentPath, &rangeWithSeparator);
// The following is not a typo. We use rangeWithSeparator to find where to insert the
// "/" and the range length to decide whether we /need/ to insert the "/".
if ( (rangeWithSeparator.location != kCFNotFound) && (range.length == 0) ) {
//assert(range.location >= 0);
//assert(range.length >= 0);
//assert(rangeWithSeparator.location >= 0);
//assert(rangeWithSeparator.length >= 0);
[urlData replaceBytesInRange:NSMakeRange( (NSUInteger) rangeWithSeparator.location + (NSUInteger) bytesInserted, 0) withBytes:"/" length:1];
bytesInserted += 1;
}
return bytesInserted;
}
/*! If the user specified the default port (80 for HTTP, 443 for HTTPS), remove it from the URL.
* \details Actually this code is disabled because the equivalent code in the default protocol
* handler has also been disabled; some setups depend on get the port number in the URL, even if it
* is the default.
* \param url The original URL to work on.
* \param urlData The URL as a mutable buffer; the routine modifies this.
* \param bytesInserted The number of bytes that have been inserted so far the mutable buffer.
* \returns An updated value of bytesInserted or kCFNotFound if the URL must be reparsed.
*/
__attribute__((unused)) static CFIndex DeleteDefaultPort(NSURL *url, NSMutableData *urlData, CFIndex bytesInserted)
{
NSString * scheme;
BOOL isHTTP;
BOOL isHTTPS;
CFRange range;
uint8_t * urlDataBytes;
NSString * portNumberStr;
int portNumber;
//assert(url != nil);
//assert(urlData != nil);
//assert(bytesInserted >= 0);
scheme = [[url scheme] lowercaseString];
//assert(scheme != nil);
isHTTP = [scheme isEqual:@"http" ];
isHTTPS = [scheme isEqual:@"https"];
range = CFURLGetByteRangeForComponent( (CFURLRef) url, kCFURLComponentPort, NULL);
if (range.location != kCFNotFound) {
//assert(range.location >= 0);
//assert(range.length >= 0);
urlDataBytes = [urlData mutableBytes];
portNumberStr = [[NSString alloc] initWithBytes:&urlDataBytes[range.location + bytesInserted] length:(NSUInteger) range.length encoding:NSUTF8StringEncoding];
if (portNumberStr != nil) {
portNumber = [portNumberStr intValue];
if ( (isHTTP && (portNumber == 80)) || (isHTTPS && (portNumber == 443)) ) {
// -1 and +1 to account for the leading ":"
[urlData replaceBytesInRange:NSMakeRange((NSUInteger) range.location + (NSUInteger) bytesInserted - 1, (NSUInteger) range.length + 1) withBytes:NULL length:0];
bytesInserted -= (range.length + 1);
}
}
}
return bytesInserted;
}
#pragma mark * Other request canonicalization
/*! Canonicalise the request headers.
* \param request The request to canonicalise.
*/
static void CanonicaliseHeaders(NSMutableURLRequest * request)
{
// If there's no content type and the request is a POST with a body, add a default
// content type of "application/x-www-form-urlencoded".
if ( ([request valueForHTTPHeaderField:@"Content-Type"] == nil)
&& ([[request HTTPMethod] caseInsensitiveCompare:@"POST"] == NSOrderedSame)
&& (([request HTTPBody] != nil) || ([request HTTPBodyStream] != nil)) ) {
[request setValue:@"application/x-www-form-urlencoded" forHTTPHeaderField:@"Content-Type"];
}
// If there's no "Accept" header, add a default.
if ([request valueForHTTPHeaderField:@"Accept"] == nil) {
[request setValue:@"*/*" forHTTPHeaderField:@"Accept"];
}
// If there's not "Accept-Encoding" header, add a default.
if ([request valueForHTTPHeaderField:@"Accept-Encoding"] == nil) {
[request setValue:@"gzip, deflate" forHTTPHeaderField:@"Accept-Encoding"];
}
// If there's not an "Accept-Language" headre, add a default. This is quite bogus; ideally we
// should derive the correct "Accept-Language" value from the langauge that the app is running
// in. However, that's quite difficult to get right, so rather than show some general purpose
// code that might fail in some circumstances, I've decided to just hardwire US English.
// If you use this code in your own app you can customise it as you see fit. One option might be
// to base this value on -[NSBundle preferredLocalizations], so that the web page comes back in
// the language that the app is running in.
if ([request valueForHTTPHeaderField:@"Accept-Language"] == nil) {
[request setValue:@"en-us" forHTTPHeaderField:@"Accept-Language"];
}
}
#pragma mark * API
extern NSMutableURLRequest * CanonicalRequestForRequest(NSURLRequest *request)
{
NSMutableURLRequest * result;
NSString * scheme;
//assert(request != nil);
// Make a mutable copy of the request.
result = [request mutableCopy];
// First up check that we're dealing with HTTP or HTTPS. If not, do nothing (why were we
// we even called?).
scheme = [[[request URL] scheme] lowercaseString];
//assert(scheme != nil);
if ( ! [scheme isEqual:@"http" ] && ! [scheme isEqual:@"https"]) {
//assert(NO);
} else {
CFIndex bytesInserted;
NSURL * requestURL;
NSMutableData * urlData;
static const CanonicalRequestStepFunction kStepFunctions[] = {
FixPostSchemeSeparator,
LowercaseScheme,
LowercaseHost,
FixEmptyHost,
// DeleteDefaultPort, -- The built-in canonicalizer has stopped doing this, so we don't do it either.
FixEmptyPath
};
size_t stepIndex;
size_t stepCount;
// Canonicalise the URL by executing each of our step functions.
bytesInserted = kCFNotFound;
urlData = nil;
requestURL = [request URL];
//assert(requestURL != nil);
stepCount = sizeof(kStepFunctions) / sizeof(*kStepFunctions);
for (stepIndex = 0; stepIndex < stepCount; stepIndex++) {
// If we don't have valid URL data, create it from the URL.
//assert(requestURL != nil);
if (bytesInserted == kCFNotFound) {
NSData * urlDataImmutable;
urlDataImmutable = CFBridgingRelease( CFURLCreateData(NULL, (CFURLRef) requestURL, kCFStringEncodingUTF8, true) );
//assert(urlDataImmutable != nil);
urlData = [urlDataImmutable mutableCopy];
//assert(urlData != nil);
bytesInserted = 0;
}
//assert(urlData != nil);
// Run the step.
bytesInserted = kStepFunctions[stepIndex](requestURL, urlData, bytesInserted);
// Note: The following logging is useful when debugging this code. Change the
// if expression to YES to enable it.
if (/* DISABLES CODE */ (NO)) {
// fprintf(stderr, " [%zu] %.*s\n", stepIndex, (int) [urlData length], (const char *) [urlData bytes]);
}
// If the step invalidated our URL (or we're on the last step, whereupon we'll need
// the URL outside of the loop), recreate the URL from the URL data.
if ( (bytesInserted == kCFNotFound) || ((stepIndex + 1) == stepCount) ) {
requestURL = CFBridgingRelease( CFURLCreateWithBytes(NULL, [urlData bytes], (CFIndex) [urlData length], kCFStringEncodingUTF8, NULL) );
//assert(requestURL != nil);
urlData = nil;
}
}
[result setURL:requestURL];
// Canonicalise the headers.
CanonicaliseHeaders(result);
}
return result;
}

View File

@@ -0,0 +1,138 @@
//
// Example
// man
//
// Created by man 11/11/2018.
// Copyright © 2020 man. All rights reserved.
//
@import Foundation;
@protocol _CustomHTTPProtocolDelegate;
/*! An NSURLProtocol subclass that overrides the built-in HTTP/HTTPS protocol to intercept
* authentication challenges for subsystems, ilke UIWebView, that don't otherwise allow it.
* To use this class you should set up your delegate (+setDelegate:) and then call +start.
* If you don't call +start the class is completely benign.
*
* The really tricky stuff here is related to the authentication challenge delegate
* callbacks; see the docs for _CustomHTTPProtocolDelegate for the details.
*/
@interface _CustomHTTPProtocol : NSURLProtocol
/*! Call this to start the module. Prior to this the module is just dormant, and
* all HTTP requests proceed as normal. After this all HTTP and HTTPS requests
* go through this module.
*/
+ (void)start;
//liman
+ (void)stop;
/*! Sets the delegate for the class.
* \details Note that there's one delegate for the entire class, not one per
* instance of the class as is more normal. The delegate is not retained in general,
* but is retained for the duration of any given call. Once you set the delegate to nil
* you can be assured that it won't be called unretained (that is, by the time that
* -setDelegate: returns, we've already done all possible retains on the delegate).
* \param newValue The new delegate to use; may be nil.
*/
+ (void)setDelegate:(id<_CustomHTTPProtocolDelegate>)newValue;
/*! Returns the class delegate.
*/
+ (id<_CustomHTTPProtocolDelegate>)delegate;
@property (atomic, strong, readonly ) NSURLAuthenticationChallenge * pendingChallenge; ///< The current authentication challenge; it's only safe to access this from the main thread.
/*! Call this method to resolve an authentication challeng. This must be called on the main thread.
* \param challenge The challenge to resolve. This must match the pendingChallenge property.
* \param credential The credential to use, or nil to continue without a credential.
*/
- (void)resolveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge withCredential:(NSURLCredential *)credential;
@end
/*! The delegate for the CustomHTTPProtocol class (not its instances).
* \details The delegate handles two different types of callbacks:
*
* - authentication challenges
*
* - logging
*
* The latter is very simple. The former is quite tricky. The basic idea is that each CustomHTTPProtocol
* instance sends the delegate a serialised stream of authentication challenges, each of which it is
* expected to resolve. The sequence is as follows:
*
* -# It calls -customHTTPProtocol:canAuthenticateAgainstProtectionSpace: to determine if the delegate
* can handle the challenge. This can be call on an arbitrary background thread.
*
* -# If the delegate returns YES, it calls -customHTTPProtocol:didReceiveAuthenticationChallenge: to
* actually process the challenge. This is always called on the main thread. The delegate can resolve
* the challenge synchronously (that is, before returning from the call) or it can return from the call
* and then, later on, resolve the challenge. Resolving the challenge involves calling
* -[CustomHTTPProtocol resolveAuthenticationChallenge:withCredential:], which also must be called
* on the main thread. Between the calls to -customHTTPProtocol:didReceiveAuthenticationChallenge:
* and -[CustomHTTPProtocol resolveAuthenticationChallenge:withCredential:], the protocol's
* pendingChallenge property will contain the challenge.
*
* -# While there is a pending challenge, the protocol may call -customHTTPProtocol:didCancelAuthenticationChallenge:
* to cancel the challenge. This is always called on the main thread.
*
* Note that this design follows the original NSURLConnection model, not the newer NSURLConnection model
* (introduced in OS X 10.7 / iOS 5) or the NSURLSession model, because of my concerns about performance.
* Specifically, -customHTTPProtocol:canAuthenticateAgainstProtectionSpace: can be called on any thread
* but -customHTTPProtocol:didReceiveAuthenticationChallenge: is called on the main thread. If I unified
* them I'd end up calling the resulting single routine on the main thread, which meanings a lot more
* bouncing between threads, much of which would be pointless in the common case where you don't want to
* customise the default behaviour. Alternatively I could call the unified routine on an arbitrary thread,
* but that would make it harder for clients and require a major rework of my implementation.
*/
@protocol _CustomHTTPProtocolDelegate <NSObject>
@optional
/*! Called by an CustomHTTPProtocol instance to ask the delegate whether it's prepared to handle
* a particular authentication challenge. Can be called on any thread.
* \param protocol The protocol instance itself; will not be nil.
* \param protectionSpace The protection space for the authentication challenge; will not be nil.
* \returns Return YES if you want the -customHTTPProtocol:didReceiveAuthenticationChallenge: delegate
* callback, or NO for the challenge to be handled in the default way.
*/
- (BOOL)customHTTPProtocol:(_CustomHTTPProtocol *)protocol canAuthenticateAgainstProtectionSpace:(NSURLProtectionSpace *)protectionSpace;
/*! Called by an CustomHTTPProtocol instance to request that the delegate process on authentication
* challenge. Will be called on the main thread. Unless the challenge is cancelled (see below)
* the delegate must eventually resolve it by calling -resolveAuthenticationChallenge:withCredential:.
* \param protocol The protocol instance itself; will not be nil.
* \param challenge The authentication challenge; will not be nil.
*/
- (void)customHTTPProtocol:(_CustomHTTPProtocol *)protocol didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge;
/*! Called by an CustomHTTPProtocol instance to cancel an issued authentication challenge.
* Will be called on the main thread.
* \param protocol The protocol instance itself; will not be nil.
* \param challenge The authentication challenge; will not be nil; will match the challenge
* previously issued by -customHTTPProtocol:canAuthenticateAgainstProtectionSpace:.
*/
- (void)customHTTPProtocol:(_CustomHTTPProtocol *)protocol didCancelAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge;
/*! Called by the CustomHTTPProtocol to log various bits of information.
* Can be called on any thread.
* \param protocol The protocol instance itself; nil to indicate log messages from the class itself.
* \param format A standard NSString-style format string; will not be nil.
* \param arguments Arguments for that format string.
*/
- (void)customHTTPProtocol:(_CustomHTTPProtocol *)protocol logWithFormat:(NSString *)format arguments:(va_list)arguments;
@end

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,52 @@
//
// Example
// man
//
// Created by man 11/11/2018.
// Copyright © 2020 man. All rights reserved.
//
#import <Foundation/Foundation.h>
/*! A simple class for demultiplexing NSURLSession delegate callbacks to a per-task delegate object.
You initialise the class with a session configuration. After that you can create data tasks
within that session by calling -dataTaskWithRequest:delegate:modes:. Any delegate callbacks
for that data task are redirected to the delegate on the thread that created the task in
one of the specified run loop modes. That thread must run its run loop in order to get
these callbacks.
*/
@interface _QNSURLSessionDemux : NSObject
/*! Create a demultiplex for the specified session configuration.
* \param configuration The session configuration to use; if nil, a default session is created.
* \returns An initialised instance.
*/
- (instancetype)initWithConfiguration:(NSURLSessionConfiguration *)configuration;
@property (atomic, copy, readonly ) NSURLSessionConfiguration * configuration; ///< A copy of the configuration passed to -initWithConfiguration:.
@property (atomic, strong, readonly ) NSURLSession * session; ///< The session created from the configuration passed to -initWithConfiguration:.
/*! Creates a new data task whose delegate callbacks are routed to the supplied delegate.
* \details The callbacks are run on the current thread (that is, the thread that called this
* method) in the specified modes.
*
* The delegate is retained until the task completes, that is, until after your
* -URLSession:task:didCompleteWithError: delegate callback returns.
*
* The returned task is suspend. You must resume the returned task for the task to
* make progress. Furthermore, it's not safe to simply discard the returned task
* because in that case the task's delegate is never released.
*
* \param request The request that the data task executes; must not be nil.
* \param delegate The delegate to receive the data task's delegate callbacks; must not be nil.
* \param modes The run loop modes in which to run the data task's delegate callbacks; if nil or
* empty, the default run loop mode (NSDefaultRunLoopMode is used).
* \returns A suspended data task that you must resume.
*/
- (NSURLSessionDataTask *)dataTaskWithRequest:(NSURLRequest *)request delegate:(id<NSURLSessionDataDelegate>)delegate modes:(NSArray *)modes;
@end

View File

@@ -0,0 +1,279 @@
//
// Example
// man
//
// Created by man 11/11/2018.
// Copyright © 2020 man. All rights reserved.
//
#import "_QNSURLSessionDemux.h"
@interface _QNSURLSessionDemuxTaskInfo : NSObject
- (instancetype)initWithTask:(NSURLSessionDataTask *)task delegate:(id<NSURLSessionDataDelegate>)delegate modes:(NSArray *)modes;
@property (atomic, strong, readonly ) NSURLSessionDataTask * task;
@property (atomic, strong, readonly ) id<NSURLSessionDataDelegate> delegate;
@property (atomic, strong, readonly ) NSThread * thread;
@property (atomic, copy, readonly ) NSArray * modes;
- (void)performBlock:(dispatch_block_t)block;
- (void)invalidate;
@end
@interface _QNSURLSessionDemuxTaskInfo ()
@property (atomic, strong, readwrite) id<NSURLSessionDataDelegate> delegate;
@property (atomic, strong, readwrite) NSThread * thread;
@end
@implementation _QNSURLSessionDemuxTaskInfo
- (instancetype)initWithTask:(NSURLSessionDataTask *)task delegate:(id<NSURLSessionDataDelegate>)delegate modes:(NSArray *)modes
{
//assert(task != nil);
//assert(delegate != nil);
//assert(modes != nil);
self = [super init];
if (self != nil) {
self->_task = task;
self->_delegate = delegate;
self->_thread = [NSThread currentThread];
self->_modes = [modes copy];
}
return self;
}
- (void)performBlock:(dispatch_block_t)block
{
//assert(self.delegate != nil);
//assert(self.thread != nil);
[self performSelector:@selector(performBlockOnClientThread:) onThread:self.thread withObject:[block copy] waitUntilDone:NO modes:self.modes];
}
- (void)performBlockOnClientThread:(dispatch_block_t)block
{
//assert([NSThread currentThread] == self.thread);
block();
}
- (void)invalidate
{
self.delegate = nil;
self.thread = nil;
}
@end
@interface _QNSURLSessionDemux () <NSURLSessionDataDelegate>
@property (atomic, strong, readonly ) NSMutableDictionary * taskInfoByTaskID; // keys NSURLSessionTask taskIdentifier, values are SessionManager
@property (atomic, strong, readonly ) NSOperationQueue * sessionDelegateQueue;
@end
@implementation _QNSURLSessionDemux
- (instancetype)init
{
return [self initWithConfiguration:nil];
}
- (instancetype)initWithConfiguration:(NSURLSessionConfiguration *)configuration
{
// configuration may be nil
self = [super init];
if (self != nil) {
if (configuration == nil) {
configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
}
self->_configuration = [configuration copy];
self->_taskInfoByTaskID = [[NSMutableDictionary alloc] init];
self->_sessionDelegateQueue = [[NSOperationQueue alloc] init];
[self->_sessionDelegateQueue setMaxConcurrentOperationCount:1];
[self->_sessionDelegateQueue setName:@"_QNSURLSessionDemux"];
self->_session = [NSURLSession sessionWithConfiguration:self->_configuration delegate:self delegateQueue:self->_sessionDelegateQueue];
self->_session.sessionDescription = @"_QNSURLSessionDemux";
}
return self;
}
- (NSURLSessionDataTask *)dataTaskWithRequest:(NSURLRequest *)request delegate:(id<NSURLSessionDataDelegate>)delegate modes:(NSArray *)modes
{
NSURLSessionDataTask * task;
_QNSURLSessionDemuxTaskInfo * taskInfo;
//assert(request != nil);
//assert(delegate != nil);
// modes may be nil
if ([modes count] == 0) {
modes = @[ NSDefaultRunLoopMode ];
}
task = [self.session dataTaskWithRequest:request];
//assert(task != nil);
taskInfo = [[_QNSURLSessionDemuxTaskInfo alloc] initWithTask:task delegate:delegate modes:modes];
@synchronized (self) {
self.taskInfoByTaskID[@(task.taskIdentifier)] = taskInfo;
}
return task;
}
- (_QNSURLSessionDemuxTaskInfo *)taskInfoForTask:(NSURLSessionTask *)task
{
_QNSURLSessionDemuxTaskInfo * result;
//assert(task != nil);
@synchronized (self) {
result = self.taskInfoByTaskID[@(task.taskIdentifier)];
//assert(result != nil);
}
return result;
}
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task willPerformHTTPRedirection:(NSHTTPURLResponse *)response newRequest:(NSURLRequest *)newRequest completionHandler:(void (^)(NSURLRequest *))completionHandler
{
_QNSURLSessionDemuxTaskInfo * taskInfo;
taskInfo = [self taskInfoForTask:task];
if ([taskInfo.delegate respondsToSelector:@selector(URLSession:task:willPerformHTTPRedirection:newRequest:completionHandler:)]) {
[taskInfo performBlock:^{
[taskInfo.delegate URLSession:session task:task willPerformHTTPRedirection:response newRequest:newRequest completionHandler:completionHandler];
}];
} else {
completionHandler(newRequest);
}
}
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler
{
_QNSURLSessionDemuxTaskInfo * taskInfo;
taskInfo = [self taskInfoForTask:task];
if ([taskInfo.delegate respondsToSelector:@selector(URLSession:task:didReceiveChallenge:completionHandler:)]) {
[taskInfo performBlock:^{
[taskInfo.delegate URLSession:session task:task didReceiveChallenge:challenge completionHandler:completionHandler];
}];
} else {
completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, nil);
}
}
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task needNewBodyStream:(void (^)(NSInputStream *bodyStream))completionHandler
{
_QNSURLSessionDemuxTaskInfo * taskInfo;
taskInfo = [self taskInfoForTask:task];
if ([taskInfo.delegate respondsToSelector:@selector(URLSession:task:needNewBodyStream:)]) {
[taskInfo performBlock:^{
[taskInfo.delegate URLSession:session task:task needNewBodyStream:completionHandler];
}];
} else {
completionHandler(nil);
}
}
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didSendBodyData:(int64_t)bytesSent totalBytesSent:(int64_t)totalBytesSent totalBytesExpectedToSend:(int64_t)totalBytesExpectedToSend
{
_QNSURLSessionDemuxTaskInfo * taskInfo;
taskInfo = [self taskInfoForTask:task];
if ([taskInfo.delegate respondsToSelector:@selector(URLSession:task:didSendBodyData:totalBytesSent:totalBytesExpectedToSend:)]) {
[taskInfo performBlock:^{
[taskInfo.delegate URLSession:session task:task didSendBodyData:bytesSent totalBytesSent:totalBytesSent totalBytesExpectedToSend:totalBytesExpectedToSend];
}];
}
}
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error
{
_QNSURLSessionDemuxTaskInfo * taskInfo;
taskInfo = [self taskInfoForTask:task];
// This is our last delegate callback so we remove our task info record.
@synchronized (self) {
[self.taskInfoByTaskID removeObjectForKey:@(taskInfo.task.taskIdentifier)];
}
// Call the delegate if required. In that case we invalidate the task info on the client thread
// after calling the delegate, otherwise the client thread side of the -performBlock: code can
// find itself with an invalidated task info.
if ([taskInfo.delegate respondsToSelector:@selector(URLSession:task:didCompleteWithError:)]) {
[taskInfo performBlock:^{
[taskInfo.delegate URLSession:session task:task didCompleteWithError:error];
[taskInfo invalidate];
}];
} else {
[taskInfo invalidate];
}
}
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(NSURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler
{
_QNSURLSessionDemuxTaskInfo * taskInfo;
taskInfo = [self taskInfoForTask:dataTask];
if ([taskInfo.delegate respondsToSelector:@selector(URLSession:dataTask:didReceiveResponse:completionHandler:)]) {
[taskInfo performBlock:^{
[taskInfo.delegate URLSession:session dataTask:dataTask didReceiveResponse:response completionHandler:completionHandler];
}];
} else {
completionHandler(NSURLSessionResponseAllow);
}
}
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didBecomeDownloadTask:(NSURLSessionDownloadTask *)downloadTask
{
_QNSURLSessionDemuxTaskInfo * taskInfo;
taskInfo = [self taskInfoForTask:dataTask];
if ([taskInfo.delegate respondsToSelector:@selector(URLSession:dataTask:didBecomeDownloadTask:)]) {
[taskInfo performBlock:^{
[taskInfo.delegate URLSession:session dataTask:dataTask didBecomeDownloadTask:downloadTask];
}];
}
}
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data
{
_QNSURLSessionDemuxTaskInfo * taskInfo;
taskInfo = [self taskInfoForTask:dataTask];
if ([taskInfo.delegate respondsToSelector:@selector(URLSession:dataTask:didReceiveData:)]) {
[taskInfo performBlock:^{
[taskInfo.delegate URLSession:session dataTask:dataTask didReceiveData:data];
}];
}
}
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask willCacheResponse:(NSCachedURLResponse *)proposedResponse completionHandler:(void (^)(NSCachedURLResponse *cachedResponse))completionHandler
{
_QNSURLSessionDemuxTaskInfo * taskInfo;
taskInfo = [self taskInfoForTask:dataTask];
if ([taskInfo.delegate respondsToSelector:@selector(URLSession:dataTask:willCacheResponse:completionHandler:)]) {
[taskInfo performBlock:^{
[taskInfo.delegate URLSession:session dataTask:dataTask willCacheResponse:proposedResponse completionHandler:completionHandler];
}];
} else {
completionHandler(proposedResponse);
}
}
@end