多是最全的iOS端HttpDns集成方案 | 掘金技術徵文

科普片

一、DNS劫持的危害

  不知道你們有沒有發現這樣一個現象,在打開一些網頁的時候會彈出一些與所瀏覽網頁不相關的內容好比這樣奇(se)怪(qing)的東西
javascript

圖一
或者這樣
圖二
,其實形成這樣的緣由就是DNS劫持,在咱們正常瀏覽的網頁連接裏面被惡意插入一些奇怪的東西。不止是這些,DNS劫持還會對咱們的我的信息安全形成很大的傷害,釣魚網站之類的,也許咱們所訪問的網站根本不是咱們須要的網站,或者根本打不開網頁,有時還會消耗咱們過多的流量。

二、什麼是DNS解析

  如今假如咱們訪問一個網站www.baidu.com從按下回車到百度頁面顯示到咱們的電腦上會經歷以下幾個步驟html

  • 1:計算機會向咱們的運營商(移動、電信、聯通等)發出打開www.baidu.com的請求。
  • 2:運營商收到請求後會到本身的DNS服務器中找www.baidu.com這個域名所對應的服務器的IP地址(也就是百度的服務器的IP地址),這裏好比是180.149.132.47。
  • 3:運營商用第二步獲得的IP地址去找到百度的服務器請求獲得數據後返回給咱們。

其中第二步就是咱們所說的DNS解析過程,域名和IP地址的關係其實就是咱們的身份證號和姓名的關係,都是來標記一我的或者是一個網站的,只是IP地址\身份證號只是一串沒有意義的數字,辨識度低,又很差記,因此就會在IP上加上一個域名以便區分,或是作的更加個性化,可是若是真的要來準確的區分仍是要靠身份證號碼或者是IP的,因此DNS解析就應運而生了。java

3:什麼是DNS劫持

  根本緣由就是如下兩點:ios

  • 1:惡意攻擊,攔截運營商的解析過程,把本身的非法東西嵌入其中。
  • 2:運營商爲了利益或者一些其餘的因素,容許一些第三方在本身的連接裏打打廣告之類的。

4:防止DNS劫持

  瞭解了DNS劫持的相關資料後咱們就知道了,防止NDS劫持就要從第二步入手,由於DNS解析過程是運營商來操做的,咱們不能去幹涉他們,否則咱們也就成了劫持者了,因此咱們要作的就是在咱們請求以前對咱們的請求連接作一些修改,將咱們本來的請求連接www.baidu.com 修改成180.149.132.47,而後請求出去,這樣的話就運營商在拿到咱們的請求後發現咱們直接用的就是IP地址就會直接給咱們放行,而不會去走他本身DNS解析了,也就是說咱們把運營商要作的事情本身先作好了。不走他的DNS解析也就不會存在DNS被劫持的問題,從根本是解決了。git

技術篇

5:項目中的實際操做

5.1:DNSPOD相關

  咱們知道要要把項目中請求的接口替換成成IP其實很簡單,URL是字符串,域名替換IP,無非就是一個字符串替換而已,的確這塊其實沒有什麼技術含量,並且如今像阿里雲(沒開源),七牛雲(開源),等一些比較大的平臺在這方面也都有了比較成熟的解決方案,一個SDK,傳個普通的URL進去就會返回一個域名被替換成IP的URL出來,也比較好用,這裏要說一下IP地址的來源,如何拿到一個域名所對應的IP呢?這裏就是須要用到另外一個服務——HTTPDNS,國內比較有名的就是DNSPOD,包括阿里,七牛等也是使用他們的DNS服務來解析,就是這個
github

DNSPOD logo

簡介
他會給咱們提供一個接口,咱們使用HTTP請求的方式去請求這個接口,參數帶上咱們的域名,他們就會把域名對應的IP列表返回回來。相似這樣:

///這個請求URL的結構是固定的119.29.29.29是DNSPOD固定的服務器地址,ttl參數的意思是返回結果是否帶ttl是個BOOL,dn就是咱們須要解析的域名,id就是咱們在dnspod上註冊時候他給咱們的一個KEY
NSString *url = [NSString stringWithFormat:@"http://119.29.29.29/d?ttl=1&dn=www.baidu.com&id=KEY"];
NSMutableURLRequest * request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:url] cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval:10];
NSData * data = [NSURLConnection sendSynchronousRequest:request returningResponse:nil error:&networkError];複製代碼

