iOS in-app packet capture, NSURLProtocol intercepts network requests within APP

Foreword

When developing, you need to obtain data in the SDK. Since you cannot see the code, you can only monitor all network request data and intercept the corresponding return data. This can be achieved through NSURLProtocol, and can also be used to interact with H5.

1. NSURLProtocol intercepts requests

1. Introduction to NSURLProtoco

Official definition of NSURLProtocol

  1. An NSURLProtocol object handles the loading of protocol-specific URL data.

  2. The NSURLProtocol class itself is an abstract class that provides the infrastructure for processing URLs with a specific URL scheme.

  3. You create subclasses for any custom protocols or URL schemes that your app supports.

The Foundation framework of iOS provides the URL Loading System library (hereinafter abbreviated as ULS). All protocols based on URL (such as http://, https://, ftp://, these application layer transmission protocols) can pass ULS. Provided basic classes and protocols to implement, you can even customize your own private application layer communication protocol.

The ULS library provides a powerful weapon NSURLProtocol. Subclasses that inherit NSURLProtocol can implement interception behavior. The specific method is: if a certain NSURLProtocol subclass is registered, the traffic managed by ULS will be handled by this subclass first, which is equivalent to implementing an interceptor. Since the currently dominant http client libraries AFNetworking and Alamofire are both implemented based on the URL Loading System, both of them and the traffic generated using the basic URL Loading System API can theoretically be intercepted.

Note that NSURLProtocol is an abstract class, not a protocol.

In fact, the function of NSURLProtocol is to allow us to intercept all url requests within the app (note, not just requests within the webView, but all requests within the entire app). If you filter out the things you are interested in and process them, you will not be interested. Just let it go. Since we can intercept, we can do at least two things. The first is to intercept existing url requests, such as the commonly used http://. The second is that we can customize the url protocol, such as boris://

To give a few examples:

  • All requests in our APP need to add public headers. We can implement this directly through NSURLProtocol. Of course, there are many ways to implement it.
  • For another example, we need to perform some access statistics on a certain API of the APP.
  • For another example, we need to count the network request failure rate within the APP.
2. Intercept data requests

In NSURLProtocol, we need to tell it which network requests we need to intercept. This is achieved through the method canInitWithRequest:. For example, if we now need to intercept all HTTP and HTTPS requests, then we can do this logic in canInitWithRequest:. definition.

Let’s focus on the tag kProtocolHandledKey: Whenever a URL resource needs to be loaded, the URL Loading System will ask ZJHURLProtocol whether to process it. If YES is returned, the URL Loading System will create a ZJHURLProtocol instance. After the instance completes the interception work , the original method, such as session GET, will be called again, and the URL Loading System will be called again. If YES is always returned in + canInitWithRequest:, the URL Loading System will create a ZJHURLProtocol instance. . . . This results in an infinite loop. In order to avoid this problem, we can use + setProperty:forKey:inRequest: to label the processed request, and then query in + canInitWithRequest: whether the request has been processed, and if so, return NO. The kProtocolHandledKey above is a label. The label is a string and can be named arbitrarily. This labeling method usually involves

/**
Requests requiring control
@param request this request
@return Whether monitoring is required
*/
 + (BOOL)canInitWithRequest:(NSURLRequest *)request {
    // If it has been intercepted, release it to avoid an infinite loop.
    if ([NSURLProtocol propertyForKey:kProtocolHandledKey inRequest:request] ) {
        return NO;
    }
    // It is not a network request and will not be processed.
    if (![request.URL.scheme isEqualToString:@"http"] & amp; & amp;
    ![request.URL.scheme isEqualToString:@"https"]) {
        return NO;
    }
    // intercept all
    return YES;
}

In the method canonicalRequestForRequest:, we can customize the current request request. Of course, if there is no need to customize it, just return it directly.

/**

Set up our own custom request

You can add headers and the like here.
@param request This request is applied

@return our customized request

*/

 + (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request {

    NSMutableURLRequest *mutableReqeust = [request mutableCopy];
    //Set the processed flag
    [NSURLProtocol setProperty:@(YES) forKey:kProtocolHandledKey inRequest:mutableReqeust];
    return [mutableReqeust copy];
}

Next, we need to send this request, because if we do not process this request, the system will automatically send this network request, but when we process this request, we need to send it manually.

We need to manually send this network request and need to override the startLoading method.

//Restart the loading method of the parent class

- (void)startLoading {

    NSLog(@"***ZJH listening interface: %@", self.request.URL.absoluteString);
    NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
    self.sessionDelegateQueue = [[NSOperationQueue alloc] init];
    self.sessionDelegateQueue.maxConcurrentOperationCount = 1;
    self.sessionDelegateQueue.name = @"com.hujiang.wedjat.session.queue";
    NSURLSession *session = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:self.sessionDelegateQueue];
    self.dataTask = [session dataTaskWithRequest:self.request];
    [self.dataTask resume];
}

Of course, if there is a start, there is a stop. Stop is very simple.

//End loading
- (void)stopLoading {
    [self.dataTask cancel];
}
3. Interception data return

Through the above code, we successfully obtained some information in the request body, but how to obtain the return information? Since ULS is an asynchronous framework, the response will be pushed to the callback function, and we must intercept it in the callback function. In order to achieve this function, we need to implement the NSURLSessionDataDelegate delegation protocol.

#pragma mark - NSURLSessionTaskDelegate
 
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
    if (!error) {
        [self.client URLProtocolDidFinishLoading:self];
    } else if ([error.domain isEqualToString:NSURLErrorDomain] & amp; & amp; error.code == NSURLErrorCancelled) {
    } else {
        [self.client URLProtocol:self didFailWithError:error];
    }
    self.dataTask = nil;
}
#pragma mark - NSURLSessionDataDelegate
// When the server returns information, this callback function will be called by ULS, and interception of http return information is implemented here.
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask
    didReceiveData:(NSData *)data {
    // Return the data received by the URL Loading System. This is very important. Otherwise, just intercepting and not returning it will be blind.
    [self.client URLProtocol:self didLoadData:data];
    
    //Print return data
    NSString *dataStr = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
    if (dataStr) {
        NSLog(@"***ZJH intercept data: %@", dataStr);
    }
}
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(NSURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition))completionHandler {
    [[self client] URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageAllowed];
    completionHandler(NSURLSessionResponseAllow);
    self.response = response;
}
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task willPerformHTTPRedirection:(NSHTTPURLResponse *)response newRequest:(NSURLRequest *)request completionHandler:(void (^)(NSURLRequest * _Nullable))completionHandler {
    if (response != nil){
        self.response = response;
        [[self client] URLProtocol:self wasRedirectedToRequest:request redirectResponse:response];
    }
}

