不知道你們有沒有發現這樣一個現象,在打開一些網頁的時候會彈出一些與所瀏覽網頁不相關的內容好比這樣奇(se)怪(qing)的東西
javascript
如今假如咱們訪問一個網站www.baidu.com從按下回車到百度頁面顯示到咱們的電腦上會經歷以下幾個步驟html
其中第二步就是咱們所說的DNS解析過程,域名和IP地址的關係其實就是咱們的身份證號和姓名的關係,都是來標記一我的或者是一個網站的,只是IP地址\身份證號只是一串沒有意義的數字,辨識度低,又很差記,因此就會在IP上加上一個域名以便區分,或是作的更加個性化,可是若是真的要來準確的區分仍是要靠身份證號碼或者是IP的,因此DNS解析就應運而生了。java
根本緣由就是如下兩點:ios
瞭解了DNS劫持的相關資料後咱們就知道了,防止NDS劫持就要從第二步入手,由於DNS解析過程是運營商來操做的,咱們不能去幹涉他們,否則咱們也就成了劫持者了,因此咱們要作的就是在咱們請求以前對咱們的請求連接作一些修改,將咱們本來的請求連接www.baidu.com 修改成180.149.132.47,而後請求出去,這樣的話就運營商在拿到咱們的請求後發現咱們直接用的就是IP地址就會直接給咱們放行,而不會去走他本身DNS解析了,也就是說咱們把運營商要作的事情本身先作好了。不走他的DNS解析也就不會存在DNS被劫持的問題,從根本是解決了。git
咱們知道要要把項目中請求的接口替換成成IP其實很簡單,URL是字符串,域名替換IP,無非就是一個字符串替換而已,的確這塊其實沒有什麼技術含量,並且如今像阿里雲(沒開源),七牛雲(開源),等一些比較大的平臺在這方面也都有了比較成熟的解決方案,一個SDK,傳個普通的URL進去就會返回一個域名被替換成IP的URL出來,也比較好用,這裏要說一下IP地址的來源,如何拿到一個域名所對應的IP呢?這裏就是須要用到另外一個服務——HTTPDNS,國內比較有名的就是DNSPOD,包括阿里,七牛等也是使用他們的DNS服務來解析,就是這個
github
///這個請求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
其實dnspod最難的部分是接入的部分,由於不一樣的APP不一樣的網絡環境會致使各類各樣的問題,若是你是一個新的項目那麼接入難度會大大下降,由於你徹底能夠本身封裝一套網絡請求,把DNS解析相關的邏輯都封裝到本身的網絡請求中,這樣你就能夠獲得APP全部的網絡層的控制權,想幹什麼就幹什麼,可是若是是在一個已經比較完善的APP中加入DNS防劫持的話那就是比較困難,由於你不能拿到全部網絡請求的控制權這篇文章中我主要使用是NSURLProtocol + Runtime hook方式來處理這些東西的,NSURLProtocol屬於iOS黑魔法的一種能夠攔截任何從APP的 URL Loading System系統中發出的請求,其中包括以下api
若是你的請求不在以上列表中就不能進行攔截了,好比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;複製代碼
經過返回值來告訴NSUrlProtocol對進來的請求是否攔截,好比我只攔截HTTP的,或者是某個域名的請求之類
若是上面的方法返回YES那麼request會傳到這裏,這個地方一般不作處理 直接返回request
這個地方就是對咱們攔截的請求作一些處理,咱們文中所作的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在後面會有講解。
請求完畢後調用
大概的執行流程是這樣
/*! @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的格式。若是發現請求不成功就往下看吧)
[mutableRequest setValue:self.request.URL.host forHTTPHeaderField:@"HOST"];複製代碼
[mutableRequest setValue:YOUR Cookie forHTTPHeaderField:@"Cookie"];複製代碼
// 註冊自定義protocol
[NSURLProtocol registerClass:[CustomURLProtocol class]];
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
configuration.protocolClasses = @[[CustomURLProtocol class]];複製代碼
AFHTTPSessionManager * sessionManager = [AFHTTPSessionManager 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];
.
.
.
}複製代碼
+ (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;複製代碼
/*! @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);複製代碼
[self swizzle_initWithURL:videoURL options:@{AVURLAssetHTTPHeaderFieldsKey : @{@"Host":host}}]複製代碼
//加密後的KEY
const NSString * headerKey = @"35905FF45AFA4C579B7DE2403C7CA0CCB59AA83D660E60C9D444AFE13323618F";
.
.
.
//getRequestHeaderKey方法爲解密方法
return [self swizzle_initWithURL:videoURL options:@{[self getRequestHeaderKey] : @{@"Host":host}}];複製代碼
######三行文字加三個連接就完事了。其實在遇到這個坑的時候我也查過不少相關資料,無非就是這三行話加這三個連接複製來複制去,沒有實質性的進展,大部分公司或者是項目沒有這麼重的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];
}複製代碼
完成了以上的步驟以後你回發如今DNS壞掉的狀況下手機裏面除了微信QQ(他們也作了DNS解析)以外其餘應用都不能上網了可是你的App依然能夠正常瀏覽網絡數據。這就是我最近在作的時候遇到的一些問題,有什麼問題及時與我交流吧。
juejin.im/post/58d8e9…