AFNetworking 原作者都無法解決的問題: 如何使用ip直接訪問https網站?

背景

最近App似乎有報異常是DNS無法解析,嘗試解決此問題.蒐集到的資料很少,甚至連AFN原作者都判定這可能是一個無解的問題,參見: https://github.com/AFNetworking/AFNetworking/issues/2954,不過最終還是靠着stackoverflow上的一丁點提示,順利找到並彙集成了一個可用的解決方案.大喜,與君共享!

問題描述

通過IP直接訪問網站,可以解決DNS劫持問題.DNS劫持,可以通過修改電腦的host文件模擬.如果是HTTP請求,使用ip地址直接訪問接口,配合header中Host字段帶上原來的域名信息即可;如果是 https請求,會很麻煩,需要 Overriding TLS Chain Validation Correctly;curl 中有一個 -resolve 方法可以實現使用指定ip訪問https網站,iOS中集成curl庫應該也可以,不過改動太大,未驗證;對於服務器IP經常變的情況,可能需要使用httpDNS服務,參見:https://www.dnspod.cn/httpdns.

解決方案討論

1. 最直接的方式是允許無效的SSL證書,生產環境不建議使用;

2.一個需要部分重寫AFN源碼的方法.

  • 在Info.plist中添加NSAppTransportSecurity類型Dictionary,在NSAppTransportSecurity下添加NSAllowsArbitraryLoads類型Boolean,值設爲YES.這些本來是用來解決iOS9下,允許HTTP請求訪問網絡的,當然作用不止這些.具體原因感興趣的自行google.

  • 給 AFURLConnectionOperation 類添加新屬性:

/** 可信任的域名,用於支持通過ip訪問此域名下的https鏈接.
 Trusted domain, this domain for support via IP access HTTPS links.
 */
@property(nonatomic, strong) NSMutableArray * trustHostnames;
  • 給 AFURLConnectionOperation 實現的代理方法: - (void)connection:(NSURLConnection *)connection
    willSendRequestForAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge 添加添加可信任的域名的相關邏輯代碼:
- (void)connection:(NSURLConnection *)connection
willSendRequestForAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge
{
    if (self.authenticationChallenge) {
        self.authenticationChallenge(connection, challenge);
        return;
    }

    if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
        SecTrustRef serverTrust = challenge.protectionSpace.serverTrust;

        /* 添加可信任的域名,以支持:直接使用ip訪問特定https服務器.
         Add trusted domain name to support: direct use of IP access specific HTTPS server.*/
        for (NSString * trustHostname  in [self trustHostnames]) {
            serverTrust = AFChangeHostForTrust(serverTrust, trustHostname);
        }

    ....
  • 參考Apple官方文檔,實現自定義的添加可信域名的函數: AFChangeHostForTrust
static inline SecTrustRef AFChangeHostForTrust(SecTrustRef trust, NSString * trustHostname)
{
    if ( ! trustHostname || [trustHostname isEqualToString:@""]) {
        return trust;
    }

    CFMutableArrayRef newTrustPolicies = CFArrayCreateMutable(
                                                              kCFAllocatorDefault, 0, &kCFTypeArrayCallBacks);

    SecPolicyRef sslPolicy = SecPolicyCreateSSL(true, (CFStringRef)trustHostname);

    CFArrayAppendValue(newTrustPolicies, sslPolicy);


#ifdef MAC_BACKWARDS_COMPATIBILITY
    /* This technique works in OS X (v10.5 and later) */

    SecTrustSetPolicies(trust, newTrustPolicies);
    CFRelease(oldTrustPolicies);

    return trust;
#else
    /* This technique works in iOS 2 and later, or
     OS X v10.7 and later */

    CFMutableArrayRef certificates = CFArrayCreateMutable(
                                                          kCFAllocatorDefault, 0, &kCFTypeArrayCallBacks);

    /* Copy the certificates from the original trust object */
    CFIndex count = SecTrustGetCertificateCount(trust);
    CFIndex i=0;
    for (i = 0; i < count; i++) {
        SecCertificateRef item = SecTrustGetCertificateAtIndex(trust, i);
        CFArrayAppendValue(certificates, item);
    }

    /* Create a new trust object */
    SecTrustRef newtrust = NULL;
    if (SecTrustCreateWithCertificates(certificates, newTrustPolicies, &newtrust) != errSecSuccess) {
        /* Probably a good spot to log something. */

        return NULL;
    }

    return newtrust;
#endif
}
  • 使用AOP方法,重寫 AFURLConnectionOperation 的trustHostnames屬性:
    /* 使用AOP方式,指定可信任的域名, 以支持:直接使用ip訪問特定https服務器.*/
    [AFURLConnectionOperation aspect_hookSelector:@selector(trustHostnames) withOptions:AspectPositionInstead usingBlock: ^(id<AspectInfo> info){
        __autoreleasing NSArray * trustHostnames = @[@"www.example.com"];

        NSInvocation *invocation = info.originalInvocation;
        [invocation setReturnValue:&trustHostnames];
    }error:NULL];

此處用到的是一個 iOS AOP庫,不熟悉的點這裏: http://www.ios122.com/2015/08/aspects/.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章