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

NSURLProtocol

最近更新时间2024.04.16 18:55:45

首次发布时间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 服务端的解析会失败,您只能获得 Local DNS 服务器的解析结果。参见 添加需要解析的域名了解如何添加域名。

实现步骤

注意

为了演示需要,示例代码仅提供了集成方案中最基本的逻辑。移动解析 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];