NSURLProtocol -- DNS劫持和Web资源本地化

什么是DNS劫持

DNS劫持就是通过劫持了DNS服务器,通过某些手段取得某域名的解析记录控制权,进而修改此域名的解析结果,导致对该域名的访问由原IP地址转入到修改后的指定IP,其结果就是对特定的网址不能访问或访问的是假网址,从而实现窃取资料或者破坏原有正常服务的目的。
 常见的DNS劫持现象网络运营商向网页中注入了Javascript代码,甚至直接将我们的网页请求转发到他们自己的广告页面或者通过自己的DNS服务器将用户请求的域名指向到非法地址

如何解决DNS被劫持

全站使用HTTPS协议,或者采用HttpDNS,通过HTTP向自建的DNS服务器或者安全的DNS服务器发送域名解析请求,然后根据解析结果设置客户端的Host指向,从而绕过网络运营商的DNS解析服务。

本文的解决方案

客户端对WebView的html请求进行DNS解析。优先使用阿里、腾讯、114等公共安全的DNS服务器解析客户端的所有指定域名的http请求。相对来讲我们自己的服务域名变化较少,对此我们做了一个白名单,把凡是访问包含我们公司域名的请求都必须通过白名单的解析和DNS验证。从而杜绝被劫持的情况出现,这时候NSURLProtocol就派上用场了。

NSURLProtocol

这是一个抽象类,所以在oc中只能通过继承来重写父类的方法。

然后在AppDelegate的 application:didFinishLaunchingWithOptions: 方法或者程序首次请求网络数据之前去注册这个NSURLProtocol的子类

注册了自定义的urlProtocol子类后,之后每一个http请求都会先经过该类过滤并且通过+canInitWithRequest:这个方法返回一个布尔值告诉系统该请求是否需要处理,返回Yes才能进行后续处理。

+canonicalRequestForRequest:这个父类的抽象方法子类必须实现。

以下是官方对这个方法的解释。当我们想对某个请求添加请求头或者返回新的请求时,可以在这个方法里自定义然后返回,一般情况下直接返回参数里的NSURLRequest实例即可。

It is up to each concrete protocol implementation to define what “canonical” means. A protocol should guarantee that the same input request always yields the same canonical form.

+requestIsCacheEquivalent:toRquest:这个方法能够判断当拦截URL相同时是否使用缓存数据,以下例子是直接返回父类实现。

-startLoading-stopLoading两个方法分别告诉NSURLProtocol实现开始和取消请求的处理。

由于我们在-startLoading中新建了一个NSURLConnection实例,因此要实现NSURLConnectionDelegate的委托方法。

至此,通过NSURLProtocol和QNDnsManager(七牛DNS解析开源库)可以解决DNS劫持问题。但是NSURLProtocol还有更多的用途,以下是本文第二个内容:webView上web请求的资源本地化。

Web资源本地化

这里只举一个简单的示例,同样是在上述NSURLProtocol的子类的-startLoading方法里

NSURLProtocol作为URL Loading System中的一个独立部分存在,能够拦截所有的URL Loading System发出的网络请求,拦截之后便可根据需要做各种自定义处理,是iOS网络层实现AOP(面向切面编程)的终极利器,所以功能和影响力都是非常强大的。但是关于NSURLProtocol的文档非常少,文档陈旧,包括苹果官方的文档也介绍得比较简单。而且,对于NSURLProtocol的使用,有坑的地方非常多。所以说它也是晦涩的并且是危险的。


什么是 NSURLProtocol

NSURLProtocol是URL Loading System的重要组成部分。
首先虽然名叫NSURLProtocol,但它却不是协议。它是一个抽象类。我们要使用它的时候需要创建它的一个子类。
NSURLProtocol在iOS系统中大概处于这样一个位置:

NSURLProtocol能拦截哪些网络请求

NSURLProtocol能拦截所有基于URL Loading System的网络请求。
这里先贴一张URL Loading System的图:

所以,可以拦截的网络请求包括NSURLSession,NSURLConnection以及UIWebVIew。
基于CFNetwork的网络请求,以及WKWebView的请求是无法拦截的。
现在主流的iOS网络库,例如AFNetworking,Alamofire等网络库都是基于NSURLSession或NSURLConnection的,所以这些网络库的网络请求都可以被NSURLProtocol所拦截。
还有一些年代比较久远的网络库,例如ASIHTTPRequest,MKNetwokit等网路库都是基于CFNetwork的,所以这些网络库的网络请求无法被NSURLProtocol拦截。


使用 NSURLProtocol

如上文所说,NSURLProtocol是一个抽象类。我们要使用它的时候需要创建它的一个子类。

使用NSURLProtocol的主要可以分为5个步骤:
注册—>拦截—>转发—>回调—>结束

注册:

