You need to enable JavaScript to run this app.
导航

自定义 NSURLProtocol

最近更新时间2023.06.21 19:27:09

首次发布时间2023.03.22 20:49:16

方案描述

注意

App 开启代理时,如果代理无法读取 Host header,您无法将请求改写成 IP 直连请求,这样会导致请求无法正常发送。因此,我们建议您针对这种情况增加一层异常处理逻辑。如果请求无法改写成 IP 直连请求,您可以直接通过 NSURLSession 发送请求。

注意

自定义 NSURLProtocol 方案仅支持 HTTP 1.1。如果您的 app 向不支持 HTTP 1.1 的服务器发送请求,服务器会返回 505 错误码。

如果您的 app 自定义了 NSURLProtocol,您可以参考以下集成方案:

如果您的 app 发送的是 SNI 请求

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 进行以下操作:

  1. 通过自定义 Protocol 拦截请求。
  2. 将 URL 请求改写成 IP 直连请求。
  3. 为请求设置 SNI 信息。
  4. 处理 Cookie 和重定向。
  5. 使用 NSURLSession 发送请求。

如果您的 app 发送的是非 SNI 请求

  1. 通过 NSURLSessionDelegate 拦截请求。
  2. 将 URL 请求改写成 IP 直连请求。
  3. 处理 SSL/TLS 校验、Cookie 和重定向。
  4. 使用 NSURLSession 发送请求。

前提条件

实现步骤

注意

为了演示需要,示例代码仅提供了集成方案中最基本的逻辑。移动解析 HTTPDNS 仅保证 HTTPDNS SDK 本身的 可用性。在生产环境下,您需要自行保证集成方案的健壮性。

处理 SNI 请求

说明

本文以 BDHttpMessageURLProtocol 代表您的自定义 Protocol。

@interface BDHttpMessageURLProtocol : NSURLProtocol

@end

参见以下步骤处理 SNI 请求。

  1. 配置 NSURLSession 使用自定义 Protocol。

    // 配置 NSURLSession 使用自定义 Protocol
    [NSURLProtocol registerClass:[BDHttpMessageURLProtocol class]];
    [TTDnsSdkConfig sharedInstance].myHttpDnsSession = [NSURLSession sharedSession];
    
  2. 把请求域名改写成 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];
    }
    
  3. 把请求改写为 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);
    }
    
  4. 处理重定向、SSL/TLS 校验和 Cookie。示例代码可以参考 示例项目 中的 (void)stream:(NSStream *)aStream handleEvent:(NSStreamEvent)eventCode 方法。

  5. 使用您的网络库(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 请求

参见以下步骤处理非 SNI 请求。

  1. 使用 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];
    
  2. 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];
    }
    
  3. 处理 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
    
    
  4. 使用您的网络库(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];