1. 程式人生 > >iOS應用內抓包、NSURLProtocol 攔截 APP 內的網絡請求

iOS應用內抓包、NSURLProtocol 攔截 APP 內的網絡請求

nslog 常用 add and static 委托 config arr hang

前言

開發中遇到需要獲取SDK中的數據,由於無法看到代碼,所以只能通過監聽所有的網絡請求數據,截取相應的返回數據,可以通過NSURLProtocol實現,還可用於與H5的交互

一、NSURLProtocol攔截請求

1、NSURLProtoco簡介

NSURLProtocol的官方定義

An NSURLProtocol object handles the loading of protocol-specific URL data.
The NSURLProtocol class itself is an abstract class that provides the infrastructure
for processing URLs with a specific URL scheme.
You create subclasses for any custom protocols or URL schemes that your app supports. 

iOS的Foundation框架提供了 URL Loading System 這個庫(後面簡寫為ULS),所有基於URL(例如http://,https:// ,ftp://這些應用層的傳輸協議)的協議都可以通過ULS提供的基礎類和協議來實現,你甚至可以自定義自己的私有應用層通訊協議。

而ULS庫裏提供了一個強有力的武器 NSURLProtocol。 繼承NSURLProtocol 的子類都可以實現截取行為,具體的方式就是:如果註冊了某個NSURLProtocol子類,ULS管理的流量都會先交由這個子類處理,這相當於實現了一個攔截器。由於現在處於統治地位的的http client庫 AFNetworking和 Alamofire 都是基於 URL Loading System實現的,所以他們倆和使用基礎URL Loading System API產生的流量理論上都可以被截取到。

註意一點,NSURLProtocol是一個抽象類,而不是一個協議(protocol)。

其實NSURLProtocol這個東西的作用就是讓我們在app的內部攔截一切url請求(註意,不只是webView內的請求,而是整個app內的所有請求),如果篩選出來自己感興趣的東西去處理,不感興趣的就放過去就是了。既然能攔截,那麽我們至少能做兩件事,第一是攔截現有的url請求,比如常用的http://。第二就是我們可以自定義url協議了,比如boris://

舉幾個例子:

  • 我們的APP內的所有請求都需要增加公共的頭,像這種我們就可以直接通過NSURLProtocol來實現,當然實現的方式有很多種
  • 再比如我們需要將APP某個API進行一些訪問的統計
  • 再比如我們需要統計APP內的網絡請求失敗率
2、攔截數據請求

在NSURLProtocol中,我們需要告訴它哪些網絡請求是需要我們攔截的,這個是通過方法canInitWithRequest:來實現的,比如我們現在需要攔截全部的HTTP和HTTPS請求,那麽這個邏輯我們就可以在canInitWithRequest:中來定義.

重點說一下標簽kProtocolHandledKey:每當需要加載一個URL資源時,URL Loading System會詢問ZJHURLProtocol是否處理,如果返回YES,URL Loading System會創建一個ZJHURLProtocol實例,實例做完攔截工作後,會重新調用原有的方法,如session GET,URL Loading System會再一次被調用,如果在+canInitWithRequest:中總是返回YES,這樣URL Loading System又會創建一個ZJHURLProtocol實例。。。。這樣就導致了無限循環。為了避免這種問題,我們可以利用+setProperty:forKey:inRequest:來給被處理過的請求打標簽,然後在+canInitWithRequest:中查詢該request是否已經處理過了,如果是則返回NO。 上文中的kProtocolHandledKey就是打的一個標簽,標簽是一個字符串,可以任意取名。而這個打標簽的方法,通常會在

/**
 需要控制的請求
 
 @param request 此次請求
 @return 是否需要監控
 */
+ (BOOL)canInitWithRequest:(NSURLRequest *)request {
    // 如果是已經攔截過的就放行,避免出現死循環
    if ([NSURLProtocol propertyForKey:kProtocolHandledKey inRequest:request] ) {
        return NO;
    }
    
    // 不是網絡請求,不處理
    if (![request.URL.scheme isEqualToString:@"http"] &&
        ![request.URL.scheme isEqualToString:@"https"]) {
        return NO;
    }
    
    // 攔截所有
    return YES;
}

在方法canonicalRequestForRequest:中,我們可以自定義當前的請求request,當然如果不需要自定義,直接返回就行

/**
 設置我們自己的自定義請求
 可以在這裏統一加上頭之類的
 
 @param request 應用的此次請求
 @return 我們自定義的請求
 */
+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request {
    NSMutableURLRequest *mutableReqeust = [request mutableCopy];
    // 設置已處理標誌
    [NSURLProtocol setProperty:@(YES)
                        forKey:kProtocolHandledKey
                     inRequest:mutableReqeust];
    return [mutableReqeust copy];
}

接下來,就是需要將這個request發送出去了,因為如果我們不處理這個request請求,系統會自動發出這個網絡請求,但是當我們處理了這個請求,就需要我們手動來進行發送了。

我們要手動發送這個網絡請求,需要重寫startLoading方法

// 重新父類的開始加載方法
- (void)startLoading {
    NSLog(@"***ZJH 監聽接口:%@", 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];
}

當然,有start就有stop,stop就很簡單了

// 結束加載
- (void)stopLoading {
    [self.dataTask cancel];
}
3、攔截數據返回

通過上述代碼,我們成功的獲取請求體的一些信息,但是如何獲取返回信息呢?由於ULS是異步框架,所以,響應會推給回調函數,我們必須在回調函數裏進行截取。為了實現這一功能,我們需要實現 NSURLSessionDataDelegate 這個委托協議。

#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] && error.code == NSURLErrorCancelled) {
    } else {
        [self.client URLProtocol:self didFailWithError:error];
    }
    self.dataTask = nil;
}

