前一段时间一直在研究 iOS 应用内的 HTTP 代理问题,在 iOS 6之前可以用 AppProxyCap 这样的 Hack 方法实现,但从 iOS7 开始就不能用了。

虽然 NSURLSession 提供了设置代理的方法,但是它只是针对单个 Session 的,没法实现全局的,尤其是 UIWebView 的代理。

无奈之下,只能选择 NSURLProtocol 这个不完美的实现方案了,为什么说它是不完美的,下文将有介绍。

raywenderlich.com 上有一篇详细的 NSURLProtocol 教程,很好的讲解了它的用法。按照惯例我这里就不重复了,只说一下我做的工程中遇到的难点。

##原理

NSURLProtocol 可以用来处理自定义的 URL Scheme,或者是改写对已经存在的 Scheme 的处理方式。比如,我可以定义一个 URLProtocol 来处理“certificate://xxx.pem” 这样的 URL 请求,用来查找目录下的证书并返回。这样当我用 NSURLConnection 发送这样的请求的时候,得到的将是证书内容。再比如,我可以定义一个 URLProtocol 来处理发到后台 Server 的 HTTP 请求,把请求中的 user-agent 改成 Server 端能识别的形式。

NSURLProtocol 的作用是全局的,也就是说一旦注册之后,所有的 NSURLRequest (包括 UIWebView 发送的) 都会先经 URLProtocol 处理,这也就是为什么能用它来实现全局代理的原因(忽略掉 CFNetwork)。

用 NSURLPRotocol 来实现全局 HTTP 代理时,需要用自定义的 URLProtocol 来处理所有的 HTTP/HTTPS 请求,然后再用 NSURLSession 或者 CFNetwork 这样支持代理的库把请求通过 HTTP 代理转发出去,并把结果返回给上层调用者。(这个调用者在 NSURLProtocol 里就是client这个属性)

##为 NSURLSession 设置代理

为 NSURLSession 设置代理可以通过为其指定一个 NSURLSessionConfiguration 来实现,比如:

NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
configuration.connectionProxyDictionary =
@{(NSString *)kCFStreamPropertyHTTPProxyHost: @"127.0.0.1",
  (NSString *)kCFStreamPropertyHTTPProxyPort: @8080};
NSURLSession *session = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:[NSOperationQueue currentQueue]];

这里有几点需要注意:

  1. connectionProxyDictionary 字典的 Key 一定得正确,据我的搜索结果,网上很多例子都不对……其实这些 Key 可以从函数CFNetworkCopySystemProxySettings()返回的结果中获取。
  2. kCFStreamPropertyHTTPProxyPort对应的 Value 类型一定得是 NSNumber,这是用大量时间换回来的教训……

Authentication Challenge

如果要发送的请求比较简单,不涉及到证书、用户认证等复杂情况,或者即使涉及到这些情况,但处理方式比较简单,在 URLProtocol 内部就可以处理时,NSURLProtocol 还算是比较完美的。但是一旦需求比较复杂,比如某些时候需要上层的代码来处理用户认证,这时候 NSURLProtocol 机制就不完美了,因为它做不到对上层调用者的透明。

用过 NSURLSession 的一般都明白,处理 Authentication 的 Challenge 可以在 NSURLSessionTaskDelegate 协议的URLSession:task:didReceiveChallenge:completionHandler:方法中做一些处理,之后调用 completionHandler 就可以了。

但是如果用了 NSRULProtocol,并且想要让上层代码来处理 Challenge 时就比较困难了。自定义的 URLProtocol 和它的上层调用者之间只能通过NSURLProtocolClient协议来通信。这个协议(URLProtocol:didReceiveAuthenticationChallenge: )并没有实现将 completionHandler 传递过去的方法。这就导致上层调用者的 Challenge 处理方法(比如connection:willSendRequestForAuthenticationChallenge: )在这种情况下是无效的。

因此,必须得用另外的方式解决 Challenge。

Apple 有个示例程序 CustomHTTPProtocol,它的实现方式是为类对象(注意,是类对象)添加了代理。当 CustomHTTPProtocol 收到 Chellenge 时,会调用代理来完成进一步工作,然后再将结果返回给 CustomHTTPProtocol 实例,后者会调用completionHandler来发送结果。(这里涉及上下文切换,状态保存等,而且需要注意调用过程中的多线程问题,其代码中有比较详细的描述。)

NSURLProtocol 的实例化过程对开发者来说是不透明的,也就是说无法通过自定义代码控制,这也就是为啥 CustomHTTPProtocol 要为类对象实现代理。

但是问题又来了,如果遇到奇葩的情况,比如有些请求我想处理 Challenge,有请求又不想处理,该咋办捏?

这时候就应该祭出 NSURLProtocol 的setProperty:forKey:inRequest:方法了,这个方法可以为 URLRequest 设置一个属性,当处理 Challenge 时可以通过检查这个属性来判断是否需要处理。

先到这吧,如有问题再补充~

##参考链接

  1. AppProxyCap: https://github.com/freewizard/AppProxyCap/issues/7
  2. NSURLProtocol Tutorial: http://www.raywenderlich.com/59982/nsurlprotocol-tutorial
  3. CustomHTTPProtocol: https://developer.apple.com/library/ios/samplecode/CustomHTTPProtocol/