注意
App 开启代理时,如果代理无法读取 Host header,您无法将请求改写成 IP 直连请求,这样会导致请求无法正常发送。因此,我们建议您针对这种情况增加一层异常处理逻辑。如果请求无法改写成 IP 直连请求,您可以直接通过 NSURLSession 发送请求。
注意
自定义 NSURLProtocol 方案仅支持 HTTP 1.1。如果您的 app 向不支持 HTTP 1.1 的服务器发送请求,服务器会返回 505 错误码。
如果您的 app 使用了 NSURLProtocol,您可以参考以下集成方案:
HTTPS 请求使用 SSL/TLS 协议。SNI(Server Name Indication) 是 SSL/TLS 协议的扩展,在 RFC 6066 中定义。SNI 可以解决一个服务端 IP 地址对应多个主机名时,SSL 证书无法正常认证的问题。发送 SNI 请求时,您需要通过 SNI 将服务端的主机名传递到 SSL/TLS 握手进程。这样,SSL/TLS 握手进程可以生成正确的 SSL/TLS 证书。
您可以配置 NSURLSession
使用自定义 Protocol。然后,您需要在自定义 Protocol 中使用 CFNetwork 进行以下操作:
NSURLSession
发送请求。NSURLSession
发送请求。警告
对于没有在控制台添加的域名,HTTPDNS 服务端的解析会失败,您只能获得 Local DNS 服务器的解析结果。参见 添加需要解析的域名了解如何添加域名。
注意
为了演示需要,示例代码仅提供了集成方案中最基本的逻辑。移动解析 HTTPDNS 仅保证 HTTPDNS SDK 本身的 可用性。在生产环境下,您需要自行保证集成方案的健壮性。
说明
本文以 BDHttpMessageURLProtocol
代表您的自定义 Protocol。
@interface BDHttpMessageURLProtocol : NSURLProtocol @end
参见以下步骤处理 SNI 请求。
配置 NSURLSession 使用自定义 Protocol。
// 配置 NSURLSession 使用自定义 Protocol [NSURLProtocol registerClass:[BDHttpMessageURLProtocol class]]; [TTDnsSdkConfig sharedInstance].myHttpDnsSession = [NSURLSession sharedSession];
把请求域名改写成 IP 地址。您需要通过 getDnsResultForHost
方法获取当前域名的 DNS 解析结果。然后,您需要根据域名改写的 IP 地址创建 IP 直连请求。
说明
SDK 提供以下类型的 getDnsResult
方法。示例代码中使用了 getDnsResultForHost 方法。该方法会阻塞后续代码的运行,直到 SDK 获取到域名解析结果。您也可以根据需求使用其他类型的 getHttpDnsResult
方法。
// 把请求域名改写成 IP 地址。 + (NSURL*)getIpAndReplace:(NSString*)urlString { NSURL* url = [NSURL URLWithString:urlString]; NSString* originHost = url.host; NSTimeInterval start = [[NSDate date] timeIntervalSince1970]; TTDnsExportResult* dnsResult = [[TTDnsResolver shareInstance] getDnsResultForHost:originHost]; // key method NSTimeInterval totalDnsTime = [[NSDate date] timeIntervalSince1970] - start; NSString* ip = [NSString string]; if (dnsResult && [dnsResult.ipv4List count] > 0) { NSLog(@"originUrlString is %@, originHost is %@, ip is %@", urlString, originHost, ip); // 建议优先使用 IPv4 的第一个地址 ip = dnsResult.ipv4List[0]; NSString* logStr = [dnsResult convertDnsResultToJsonString]; [TTViewController logOutput:logStr isNSLog:YES isLogWindow:YES]; } if (originHost.length > 0 && ip.length > 0) { NSString* originUrlStringafterdispatch = [url absoluteString]; NSRange hostRange = [originUrlStringafterdispatch rangeOfString:url.host]; NSString* urlString = [originUrlStringafterdispatch stringByReplacingCharactersInRange:hostRange withString:ip]; url = [NSURL URLWithString:urlString]; } return url; } // 根据域名改写的 IP 地址创建 IP 直连请求 + (NSURLRequest*)applyHttpDnsIpDirectConnect:(NSURLRequest*)request { NSURL* originUrl = request.URL; NSString* originHost = originUrl.host; NSString *cookie = [[BDDnsCookieManager sharedInstance] cookieForURL:originUrl]; NSURL* newUrl = [self.class getIpAndReplace:[originUrl absoluteString]]; NSMutableURLRequest* mutableRequest = [request copy]; mutableRequest.URL = newUrl; [mutableRequest setValue:originHost forHTTPHeaderField:@"Host"]; [mutableRequest setValue:cookie forHTTPHeaderField:@"Cookie"]; return [mutableRequest copy]; }
把请求改写为 IP 直连请求之后,该集成方案把 URL 中的域名更改成了 IP 地址。因此,HTTPS 请求中的 SNI 信息是不正确的。您需要重新设置 SNI。
注意
对于 POST 请求,自定义 Protocol 拦截之后,body 可能为空。您可以使用 InputStream
把 body 传入 NSData,避免 body 为空。
- (void)startRequest { // 根据原 request 的 url 创建 CF request. CFStringRef url = (__bridge CFStringRef) [curRequest.URL absoluteString]; CFURLRef requestURL = CFURLCreateWithString(kCFAllocatorDefault, url, NULL); CFStringRef requestMethod = (__bridge CFStringRef) curRequest.HTTPMethod; CFHTTPMessageRef cfrequest = CFHTTPMessageCreateRequest(kCFAllocatorDefault, requestMethod, requestURL, kCFHTTPVersion2_0); // 拦截后,POST 请求 body 为 nil 时的,您可以使用 InputStream 把 body 传入 NSData NSDictionary *headFields = curRequest.allHTTPHeaderFields; CFStringRef requestBody = CFSTR(""); CFDataRef bodyData = CFStringCreateExternalRepresentation(kCFAllocatorDefault, requestBody, kCFStringEncodingUTF8, 0); if (curRequest.HTTPBody) { CFHTTPMessageSetBody(cfrequest, (__bridge_retained CFDataRef) curRequest.HTTPBody); } else if(curRequest.HTTPBodyStream) { NSData *data = [self dataWithInputStream:curRequest.HTTPBodyStream]; CFDataRef body = (__bridge_retained CFDataRef) data; CFHTTPMessageSetBody(cfrequest, body); CFRelease(body); } else { CFHTTPMessageSetBody(cfrequest, bodyData); } // 建立 inputstream,并注入 SSL/TLS 相关信息 CFReadStreamRef readStream = CFReadStreamCreateForHTTPRequest(kCFAllocatorDefault, cfrequest); inputStream = (__bridge_transfer NSInputStream *)readStream; // 配置SNI字段 stream.property[kCFStreamPropertySSLSettings][kCFStreamSSLPeerName] = originalHost NSString *host = [curRequest.allHTTPHeaderFields objectForKey:@"Host"]; // 可以选择使用SSL 或者TLS1.2,目前CFNetwork不支持HTTP2.0. [inputStream setProperty:(__bridge id)CFSTR("kCFStreamSocketSecurityLevelTLSv1_2") forKey:(__bridge id)kCFStreamPropertySocketSecurityLevel]; NSDictionary *sslProperties = [[NSDictionary alloc] initWithObjectsAndKeys:host, (__bridge id) kCFStreamSSLPeerName, nil]; [inputStream setProperty:sslProperties forKey:(__bridge_transfer NSString *) kCFStreamPropertySSLSettings]; // 设置处理请求事件的 delegate, 此处为自己 [inputStream setDelegate:self]; if (!curRunLoop) curRunLoop = [NSRunLoop currentRunLoop]; [inputStream scheduleInRunLoop:curRunLoop forMode:NSRunLoopCommonModes]; [inputStream open]; CFRelease(bodyData); CFRelease(requestURL); CFRelease(cfrequest); }
处理重定向、SSL/TLS 校验和 Cookie。示例代码可以参考 示例项目 中的 (void)stream:(NSStream *)aStream handleEvent:(NSStreamEvent)eventCode
方法。
使用您的网络库(NSURLSession、AFNetworking 或 AlamoFire)发送请求。
// 使用 NSURLSession 发送请求 [NSURLProtocol registerClass:[BDHttpMessageURLProtocol class]]; [TTDnsSdkConfig sharedInstance].myHttpDnsSession = [NSURLSession sharedSession]; NSURLSession* session = [TTDnsSdkConfig sharedInstance].myHttpDnsSession; [[session dataTaskWithRequest:request completionHandler:^(NSData* _Nullable data, NSURLResponse* _Nullable response, NSError* _Nullable error) { NSLog(@"request error is %@",error); if (!error) { NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse*)response; [[BDDnsCookieManager sharedInstance] parseHeaderFields:httpResponse.allHeaderFields forURL:originUrl]; responseString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; [self.class logOutput:responseString isNSLog:YES isLogWindow:NO]; } }] resume];
参见以下步骤处理非 SNI 请求。
使用 NSURLSessionDelegate 拦截请求。在示例代码中,TTNoneSNISessionDelegate
继承了 NSURLSessionDelegate
。
@interface TTNoneSNISessionDelegate : NSObject<NSURLSessionDelegate> @end
// 使用 NSURLSessionDelegate 拦截请求 NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration]; config.protocolClasses = @[[TTHttpMnetURLProtocol class]]; [TTDnsSdkConfig sharedInstance].myHttpDnsSession = [NSURLSession sessionWithConfiguration:config delegate:[[TTNoneSNISessionDelegate alloc] init] delegateQueue:nil];
将 NSURLRequest 中的域名改写成 IP 地址。您需要通过 getDnsResultForHost
方法获取当前域名的 DNS 解析结果。然后,您需要根据域名改写的 IP 地址创建 IP 直连请求。
说明
SDK 提供以下类型的 getDnsResult
方法。示例代码中使用了 getDnsResultForHost 方法。该方法会阻塞后续代码的运行,直到 SDK 获取到域名解析结果。您也可以根据需求使用其他类型的 getHttpDnsResult
方法。
// 把请求域名改写成 IP 地址。 + (NSURL*)getIpAndReplace:(NSString*)urlString { NSURL* url = [NSURL URLWithString:urlString]; NSString* originHost = url.host; NSTimeInterval start = [[NSDate date] timeIntervalSince1970]; TTDnsExportResult* dnsResult = [[TTDnsResolver shareInstance] getDnsResultForHost:originHost]; // key method NSTimeInterval totalDnsTime = [[NSDate date] timeIntervalSince1970] - start; NSString* ip = [NSString string]; if (dnsResult && [dnsResult.ipv4List count] > 0) { NSLog(@"originUrlString is %@, originHost is %@, ip is %@", urlString, originHost, ip); // 建议优先使用 IPv4 的第一个地址 ip = dnsResult.ipv4List[0]; NSString* logStr = [dnsResult convertDnsResultToJsonString]; [TTViewController logOutput:logStr isNSLog:YES isLogWindow:YES]; } if (originHost.length > 0 && ip.length > 0) { NSString* originUrlStringafterdispatch = [url absoluteString]; NSRange hostRange = [originUrlStringafterdispatch rangeOfString:url.host]; NSString* urlString = [originUrlStringafterdispatch stringByReplacingCharactersInRange:hostRange withString:ip]; url = [NSURL URLWithString:urlString]; } return url; } // 根据域名改写的 IP 地址创建 IP 直连请求 + (NSURLRequest*)applyHttpDnsIpDirectConnect:(NSURLRequest*)request { NSURL* originUrl = request.URL; NSString* originHost = originUrl.host; NSString *cookie = [[BDDnsCookieManager sharedInstance] cookieForURL:originUrl]; NSURL* newUrl = [self.class getIpAndReplace:[originUrl absoluteString]]; NSMutableURLRequest* mutableRequest = [request copy]; mutableRequest.URL = newUrl; [mutableRequest setValue:originHost forHTTPHeaderField:@"Host"]; [mutableRequest setValue:cookie forHTTPHeaderField:@"Cookie"]; return [mutableRequest copy]; }
处理 SSL/TLS 校验、重定向和 Cookie。示例代码中暂未实现 Cookie 的处理逻辑。
@interface TTNoneSNISessionDelegate : NSObject<NSURLSessionDelegate> @end
@implementation TTNoneSNISessionDelegate // 非 SNI 请求不需要通过自定义 Protocol 拦截, 但您需要处理重定向、SSL/TLS 校验和 Cookie #pragma mark -- NSURLSessionDelegate - (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task willPerformHTTPRedirection:(NSHTTPURLResponse *)response newRequest:(NSURLRequest *)newRequest completionHandler:(void (^)(NSURLRequest *))completionHandler { NSLog(@"-----%@",NSStringFromSelector(_cmd)); NSLog(@"-----new request: %@",newRequest.URL); // 将newRequest进行域名解析,生成新的request NSString *host = newRequest.URL.host; NSURL *ipURL = [TTDnsUtils getIpAndReplace:[newRequest.URL absoluteString]]; NSMutableURLRequest *ipRequest = [NSMutableURLRequest requestWithURL:ipURL]; [ipRequest setValue:host forHTTPHeaderField:@"host"]; completionHandler(ipRequest); } // 处理证书异常,默许IP直连方式 - (void) URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler { NSLog(@"-----%@",NSStringFromSelector(_cmd)); if (!challenge) { return; } NSURLSessionAuthChallengeDisposition disposition = NSURLSessionAuthChallengePerformDefaultHandling; NSURLCredential *credential = nil; // 判断服务器返回的证书是否是服务器信任的 if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) { // 创建证书校验策略 NSMutableArray *policies = [NSMutableArray array]; // 使用域名代替IP进行校验 NSString* host = [[task originalRequest] valueForHTTPHeaderField:@"host"]; [policies addObject:(__bridge_transfer id) SecPolicyCreateSSL(true, (__bridge CFStringRef) host)]; // 绑定校验策略到服务端的证书上 SecTrustSetPolicies(challenge.protectionSpace.serverTrust, (__bridge CFArrayRef) policies); // 评估当前serverTrust是否可信任, // 官方建议在result = kSecTrustResultUnspecified 或 kSecTrustResultProceed // 的情况下serverTrust可以被验证通过,https://developer.apple.com/library/ios/technotes/tn2232/_index.html // 关于SecTrustResultType的详细信息请参考SecTrust.h SecTrustResultType result; SecTrustEvaluate(challenge.protectionSpace.serverTrust, &result); BOOL isTrusted = (result == kSecTrustResultUnspecified || result == kSecTrustResultProceed); // 新的校验策略添加成功 if (isTrusted) { // disposition:如何处理证书 // NSURLSessionAuthChallengePerformDefaultHandling:默认方式处理 // NSURLSessionAuthChallengeUseCredential:使用指定的证书 // NSURLSessionAuthChallengeCancelAuthenticationChallenge:取消请求 disposition = NSURLSessionAuthChallengeUseCredential; credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust]; } else { disposition = NSURLSessionAuthChallengePerformDefaultHandling; } } else { disposition = NSURLSessionAuthChallengePerformDefaultHandling; } // 应用证书策略 if (completionHandler) { completionHandler(disposition, credential); } } - (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(NSURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition))completionHandler { NSLog(@"-----%@",NSStringFromSelector(_cmd)); NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse*)response; NSLog(@"-----Response for url(%@), httpcode(%ld)", httpResponse.URL, (long)httpResponse.statusCode); completionHandler(NSURLSessionResponseAllow); } - (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data { NSLog(@"-----%@",NSStringFromSelector(_cmd)); } - (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error { NSLog(@"-----%@",NSStringFromSelector(_cmd)); } @end
使用您的网络库(NSURLSession、AFNetworking 或 AlamoFire)发送请求。
// 使用 NSURLSession 发送请求 NSURLSession* session = [TTDnsSdkConfig sharedInstance].myHttpDnsSession; [[session dataTaskWithRequest:request completionHandler:^(NSData* _Nullable data, NSURLResponse* _Nullable response, NSError* _Nullable error) { NSLog(@"request error is %@",error); if (!error) { NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse*)response; [[BDDnsCookieManager sharedInstance] parseHeaderFields:httpResponse.allHeaderFields forURL:originUrl]; responseString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; [self.class logOutput:responseString isNSLog:YES isLogWindow:NO]; } }] resume];