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中只能通过继承来重写父类的方法。

@interface XRKURLProtocol : NSURLProtocol
@end

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

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
     [NSURLProtocol registerClass:[XRKURLProtocol class]];
}

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

+ (BOOL)canInitWithRequest:(NSURLRequest *)request {
    if ([NSURLProtocol propertyForKey:URLProtocolHandledKey inRequest:request]) {
        return NO;
    }

    //http和https都会出现dns劫持情况,都需要处理
    NSString *scheme = [[request URL] scheme];
    if (([scheme caseInsensitiveCompare:@"http"] == NSOrderedSame)) {
        // 判断请求是否为白名单
        NSArray *whiteLists = [XRKConfigManager sharedManager].whiteList;
        if (whiteLists && [whiteLists isKindOfClass:[NSArray class]]) {
            for (NSString *url in whiteLists) {
                if (request.URL.host && [request.URL.host hasSuffix:url]) {
                    return YES;
                }
            }
        }
    }

    return NO;
}

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

+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request {
    return request;
}

以下是官方对这个方法的解释。当我们想对某个请求添加请求头或者返回新的请求时,可以在这个方法里自定义然后返回,一般情况下直接返回参数里的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相同时是否使用缓存数据,以下例子是直接返回父类实现。

+ (BOOL)requestIsCacheEquivalent:(NSURLRequest *)a toRequest:(NSURLRequest *)b {
    return [super requestIsCacheEquivalent:a toRequest:b];
}

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

- (void)startLoading {
    NSMutableURLRequest *mutableReqeust = [[self request] mutableCopy];

    //打标签,防止无限循环
    [NSURLProtocol setProperty:@YES forKey:URLProtocolHandledKey inRequest:mutableReqeust];
    // dns解析
    NSMutableURLRequest *request = [self.class replaceHostInRequset:mutableReqeust];
    self.connection = [NSURLConnection connectionWithRequest:request delegate:self];
}

+ (NSMutableURLRequest *)replaceHostInRequset:(NSMutableURLRequest *)request {
    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;
}
- (void)stopLoading {
    [self.connection cancel];
}

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

- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
    [self.client URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed];
}

- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
    [self.client URLProtocol:self didLoadData:data];
}

- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
    [self.client URLProtocolDidFinishLoading:self];
}

- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
    [self.client URLProtocol:self didFailWithError:error];
}

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

Web资源本地化

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

- (void)startLoading {
    NSMutableURLRequest *mutableReqeust = [[self request] mutableCopy];
    // 处理MIME type
    NSString *mimeType = nil;
    mutableReqeust = [self.class replaceLocalSource:mutableReqeust];
    NSString *pathComponent = mutableReqeust.URL.absoluteString.lastPathComponent;
    if ([pathComponent hasSuffix:@"js"]) {
        mimeType = @"text/javascript";
    } else if ([pathComponent hasSuffix:@"css"]) {
        mimeType = @"text/css";
    }
    
    if (mimeType) {
        NSData *data = [NSData dataWithContentsOfFile:mutableReqeust.URL.absoluteString];
        
        NSURLResponse *response = [[NSURLResponse alloc] initWithURL:[[self request] URL]
                                                            MIMEType:mimeType
                                               expectedContentLength:[data length]
                                                    textEncodingName:@"UTF8"];
        [[self client] URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed];
        [[self client] URLProtocol:self didLoadData:data];
        [[self client] URLProtocolDidFinishLoading:self];
    }
}
#pragma mark - 判断是否是本地资源
+ (BOOL)canReplaceLocalSource:(NSURLRequest *)request {
    NSString *absoluteString = request.URL.absoluteString;
    for (NSString *localSourceurl in [self localSourceArray]) {
        if ([absoluteString isEqualToString:localSourceurl]) {
            return YES;
        }
    }
    return NO;
}

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是一个抽象类。我们要使用它的时候需要创建它的一个子类。

@interface CustomURLProtocol : NSURLProtocol

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

注册:

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

