前言
研究 NSURLProtocol 初衷是为了动态改变网络请求的 DNS
服务器的 IP
,事实上我已经实现了,所以和大家分享一下。
demo地址
小插曲
-
DNSPOD相关
我们知道要要把项目中请求的接口替换成成 IP
其实很简单,URL
是字符串,域名替换 IP
,无非就是一个字符串替换而已,的确这块其实没有什么技术含量,而且现在像阿里云(没开源)
,七牛云(开源)
,等一些比较大的平台在这方面也都有了比较成熟的解决方案,一个 SDK
,传个普通的 URL
进去就会返回一个域名被替换成 IP
的 URL
出来,也比较好用,这里要说一下 IP
地址的来源,如何拿到一个域名所对应的 IP
呢?这里就是需要用到另一个服务 — HTTPDNS
,国内比较有名的就是 DNSPOD
,包括阿里,七牛等也是使用他们的 DNS
服务来解析。
DNSPOD
会给我们提供一个接口,我们使用 HTTP
请求的方式去请求这个接口,参数带上我们的域名,他们就会把域名对应的 IP
列表返回,接口如下:
/*
这个请求URL的结构是固定的,119.29.29.29是DNSPOD固定的服务器地址;
ttl参数的意思是返回结果是否带ttl是个BOOL;
dn:需要解析的域名;
id:就是我们在dnspod上注册时候他给我们的一个KEY;
*/
http://119.29.29.29/d?ttl=1&dn=www.baidu.com&id=KEY
-
DNSPOD运用
如果想要在一个已经比较完善的 APP
中加入 DNS
防劫持的话,就必须拿到所有网络请求的控制权,这篇文章中我主要使用是 NSURLProtocol + Runtime hook
方式来处理这些东西的。
NSURLProtocol
属于 iOS
黑魔法的一种,是属于 Foundation
框架里的 URL Loading System 的一部分,它是一个抽象类,不能去实例化它,只能子类化 NSURLProtocol
,然后使用的时候注册子类。
NSURLProtocol
可以拦截任何从 App
的 URL Loading System
系统中发出的请求,包括如下:
-
ftp://
File Transfer Protocol -
http://
Hypertext Transfer Protocol -
https://
Hypertext Transfer Protocol with encryption -
file:///
Local file URLs -
data://
Data URLs
注:如果你的请求不在以上列表中就不能进行拦截了,比如 WKWebview
,AVPlayer
(比较特殊,虽然请求也是 http/https
但是就是不走这套系统)等,其实对于正常来说光用 NSURLProtocol
已经足够了。
了解几个概念
-
DNS解析
现在假如我们访问一个网站 www.baidu.com
从按下回车到百度页面显示到终端会经历如下几个步骤:
- 计算机会向我们的
运营商
(移动
、电信
、联通
等)发出打开www.baidu.com
的请求。 - 运营商收到请求后会到自己的DNS服务器中找
www.baidu.com
这个域名所对应的服务器的IP
地址(也就是百度的服务器的IP
地址),这里比如是180.149.132.47
。 - 运营商用第二步得到的
IP
地址去找到百度的服务器请求得到数据后返回给我们。
其中第二步就是我们所说的 DNS解析过程
,域名和 IP
地址的关系其实就是我们的身份证号和姓名的关系,都是来标记一个人或者是一个网站的,只是 IP
地址或身份证号只是一串没有意义的数字,辨识度低,又不好记,所以就会在 IP
上加上一个域名以便区分,或是做的更加个性化,但是如果真的要来准确的区分还是要靠身份证号码或者是 IP
的,所以 DNS
解析就应运而生了。
-
DNS劫持
DNS
劫持,是指在 DNS
解析过程中拦截域名解析的请求,然后做一些自己的处理,比如返回假的 IP
地址或者什么都不做使请求失去响应,其效果就是对特定的网络不能反应或访问的是假网址。根本原因就是以下两点:
- 恶意攻击,拦截运营商的解析过程,把自己的非法东西嵌入其中。
- 运营商为了利益或者一些其他的因素,允许一些第三方在自己的链接里打打广告之类的。
-
DNS劫持的解决方案
了解了 DNS
劫持的相关资料后我们就知道了,防止 NDS
劫持就要从第二步入手,因为 DNS
解析过程是运营商来操作的,我们不能去干涉他们,不然我们也就成了劫持者了,所以我们要做的就是在我们请求之前对我们的请求链接做一些修改,将我们原本的请求链接 www.baidu.com
修改为 180.149.132.47
,然后请求出去,这样的话就运营商在拿到我们的请求后发现我们直接用的就是 IP
地址就会直接给我们放行,而不会去走他自己 DNS
解析了,也就是说我们把运营商要做的事情自己先做好了。不走他的 DNS
解析也就不会存在 DNS
被劫持的问题,从根本是解决了。
NSURLProtocol可以实现的功能
- 重定向网络请求(可以解决之前电信的
DNS
域名劫持问题) - 缓存
- 自定义
Response
(过滤敏感信息) - 全局网络请求设置
HTTP Mocking
NSURLProtocol的子类需要实现的方法
// 这个方法返回一个布尔值告诉系统该请求是否需要处理,返回Yes才能进行后续处理。
+ (BOOL)canInitWithRequest:(NSURLRequest *)request;
// 如果想对某个请求添加请求头或者返回新的请求时,可以在这个方法里自定义然后返回,一般情况下直接返回参数里的NSURLRequest实例即可。
+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request;
// 这个方法能够判断当拦截URL相同时是否使用缓存数据
+ (BOOL)requestIsCacheEquivalent:(NSURLRequest *)a toRequest:(NSURLRequest *)b;
// 开始请求
- (void)startLoading;
// 取消请求
- (void)stopLoading;
方法的大致执行流程
使用
1. 子类化
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface XWCustomURLProtocol : NSURLProtocol
@end
NS_ASSUME_NONNULL_END
2. 注册
[NSURLProtocol registerClass:[XWCustomURLProtocol class]];
NSURLConnection
发起请求的时候,会让所有已注册的 XWCustomURLProtocol
来“审批”这个请求
注意: 如果是基于 NSURLSession
进行的请求,注册的时候需要注册到 NSURLSessionConfiguration
中,如下:
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
NSArray *protocolArray = @[[XWCustomURLProtocol class]];
configuration.protocolClasses = protocolArray;
NSURLSession *session = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:[NSOperationQueue mainQueue]];
NSURLSessionTask *task = [session dataTaskWithRequest:request];
[task resume];
3. 注册必须实现的五个方法
- 这个方法返回一个布尔值告诉系统该请求是否需要处理,返回
Yes
才能进行后续处理。
+ (BOOL)canInitWithRequest:(NSURLRequest *)request {
NSString * scheme = [[request.URL scheme] lowercaseString];
// 看看是否已经处理过了,防止无限循环 根据业务来截取
if ([NSURLProtocol propertyForKey: URLProtocolHandledKey inRequest:request]) {
return NO;
}
// TODO - 这里是自己需要处理的逻辑,这里就简单举个例子处理一下
if ([scheme isEqual:@"http"] || [scheme isEqual:@"https"]) {
return YES;
}
return NO;
}
- 如果想对某个请求添加请求头或者返回新的请求时,可以在这个方法里自定义然后返回,一般情况下直接返回参数里的
NSURLRequest
实例即可。
+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request {
return request;
}
- 这个方法能够判断当拦截
URL
相同时是否使用缓存数据
+ (BOOL)requestIsCacheEquivalent:(NSURLRequest *)a toRequest:(NSURLRequest *)b {
return [super requestIsCacheEquivalent:a toRequest:b];
}
- 开始请求。可以在这里修改请求信息,重定向,
DNS
解析,返回自定义的测试数据等。以下代码实现的是:修改DNS
解析的服务器地址,用新的DNS
解析的服务器地址去做DNS
解析。
- (void)startLoading {
NSLog(@"***监听接口:%@", self.request.URL.absoluteString);
NSMutableURLRequest *mutableReqeust = [[self request] mutableCopy];
//标示该request已经处理过了,防止无限循环
[NSURLProtocol setProperty:@(YES) forKey:URLProtocolHandledKey inRequest:mutableReqeust];
// dns解析
NSMutableURLRequest *request = [self.class replaceHostInRequset:mutableReqeust];
// 使用NSURLSession继续把request发送出去
NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
NSOperationQueue *mainQueue = [NSOperationQueue mainQueue];
self.session = [NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:mainQueue];
NSURLSessionDataTask *task = [self.session dataTaskWithRequest:request];
[task resume];
}
+ (NSMutableURLRequest *)replaceHostInRequset:(NSMutableURLRequest *)request {
NSLog(@"%s", __func__);
if ([request.URL host].length == 0) {
return request;
}
NSString *originUrlString = [request.URL absoluteString];
NSString *originHostString = [request.URL host];
NSRange hostRange = [originUrlString rangeOfString:originHostString];
if (hostRange.location == NSNotFound) {
return request;
}
// 用HappyDNS 替换host
NSMutableArray *array = [NSMutableArray array];
// 第一dns解析为114,第二解析才是系统dns
[array addObject:[[QNResolver alloc] initWithAddress:@"114.114.115.115"]];
[array addObject:[QNResolver systemResolver]];
QNDnsManager *dnsManager = [[QNDnsManager alloc] init:array networkInfo:[QNNetworkInfo normal]];
NSArray *queryArray = [dnsManager query:originHostString];
if (queryArray && queryArray.count > 0) {
NSString *ip = queryArray[0];
if (ip && ip.length) {
// 替换host
NSString *urlString = [originUrlString stringByReplacingCharactersInRange:hostRange withString:ip];
NSURL *url = [NSURL URLWithString:urlString];
request.URL = url;
[request setValue:originHostString forHTTPHeaderField:@"Host"];
}
}
return request;
}
其中 URLProtocolHandledKey
为:
static NSString * const URLProtocolHandledKey = @"URLProtocolHandledKey";
- 取消请求。
- (void)stopLoading {
[self.session invalidateAndCancel];
self.session = nil;
}
4. 拦截之后的处理过程
需要注意的是 XWCustomURLProtocol
的父类中有一个 client
属性,如下:
/*!
@abstract Returns the NSURLProtocolClient of the receiver.
@result The NSURLProtocolClient of the receiver.
*/
@property (nullable, readonly, retain) id <NSURLProtocolClient> client;
遵守代理 NSURLProtocolClient
,需要实现的方法如下:
- (void)URLProtocol:(NSURLProtocol *)protocol wasRedirectedToRequest:(NSURLRequest *)request redirectResponse:(NSURLResponse *)redirectResponse;
- (void)URLProtocol:(NSURLProtocol *)protocol cachedResponseIsValid:(NSCachedURLResponse *)cachedResponse;
- (void)URLProtocol:(NSURLProtocol *)protocol didReceiveResponse:(NSURLResponse *)response cacheStoragePolicy:(NSURLCacheStoragePolicy)policy;
- (void)URLProtocol:(NSURLProtocol *)protocol didLoadData:(NSData *)data;
- (void)URLProtocolDidFinishLoading:(NSURLProtocol *)protocol;
- (void)URLProtocol:(NSURLProtocol *)protocol didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge;
- (void)URLProtocol:(NSURLProtocol *)protocol didCancelAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge;
对于需要处理的 NSURLSession
,可以在 NSURLSessionDelegate
中进行操作:
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(NSURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition))completionHandler {
[self.client URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed];
completionHandler(NSURLSessionResponseAllow);
}
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data {
// 打印返回数据
NSString *dataStr = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
if (dataStr) {
NSLog(@"***截取数据***: %@", dataStr);
}
[self.client URLProtocol:self didLoadData:data];
}
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
if (error) {
[self.client URLProtocol:self didFailWithError:error];
} else {
[self.client URLProtocolDidFinishLoading:self];
}
}
Author
如果你有什么建议,可以关注我的公众号:iOS开发者进阶
,直接留言,留言必回。
参考
NSURLProtocol
NSURLProtocol 详解
IOS下三种DNS解析方式分析(LocalDns)
可能是最全的iOS端HttpDns集成方案
NSURLProtocol -- DNS劫持和Web资源本地化
网友评论