原理
SNI(Server Name Indication)是为了解决一个服务器使用多个域名和证书的 SSL/TLS 扩展。工作原理如下:
在连接到服务器建立 SSL 连接之前先发送要访问站点的域名(Hostname)。
服务器根据这个域名返回一个合适的证书。
上述过程中,当客户端使用 HTTPDNS 解析域名时,请求 URL 中的 host 会被替换成 HTTPDNS 解析出来的 IP,导致服务器获取到的域名为解析后的 IP,无法找到匹配的证书,只能返回默认的证书或者不返回,所以会出现 SSL/TLS 握手不成功的错误。
由于 iOS 上层网络库 NSURLConnection/NSURLSession 没有提供接口进行 SNI 字段的配置,因此可以考虑使用 NSURLProtocol 拦截网络请求,然后使用 CFHTTPMessageRef 创建 NSInputStream 实例进行 Socket 通信,并设置其 kCFStreamSSLPeerName 的值。
方案描述
注意
本文档提出了 WebView 场景下 HTTPDNS 集成的参考方案,示例代码非线上生产环境正式代码。在接入之前,我们建议您充分评估本文档内容,以确保方案的健壮性符合您的生产标准。
如果您需要自定义 NSURLProtocol,您可以参考 MSDKDnsHttpMessageTools 的 源码。
HTTPDNS iOS SDK 提供了 MSDKDnsHttpMessageTools。MSDKDnsHttpMessageTools 是 HTTPDNS iOS SDK 基于 NSURLProtocol 封装的 Protocol。MSDKDnsHttpMessageTools 继承了 NSURLProtocol,可以自动拦截 NSURLSession 中的请求。MSDKDnsHttpMessageTools 解决了自定义 NSURLProtocol 使用的 CFNetwork 库功能受限,扩展性差的问题。
代码示例
// 注册拦截请求的 NSURLProtocol[NSURLProtocol registerClass:[MSDKDnsHttpMessageTools class]];// 需要设置 SNI 的 URL,比如 https://www.qq.comNSString *originalUrl = @"your url";NSURL *url = [NSURL URLWithString:originalUrl];NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url];// NSURLConnection 例子self.connection = [[NSURLConnection alloc] initWithRequest:request delegate:self];[self.connection start];// NSURLSession 例子NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];NSArray *protocolArray = @[ [MSDKDnsHttpMessageTools class] ];configuration.protocolClasses = protocolArray;NSURLSession *session = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:[NSOperationQueue mainQueue]];self.task = [session dataTaskWithRequest:request];[self.task resume];// AFNetworking 例子NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];NSArray *protocolArray = @[[MSDKDnsHttpMessageTools class]];config.protocolClasses = protocolArray;AFHTTPSessionManager* sessionManager = [[AFHTTPSessionManager alloc] initWithSessionConfiguration:config];sessionManager.responseSerializer = [AFHTTPResponseSerializer serializer];NSURLSessionDataTask* task = [sessionManager dataTaskWithRequest:request uploadProgress:^(NSProgress * _Nonnull uploadProgress) {NSLog(@"update upload progress %@", uploadProgress.description);} downloadProgress:^(NSProgress * _Nonnull downloadProgress) {NSLog(@"update download progress %@", downloadProgress.description);} completionHandler:^(NSURLResponse * _Nonnull response, id _Nullable responseObject, NSError * _Nullable error) {NSLog(@"request complete ==== response: %@ ===== error: %@", [NSString stringWithFormat:@"%@", responseObject], error);}];[task resume];
使用说明
需调用以下接口设置需要拦截域名或无需拦截的域名:
#pragma mark - SNI 场景,仅调用一次即可,请勿多次调用/**SNI 场景下设置需要拦截的域名列表建议使用该接口设置,仅拦截 SNI 场景下的域名,避免拦截其它场景下的域名@param hijackDomainArray 需要拦截的域名列表*/- (void) WGSetHijackDomainArray:(NSArray *)hijackDomainArray;/**SNI 场景下设置不需要拦截的域名列表@param noHijackDomainArray 不需要拦截的域名列表*/- (void) WGSetNoHijackDomainArray:(NSArray *)noHijackDomainArray;
如设置了需要拦截的域名列表,则仅会拦截处理该域名列表中的 HTTPS 请求,其他域名不做处理。
如设置了不需要拦截的域名列表,则不会拦截处理该域名列表中的 HTTPS 请求。
注意
建议使用 WGSetHijackDomainArray 仅拦截 SNI 场景下的域名,避免拦截其他场景下的域名。