Welcome to my blog post

What is NSURLProtocol

NSURLProtocol is part of the URL Loading System in the Foundation framework. It allows developers to change all the details of URL loading without modifying the original request code in the application. In other words, NSURLProtocol is a man-in-the-middle attack with Apple’s tacit approval.

Although NSURLProtocol is called “Protocol”, it is not a Protocol, but an abstract class.

Since NSURLProtocol is an abstract class, meaning it cannot be instantiated, how does it implement network request interception?

The answer is to subclass new or existing URL loading behavior. If the current network request can be intercepted, all the developer needs to do is register a custom NSURLProtocol subclass into the App, where all requests can be intercepted and modified.

So which network requests can be intercepted?

NSURLProtocol Application scenario

As mentioned earlier, NSURLProtocol is part of the URL Loading System, so it can intercept all network requests based on URL Loading System:

  • NSURLSession
  • NSURLConnection
  • NSURLDownload
  • NSURLResponse
    • NSHTTPURLResponse
  • NSURLRequest
    • NSMutableURLRequest

Accordingly, network requests from AFNetworking and Alamofire, third-party networking frameworks based on their implementation, can also be intercepted by NSURLProtocol.

But earlier implementations based on CFNetwork, such as ASIHTTPRequest, could not intercept network requests.

In addition, UIWebView can also be intercepted by NSURLProtocol, but WKWebView cannot. (Because WKWebView is based on WebKit and does not use C sockets.)

Therefore, in practical applications, its functions are very powerful, such as:

  • Redirect network requests to resolve DNS domain name hijacking
  • Perform global or local network request Settings, such as modifying request addresses, headers, and so on
  • Ignore network requests, use H5 offline packages or cache data, etc
  • Customize the return results of network requests, such as filtering sensitive information

Let’s look at the related methods of NSURLProtocol.

Related methods of NSURLProtocol

Creating a protocol Object

// Create an instance of the URL protocol to handle the request
- (instancetype)initWithRequest:(NSURLRequest *)request cachedResponse:(NSCachedURLResponse *)cachedResponse client:(id<NSURLProtocolClient>)client;
// Create a URL protocol instance to handle session task requests
- (instancetype)initWithTask:(NSURLSessionTask *)task cachedResponse:(NSCachedURLResponse *)cachedResponse client:(id<NSURLProtocolClient>)client;
Copy the code

Register and deregister protocol classes

// Try to subclass NSURLProtocol to make it visible in the URL loading system
+ (BOOL)registerClass:(Class)protocolClass;
// Unregister the specified subclass of NSURLProtocol
+ (void)unregisterClass:(Class)protocolClass;
Copy the code

Determines whether a subclass can handle requests

The primary task of subclassing NSProtocol is to tell it what type of network requests it needs to control.

// Determines whether a protocol subclass can handle the specified request. If it returns YES, the request will be controlled, and if it returns NO, it will jump to the next protocol
+ (BOOL)canInitWithRequest:(NSURLRequest *)request;
// Determines whether the protocol subclass can handle the specified task request
+ (BOOL)canInitWithTask:(NSURLSessionTask *)task;
Copy the code

Gets and sets the request properties

NSURLProtocol allows developers to retrieve, add, and delete arbitrary metadata from a Request object. These methods are often used to deal with the problem of requesting an infinite loop.

Gets the attribute associated with the specified key on the specified request
+ (id)propertyForKey:(NSString *)key inRequest:(NSURLRequest *)request;
// Sets the property associated with the specified key in the specified request
+ (void)setProperty:(id)value forKey:(NSString *)key inRequest:(NSMutableURLRequest *)request;
// Deletes the attribute associated with the specified key in the specified request
+ (void)removePropertyForKey:(NSString *)key inRequest:(NSMutableURLRequest *)request;
Copy the code

Provide a canonical version of the request

If you want to modify the request in a specific way, use the following method.

// Returns the specification version of the specified request
+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request;
Copy the code

Determine if the requests are the same

// Determine whether two requests are the same. If they are, you can use the cached data, usually just call the parent's implementation
+ (BOOL)requestIsCacheEquivalent:(NSURLRequest *)a toRequest:(NSURLRequest *)b;
Copy the code

Start and stop loading

These are the two most important methods in the subclass, and different custom subclasses call them with different content, but they all operate around the Protocol client.

// Start loading
- (void)startLoading;
// Stop loading
- (void)stopLoading;
Copy the code

Obtaining protocol attributes

// Get the cache of the protocol receiver
- (NSCachedURLResponse *)cachedResponse;
// The object that the recipient uses to communicate with the URL-loading system, which is owned by each subclass instance of NSProtocol
- (id<NSURLProtocolClient>)client;
// Request from the receiver
- (NSURLRequest *)request;
// The task of the receiver
- (NSURLSessionTask *)task;
Copy the code

In practical application, NSURLProtocol mainly completes two steps: URL interception and URL forwarding. Let’s start with how to intercept network requests.