[NSURLProtocol registerClass:[NSClassFromString(@"CustomURLProtocol") class]];

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

NSURLSessionConfiguration *sessionConfiguration = [NSURLSessionConfiguration defaultSessionConfiguration]; 

sessionConfiguration.protocolClasses = @[[NSClassFromString(@"CustomURLProtocol") class]];

拦截:

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

+ (BOOL)canInitWithRequest:(NSURLRequest *)request

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

+ (BOOL)canInitWithRequest:(NSURLRequest *)request {
    
    NSString * scheme = [[request.URL scheme] lowercaseString];
    
    if ([scheme isEqual:@"http"]) {
        return YES;
    }
    return NO;
}

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

+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request

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

转发:

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

- (id)initWithRequest:(NSURLRequest *)request cachedResponse:(NSCachedURLResponse *)cachedResponse client:(id<NSURLProtocolClient>)client

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

- (void)startLoading

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

回调:

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

[self.client URLProtocol:self didFailWithError:error];

[self.client URLProtocolDidFinishLoading:self];

[self.client URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed];

[self.client URLProtocol:self didLoadData:data];

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

- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
    if (error) {
        [self.client URLProtocol:self didFailWithError:error];
    } else {
        [self.client URLProtocolDidFinishLoading:self];
    }
}
- (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 {
    [self.client URLProtocol:self didLoadData:data];
}

结束:

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

- (void)stopLoading

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

- (void)stopLoading {
    [self.session invalidateAndCancel];
    self.session = nil;
}

以上便是NSURLProtocol的基本流程。


应用:

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

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

坑&注意事项:

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

多个NSURLProtocol嵌套使用

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

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

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

    + (void)setEnabled:(BOOL)enable forSessionConfiguration:(NSURLSessionConfiguration*)sessionConfig
    {
        // Runtime check to make sure the API is available on this version
        if (   [sessionConfig respondsToSelector:@selector(protocolClasses)]
            && [sessionConfig respondsToSelector:@selector(setProtocolClasses:)])
        {
            NSMutableArray * urlProtocolClasses = [NSMutableArray arrayWithArray:sessionConfig.protocolClasses];
            Class protoCls = OHHTTPStubsProtocol.class;
            if (enable && ![urlProtocolClasses containsObject:protoCls])
            {
                [urlProtocolClasses insertObject:protoCls atIndex:0];
            }
            else if (!enable && [urlProtocolClasses containsObject:protoCls])
            {
                [urlProtocolClasses removeObject:protoCls];
            }
            sessionConfig.protocolClasses = urlProtocolClasses;
        }
        else
        {
            NSLog(@"[OHHTTPStubs] %@ is only available when running on iOS7+/OSX9+. "
                  @"Use conditions like 'if ([NSURLSessionConfiguration class])' to only call "
                  @"this method if the user is running iOS7+/OSX9+.", NSStringFromSelector(_cmd));
        }
    }

    就是把自己的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部分的证书认证也进行了相应的处理:

//
//  XRKURLProtocol.h
//
//  Created by xx on 2022/3/29.
//

#ifndef XRKURLProtocol_h
#define XRKURLProtocol_h

@interface XRKURLProtocol : NSURLProtocol<NSURLSessionDelegate,NSURLSessionTaskDelegate>

@end

#endif /* XRKURLProtocol_h */
//
//  XRKURLProtocol.m
//
//
//  Created by xx on 2022/3/29.
//

#import <Foundation/Foundation.h>
#import "XRKURLProtocol.h"
#include <arpa/inet.h>

#define MAX_DNS_CACHE_SECONDS (5* 60.0f) // DNS缓存在超过此时间后会被刷新
/*!
 @abstract DNS解析记录信息
 */
@interface DnsRecord : NSObject
{
    NSArray<NSString*> *ipsArr; // IP 地址序列
    CFAbsoluteTime updateTime; // 记录更新的时间
}

- (nullable NSArray<const NSString*> *) getIpArr;

- (CFAbsoluteTime) getUpdateTime;
@end

@implementation DnsRecord

- (id)initWithIpArr:(nullable NSArray<NSString*> *) ips {
    self = [super init];
    if (self) {
        updateTime = CFAbsoluteTimeGetCurrent();
        ipsArr = ips;
    }
    return self;
}

- (nullable NSArray<NSString*> *) getIpArr {
    return ipsArr;
}

- (CFAbsoluteTime) getUpdateTime {
    return updateTime;
}
@end

@interface XRKURLProtocol (){
    
}

@property (nonatomic, strong) NSURLSession* session;
@end

@implementation XRKURLProtocol

/*!
 @abstract 自定义标签,代表这个请求是我们自己创建的,后续不需要再进行处理,防止无限循环
 */
static NSString * URLProtocolHandledKey = @"URLProtocolHandledKey";

static NSCache<NSString*, DnsRecord *> *_dnsCache = NULL;

+(NSCache<NSString*, DnsRecord*> *)dnsCache{
    if (!_dnsCache) {
        @synchronized (self) {
            if (!_dnsCache) {
                _dnsCache = [[NSCache<NSString*, DnsRecord *> alloc]init];
            }
        }
    }
    return _dnsCache;
}

+ (BOOL)canInitWithRequest:(NSURLRequest *)request {
    // 如果是我们自己发出的请求,则不进行拦截操作,直接放行
    if ([NSURLProtocol propertyForKey:URLProtocolHandledKey inRequest:request]) {
        return NO;
    }
    //http和https都会出现dns劫持情况,都需要处理
    NSString *scheme = [[request URL] scheme];
    if ((NSOrderedSame == [scheme caseInsensitiveCompare:@"https"]) || (NSOrderedSame == [scheme caseInsensitiveCompare:@"http"])) {
        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 {
    NSMutableURLRequest *mutableReqeust = [[self request] mutableCopy];
    
    //打标签,防止无限循环
    [NSURLProtocol setProperty:@YES forKey:URLProtocolHandledKey inRequest:mutableReqeust];
    
    NSMutableURLRequest *request = [self.class replaceHostInRequset:mutableReqeust];
    
    //使用NSURLSession继续把request发送出去
    NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
    self.session = [NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:nil];
    NSURLSessionDataTask *task = [self.session dataTaskWithRequest:request];
    [task resume];
}

/*!
 @abstract 进行DNS查询,并对返回的结果进行排序,返回顺序中,IPV4优先
 */
+(nonnull NSArray<NSString *> *)fireDnsQueryCommand:(const NSString *)host {
    
    const CFStringRef hostNameRef = CFStringCreateWithCString(kCFAllocatorDefault, [host UTF8String], kCFStringEncodingASCII);
    
    const CFHostRef hostRef = CFHostCreateWithName(kCFAllocatorDefault, hostNameRef);
    const CFAbsoluteTime start = CFAbsoluteTimeGetCurrent();
    Boolean result = CFHostStartInfoResolution(hostRef, kCFHostAddresses, NULL);
    CFArrayRef addresses = NULL;
    if (TRUE == result) {
        addresses = CFHostGetAddressing(hostRef, &result);
    }
    
    const Boolean bResolved = TRUE == result ? true : false;
    
    NSMutableArray<NSString *> * ipsArr = [[NSMutableArray<NSString *> alloc] init];
    
    if(bResolved && (NULL != addresses))
    {
        for(int i = 0; i < CFArrayGetCount(addresses); i++)
        {
            const CFDataRef data = (CFDataRef)CFArrayGetValueAtIndex(addresses, i);
            const struct sockaddr_in*  addr = (struct sockaddr_in*)CFDataGetBytePtr(data);
            
            //获取IP地址
            if((NULL != addr) &&(AF_INET == addr->sin_family))
            {
                char ip[16];
                const char* ipv4s = inet_ntop(AF_INET, &(addr->sin_addr),
                                              ip, sizeof(ip));
                NSString * const hostipv4 = [NSString stringWithCString:ipv4s encoding:NSUTF8StringEncoding];
                
                [ipsArr addObject:hostipv4];
                
            }
        }
        for(int i = 0; i < CFArrayGetCount(addresses); i++)
        {
            const CFDataRef data = (CFDataRef)CFArrayGetValueAtIndex(addresses, i);
            const struct sockaddr_in* addr = (struct sockaddr_in*)CFDataGetBytePtr(data);
            
            //获取IP地址
            if((NULL != addr) &&(AF_INET6 == addr->sin_family))
            {
                char ip[64];
                const struct sockaddr_in6 * ipv6_addr =(struct sockaddr_in6 *)addr;
                const char* ipv6s = inet_ntop(AF_INET6, &(ipv6_addr->sin6_addr),
                                              ip, sizeof(ip));
                const NSString * nsipv6 = [NSString stringWithCString:ipv6s encoding:NSUTF8StringEncoding];
                NSString * const hostipv6 = [NSString stringWithFormat:@"[%@]",nsipv6];
                
                [ipsArr addObject:hostipv6];
            }
        }
        
    }
    
    CFAbsoluteTime end = CFAbsoluteTimeGetCurrent();
    NSLog(@"=== ip === %@ === time cost: %0.3fs", ipsArr,end - start);
    CFRelease(hostNameRef);
    CFRelease(hostRef);
    return ipsArr;
}

/*!
 @abstract 进行DNS查询,并对返回的结果进行排序,返回顺序中,IPV4优先,并对查询结果进行缓存
 */
+(nonnull NSArray<NSString *> *)doDnsQueryCacheCommand:(NSString * const)host {
    NSArray<NSString*>* ips = [self fireDnsQueryCommand:host];
    if (ips && ips.count > 0) {
        DnsRecord* dnsRec =  [[DnsRecord alloc] initWithIpArr:ips];
        [[self dnsCache] setObject:dnsRec forKey:host];
    }
    return ips;
}

/*!
 @abstract 进行DNS查询,并对返回的结果进行排序,返回顺序中,IPV4优先,并对查询结果进行缓存
 */
+(nullable NSArray<NSString *> *)doDnsQueryCommand:(NSString * const)host {
    NSArray<NSString*> * ips = nil;
    DnsRecord* dnsRec = [[self dnsCache] objectForKey:host];
    if(dnsRec) {
        ips = [dnsRec getIpArr];
        if (ips && ips.count > 0) {
            // 检查是否超时,超时则发出异步更新命令
            CFAbsoluteTime recTime = [dnsRec getUpdateTime];
            CFAbsoluteTime currTime = CFAbsoluteTimeGetCurrent();
            if(fabs(currTime - recTime) > MAX_DNS_CACHE_SECONDS) {
                dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
                    [self doDnsQueryCacheCommand:host];
                });
            }
        } else {
            ips = [self doDnsQueryCacheCommand:host];
        }
    } else {
        ips = [self doDnsQueryCacheCommand:host];
    }
    return ips;
}

/*!
 @abstract 使用IP替换原来的域名,实现访问控制
 */
+ (NSMutableURLRequest *)replaceHostInRequset:(NSMutableURLRequest *)request {
    if ([request.URL host].length == 0) {
        return request;
    }
    
    NSString *nsOriginUrl = [request.URL absoluteString];
    NSString *nsOriginHost = [request.URL host];
    NSRange hostRange = [nsOriginUrl rangeOfString:nsOriginHost];
    if (NSNotFound == hostRange.location) {
        return request;
    }
    
    //先从缓存中获取IP地址,如果获取不到则进行解析并缓存
    NSArray<NSString*> * ips = [self doDnsQueryCommand: nsOriginHost];
    if (ips && ips.count > 0) {
        NSString *ip = ips[0];
        if (ip && ip.length) {
            // 替换host
            NSString *urlString = [nsOriginUrl stringByReplacingCharactersInRange:hostRange withString:ip];
            NSURL *url = [NSURL URLWithString:urlString];
            request.URL = url;
            
            [request setValue:nsOriginHost forHTTPHeaderField:@"Host"];
        }
    }
    
    return request;
}

- (void)stopLoading {
    [self.session invalidateAndCancel];
    self.session = nil;
}


/*!
 @abstract 需要进行HTTPS的证书认证的时候,我们需要使用被替换成IP的域名来进行证书的查询操作
 */
-(void)URLSession:(NSURLSession *)session didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, NSURLCredential * _Nullable))completionHandler{
    
    if (!challenge) {
        return;
    }
    NSURLSessionAuthChallengeDisposition disposition = NSURLSessionAuthChallengePerformDefaultHandling;
    NSURLCredential *credential = nil;
    /*
     * 获取原始域名信息。
     */
    NSString* host = [[self.request allHTTPHeaderFields] objectForKey:@"Host"];
    if (!host) {
        host = self.request.URL.host;
    }
    // 检查质询的验证方式是否是服务器端证书验证,HTTPS的验证方式就是服务器端证书验证
    if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
        if ([self evaluateServerTrust:challenge.protectionSpace.serverTrust forDomain:host]) {
            disposition = NSURLSessionAuthChallengeUseCredential;
            credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
        } else {
            disposition = NSURLSessionAuthChallengePerformDefaultHandling;
        }
    } else {
        disposition = NSURLSessionAuthChallengePerformDefaultHandling;
    }
    // 对于其他的challenges直接使用默认的验证方案
    completionHandler(disposition,credential);
}

