多是最全的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…

相关文章
相关标签/搜索