How to use NSProtocol to intercept network requests

Create the NSURLProtocol subclass

Create a subclass called HTCustomURLProtocol.

@interface HTCustomURLProtocol : NSURLProtocol
@end
Copy the code

Register a subclass of NSURLProtocol

Register the subclass where appropriate. Call the registerClass method for network requests created based on NSURLConnection or using the [NSURLSession sharedSession] initialization object.

[NSURLProtocol registerClass:[NSClassFromString(@"HTCustomURLProtocol") class]]./ / or
// [NSURLProtocol registerClass:[HTCustomURLProtocol class]]; 
Copy the code

If you need a global listening, can set the AppDelegate. M didFinishLaunchingWithOptions method. If you only need to use it in a single UIViewController, unlog the listener when appropriate:

[NSURLProtocol unregisterClass:[NSClassFromString(@"HTCustomURLProtocol") class]].Copy the code

If is based on the NSURLSession network requests, and not through [NSURLSession sharedSession] created, will have to configure NSURLSessionConfiguration protocolClasses attributes of objects.

NSURLSessionConfiguration *sessionConfiguration = [NSURLSessionConfiguration defaultSessionConfiguration];
sessionConfiguration.protocolClasses = @[[NSClassFromString(@"HTCustomURLProtocol") class]].Copy the code

Implement the NSURLProtocol subclass

The implementation subclass is divided into five steps:

Register → intercept → forward → callback → end

Taking intercepting UIWebView as an example, you need to override these five core methods of the parent class.

// Define a protocol key
static NSString * const HTCustomURLProtocolHandledKey = @"HTCustomURLProtocolHandledKey";

// Define an NSURLConnection property in the extension. Interception is also possible through NSURLSession, but NSURLConnection is used as an example.
@property (nonatomic.strong) NSURLConnection *connection;
// Define a variable request return value,
@property (nonatomic.strong) NSMutableData *responseData;

// Method 1: this method is called after a network request is intercepted, and you can handle the interception logic again, such as setting only HTTP and HTTPS requests to be processed.
+ (BOOL)canInitWithRequest:(NSURLRequest *)request {
    // Only HTTP and HTTPS requests are processed
    NSString *scheme = [[request URL] scheme];
    if ( ([scheme caseInsensitiveCompare:@"http"] = =NSOrderedSame ||
          [scheme caseInsensitiveCompare:@"https"] = =NSOrderedSame)) {
        // See if it has been processed to prevent an infinite loop
        if ([NSURLProtocol propertyForKey:HTCustomURLProtocolHandledKey inRequest:request]) {
            return NO;
        }
        // If you still need to intercept the link in the DNS resolution request, you can continue to determine whether to intercept the domain name request link, if yes, return NO
        return YES;
    }
    return NO;
}

// Method 2: [key method] The request can be processed here, such as changing the address, retrieving the request information, setting the request first.
+ (NSURLRequest *) canonicalRequestForRequest:(NSURLRequest *)request {
    // Can print out all request links including CSS and Ajax requests, etc
    NSLog(@"request.URL.absoluteString = %@",request.URL.absoluteString);
    NSMutableURLRequest *mutableRequest = [request mutableCopy];
    return mutableRequest;
}

// Method 3: [Key method] Set up network proxy here, create a new object to forward the processed request. The corresponding callback method here corresponds to the 
      
        protocol method
      
- (void)startLoading {
    // You can modify the request
    NSMutableURLRequest *mutableRequest = [[self request] mutableCopy];
    // Tag to prevent recursive calls
    [NSURLProtocol setProperty:@YES forKey:HTCustomURLProtocolHandledKey inRequest:mutableRequest];
    // You can also check the cache here
    // Forwarding the request, in the case of NSURLConnection, creates an NSURLConnection object; In the case of NSURLSession, it's launching an NSURlssession task.
    self.connection = [NSURLConnection connectionWithRequest:mutableRequest delegate:self];
}

// Method 4: Check whether two requests are the same. If they are, you can use cached data, usually just calling the parent class implementation.
+ (BOOL)requestIsCacheEquivalent:(NSURLRequest *)a toRequest:(NSURLRequest *)b {
    return [super requestIsCacheEquivalent:a toRequest:b];
}

// Method 5: Stop the request and clear the connection or session
- (void)stopLoading {
    if (self.connection ! =nil) {[self.connection cancel];
        self.connection = nil; }}// Follow the custom requirements as described in the above method to call back the forward request at the appropriate time.
#pragma mark- NSURLConnectionDelegate

- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
    [self.client URLProtocol:self didFailWithError:error];
}

#pragma mark - NSURLConnectionDataDelegate

// Called when a response is received from the server (connected to the server)
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
    self.responseData = [[NSMutableData alloc] init];
    // Can handle different statusCode scenarios
    // NSInteger statusCode = [(NSHTTPURLResponse *)response statusCode];
    // Cookies can be set
    [self.client URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed];
}

