今是昨非

今是昨非

日出江花红胜火,春来江水绿如蓝

iOS 接続 HTTPDNS

背景#

最近開発した水印カメラで、ユーザーのネットワークは正常ですが、アップロードタイムアウトやアップロード失敗の問題が発生しました。聞云のバックエンドでインターフェースエラーログを確認したところ、ユーザーの localDNS が空であったため、HTTPDNS の接続が必要になりました。

実践#

プロジェクトで使用しているネットワークリクエストは AFNetworking フレームワークを使用しているため、サードパーティの HTTPDNS を接続した後、AFNetworking の内容を変更してリクエストを IP に通す必要があります。

大まかな流れは SDK を接続する ——> SDK を登録する ——> IP を取得する ——> 保存する ——> 使用する、です。ここでは個人の状況に応じて、起動時に SDK を登録し、IP を取得する方法は二通りあります。一つはアプリ起動時に一度だけ取得し、その後保存してアプリ使用中は更新しない方法、もう一つは特定のインターフェースを使用するたびに取得する方法です。

以下で接続のプロセスを詳しく見ていきましょう。

阿里 HTTPDNS#

  1. クイックスタートの手順に従って設定します。
    • ドメインを追加します。阿里のドメイン追加では、完全一致と二次ドメインの方法を追加できます。
  2. iOS SDK 接続を参考に接続します。
    1. CocoaPods を使用して接続します。
      ここで文句を言いたくなるところですが、阿里の公式文書に書かれている CocoaPod でインストールした SDK は最新ではありません。
      
         source 'https://github.com/CocoaPods/Specs.git'
         source 'https://github.com/aliyun/aliyun-specs.git'
      
          pod 'AlicloudHTTPDNS' # 注意、公式文書に書かれている方法ではなく、後ろに指定バージョンを追加しないでください。
      
    2. プロジェクトで使用します。
          - (void)registerAliDNS{
               HttpDnsService *httpdns = [[HttpDnsService alloc]initWithAccountID:@"Your Account ID" secretKey:@"Your Secret Key"];
               [httpdns setCachedIPEnabled:NO];
           //    [httpdns cleanHostCache:nil];
               [httpdns setHTTPSRequestEnabled:YES];
               [httpdns setPreResolveHosts:@[
                   @"baidu.com",
               ]];
               [httpdns setLogEnabled:YES];
               [httpdns setPreResolveAfterNetworkChanged:YES];
           //    [httpdns setExpiredIPEnabled:YES];
           //    [httpdns setDelegateForDegradationFilter:self];
      
               self.httpdns = httpdns;
               NSUInteger delaySeconds = 1;
               dispatch_time_t when = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delaySeconds * NSEC_PER_SEC));
               dispatch_after(when, dispatch_get_main_queue(), ^{
                   [self getUpFileHostIp];
               });
           }
      
           - (void)getUpFileHostIp {
               NSString *originalUrl = @"https://www.baidu.com";
               NSURL *url = [NSURL URLWithString:originalUrl];
               NSArray *ipsArray = [self.httpdns getIpsByHostAsync:url.host];
               if (!IsNilArray(ipsArray)) {
                   self.hostIpStr = ipsArray.firstObject;
               }
           }
      
      