In fact, from the above code, we can see that we just performed a transfer process in our own customized protocol, and no other operations were performed.

In this way, the basic protocol has been implemented, so how to intercept the network. We need to register our customized ZJHURLProtocol into our network loading system through NSURLProtocol, telling the system that our network request processing class is no longer the default NSURLProtocol, but our customized ZJHURLProtocol

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    [NSURLProtocol registerClass:[ZJHURLProtocol class]];
    return YES;
}

2. Monitor AFNETWorking network requests

So far, our code above has been able to monitor most network requests. However, if you use AFNETworking, you will find that your code is not called at all. In fact, ULS allows loading multiple NSURLProtocols, which are stored in an array. By default, AFNETWorking will only use the first protocol in the array.

For network requests initiated by NSURLSession, we found that network requests initiated by sessions obtained through shared can be monitored, but we cannot monitor the sessions obtained through the method sessionWithConfiguration:delegate:delegateQueue:. The reason lies in NSURLSessionConfiguration. Let’s go into NSURLSessionConfiguration and take a look. It has an attribute

@property(nullable, copy) NSArray<Class> *protocolClasses;

We can see that this is an NSURLProtocol array. As we mentioned above, we monitor the network by registering NSURLProtocol for network monitoring, but the session obtained through sessionWithConfiguration:delegate:delegateQueue: already has an NSURLProtocol in its configuration. So he won’t use our protocol. How to solve this problem? In fact, it is very simple. We hook the get method of the attribute protocolClasses of NSURLSessionConfiguration and return our own protocol. In this way, we can monitor the network request of the session obtained through sessionWithConfiguration:delegate:delegateQueue:

@implementation ZJHSessionConfiguration
 
 + (ZJHSessionConfiguration *)defaultConfiguration {
    static ZJHSessionConfiguration *staticConfiguration;
    static dispatch_once_t onceToken;
    dispatch_once( & amp;onceToken, ^{
        staticConfiguration=[[ZJHSessionConfiguration alloc] init];
    });
    return staticConfiguration;
}
- (instancetype)init {
    self = [super init];
    if (self) {
        self.isSwizzle = NO;
    }
    return self;
}
- (void)load {
    self.isSwizzle=YES;
    Class cls = NSClassFromString(@"__NSCFURLSessionConfiguration") ?: NSClassFromString(@"NSURLSessionConfiguration");
    [self swizzleSelector:@selector(protocolClasses) fromClass:cls toClass:[self class]];
    
}
- (void)unload {
    self.isSwizzle=NO;
    Class cls = NSClassFromString(@"__NSCFURLSessionConfiguration") ?: NSClassFromString(@"NSURLSessionConfiguration");
    [self swizzleSelector:@selector(protocolClasses) fromClass:cls toClass:[self class]];
}
- (void)swizzleSelector:(SEL)selector fromClass:(Class)original toClass:(Class)stub {
    Method originalMethod = class_getInstanceMethod(original, selector);
    Method stubMethod = class_getInstanceMethod(stub, selector);
    if (!originalMethod || !stubMethod) {
        [NSException raise:NSInternalInconsistencyException format:@"Couldn't load NEURLSessionConfiguration."];
    }
    method_exchangeImplementations(originalMethod, stubMethod);
}
- (NSArray *)protocolClasses {
    // If there are other monitoring protocols, you can also add them here.
    return @[[ZJHURLProtocol class]];
}
@end

Then start monitoring and cancel monitoring

/// Start monitoring
 + (void)startMonitor {
    ZJHSessionConfiguration *sessionConfiguration = [ZJHSessionConfiguration defaultConfiguration];
    [NSURLProtocol registerClass:[ZJHURLProtocol class]];
    if (![sessionConfiguration isSwizzle]) {
        [sessionConfiguration load];
    }
}
/// Stop listening
 + (void)stopMonitor {
    ZJHSessionConfiguration *sessionConfiguration = [ZJHSessionConfiguration defaultConfiguration];
    [NSURLProtocol unregisterClass:[ZJHURLProtocol class]];
    if ([sessionConfiguration isSwizzle]) {
        [sessionConfiguration unload];
    }
}

Finally, add this sentence when the program starts:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    [ZJHURLProtocol startMonitor];
    return YES;
}

In this way, a simple monitoring function is implemented. In fact, there are still countless pitfalls to be filled in order to make it practical, and the amount of code will probably increase by another 20 times. These pitfalls include: https certificate verification, NSURLConnection and NSURLSession compatibility, redirection, timeout processing, and return Value content analysis, various exception handling (you can’t let the program crash because you crash), switches, local storage strategies for intercepted information, return server strategies, etc.