研究筆記:iOS中使用WebViewProxy攔截URL請求

ruobin發表於2016-03-03

概述

先說明下iOS中載入url的正常流程:
1.客戶端傳送NSURLRequest給server
2.server返回對應的NSURLResponse

如果被WebViewProxy攔截,則流程變為:
1.客戶端傳送NSURLRequest給server
2.這個request被WebViewProxy攔截
3.proxy將修改後的新request傳送給server
4.server返回response給proxy
5.proxy將返回的資料以url response或者回撥的形式返回給客戶端。

那麼WebViewProxy的攔截原理是怎樣的呢?

攔截原理

首先,WebViewProxy定義一個自定義的protocol(NSURLProtocol的子類)

@interface WebViewProxyURLProtocol : NSURLProtocol

題外話,子類必須實現NSURLProtocol的以下幾個方法才能正常工作,當然這部分工作WebViewProxy已經都幫我們搞定了:

+ (BOOL)canInitWithRequest:(NSURLRequest *)request;
+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request;
+ (BOOL)requestIsCacheEquivalent:(NSURLRequest *)a toRequest:(NSURLRequest *)b;
- (id)initWithRequest:(NSURLRequest *)request cachedResponse:(NSCachedURLResponse *)cachedResponse client:(id<NSURLProtocolClient>)client;
- (void)startLoading;
- (void)stopLoading;

然後把URLProtocol的子類註冊到url loading system中

+ (void)initialize {
    ...
    [NSURLProtocol registerClass:[WebViewProxyURLProtocol class]];
}

這樣,以後app每次傳送url request時都檢查此request是否適用於WebViewProxyURLProtocol(過濾條件也是使用者定義的),如果符合篩選條件,則使用WebViewProxyURLProtocol來載入該url。

這裡需要注意下,原始的request和修改後的新request,可能都符合攔截條件,所以為了不使其無限制的攔截並迴圈處理下去,需要設定一個檢測條件,保證每個request至多被處理一次。流程見下圖。
Copy_of_Copy_of_CLTV_DOL_Flowchart

攔截的原理依據如下(NSURLProtocol的registerClass方法):

+ (BOOL)registerClass:(Class)protocolClass

When the URL loading system begins to load a request, each registered protocol class is consulted in turn to see if it can be initialized with the specified request. The first NSURLProtocol subclass to return YES when sent a canInitWithRequest: message is used to perform the URL load. There is no guarantee that all registered protocol classes will be consulted.
Classes are consulted in the reverse order of their registration.

使用舉例

簡單的攔截例子:

[WebViewProxy handleRequestsMatching:[NSPredicate predicateWithFormat:@"host MATCHES[cd] `[foo|bar]`"]  handler:^(NSURLRequest* req, WVPResponse *res) {
    [res respondWithText:@"Hi!"];
}];

簡單的轉發例子:

[WebViewProxy handleRequestsWithHost:@"example.proxy" handler:^(NSURLRequest *req, WVPResponse *res) {
    NSString* proxyUrl = [req.URL.absoluteString stringByReplacingOccurrencesOfString:@"example.proxy" withString:@"example.com"];
    NSURLRequest* proxyReq = [NSURLRequest requestWithURL:[NSURL URLWithString:proxyUrl]];
    [NSURLConnection connectionWithRequest:proxyReq delegate:res];
}];

更多詳見https://github.com/marcuswestin/WebViewProxy
值得注意的是處理server返回的response時,有三套api可供選擇:

High level API返回image, text, html or json

Low level API返回HTTP頭和NSData

Piping API:從NSURLConnection返回data/error

注意事項:

註冊自定義protocol可能和其他模組或sdk的攔截功能衝突,導致攔截無效,依據是上文中的:The first NSURLProtocol subclass to return YES when sent a canInitWithRequest: message is used to perform the URL load.

WebViewProxy每次攔截成功後,都會在請求的url尾部加上一個fragment字尾(#__webviewproxyreq__)用來標記該url已攔截,防止下次再次攔截從而造成死迴圈。這樣有個隱患,就是url會被汙染,可能影響某些正常功能。

一個解決方案是在http頭中增加一個標記欄位來表示該url已經被攔截過,從而跳出迴圈。

- (id)initWithRequest:(NSURLRequest *)request cachedResponse:(NSCachedURLResponse *)cachedResponse client:(id<NSURLProtocolClient>)client {
    ...
        // 原有邏輯,註釋掉
        //NSString* correctedFragment;
        //if (_correctedRequest.URL.fragment) {
        //    correctedFragment = @"__webviewproxyreq__";
        //} else {
        //    correctedFragment = @"#__webviewproxyreq__";
        //}
        //_correctedRequest.URL = [NSURL URLWithString:[request.URL.absoluteString stringByAppendingString:correctedFragment]];

        //使用http頭來標記的新邏輯     Add by zhouyi.
        [_correctedRequest addValue:[@(YES) stringValue] forHTTPHeaderField:webViewProxyFlagKey];
    ...
}
+ (BOOL)canInitWithRequest:(NSURLRequest *)request
{
   ...
    // 新增標記欄位,表示已經被WebViewProxy處理過
    // Add by zhouyi.
    NSString* proxyFlag = request.allHTTPHeaderFields[webViewProxyFlagKey];
    if (proxyFlag)
    {
        return NO;
    }
    // 這是原有邏輯,註釋掉
    //if ([webViewProxyLoopDetection evaluateWithObject:request.URL])
    //{
    //    return NO;
    //}
    ...
}


相關文章