問題:因dns發生域名劫持 需要手動將URL請求的域名重定向到指定的IP地址
最近在項目里由於電信那邊發生dns發生域名劫持,因此需要手動將URL請求的域名重定向到指定的IP地址,但是由於請求可能是通過NSURLConnection,NSURLSession或者AFNetworking等方式,因此要想統一進行處理,一開始是想通過Method Swizzling去hook cfnetworking底層方法,后來發現其實有個更好的方法--NSURLProtocol。
NSURLProtocol
NSURLProtocol能夠讓你去重新定義蘋果的URL加載系統 (URL Loading System)的行為,URL Loading System里有許多類用於處理URL請求,比如NSURL,NSURLRequest,NSURLConnection和NSURLSession等,當URL Loading System使用NSURLRequest去獲取資源的時候,它會創建一個NSURLProtocol子類的實例,你不應該直接實例化一個NSURLProtocol,NSURLProtocol看起來像是一個協議,但其實這是一個類,而且必須使用該類的子類,並且需要被注冊。
使用場景
不管你是通過UIWebView, NSURLConnection 或者第三方庫 (AFNetworking, MKNetworkKit等),他們都是基於NSURLConnection或者 NSURLSession實現的,因此你可以通過NSURLProtocol做自定義的操作。
- 重定向網絡請求
- 忽略網絡請求,使用本地緩存
- 自定義網絡請求的返回結果
- 攔截圖片加載請求,轉為從本地文件加載
- 一些全局的網絡請求設置
- 快速進行測試環境的切換
- 過濾掉一些非法請求
- 網絡的緩存處理(H5離線包 和 網絡圖片緩存)
- 可以攔截UIWebView,基於系統的NSURLConnection或者NSURLSession進行封裝的網絡請求。目前WKWebView無法被NSURLProtocol攔截。
- 當有多個自定義NSURLProtocol注冊到系統中的話,會按照他們注冊的反向順序依次調用URL加載流程。當其中有一個NSURLProtocol攔截到請求的話,后續的NSURLProtocol就無法攔截到該請求。
攔截網絡請求
子類化NSURLProtocol並注冊
@interface CustomURLProtocol : NSURLProtocol
@end
然后在application:didFinishLaunchingWithOptions:方法中注冊該CustomURLProtocol,一旦注冊完畢后,它就有機會來處理所有交付給URL Loading system的網絡請求。
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { //注冊protocol [NSURLProtocol registerClass:[CustomURLProtocol class]]; return YES; }
實現CustomURLProtocol
#import "CustomURLProtocol.h" static NSString* const URLProtocolHandledKey = @"URLProtocolHandledKey"; @interface CustomURLProtocol ()<NSURLConnectionDelegate> @property (nonatomic, strong) NSURLConnection *connection; @end @implementation CustomURLProtocol //這里面寫重寫的方法 @end
注冊好了之后,現在可以開始實現NSURLProtocol的一些方法:
- +canInitWithRequest:
這個方法主要是說明你是否打算處理對應的request,如果不打算處理,返回NO,URL Loading System會使用系統默認的行為去處理;如果打算處理,返回YES,然后你就需要處理該請求的所有東西,包括獲取請求數據並返回給 URL Loading System。
網絡數據可以簡單的通過NSURLConnection去獲取,而且每個NSURLProtocol對象都有一個NSURLProtocolClient實例,可以通過該client將獲取到的數據返回給URL Loading System。
這里有個需要注意的地方,想象一下,當你去加載一個URL資源的時候,URL Loading System會詢問CustomURLProtocol是否能處理該請求,你返回YES,然后URL Loading System會創建一個CustomURLProtocol實例然后調用NSURLConnection去獲取數據,然而這也會調用URL Loading System,而你在+canInitWithRequest:中又總是返回YES,這樣URL Loading System又會創建一個CustomURLProtocol實例導致無限循環。我們應該保證每個request只被處理一次,可以通過+setProperty:forKey:inRequest:標示那些已經處理過的request,然后在+canInitWithRequest:中查詢該request是否已經處理過了,如果是則返回NO。+ (BOOL)canInitWithRequest:(NSURLRequest *)request { //只處理http和https請求 返回NO默認讓系統去處理 NSString *scheme = [[request URL] scheme]; if ( ([scheme caseInsensitiveCompare:@"http"] == NSOrderedSame || [scheme caseInsensitiveCompare:@"https"] == NSOrderedSame)) { //看看是否已經處理過了,防止無限循環 根據業務來截取 if ([NSURLProtocol propertyForKey:URLProtocolHandledKey inRequest:request]) { return NO; } //還要在這里截取DSN解析請求中的鏈接 判斷攔截域名請求的鏈接如果是返回NO if (判斷攔截域名請求的鏈接) { return NO; } return YES; } return NO; }
- +canonicalRequestForRequest 如果沒有特殊需求,直接返回request就可以了, 可以在開始加載中startLoading方法中 修改request,比如添加header,修改host,請求重定向等
+ (NSURLRequest *) canonicalRequestForRequest:(NSURLRequest *)request { return request;
}
- +requestIsCacheEquivalent:toRequest:
主要判斷兩個request是否相同,如果相同的話可以使用緩存數據,通常只需要調用父類的實現。
+ (BOOL)requestIsCacheEquivalent:(NSURLRequest *)a toRequest:(NSURLRequest *)b { return [super requestIsCacheEquivalent:a toRequest:b]; }
- -startLoading -stopLoading
這兩個方法主要是開始和取消相應的request,而且需要標示那些已經處理過的request。- (void)startLoading { NSMutableURLRequest *mutableReqeust = [[self request] mutableCopy]; //標示改request已經處理過了,防止無限循環 [NSURLProtocol setProperty:@YES forKey:URLProtocolHandledKey inRequest:mutableReqeust]; //重定向 self.connection = [NSURLConnection connectionWithRequest:[self dealDNS: mutableReqeust] delegate:self]; } - (void)stopLoading { [self.connection cancel]; self.connection = nil ; } //解決劫持 重定向到ip地址 - (NSMutableURLRequest *)dealDNS:(NSMutableURLRequest *)request{ if ([request.URL host].length == 0) { return request; } NSString *originUrlString = [request.URL absoluteString]; NSString *originHostString = [request.URL host]; NSRange hostRange = [originUrlString rangeOfString:originHostString]; if (hostRange.location == NSNotFound) { return request; } //根據當前host(如baidu.com)請求獲取到IP(如172.128.3.3) 並替換到URL中 //定向到IP 改IP需要根據 host 去請求獲取到 NSString *ip = 根據 host請求獲取; // (如baidu.com)同步請求獲取到IP(如172.128.3.3) 注意⚠️這個請求的鏈接需要在canInitWithRequest里面過濾掉 // 替換域名 //URL:https://www.baidu.com 替換成了https://172.128.3.3 NSString *urlString = [originUrlString stringByReplacingCharactersInRange:hostRange withString:ip]; NSURL *url = [NSURL URLWithString:urlString]; request.URL = url; //給改IP設置Cookie [CookieManager setWebViewCookieForDomain:ipUrl.host]; return request; }
- NSURLConnectionDataDelegate方法
在處理網絡請求的時候會調用到該代理方法,我們需要將收到的消息通過client返回給URL Loading System。
- (void) connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response { [self.client URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed]; } - (void) connection:(NSURLConnection *)connection didReceiveData:(NSData *)data { [self.client URLProtocol:self didLoadData:data]; } - (void) connectionDidFinishLoading:(NSURLConnection *)connection { [self.client URLProtocolDidFinishLoading:self]; } - (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error { [self.client URLProtocol:self didFailWithError:error]; } //解決發送IP地址的HTTPS請求 證書驗證 - (void)connection:(NSURLConnection *)connection willSendRequestForAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge{ if (!challenge) { return; } if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) { //構造一個NSURLCredential發送給發起方 NSURLCredential *credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust]; [[challenge sender] useCredential:credential forAuthenticationChallenge:challenge]; } else { //對於其他驗證方法直接進行處理流程 [[challenge sender] continueWithoutCredentialForAuthenticationChallenge:challenge]; } }
注意⚠️:這里面在 手動將URL請求的域名重定向到指定的IP地址 去請求的時候需要 對HTTPS請求校驗證書