這裏使用同步仍是異步都是能夠的,具體根據大家業務需求。web

5.2:項目中的使用

  其實dnspod最難的部分是接入的部分,由於不一樣的APP不一樣的網絡環境會致使各類各樣的問題,若是你是一個新的項目那麼接入難度會大大下降,由於你徹底能夠本身封裝一套網絡請求,把DNS解析相關的邏輯都封裝到本身的網絡請求中,這樣你就能夠獲得APP全部的網絡層的控制權,想幹什麼就幹什麼,可是若是是在一個已經比較完善的APP中加入DNS防劫持的話那就是比較困難,由於你不能拿到全部網絡請求的控制權這篇文章中我主要使用是NSURLProtocol + Runtime hook方式來處理這些東西的,NSURLProtocol屬於iOS黑魔法的一種能夠攔截任何從APP的 URL Loading System系統中發出的請求,其中包括以下api

  • File Transfer Protocol (ftp://)
  • Hypertext Transfer Protocol (http://)
  • Hypertext Transfer Protocol with encryption (https://)
  • Local file URLs (file:///)
  • Data URLs (data://)

若是你的請求不在以上列表中就不能進行攔截了,好比WKWebview,AVPlayer(比較特殊,雖然請求也是http/https可是就是不走這套系統,蘋果爸爸就是這樣~)等,其實對於正常來講光用已經NSURLProtocol足夠了。
  NSURLProtocol這個類咱們不能直接使用,咱們須要本身建立一個他的子類而後在咱們的子類中操做他們像這樣數組

// 註冊自定義protocol
[NSURLProtocol registerClass:[CustomURLProtocol class]];
 NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
configuration.protocolClasses = @[[CustomURLProtocol class]];複製代碼

在這個類中咱們能夠攔截到請求,而後進行處理。這個類中有四個很是重要的方法瀏覽器

+ (BOOL)canInitWithRequest:(NSURLRequest *)request;
+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request;
- (void)startLoading;
//對於攔截的請求,NSURLProtocol對象在中止加載時調用該方法
- (void)stopLoading;複製代碼
+ (BOOL)canInitWithRequest:(NSURLRequest *)request;

經過返回值來告訴NSUrlProtocol對進來的請求是否攔截,好比我只攔截HTTP的,或者是某個域名的請求之類

+ (NSURLRequest )canonicalRequestForRequest:(NSURLRequest )request;

若是上面的方法返回YES那麼request會傳到這裏,這個地方一般不作處理 直接返回request

- (void)startLoading;

這個地方就是對咱們攔截的請求作一些處理,咱們文中所作的IP對域名的替換就在這裏進行,處理完以後將請求轉發出去,好比這樣

- (void)startLoading {
///其中customRequest是處理過的請求(域名替換後的)
    NSURLSession *session = [NSURLSession sessionWithConfiguration:[[NSURLSessionConfiguration alloc] init] delegate:self delegateQueue:nil];
    NSURLSessionDataTask *task = [session dataTaskWithRequest:customRequest];
    [task resume];
}複製代碼

你能夠在 - startLoading 中使用任何方法來對協議對象持有的 request 進行轉發,包括 NSURLSession、 NSURLConnection 甚至使用 AFNetworking 等網絡庫,只要你能在回調方法中把數據傳回 client,幫助其正確渲染就能夠,好比這樣:

- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(NSURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition))completionHandler {
    [[self client] URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageAllowed];

    completionHandler(NSURLSessionResponseAllow);
}

- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data {
    [[self client] URLProtocol:self didLoadData:data];
}複製代碼

client在後面會有講解。

- (void)stopLoading;

請求完畢後調用
大概的執行流程是這樣

流程

在NSURLProtocol中有一個貫穿始終的變量

/*! @method client @abstract Returns the NSURLProtocolClient of the receiver. @result The NSURLProtocolClient of the receiver. */
@property (nullable, readonly, retain) id <NSURLProtocolClient> client;複製代碼

你能夠認爲是這個是請求的發送者,打個比方,A想給B發送一個消息,因爲距離遙遠因而A去了郵局,A把消息內容告訴了郵局,而且A在郵局登記了本身名字方便B有反饋的時候郵局來通知A查收。這個例子中郵局就是NSURLProtocol,A在郵局登記的名字就是client。全部的 client 都實現了 NSURLProtocolClient 協議,協議的做用就是在 HTTP 請求發出以及接受響應時向其它對象傳輸數據:

@protocol NSURLProtocolClient <NSObject>
...
- (void)URLProtocol:(NSURLProtocol *)protocol didReceiveResponse:(NSURLResponse *)response cacheStoragePolicy:(NSURLCacheStoragePolicy)policy;

- (void)URLProtocol:(NSURLProtocol *)protocol didLoadData:(NSData *)data;

- (void)URLProtocolDidFinishLoading:(NSURLProtocol *)protocol;
...
@end複製代碼

固然這個協議中還有不少其餘的方法,好比 HTTPS 驗證、重定向以及響應緩存相關的方法,你須要在合適的時候調用這些代理方法,對信息進行傳遞。
到此正常狀況下的DNS的解析過程已經結束,若是你發現按照如上操做以後並無達到預期效果那麼請往下看,(一般狀況下完成以上操做 原有的URL的就會變成http://123.456.789.123/XXX/XXX/XXX的格式。若是發現請求不成功就往下看吧)

6:遇到的坑點

  6.1:咱們知道運營商原本是根據域名來肯定一個URL的,咱們將域名改成IP以後雖然不用運營商幫咱們解析了,可是運營商在收到一串數字的時候也是懵逼狀態,咱們仍是須要將域名傳給他們,可是不能用正常的方式傳,咱們須要把原來的域名加到http請求的Header中的host字段下,根據Http協議的規定,若是在URL中沒法找到域名的話就會去Header中找,這樣一來咱們既把域名告訴了運營商同時也直接制定了IP地址,這個是必須配置的,否則的話是請求不成功的。
[mutableRequest setValue:self.request.URL.host forHTTPHeaderField:@"HOST"];複製代碼
[mutableRequest setValue:YOUR Cookie forHTTPHeaderField:@"Cookie"];複製代碼
  6.2:關於AfNetworking的問題,如今大部分網絡請求是基於Afnetworking的,這裏有一個坑,咱們知道咱們註冊CustomProtocol的時候是這樣
// 註冊自定義protocol
[NSURLProtocol registerClass:[CustomURLProtocol class]];
 NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
configuration.protocolClasses = @[[CustomURLProtocol class]];複製代碼
在系統的configuration加入咱們的CustomProtocol,protocolClasses是一個數組裏面能夠放不少各類不一樣的CustomProtocol,咱們看一下afnetworking的初始化方法。
AFHTTPSessionManager * sessionManager = [AFHTTPSessionManager manager];複製代碼
我相信你們一般都會這麼來建立,可是這裏我要說下manager並非一個單利,最後都會調到一個方法
- (instancetype)initWithSessionConfiguration:(NSURLSessionConfiguration *)configuration {
    self = [super init];
    if (!self) {
        return nil;
    }

    if (!configuration) {
        configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
    }

    self.sessionConfiguration = configuration;
    self.operationQueue = [[NSOperationQueue alloc] init];
    self.operationQueue.maxConcurrentOperationCount = 1;

    self.session = [NSURLSession sessionWithConfiguration:self.sessionConfiguration delegate:self delegateQueue:self.operationQueue];
    .
    .
    .
}複製代碼
你們注意第二個判斷,若是沒有傳入configuration的話他會建立一個默認的,這樣以致於咱們以前在configuration的protocolClasses中註冊類所有被這個新的configuration替換掉了,因此沒法解析。這裏我採起的辦法就是runtime hook,由於hook第三方的代碼並非一個很好的辦法,因此我直接hook NSURLSession的sessionWithConfiguration方法,由於經過觀察Afnetworking的源碼最終都是走到這裏的。Hook以後把本身的configuration換進去,像這樣
+ (NSURLSession *)swizzle_sessionWithConfiguration:(NSURLSessionConfiguration *)configuration {

    NSURLSessionConfiguration *newConfiguration = configuration;
    // 在現有的Configuration中插入咱們自定義的protocol
    if (configuration) {
        NSMutableArray *protocolArray = [NSMutableArray arrayWithArray:configuration.protocolClasses];
        [protocolArray insertObject:[CustomProtocol class] atIndex:0];
        newConfiguration.protocolClasses = protocolArray;
    }
    else {
        newConfiguration = [NSURLSessionConfiguration defaultSessionConfiguration];
        NSMutableArray *protocolArray = [NSMutableArray arrayWithArray:configuration.protocolClasses];
        [protocolArray insertObject:[CustomProtocol class] atIndex:0];
        newConfiguration.protocolClasses = protocolArray;
    }

    return [self swizzle_sessionWithConfiguration:newConfiguration];
}複製代碼
而後就完美解決了。不過要注意下系統的是有兩個方法的
/* * Customization of NSURLSession occurs during creation of a new session. * If you only need to use the convenience routines with custom * configuration options it is not necessary to specify a delegate. * If you do specify a delegate, the delegate will be retained until after * the delegate has been sent the URLSession:didBecomeInvalidWithError: message. */
+ (NSURLSession *)sessionWithConfiguration:(NSURLSessionConfiguration *)configuration;
+ (NSURLSession *)sessionWithConfiguration:(NSURLSessionConfiguration *)configuration delegate:(nullable id <NSURLSessionDelegate>)delegate delegateQueue:(nullable NSOperationQueue *)queue;複製代碼
這兩個方法不能肯定最終會走那個,因此爲了保險起見都hook下,hook的方式是同樣的
  6.3:AVPlayer請求,AVPlayer是咱們iOS系統中系統自帶的播放視頻的框架,用到地方也不少,可是這個是比較坑的,由於AVPlayer雖然也有http/https/file……請求這個概念,可是AVPlayer全部的請求都不會走URL Loading System,也就是說全部由AVPlayer發出的請求都不能被咱們的CustomProtocol攔截,這時候你們也許會問,不對呀,咱們正常調試的時候能夠被攔截到的啊。其實蘋果官方上是說AVPlayer在真機調試和模擬器調試時候走的徹底不是一套策略,也就是說在模擬器運行時候是徹底正常的,能夠被攔截到也能夠被解析,可是在真機上面就偏偏相反了,由於咱們最後仍是以真機爲準,因此咱們採起的辦法仍是hook,由於咱們須要在媒體URL傳給AVPlayer前就要將相關東西配置好,域名替換啊,加host啊之類的,因此咱們要找AVPlayer的入口,先看初始化方法,我發現項目中使用一個AVURLAsset來初始化AVPlayer,那麼AVURLAsset又是什麼呢?繼續查到AVURLAsset的初始化方法,能夠發現這個方法:
/*! @method initWithURL:options: @abstract Initializes an instance of AVURLAsset for inspection of a media resource. @param URL An instance of NSURL that references a media resource. @param options An instance of NSDictionary that contains keys for specifying options for the initialization of the AVURLAsset. See AVURLAssetPreferPreciseDurationAndTimingKey and AVURLAssetReferenceRestrictionsKey above. @result An instance of AVURLAsset. */
- (instancetype)initWithURL:(NSURL *)URL options:(nullable NSDictionary<NSString *, id> *)options NS_DESIGNATED_INITIALIZER;複製代碼
AVF_EXPORT NSString *const AVURLAssetPreferPreciseDurationAndTimingKey NS_AVAILABLE(10_7, 4_0);
AVF_EXPORT NSString *const AVURLAssetReferenceRestrictionsKey NS_AVAILABLE(10_7, 5_0);
AVF_EXPORT NSString *const AVURLAssetHTTPCookiesKey NS_AVAILABLE_IOS(8_0);
AVF_EXPORT NSString *const AVURLAssetAllowsCellularAccessKey NS_AVAILABLE_IOS(10_0);複製代碼
可是並無發現和Host相關的Key,其實這個key是有的就是AVURLAssetHTTPHeaderFieldsKey只是由於這個Key沒暴露出來。這個地方不太肯定是否是蘋果的私有API,網上查了大量的資料也沒有個說法,甚至我親自去蘋果開發者去問,蘋果也沒有給任何答覆,各類說法都有,具體使用的話就是
[self swizzle_initWithURL:videoURL options:@{AVURLAssetHTTPHeaderFieldsKey : @{@"Host":host}}]複製代碼
這樣使用是沒有任何問題的,可是畢竟是沒有暴露出來的方法,咱們不能這樣明目張膽的使用,其實對於字符串來講仍是比較好規避的,只要不要明文出現這個KEY就能夠,我在這裏使用了一個加密,吧key變成密文而後這個地方經過解密獲取,就像這樣:
//加密後的KEY
const NSString * headerKey = @"35905FF45AFA4C579B7DE2403C7CA0CCB59AA83D660E60C9D444AFE13323618F";
.
.
.
//getRequestHeaderKey方法爲解密方法
return [self swizzle_initWithURL:videoURL options:@{[self getRequestHeaderKey] : @{@"Host":host}}];複製代碼
這樣以後就大功告成了,AVPlayer能夠在DNS被劫持的狀況下播放了,
  6.4:POST請求這塊也算是一個大坑,咱們知道http的post請求會包含一個body體,裏面包含咱們須要上傳的參數等一些資料,對於POST請求咱們的NSURLProtocol是能夠正常攔截的,可是咱們攔截以後發現不管怎麼樣咱們得到的body體都爲nil!後來查了一些資料發下又是蘋果爸爸在作手腳。NSURLProtocol在攔截NSURLSession的POST請求時不能獲取到Request中的HTTPBody,這個貌似早就國外的論壇上傳開了,但國內好像還鮮有人知,據蘋果官方的解釋是Body是NSData類型,便可能爲二進制內容,並且尚未大小限制,因此可能會很大,爲了性能考慮,索性就攔截時就不拷貝了(內流滿面臉)。爲了解決這個問題,咱們能夠經過把Body數據放到Header中,不過Header的大小好像是有限制的,我試過2M是沒有問題,不過超過10M就直接Request timeout了。。。並且當Body數據爲二進制數據時這招也沒轍了,由於Header裏都是文本數據,另外一種方案就是用一個NSDictionary或NSCache保存沒有請求的Body數據,用URL爲key,最後方法就是別用NSURLSession,老老實實用古老的NSURLConnection算了。。。
  6.5:WKWebview是新出的瀏覽器控件,這裏就很少說了,WKWebview不走URL Loading System,因此也不會被攔截,不過也是有辦法的,可是由於此次項目中沒有用到,因此沒有過多的去研究,後續我會寫一篇關於這個博客,不是很難,依舊是runtime大法。
  6.6:SNI環境,這個但是坑了我很久很久的東西,因此我會放在最後去說,SNI環境由於涉及到證書驗證因此是在https的基礎上來講的,SNI(Server Name Indication)是爲了解決一個服務器使用多個域名和證書的擴展。一句話簡述它的工做原理就是,在鏈接到服務器創建SSL連接以前先發送要訪問站點的域名(Hostname),這樣服務器根據這個域名返回一個合適的證書。其實關於SNI環境在這裏就不過多解釋,阿里雲文檔有很明白的解釋,同時他也有安卓和iOS在SNI環境下的處理文檔,咱們發現安卓部分寫的很詳細,但是已到了iOS這邊就這樣了:

阿里雲文檔截圖

######三行文字加三個連接就完事了。其實在遇到這個坑的時候我也查過不少相關資料,無非就是這三行話加這三個連接複製來複制去,沒有實質性的進展,大部分公司或者是項目沒有這麼重的Httpdns需求,因此也就不會有這個環境,即便遇到了也就直接關閉httpdns了,後來只能本身去用CFNetwork一點點實現。具體代碼就不跟你們粘貼了由於涉及到一些公司內部的代碼,不過我會把我主要的參考資料發給你們。這裏有個小技巧,由於都在說CFNetwork是比較底層的網絡實現,好多東西須要開發者自行處理好比一些變量的釋放之類的,因此咱們能少用盡可能少用,由於Cfnetwork是爲SNI(https)環境服務,因此咱們在攔截判斷的時候能夠區分是用上層的網絡請求轉發仍是用底層的cfnetwork來轉發,

if ([self.request.URL.scheme isEqualToString:@"https"] ) {
//使用CFnetwork
        curRequest = req;
        self.task = [[CustomCFNetworkRequestTask alloc] initWithURLRequest:originalRequest swizzleRequest:curRequest delegate:self];
        if (self.task) {
            [self.task startLoading];
        }
    } else {
//使用普通網絡請求
        NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
        self.session = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:[NSOperationQueue mainQueue]];
        NSURLSessionTask *task = [self.session dataTaskWithRequest:req];
        [task resume];
    }複製代碼
我是這麼作的。

7:總結

  完成了以上的步驟以後你回發如今DNS壞掉的狀況下手機裏面除了微信QQ(他們也作了DNS解析)以外其餘應用都不能上網了可是你的App依然能夠正常瀏覽網絡數據。這就是我最近在作的時候遇到的一些問題,有什麼問題及時與我交流吧。
  juejin.im/post/58d8e9…

相關文章
相關標籤/搜索