腾讯云 HTTPDNS#

  1. 入門ガイドの手順に従って設定します。
    1. アカウントを登録 / ログインします。
    2. サービスを開通します。
    3. 開発設定でアプリを申請します。
    4. ドメイン管理でドメインを追加します。注意、腾讯のドメイン設定では、xxx.com のみ追加でき、xxx.yyy.com のようなものは追加できません。
  2. iOS SDK ドキュメントを参考に接続します。
    1. CocoaPods を使用して接続します。
           pod 'MSDKDns'
      
    2. プロジェクトで使用します。
          #import <MSDKDns/MSDKDns.h> // 腾讯 HTTP DNS
      
            - (void)registerMSDKDns {
                // 注意、以下の使用方法は辞書を使用して初期化しています。クラスメソッドを使用するとコンパイルが通りません。。。
                [[MSDKDns sharedInstance] initConfigWithDictionary:@{
                    @"dnsIp": @"119.29.29.98",
                    @"dnsId": @"Your AppID",
                    @"dnsKey": @"Your SecretKey", // 注意、異なる暗号化方式の SecretKey は異なります。
                    @"encryptType": @0, // 暗号化方式
                    @"debug": @1,
            //        @"routeIp": @"",
                }];
      
                NSString *hostStr = @"baidu.com"; 
                [[MSDKDns sharedInstance] WGGetHostByNameAsync:hostStr returnIps:^(NSArray *ipsArray) {
                    NSLog(@"解析 IP:%@", ipsArray);
                    if (ipsArray) {
                        self.hostIpStr = ipsArray.firstObject;
                    }
                }];
            }
      

使用#

ローカルで HTTP または HTTPS のプロキシを使用しているかどうかを検出します。プロキシがある場合は、HTTPDNS を使用しないことをお勧めします —— iOS SDK ドキュメント


// HTTP プロキシがあるかどうかを検出
- (BOOL)isUseHTTPProxy {
        CFDictionaryRef dicRef = CFNetworkCopySystemProxySettings();
        const CFStringRef proxyCFstr = (const CFStringRef)CFDictionaryGetValue(dicRef, (const void*)kCFNetworkProxiesHTTPProxy);
        NSString *proxy = (__bridge NSString *)proxyCFstr;
        if (proxy) {
            return YES;
        } else {
            return NO;
        }
  }

// HTTPS プロキシがあるかどうかを検出
  - (BOOL)isUseHTTPSProxy {
        CFDictionaryRef dicRef = CFNetworkCopySystemProxySettings();
        const CFStringRef proxyCFstr = (const CFStringRef)CFDictionaryGetValue(dicRef, (const void*)kCFNetworkProxiesHTTPSProxy);
        NSString *proxy = (__bridge NSString *)proxyCFstr;
        if (proxy) {
            return YES;
        } else {
            return NO;
        }
  }

HTTPDNS から返された IP で URL 中のドメインを置き換え、HTTP ヘッダーの host フィールドを指定します。


NSURL *httpDnsURL = [NSURL URLWithString:@"使用解析結果ip拼接的URL"];
float timeOut = 设置的超时时间;
NSMutableURLRequest *mutableReq = [NSMutableURLRequest requestWithURL:httpDnsURL cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval: timeOut];
[mutableReq setValue:@"原域名" forHTTPHeaderField:@"host"];
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
NSURLSession *session = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:[NSOperationQueue currentQueue]];
NSURLSessionTask *task = [session dataTaskWithRequest:mutableReq];
[task resume];

プロジェクトで使用しているのは AFNetworking で、AFNetworking の AFURLSessionMananger.m クラスを変更します:

  1. evaluateServerTrust:forDmain: メソッドを追加します。
  2. URLSession:task:didReceiveChallenge:completionHandler: メソッドを変更します。

- (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);

    //現在の serverTrust が信頼できるかどうかを評価
    //公式に推奨されているのは、result = kSecTrustResultUnspecified または kSecTrustResultProceed の場合、serverTrust が検証を通過できることです。
    //https://developer.apple.com/library/ios/technotes/tn2232/_index.html
    //SecTrustResultType に関する詳細情報は SecTrust.h を参照してください。
    SecTrustResultType result;
    SecTrustEvaluate(serverTrust, &result);

    return (result == kSecTrustResultUnspecified || result == kSecTrustResultProceed);
}

- (void)URLSession:(NSURLSession *)session
              task:(NSURLSessionTask *)task
didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
 completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler
{
    BOOL evaluateServerTrust = NO;
    NSURLSessionAuthChallengeDisposition disposition = NSURLSessionAuthChallengePerformDefaultHandling;
    NSURLCredential *credential = nil;

    if (self.authenticationChallengeHandler) {
        id result = self.authenticationChallengeHandler(session, task, challenge, completionHandler);
        if (result == nil) {
            return;
        } else if ([result isKindOfClass:NSError.class]) {
            objc_setAssociatedObject(task, AuthenticationChallengeErrorKey, result, OBJC_ASSOCIATION_RETAIN);
            disposition = NSURLSessionAuthChallengeCancelAuthenticationChallenge;
        } else if ([result isKindOfClass:NSURLCredential.class]) {
            credential = result;
            disposition = NSURLSessionAuthChallengeUseCredential;
        } else if ([result isKindOfClass:NSNumber.class]) {
            disposition = [result integerValue];
            NSAssert(disposition == NSURLSessionAuthChallengePerformDefaultHandling || disposition == NSURLSessionAuthChallengeCancelAuthenticationChallenge || disposition == NSURLSessionAuthChallengeRejectProtectionSpace, @"");
            evaluateServerTrust = disposition == NSURLSessionAuthChallengePerformDefaultHandling && [challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust];
        } else {
            @throw [NSException exceptionWithName:@"Invalid Return Value" reason:@"The return value from the authentication challenge handler must be nil, an NSError, an NSURLCredential or an NSNumber." userInfo:nil];
        }
    } else {
        evaluateServerTrust = [challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust];
    }

    if (evaluateServerTrust) {
        if ([self.securityPolicy evaluateServerTrust:challenge.protectionSpace.serverTrust forDomain:challenge.protectionSpace.host]) {
            disposition = NSURLSessionAuthChallengeUseCredential;
            credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
        } else {
            objc_setAssociatedObject(task, AuthenticationChallengeErrorKey,
                                     [self serverTrustErrorForServerTrust:challenge.protectionSpace.serverTrust url:task.currentRequest.URL],
                                     OBJC_ASSOCIATION_RETAIN);
            disposition = NSURLSessionAuthChallengeCancelAuthenticationChallenge;
        }
    }

    // 修正----------------------------部分
    //元のドメイン情報を取得
    NSURLRequest *request = task.currentRequest;
    NSString *host = [[request allHTTPHeaderFields] objectForKey:@"host"];
    if (!host) {
        host = challenge.protectionSpace.host;
    }
    if ([challenge.protectionSpace.authenticationMethod  isEqualToString:NSURLAuthenticationMethodServerTrust]) {
        if ([self evaluateServerTrust:challenge.protectionSpace.serverTrust forDomain:host]) {
            disposition = NSURLSessionAuthChallengeUseCredential;
            credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
        }
    }
    
    if (completionHandler) {
        completionHandler(disposition, credential);
    }
}

テスト#

上記の接続手順に従って接続を完了した後、テストを行います。画像をアップロードすると、最初は失敗し、再度成功するという状況が発生しました。失敗の原因は、信頼されていないサーバー xxx.xx.xxx.xx である可能性があるため、AFSecurityPolicy クラスをさらに修正する必要があります。


+ (instancetype)defaultPolicy {
    AFSecurityPolicy *securityPolicy = [[self alloc] init];
    securityPolicy.SSLPinningMode = AFSSLPinningModeNone;

    // 以下の2行を追加
    securityPolicy.allowInvalidCertificates = YES;
    securityPolicy.validatesDomainName = NO;

    return securityPolicy;
}

追加後、再度テストを行い、完璧に動作しました。

まとめ#

  • 接続前に、接続方法を考慮すること、つまり HTTPDNS のロジックを更新すること。
  • 接続時:
    • 阿里 HTTPDNS では Pod の依存関係を公式文書の設定バージョンに従わないことに注意。
    • 腾讯の HTTPDNS では初期化方法に注意。
    • 両社のプラットフォームでドメイン設定の違いに注意。
  • 接続後:
    • AFNetworking の AFURLSessionMananger.m クラスを修正。
    • AFSecurityPolicy クラスを修正。

参考#

読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。