All Products
Search
Document Center

HTTPDNS:Connect an iOS app to an IP address over HTTPS (including scenarios in which SNI is required)

Last Updated:Sep 22, 2022

Notice

This topic describes how to connect an iOS app to the IP address that is resolved from HTTPDNS. For information about the resolution service provided by HTTPDNS , see the iOS SDK development manual.

Background information

This topic describes how to establish an IP address-based direct connection for iOS in a common HTTPS scenario or a Server Name Indication (SNI) scenario.

HTTPS

To send an HTTPS request, your server must complete an SSL handshake or Transport Layer Security (TLS) handshake. The following section describes the procedure for completing a handshake:

  1. The SSL client or TLS client sends a handshake request that contains a random byte string and a list of supported algorithms.

  2. The SSL server or TLS server receives the handshake request, selects an applicable algorithm, and then send the public key certificate and random byte string.

  3. The SSL client or TLS client verifies the public key certificate of the server, and sends the random byte string that is encrypted by using the public key of the server.

  4. The SSL server or TLS server obtains the encrypted random byte string by using the private key.

  5. A session ticket that can be used as a secret key is generated based on the preceding process. For the duration of the SSL session or TLS session, the server and client can exchange messages that are encrypted by using the secret key.

In the preceding process, HTTPDNS is used in the third step. When the SSL client or TLS client verifies the public key certificate of the server, take note of the following points:

  1. The client unlocks the certificate chain by using the local root certificate and confirms that the certificate delivered by the server is issued by a trusted certificate authority (CA).

  2. The client checks the domain and additional domain that are bound to the certificate to determine whether the host of the handshake request is included.

If the preceding certificate verification is passed, the current server is considered trustworthy. If the preceding certificate verification fails, the server is considered untrustworthy, and the current connection is interrupted.

When the client uses HTTPDNS to resolve a domain name, the host information in the request URL is replaced with the IP address that is resolved from HTTPDNS. As a result, the resolved domain does not match the domain that is bound to the certificate, and the SSL handshake or TLS handshake fails.

SNI

SNI is an SSL- and TLS-compatible extension that is used to resolve the issue that occurs when a single server hosts multiple certificates for multiple domain names. The following section describes how SNI works:

  1. Before an SSL connection is established to the server, the domain name (host name )of the site that you want to access is sent.

  2. Then, the server returns the applicable certificate based on the domain name.

Most operating systems and browsers support the SNI extension. The SNI extension is also built in OpenSSL 0.9.8.

In the preceding process, when the client uses HTTPDNS to resolve the domain name, the host information in the request URL is replaced with the IP address that is resolved from HTTPDNS. As a result, the domain name that is obtained by the server is the IP address after resolution, and no applicable certificates for the IP address exist. In this case, no certificates are returned except for the default certificate. As a result, the SSL handshake or TLS handshake fails.

Note

For example, if you want to access resources that are hosted in a CDN-accelerated site over HTTP, you must use SNI to specify a certificate for each domain because a CDN-accelerated site hosts resources for multiple domains.

HTTPS requests in scenarios in which SNI is not required

If the resolved domain does not match the domain that is bound to a certificate, you can use a hook to implement certificate verification that is described in the preceding section. You can replace the IP address with the original domain name, and then implement certificate verification.

[Note] If an SSL verification error occurs when a network request based on this solution is initiated, check whether SNI is implemented in the scenario in which an IP addresses is used for multiple HTTPS domain names. For example, the error message kCFStreamErrorDomainSSL, -9813; The certificate for this server is invalid appears in iOS.

The following example shows the solution when the NSURLSession and NSURLConnection operations are called.

- (BOOL)evaluateServerTrust:(SecTrustRef)serverTrust
                  forDomain:(NSString *)domain
{
    /*
     * Create a certificate verification policy.
     */
    NSMutableArray *policies = [NSMutableArray array];
    if (domain) {
        [policies addObject:(__bridge_transfer id)SecPolicyCreateSSL(true, (__bridge CFStringRef)domain)];
    } else {
        [policies addObject:(__bridge_transfer id)SecPolicyCreateBasicX509()];
    }
    /*
     * Attach the verification policy to the certificate of the server.
     */
    SecTrustSetPolicies(serverTrust, (__bridge CFArrayRef)policies);
    /*
     * Check whether the certificate is trustworthy based on the information about serverTrust. 
     * Apple Inc. recommends that a certificate be allowed to pass the verification if the returned result for serverTrust is kSecTrustResultUnspecified or kSecTrustResultProceed.
     For more information, visit https://developer.apple.com/library/ios/technotes/tn2232/_index.html.
     * For information about SecTrustResultType, see the content of the SecTrust.h file.
     */
    SecTrustResultType result;
    SecTrustEvaluate(serverTrust, &result);
    return (result == kSecTrustResultUnspecified || result == kSecTrustResultProceed);
}
/*
 * NSURLConnection
 */