对于基于NSURLConnection或者使用[NSURLSession sharedSession]创建的网络请求,调用registerClass方法即可。

对于基于NSURLSession的网络请求,需要通过配置NSURLSessionConfiguration对象的protocolClasses属性。

拦截:

在拦截到网络请求后,NSURLProtocol会依次执行下列方法:

该方法会拿到request的对象,我们可以通过该方法的返回值来筛选request是否需要被NSURLProtocol做拦截处理。
比如:

这里我们就只会拦截http的请求。

在该方法中,我们可以对request进行处理。例如修改头部信息等。最后返回一个处理后的request实例。

转发:

在拦截到网络请求,并且对网络请求进行定制处理以后。我们需要将网络请求重新发送出去。

该方法会创建一个NSURLProtocol实例,这里每一个网络请求都会创建一个新的实例。

接下来就是转发的核心方法startLoading。在该方法中,我们把处理过的request重新发送出去。至于发送的形式,可以是基于NSURLConnection,NSURLSession甚至CFNetwork。

回调:

既是面向切面的编程,就不能影响到原来网络请求的逻辑。所以上一步将网络请求转发出去以后,当收到网络请求的返回,还需要再将返回值返回给原来发送网络请求的地方。
主要需要需要调用到

这四个方法来回调给原来发送网络请求的地方。
这里假设我们在转发过程中是使用NSURLSession发送的网络请求,那么在NSURLSession的回调方法中,我们做相应的处理即可。并且我们也可以对这些返回,进行定制化处理。

结束:

在一个网络请求完全结束以后,NSURLProtocol回调用到

在该方法里,我们完成在结束网络请求的操作。以NSURLSession为例:

以上便是NSURLProtocol的基本流程。


应用:

既然NSURLProtocol功能非常强大,那么在具体开发中,会有哪些应用呢?

      • 网络请求缓存
      • 网络请求mock stub,知名的库OHHTTPStubs就是基于NSURLProtocol
      • 网络相关的数据统计
    • URL重定向
  • 配合实现HTTPDNS
  • ......

坑&注意事项:

使用NSURLProtocol碰到的坑也特别多,有的是很少有文档提及所以没有注意到的,有的甚至是至今还没解释的。下面列举一些我碰到的问题:

多个NSURLProtocol嵌套使用

若一个项目中存在多个NSURLProtocol,那么NSURLProtocol的拦截顺序跟注册的方式和顺序有关。

  • *对于使用registerClass方法注册的情况:
    多个NSURLProtocol拦截顺序为注册顺序的反序,即后注册的的NSURLProtocol先拦截。

  • *对于通过配置NSURLSessionConfiguration对象的protocolClasses属性来注册的情况:
    protocolClasses这个数组里只有第一个NSURLProtocol会起作用。 所以我们看到OHHTTPStubs库在注册的时候进行了这样的处理:

    就是把自己的NSURLProtocol插入到protocolClasses的第一个,进行拦截。拦截完成之后,又进行移除。

关于不能拦截WKWebView

原因是WKWebView 在独立于 app 进程之外的进程中执行网络请求,请求数据不经过主进程,因此,在 WKWebView 上直接使用 NSURLProtocol 无法拦截请求。
具体可以参考 wkwebview的那些坑这篇文章。文章也给出了不算完美的解决方案。

canInitWithRequest方法多次调用

偶尔会出现canInitWithRequest方法多次调用的情况,这个问题出现非常的奇怪,目前还不清楚原因。但是因为我们在canInitWithRequest方法中会判断是否拦截过的标记。所以这个问题不会影响到正常使用。另外还发现,当我们在进行网络请求之前把缓存清除掉,也不会出现这个问题。

使用NSURLSession的坑

在NSURLProtocol中使用NSURLSession有很多莫名其妙的问题,基本上都是系统的bug。
我们可以在http://www.openradar.me/search?query=nsurlprotocol 这里看到关于NSURLProtocol的系统bug,基本都与NSURLSession有关。比较明显的就是:

  • 拦截到的Request中的HTTPBody为nil;
  • startLoading在某些特殊情况会出现死锁;
  • 关于注册registerClass方法只适用于sharedSession创建的网络请求;
  • ……

这些问题都是在使用NSURLProtocol需要特别注意的。


总结

NSURLProtocol的强大功能,为iOS网络开发提供了非常大的可操作空间。在商业项目中,也得到了广泛的应用,但我们在应用的同时,也要注意避免NSURLProtocol存在的问题。不过好在随着iOS系统的发展,关于NSURLProtocol的系统bug已经越来越少。

附加

下面是我这边实现DNS缓存,重新排序等方案的一个代码例子 ,对HTTPS部分的证书认证也进行了相应的处理:

调用上述代码的例子如下:

参考链接


发布者

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注