#pragma mark - NSURLSessionDataDelegate

// 當服務端返回信息時,這個回調函數會被ULS調用,在這裏實現http返回信息的截
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask
    didReceiveData:(NSData *)data {
    // 返回給URL Loading System接收到的數據,這個很重要,不然光截取不返回,就瞎了。
    [self.client URLProtocol:self didLoadData:data];
    
    // 打印返回數據
    NSString *dataStr = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
    if (dataStr) {
        NSLog(@"***ZJH 截取數據 : %@", 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];
    }
}

其實從上面的代碼,我們可以看出,我們就是在我們自己自定義的protocol中進行了一個傳遞過程,其他的也沒有做操作

這樣,基本的protocol就已經實現完成,那麽怎樣來攔截網絡。我們需要將我們自定義的ZJHURLProtocol通過NSURLProtocol註冊到我們的網絡加載系統中,告訴系統我們的網絡請求處理類不再是默認的NSURLProtocol,而是我們自定義的ZJHURLProtocol

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

二、監聽AFNETWorking網絡請求

目前為止,我們上面的代碼已經能夠監控到絕大部分的網絡請求。但是呢,如果你使用AFNETworking,你會發現,你的代碼根本沒有被調用。實際上 ULS允許加載多個NSURLProtocol,它們被存在一個數組裏,默認情況下,AFNETWorking只會使用數組裏的第一個protocol。

對於NSURLSession發起的網絡請求,我們發現通過shared得到的session發起的網絡請求都能夠監聽到,但是通過方法sessionWithConfiguration:delegate:delegateQueue:得到的session,我們是不能監聽到的,原因就出在NSURLSessionConfiguration上,我們進到NSURLSessionConfiguration裏面看一下,他有一個屬性

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

我們能夠看出,這是一個NSURLProtocol數組,上面我們提到了,我們監控網絡是通過註冊NSURLProtocol來進行網絡監控的,但是通過sessionWithConfiguration:delegate:delegateQueue:得到的session,他的configuration中已經有一個NSURLProtocol,所以他不會走我們的protocol來,怎麽解決這個問題呢? 其實很簡單,我們將NSURLSessionConfiguration的屬性protocolClasses的get方法hook掉,通過返回我們自己的protocol,這樣,我們就能夠監控到通過sessionWithConfiguration:delegate:delegateQueue:得到的session的網絡請求

@implementation ZJHSessionConfiguration

+ (ZJHSessionConfiguration *)defaultConfiguration {
    static ZJHSessionConfiguration *staticConfiguration;
    static dispatch_once_t onceToken;
    dispatch_once(&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 {
    // 如果還有其他的監控protocol,也可以在這裏加進去
    return @[[ZJHURLProtocol class]];
}

@end

然後是開始監聽與取消監聽

/// 開始監聽
+ (void)startMonitor {
    ZJHSessionConfiguration *sessionConfiguration = [ZJHSessionConfiguration defaultConfiguration];
    [NSURLProtocol registerClass:[ZJHURLProtocol class]];
    if (![sessionConfiguration isSwizzle]) {
        [sessionConfiguration load];
    }
}

/// 停止監聽
+ (void)stopMonitor {
    ZJHSessionConfiguration *sessionConfiguration = [ZJHSessionConfiguration defaultConfiguration];
    [NSURLProtocol unregisterClass:[ZJHURLProtocol class]];
    if ([sessionConfiguration isSwizzle]) {
        [sessionConfiguration unload];
    }
}

最後,在程序啟動的時候加入這麽一句:

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

這樣,一個簡單的監控功能就實現了。實際上,想讓它能夠變得實用起來還有無數的坑要填,代碼量大概再增加20倍吧,這些坑包括:https的證書校驗,NSURLConnection和NSURLSession兼容,重定向,超時處理,返回值內容解析,各種異常處理(不能因為你崩了讓程序跟著崩了),開關,截獲的信息本地存儲策略,回傳服務端策略等

參考鏈接:
使用 NSURLProtocol 攔截 APP 內的網絡請求
iOS 開發中使用 NSURLProtocol 攔截 HTTP 請求
iOS 測試 在 iOS 設備內截取 HTTP/HTTPS 信息
iOS 性能監控方案 Wedjat(下篇)
NSURLProtocol 的使用和封裝



作者:張聰2018
鏈接:https://www.jianshu.com/p/297fbff8c954
來源:簡書
簡書著作權歸作者所有,任何形式的轉載都請聯系作者獲得授權並註明出處。

iOS應用內抓包、NSURLProtocol 攔截 APP 內的網絡請求