/*!
 @abstract 验证服务器证书是否合法
 */
- (BOOL)evaluateServerTrust:(SecTrustRef)serverTrust
                  forDomain:(NSString *)domain {
    /*
     * 创建证书校验策略
     */
    NSMutableArray *policies = [NSMutableArray array];
    if (domain) {
        [policies addObject:(__bridge_transfer id)SecPolicyCreateSSL(true, (__bridge CFStringRef)domain)];
    } else {
        [policies addObject:(__bridge_transfer id)SecPolicyCreateBasicX509()];
    }
    /*
     * 绑定校验策略到服务端的证书上
     */
    SecTrustSetPolicies(serverTrust, (__bridge CFArrayRef)policies);
    
    
#if (defined(__IPHONE_OS_VERSION_MIN_REQUIRED) && __IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_12_0) || (defined(__MAC_OS_X_VERSION_MIN_REQUIRED) && __MAC_OS_X_VERSION_MIN_REQUIRED < __MAC_10_14)
    if (@available(iOS 12.0, macOS 10.14, tvOS 12.0, watchOS 5.0,*)) {
        return SecTrustEvaluateWithError(serverTrust, nil);
    } else {
        /*
         * 评估当前serverTrust是否可信任,
         * 官方建议在result = kSecTrustResultUnspecified 或 kSecTrustResultProceed
         * 的情况下serverTrust可以被验证通过,https://developer.apple.com/library/ios/technotes/tn2232/_index.html
         * 关于SecTrustResultType的详细信息请参考SecTrust.h
         */
        SecTrustResultType result;
        SecTrustEvaluate(serverTrust, &result);
        return ((kSecTrustResultUnspecified == result)  || (kSecTrustResultProceed == result));
    }
#else
    return SecTrustEvaluateWithError(serverTrust, nil);
#endif
}

- (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];
    }
}

@end

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

NSURL * url = [NSURL URLWithString:@"https://icanhazip.com"];
NSURLRequest * request = [[NSURLRequest alloc]initWithURL:url];
       
NSURLSessionConfiguration * config = [NSURLSessionConfiguration defaultSessionConfiguration];
config.protocolClasses = @[[NSClassFromString(@"XRKURLProtocol") class]];

// 此处的  delegate 为接收数据的回调通知,需要自己实现  
NSURLSession * session = [NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:nil];
    
NSURLSessionDataTask * task = [session dataTaskWithRequest:request];
    
[task resume];

参考链接


发布者

发表回复

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