// Called when data is received from the server, possibly multiple times, passing only part of the data each time
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
    [self.responseData appendData:data];
    [self.client URLProtocol:self didLoadData:data];
}

// called when the server's data is loaded
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
    [self.client URLProtocolDidFinishLoading:self];
}

// Called when a request fails, such as a request timeout or network outage, usually referring to a client error
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
    [self.client URLProtocol:self didFailWithError:error];
}

Copy the code

Some NSURLProtocolClient methods used above:

@protocol NSURLProtocolClient <NSObject>
// Request redirection
- (void)URLProtocol:(NSURLProtocol *)protocol wasRedirectedToRequest:(NSURLRequest *)request redirectResponse:(NSURLResponse *)redirectResponse;
// Whether the response cache is valid
- (void)URLProtocol:(NSURLProtocol *)protocol cachedResponseIsValid:(NSCachedURLResponse *)cachedResponse;
// The response message has just been received
- (void)URLProtocol:(NSURLProtocol *)protocol didReceiveResponse:(NSURLResponse *)response cacheStoragePolicy:(NSURLCacheStoragePolicy)policy;
// Data is loaded successfully
- (void)URLProtocol:(NSURLProtocol *)protocol didLoadData:(NSData *)data;
// Data is loaded
- (void)URLProtocolDidFinishLoading:(NSURLProtocol *)protocol;
// Failed to load data
- (void)URLProtocol:(NSURLProtocol *)protocol didFailWithError:(NSError *)error;
// Starts validation for the specified request
- (void)URLProtocol:(NSURLProtocol *)protocol didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge;
// Unvalidate the specified request
- (void)URLProtocol:(NSURLProtocol *)protocol didCancelAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge;
@end
Copy the code

Add the content

Considerations when using NSURLSession

If NSURLSession is used in NSURLProtocol, note:

  • The HTTPBody of the intercepted request is nil, but the body can be retrieved using HTTPBodyStream.
  • If you want to useregisterClassRegistration, pass only[NSURLSession sharedSession]To create a network request.

Register multiple NSURLProtocol subclasses

When multiple custom NSURLProtocol subclasses are registered in the system, the URL loading process will be called in the reverse order of their registration, that is, the last registered NSURLProtocol will be determined first.

For by configuring NSURLSessionConfiguration protocolClasses attribute of the object to register, only the first NSURLProtocol protocolClasses array can play a role, Subsequent NSURLProtocol cannot be intercepted.

So OHHTTPStubs registers the NSURLProtocol subclass like this:

+ (void)setEnabled:(BOOL)enable forSessionConfiguration:(NSURLSessionConfiguration*)sessionConfig
{
    // Runtime check to make sure the API is available on this version
    if ([sessionConfig respondsToSelector:@selector(protocolClasses)]
        && [sessionConfig respondsToSelector:@selector(setProtocolClasses:)])
    {
        NSMutableArray * urlProtocolClasses = [NSMutableArray arrayWithArray:sessionConfig.protocolClasses];
        Class protoCls = HTTPStubsProtocol.class;
        if(enable && ! [urlProtocolClasses containsObject:protoCls]) {// Insert your own NSURLProtocol into the first of protocolClasses to intercept
            [urlProtocolClasses insertObject:protoCls atIndex:0];
        }
        else if(! enable && [urlProtocolClasses containsObject:protoCls]) {// Remove after interception is complete
            [urlProtocolClasses removeObject:protoCls];
        }
        sessionConfig.protocolClasses = urlProtocolClasses;
    }
    else
    {
        NSLog(@"[OHHTTPStubs] %@ is only available when running on iOS7+/OSX9+. "
              @"Use conditions like 'if ([NSURLSessionConfiguration class])' to only call "
              @"this method if the user is running iOS7+/OSX9+.".NSStringFromSelector(_cmd)); }}Copy the code

How to intercept WKWebView

Although NSURLProtocol cannot intercept WKWebView directly, there is a solution. Is the use of WKBrowsingContextController and registerSchemeForCustomProtocol.

/ / registration scheme
Class cls = NSClassFromString(@"WKBrowsingContextController");
SEL sel = NSSelectorFromString(@"registerSchemeForCustomProtocol:");
if ([cls respondsToSelector:sel]) {
    // For HTTP and HTTPS requests, other schemes can also be used, but the URL Loading System must be satisfied
    [cls performSelector:sel withObject:@"http"];
    [cls performSelector:sel withObject:@"https"];
}
Copy the code

But because this involves a private method, direct reference can not pass apple’s computer audit, so the use of the string needs to do under the processing, such as the other method name algorithm encryption processing, measurement can also pass the audit.

In short, NSURLProtocol is very powerful. It has a strong plastic space for optimizing App performance and expanding functions. However, more attention should be paid to the problems caused by it when using it. Although it has been used in many frameworks and well-known projects, its implications are still worth exploring.