背景#
最近開発した水印カメラで、ユーザーのネットワークは正常ですが、アップロードタイムアウトやアップロード失敗の問題が発生しました。聞云のバックエンドでインターフェースエラーログを確認したところ、ユーザーの localDNS が空であったため、HTTPDNS の接続が必要になりました。
実践#
プロジェクトで使用しているネットワークリクエストは AFNetworking フレームワークを使用しているため、サードパーティの HTTPDNS を接続した後、AFNetworking の内容を変更してリクエストを IP に通す必要があります。
大まかな流れは SDK を接続する ——> SDK を登録する ——> IP を取得する ——> 保存する ——> 使用する、です。ここでは個人の状況に応じて、起動時に SDK を登録し、IP を取得する方法は二通りあります。一つはアプリ起動時に一度だけ取得し、その後保存してアプリ使用中は更新しない方法、もう一つは特定のインターフェースを使用するたびに取得する方法です。
以下で接続のプロセスを詳しく見ていきましょう。
阿里 HTTPDNS#
- クイックスタートの手順に従って設定します。
- ドメインを追加します。阿里のドメイン追加では、完全一致と二次ドメインの方法を追加できます。
- iOS SDK 接続を参考に接続します。
- CocoaPods を使用して接続します。
ここで文句を言いたくなるところですが、阿里の公式文書に書かれている CocoaPod でインストールした SDK は最新ではありません。source 'https://github.com/CocoaPods/Specs.git' source 'https://github.com/aliyun/aliyun-specs.git' pod 'AlicloudHTTPDNS' # 注意、公式文書に書かれている方法ではなく、後ろに指定バージョンを追加しないでください。
- プロジェクトで使用します。
- (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; } }
- CocoaPods を使用して接続します。
腾讯云 HTTPDNS#
- 入門ガイドの手順に従って設定します。
- アカウントを登録 / ログインします。
- サービスを開通します。
- 開発設定でアプリを申請します。
- ドメイン管理でドメインを追加します。注意、腾讯のドメイン設定では、xxx.com のみ追加でき、xxx.yyy.com のようなものは追加できません。
- iOS SDK ドキュメントを参考に接続します。
- CocoaPods を使用して接続します。
pod 'MSDKDns'
- プロジェクトで使用します。
#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; } }]; }
- CocoaPods を使用して接続します。
使用#
ローカルで 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
クラスを変更します:
evaluateServerTrust:forDmain:
メソッドを追加します。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
クラスを修正。
- AFNetworking の