- (void)connection:(NSURLConnection *)connection
willSendRequestForAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge
{
    if (!challenge) {
        return;
    }
    /*
     * Obtain the actual domain name from the HTTP header. When HTTPDNS is used, the value of the Host field in the URL is replaced with an IP address.
     */
    NSString* host = [[self.request allHTTPHeaderFields] objectForKey:@"host"];
    if (!host) {
        host = self.request.URL.host;
    }
    /*
     * Check whether the identity authentication method NSURLAuthenticationMethodServerTrust is used for the challenge object. The identity authentication method is used in HTTPS mode. 
     * If the identity authentication method is not used, the system initiates the default network request process. 
     */
    if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust])
    {
        if ([self evaluateServerTrust:challenge.protectionSpace.serverTrust forDomain:host]) {
            /*
             * After the verification is complete, construct an NSURLCredential object and send the object to the host that initiated the request.
             */
            NSURLCredential *credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
            [[challenge sender] useCredential:credential forAuthenticationChallenge:challenge];
        } else {
            /*
             * If the verification fails, the system initiates the default network request process.
             */
            [[challenge sender] continueWithoutCredentialForAuthenticationChallenge:challenge];
        }
    } else {
        /*
         * If a different verification method is used, the system initiates the network request process.
         */
        [[challenge sender] continueWithoutCredentialForAuthenticationChallenge:challenge];
    }
}
////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////
/*
 * NSURLSession
 */
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task
didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
 completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential * __nullable credential))completionHandler
{
    if (!challenge) {
        return;
    }
    NSURLSessionAuthChallengeDisposition disposition = NSURLSessionAuthChallengePerformDefaultHandling;
    NSURLCredential *credential = nil;
    /*
     * Obtain the original domain name. 
     */
    NSString* host = [[self.request allHTTPHeaderFields] objectForKey:@"host"];
    if (!host) {
        host = self.request.URL.host;
    }
    if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
        if ([self evaluateServerTrust:challenge.protectionSpace.serverTrust forDomain:host]) {
            disposition = NSURLSessionAuthChallengeUseCredential;
            credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
        } else {
            disposition = NSURLSessionAuthChallengePerformDefaultHandling;
        }
    } else {
        disposition = NSURLSessionAuthChallengePerformDefaultHandling;
    }
    // Use the default verification method for other challenge objects.
    completionHandler(disposition,credential);
}

HTTPS requests in scenarios in which SNI is required

In scenarios in which a single IP address and multiple TLS certificates or SSL certificates are used in HTTPS mode, you cannot call operations to configure the SNI field for an NSURLConnection object or NSURLSession object in the upper-layer network library of iOS. In this case, the socket-level bottom-layer network library, such as CFNetwork, is required to establish direct connections by using IP addresses. CFNetwork is a bottom-layer network framework. If you want to establish an IP address-based direct connection based on CFNetwork, you need to consider issues related to data sending, data receiving, redirection, decoding, and caching. We recommend that you evaluate the risks and resolve issues before you establish an IP address-based direct connection based on CFNetwork.

In scenarios in which SNI is required, the following types of underlying network libraries are supported at the socket level:

  • The library that is provided based on CFNetWork. In this library, a hook must be used to perform certificate verification.

  • The underlying library that support SNI fields. Example: libcurl.

The following section describes some issues and provides relevant solutions:

Support for POST requests

If NSURLProtocol is used to intercept an NSURLSession request, the request body is empty. You can refer to the following solution to resolve this issue.

Solution:

Use HTTPBodyStream to obtain the request body and specify a value for the body. Sample code:

//
//  NSURLRequest+NSURLProtocolExtension.h
//
//
//  Created by ElonChan on 28/07/2017.
//  Copyright © 2017 ChenYilong. All rights reserved.
//
#import <Foundation/Foundation.h>
@interface NSURLRequest (NSURLProtocolExtension)
- (NSURLRequest *)httpdns_getPostRequestIncludeBody;
@end
//
//  NSURLRequest+NSURLProtocolExtension.h
//
//
//  Created by ElonChan on 28/07/2017.
//  Copyright © 2017 ChenYilong. All rights reserved.
//
#import "NSURLRequest+NSURLProtocolExtension.h"
@implementation NSURLRequest (NSURLProtocolExtension)
- (NSURLRequest *)httpdns_getPostRequestIncludeBody {
    return [[self httpdns_getMutablePostRequestIncludeBody] copy];
}
- (NSMutableURLRequest *)httpdns_getMutablePostRequestIncludeBody {
    NSMutableURLRequest * req = [self mutableCopy];
    if ([self.HTTPMethod isEqualToString:@"POST"]) {
        if (!self.HTTPBody) {
            NSInteger maxLength = 1024;
            uint8_t d[maxLength];
            NSInputStream *stream = self.HTTPBodyStream;
            NSMutableData *data = [[NSMutableData alloc] init];
            [stream open];
            BOOL endOfStreamReached = NO;
            // [stream hasBytesAvailable] cannot be used to determine a while loop. When the system processes image files, the system returns YES for [stream hasBytesAvailable]. As a result, an infinite while loop occurs. 
            while (!endOfStreamReached) {
                NSInteger bytesRead = [stream read:d maxLength:maxLength];
                if (bytesRead == 0) { //The system reads content until the end of the file.
                    endOfStreamReached = YES;
                } else if (bytesRead == -1) { // An error occurs when the system reads the file.
                    endOfStreamReached = YES;
                } else if (stream.streamError == nil) {
                    [data appendBytes:(void *)d length:bytesRead];
                }
            }
            req.HTTPBody = [data copy];
            [stream close];
        }
    }
    return req;
}
@end

