手机移动程序开发iOS开发者进阶iOS程序猿

NSURLProtocol — DNS劫持的解决方案

2019-07-31  本文已影响45人  __Mr_Xie__

前言

研究 NSURLProtocol 初衷是为了动态改变网络请求的 DNS 服务器的 IP,事实上我已经实现了,所以和大家分享一下。
demo地址

小插曲

DNSPOD简介

我们知道要要把项目中请求的接口替换成成 IP 其实很简单,URL 是字符串,域名替换 IP,无非就是一个字符串替换而已,的确这块其实没有什么技术含量,而且现在像阿里云(没开源)七牛云(开源),等一些比较大的平台在这方面也都有了比较成熟的解决方案,一个 SDK,传个普通的 URL 进去就会返回一个域名被替换成 IPURL 出来,也比较好用,这里要说一下 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

如果想要在一个已经比较完善的 APP 中加入 DNS 防劫持的话,就必须拿到所有网络请求的控制权,这篇文章中我主要使用是 NSURLProtocol + Runtime hook 方式来处理这些东西的。

NSURLProtocol 属于 iOS 黑魔法的一种,是属于 Foundation 框架里的 URL Loading System 的一部分,它是一个抽象类,不能去实例化它,只能子类化 NSURLProtocol,然后使用的时候注册子类。


NSURLProtocol 可以拦截任何从 AppURL Loading System 系统中发出的请求,包括如下:

注:如果你的请求不在以上列表中就不能进行拦截了,比如 WKWebviewAVPlayer(比较特殊,虽然请求也是 http/https 但是就是不走这套系统)等,其实对于正常来说光用 NSURLProtocol 已经足够了。

了解几个概念

现在假如我们访问一个网站 www.baidu.com 从按下回车到百度页面显示到终端会经历如下几个步骤:

  1. 计算机会向我们的 运营商 (移动电信联通等)发出打开 www.baidu.com 的请求。
  2. 运营商收到请求后会到自己的DNS服务器中找 www.baidu.com 这个域名所对应的服务器的 IP 地址(也就是百度的服务器的 IP 地址),这里比如是 180.149.132.47
  3. 运营商用第二步得到的 IP 地址去找到百度的服务器请求得到数据后返回给我们。

其中第二步就是我们所说的 DNS解析过程,域名和 IP 地址的关系其实就是我们的身份证号和姓名的关系,都是来标记一个人或者是一个网站的,只是 IP 地址或身份证号只是一串没有意义的数字,辨识度低,又不好记,所以就会在 IP 上加上一个域名以便区分,或是做的更加个性化,但是如果真的要来准确的区分还是要靠身份证号码或者是 IP 的,所以 DNS 解析就应运而生了。

DNS 劫持,是指在 DNS 解析过程中拦截域名解析的请求,然后做一些自己的处理,比如返回假的 IP 地址或者什么都不做使请求失去响应,其效果就是对特定的网络不能反应或访问的是假网址。根本原因就是以下两点:

  1. 恶意攻击,拦截运营商的解析过程,把自己的非法东西嵌入其中。
  2. 运营商为了利益或者一些其他的因素,允许一些第三方在自己的链接里打打广告之类的。

了解了 DNS 劫持的相关资料后我们就知道了,防止 NDS 劫持就要从第二步入手,因为 DNS 解析过程是运营商来操作的,我们不能去干涉他们,不然我们也就成了劫持者了,所以我们要做的就是在我们请求之前对我们的请求链接做一些修改,将我们原本的请求链接 www.baidu.com 修改为 180.149.132.47,然后请求出去,这样的话就运营商在拿到我们的请求后发现我们直接用的就是 IP 地址就会直接给我们放行,而不会去走他自己 DNS 解析了,也就是说我们把运营商要做的事情自己先做好了。不走他的 DNS 解析也就不会存在 DNS 被劫持的问题,从根本是解决了。

NSURLProtocol可以实现的功能

NSURLProtocol的子类需要实现的方法

// 这个方法返回一个布尔值告诉系统该请求是否需要处理,返回Yes才能进行后续处理。
+ (BOOL)canInitWithRequest:(NSURLRequest *)request;

// 如果想对某个请求添加请求头或者返回新的请求时,可以在这个方法里自定义然后返回,一般情况下直接返回参数里的NSURLRequest实例即可。
+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request;

// 这个方法能够判断当拦截URL相同时是否使用缓存数据
+ (BOOL)requestIsCacheEquivalent:(NSURLRequest *)a toRequest:(NSURLRequest *)b;

// 开始请求
- (void)startLoading;

// 取消请求
- (void)stopLoading;
方法的大致执行流程

使用

demo地址

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. 注册必须实现的五个方法
+ (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 *)canonicalRequestForRequest:(NSURLRequest *)request {
    return request;
}
+ (BOOL)requestIsCacheEquivalent:(NSURLRequest *)a toRequest:(NSURLRequest *)b {
    return [super requestIsCacheEquivalent:a toRequest:b];
}
- (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资源本地化

上一篇 下一篇

猜你喜欢

热点阅读