Method:

Use the +canonicalRequestForRequest: method in the subclass of NSURLProtocol to intercept requests and process the requests.

+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request {
    return [request httpdns_getPostRequestIncludeBody];
}

Related methods:

 //NSURLProtocol.h
/*! 
    @method canInitWithRequest:
    @abstract This method determines whether this protocol can handle
    the given request.
    @discussion A concrete subclass should inspect the given request and
    determine whether or not the implementation can perform a load with
    that request. This is an abstract method. Sublasses must provide an
    implementation.
    @param request A request to inspect.
    @result YES if the protocol can handle the given request, NO if not.
*/
+ (BOOL)canInitWithRequest:(NSURLRequest *)request;
/*! 
    @method canonicalRequestForRequest:
    @abstract This method returns a canonical version of the given
    request.
    @discussion It is up to each concrete protocol implementation to
    define what "canonical" means. However, a protocol should
    guarantee that the same input request always yields the same
    canonical form. Special consideration should be given when
    implementing this method since the canonical form of a request is
    used to look up objects in the URL cache, a process which performs
    equality checks between NSURLRequest objects.
    <p>
    This is an abstract method; sublasses must provide an
    implementation.
    @param request A request to make canonical.
    @result The canonical form of the given request. 
*/
+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request;

Notes:

//NSURLProtocol.h
/*!
  
 @parma :
  
 */
+ (BOOL)canInitWithRequest:(NSURLRequest *)request
/*!
      
  
  
 */
+ (NSURLRequest*)canonicalRequestForRequest:(NSURLRequest *)request

Summary:

  • +[NSURLProtocol canInitWithRequest:]: specifies the network requests that you want to intercept.

  • +[NSURLProtocol canonicalRequestForRequest:]: reconstructs the network requests NSURLRequest that you want to intercept.

Note: +[NSURLProtocol canonicalRequestForRequest:] takes effect only if the return value of +[NSURLProtocol canInitWithRequest:] is YES.

If you want to intercept the requests that are initiated by using NSURLSession, add the subclass of NSURLProtocol to NSURLSessionConfiguration. Sample code:

     NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
     NSArray *protocolArray = @[ [CUSTOMEURLProtocol class] ];
     configuration.protocolClasses = protocolArray;
     NSURLSession *session = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:[NSOperationQueue mainQueue]];

Switch to other underlying network libraries that can be used to configure SNI fields by calling operations

If you use a third-party network library, you can use the -resolve method in cURL to access HTTPS websites by using a specified IP address. For information about cURL libraries that are integrated into iOS, see cURL documentation.

IPv6 addresses are supported. You need to only add --enable-ipv6 when you build an IPv6 address-based environment.

cURL allows you to specify SNI fields. When you configure SNI, construct a parameter in the following format: {HTTPS domain name}:443:{IP address}.

If you want to access www.example.org and the IP address is 127.0.0.1 , you can run the following code to configure SNI:

curl  
* 
 --resolve ‘www.example.org:443:127.0.0.1’

cURL library in iOS

If you use libcurl, the version of libcurl or cURL must be 7.18.1 or later and must be released on or after March 30, 2008. You can use libcurl to compile an SSL toolkit or TLS toolkit to support SNI. If you want to access HTPPS websites by using a specified IP address, you can use the --resolve method in cURL.

Sample code for iOS:

    //{HTTPS domain name}:443:{IP address}
    NSString *curlHost = ...;
    _hosts_list = curl_slist_append(_hosts_list, curlHost.UTF8String);
    curl_easy_setopt(_curl, CURLOPT_RESOLVE, _hosts_list);

In the preceding statements, the value of curlHost is specified in the

{HTTPS domain name}:443:{IP address} format.

The value of the _hosts_list parameter is contained in the hosts_list struct. This way, you can configure a mapping between IP addresses and the host. You can specify CURLOPT_RESOLVE for the curl_easy_setopt method to configure the mapping for HTTPS requests.

Then, SNI is implemented.

References