This topic describes solutions for establishing direct IP connections on iOS in HTTP and HTTPS (including Server Name Indication (SNI)) scenarios.
Overview
HTTP is an application-layer protocol for distributed, collaborative, and hypermedia information systems. It is the foundation of data communication for the World Wide Web. HTTP works on a client-server model. It allows browsers to request web page content from web servers and display the results. HTTP transmits data in plaintext and does not provide an encryption mechanism. This makes the data vulnerable to eavesdropping, tampering, or man-in-the-middle attacks during transmission.
HTTPS is an extension of HTTP used for secure communication over a computer network. It uses Secure Sockets Layer (SSL)/Transport Layer Security (TLS) to establish a secure communication channel and encrypt data packets. The main purpose of HTTPS is to authenticate website servers and protect the privacy and integrity of exchanged data. TLS is a transport-layer encryption protocol and the successor to SSL. HTTPS has two common scenarios: common HTTPS and SNI.
Server Name Indication (SNI) is an extension to the SSL and TLS protocols that improves server and client communication. It is primarily used when a single server provides services for multiple domain names.
HTTP scenario
In an HTTP scenario, the network link does not involve an SSL/TLS handshake or require certificate verification. To implement a direct IP connection, replace the host in the request URL with an IP address and set the Host field in the HTTP header to the original domain name.
Common HTTPS scenario
In a common HTTPS scenario, you can establish a direct IP connection by replacing the host in the request URL with an IP address and setting the Host field in the HTTP header to the original domain name. Then, during certificate verification, you must replace the IP address with the original domain name.
SNI scenario
In an SNI (single IP with multiple HTTPS certificates) scenario, the upper-layer network libraries
NSURLConnection/NSURLSessionin iOS do not provide interfaces to configure theSNI field. Therefore, a socket-level underlying network library such asCFNetworkis needed to implement an adaptation solution forIP direct connection network requests. However, solutions based on CFNetwork require developers to consider issues such as data transmission and reception, redirection, decoding, and caching (CFNetwork is a very low-level network implementation). We recommend that developers properly evaluate the usage risks of this scenario. We recommend that developers refer to iOS14 native encrypted DNS solution to solve SNI scenario issues.
If you use CDN services on the server side, refer to the SNI scenario solution.
Best practices
Solution for HTTP scenarios
1. Replace the host in the request URL with the IP address.
2. Set the Host field in the HTTP header to the original domain name.
// Construct the request.
- (NSMutableURLRequest *)createRequest {
// Assume the domain name is example.com and the IP address resolved by HTTPDNS is 1.2.3.4.
NSString *urlString = @"http://example.com/api";
// 1. Replace the host in the request URL with the IP address.
NSString *httpDnsString = [urlString stringByReplacingOccurrencesOfString:@"example.com" withString:@"1.2.3.4"];
NSURL *httpDnsURL = [NSURL URLWithString:httpDnsString];
NSMutableURLRequest *mutableReq = [NSMutableURLRequest requestWithURL:httpDnsURL];
// 2. Set the Host field in the HTTP header to the original domain name.
[mutableReq setValue:@"example.com" forHTTPHeaderField:@"Host"];
return mutableReq;
}Solution for common HTTPS scenarios
1. Replace the host in the request URL with the IP address.
2. Set the Host field in the HTTP header to the original domain name.
3. To resolve the domain mismatch issue during certificate verification, replace the IP address with the original domain name.
The following example shows the solution when the NSURLSession and NSURLConnection APIs are called.
// Construct the request.
- (NSMutableURLRequest *)createRequest {
// Assume the domain name is example.com and the IP address resolved by HTTPDNS is 1.2.3.4.
NSString *urlString = @"https://example.com/api";
// 1. Replace the host in the request URL with the IP address.
NSString *httpDnsString = [urlString stringByReplacingOccurrencesOfString:@"example.com" withString:@"1.2.3.4"];
NSURL *httpDnsURL = [NSURL URLWithString:httpDnsString];
NSMutableURLRequest *mutableReq = [NSMutableURLRequest requestWithURL:httpDnsURL];
// 2. Set the Host field in the HTTP header to the original domain name.
[mutableReq setValue:@"example.com" forHTTPHeaderField:@"Host"];
return mutableReq;
}
// 3. During certificate verification, replace the IP address with the original domain name.
/*
* NSURLConnection
*/
- (void)connection:(NSURLConnection *)connection
willSendRequestForAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge
{
if (!challenge) {
return;
}
/*
* When HTTPDNS is used, the host in the URL is set to an IP address.
* Obtain the real domain name from the HTTP header.
*/
NSString* host = [[self.request allHTTPHeaderFields] objectForKey:@"host"];
if (!host) {
host = self.request.URL.host;
}
/*
* Check whether the authentication method for the challenge is NSURLAuthenticationMethodServerTrust.
* This authentication process occurs in HTTPS mode.
* If no authentication method is configured, the default network request flow is used.
*/
if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust])
{
if ([self evaluateServerTrust:challenge.protectionSpace.serverTrust forDomain:host]) {
/*
* After verification, construct an NSURLCredential and send it to the initiator.
*/
NSURLCredential *credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
[[challenge sender] useCredential:credential forAuthenticationChallenge:challenge];
} else {
/*
* If verification fails, proceed with the default process.
*/
[[challenge sender] continueWithoutCredentialForAuthenticationChallenge:challenge];
}
} else {
/*
* For other authentication methods, proceed directly with the 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 authentication method for other challenges.
completionHandler(disposition,credential);
}
- (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 server certificate.
*/
SecTrustSetPolicies(serverTrust, (__bridge CFArrayRef)policies);
/*
* Evaluate whether the current serverTrust is trusted.
* Apple Inc. recommends that serverTrust can be verified if the result is kSecTrustResultUnspecified or kSecTrustResultProceed.
* For more information, see https://developer.apple.com/library/ios/technotes/tn2232/_index.html.
* For more information about SecTrustResultType, see SecTrust.h.
*/
SecTrustResultType result;
SecTrustEvaluate(serverTrust, &result);
return (result == kSecTrustResultUnspecified || result == kSecTrustResultProceed);
}For IP direct connection under AFNetworking network library in common scenarios, refer to the following solution:
// Use this return value as the request URL for AFHTTPSessionManager.
+(NSString *)getIPStringFromAliCloudDNSResolverWithURLString: (NSString *)URLString manager: (AFHTTPSessionManager *)manager {
NSURL *originUrl = [NSURL URLWithString :URLString];
NSString *host = originUrl.host;
NSString *ip= [[DNSResolver share] getIpsByCacheWithDomain:host andExpiredIPEnabled:YES].firstObject;
NSString *ipURLString = URLString;
if (ip) {
// 1. Replace the host in the request URL with the IP address.
ipURLString = [URLString stringByReplacingOccurrencesOfString:host withString:ip];
// 2. Set the Host field in the HTTP header to the original domain name.
[manager.requestSerializer setValue:originUrl.host forHTTPHeaderField:@"Host"];
__weak typeof (AFHTTPSessionManager *) weakSessionManager = manager;
// 3. During certificate verification, replace the IP address with the original domain name.
[manager setSessionDidReceiveAuthenticationChallengeBlock:^NSURLSessionAuthChallengeDisposition(NSURLSession*_Nonnullsession,
NSURLAuthenticationChallenge * _Nonnull challenge, NSURLCredential * _Nullable __autoreleasing * _Nullable credential) {
NSURLSessionAuthChallengeDisposition disposition = NSURLSessionAuthChallengePerformDefaultHandling;
// Obtain the original domain name.
NSString *host = [[weakSessionManager.requestSerializer HTTPRequestHeaders] 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];
} else {
disposition = NSURLSessionAuthChallengePerformDefaultHandling;
}
} else {
disposition = NSURLSessionAuthChallengePerformDefaultHandling;
}
return disposition;
}];
}
return ipURLString;
}
+(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);
SecTrustResultType result;
SecTrustEvaluate(serverTrust, &result);
return (result == kSecTrustResultUnspecified || result == kSecTrustResultProceed);
}
If you initiate a network request based on this solution and an SSL verification error is reported, such as the iOS system error kCFStreamErrorDomainSSL, -9813; The certificate for this server is invalid, check whether the application scenario is SNI (single IP with multiple HTTPS domain names).
Solution for SNI HTTPS scenarios
1. Custom NSURLProtocol solution
For solutions in SNI scenarios, refer to the Demo sample project source code. The following section describes some current challenges and coping strategies:
Support for POST requests
If NSURLProtocol is used to intercept an NSURLSession request, the request body is missing. You can use the following method to resolve this issue:
Use HTTPBodyStream to obtain the request body and assign a value to the body. Sample code:
//
// NSURLRequest+NSURLProtocolExtension.h
//
//
#import <Foundation/Foundation.h>
@interface NSURLRequest (NSURLProtocolExtension)
- (NSURLRequest *)alidns_getPostRequestIncludeBody;
@end
//
// NSURLRequest+NSURLProtocolExtension.h
//
//
#import "NSURLRequest+NSURLProtocolExtension.h"
@implementation NSURLRequest (NSURLProtocolExtension)
- (NSURLRequest *)alidns_getPostRequestIncludeBody {
return [[self alidns_getMutablePostRequestIncludeBody] copy];
}
- (NSMutableURLRequest *)alidns_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 to 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;
}
@endUsage:
Implement the +canonicalRequestForRequest: method in the NSURLProtocol subclass used to intercept requests and process the request object:
+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request {
return [request alidns_getPostRequestIncludeBody];
}The following describes the functions of related methods:
//NSURLProtocol.h
/*!
* @method: Create an NSURLProtocol instance. After NSURLProtocol is registered, all NSURLConnections will check whether to hold the HTTP request through this method.
@param:
@return: YES: Hold the HTTP request NO: Do not hold the HTTP request
*/
+ (BOOL)canInitWithRequest:(NSURLRequest *)request
/*!
* @method: The NSURLProtocol abstract class must be implemented. In most cases, the protocols that are used by input and output requests must be consistent. This is a basic requirement. This way, the request can be directly returned. In most cases, you do not need to modify the request. If you modify a request, a new request is returned. For example, if you add a title to an HTTP request to generate a new request, the new request is returned.
@param: Local HttpRequest: request
@return: Direct forwarding
*/
+ (NSURLRequest*)canonicalRequestForRequest:(NSURLRequest *)requestSummary:
+[NSURLProtocol canInitWithRequest:]is responsible for filtering which network requests need to be intercepted+[NSURLProtocol canonicalRequestForRequest:]is responsible for reconstructing theNSURLRequestof the network request that needs to be intercepted.
Note: The execution condition of +[NSURLProtocol canonicalRequestForRequest:] is that the return value of +[NSURLProtocol canInitWithRequest:] is YES.
When intercepting NSURLSession requests, you need to add the NSURLProtocol subclass used to intercept requests to NSURLSessionConfiguration, as follows:
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
NSArray *protocolArray = @[ [CUSTOMEURLProtocol class] ];
configuration.protocolClasses = protocolArray;
NSURLSession *session = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:[NSOperationQueue mainQueue]];For IP direct connection under AFNetworking network library in SNI scenarios, refer to the following solution:
// Create NSURLSessionConfiguration instance
NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
// Set to use custom HTTP DNS resolution protocol
config.protocolClasses = @[[CFHTTPDNSHTTPProtocol class]];
// Initialize AFHTTPSessionManager
AFHTTPSessionManager *sessionManager = [[AFHTTPSessionManager alloc] initWithSessionConfiguration:config];
sessionManager.responseSerializer = [AFHTTPResponseSerializer serializer];
// Create data task
NSURLSessionDataTask *task = [sessionManager dataTaskWithHTTPMethod:@"GET"
URLString:@"Your request URL"
parameters:nil
headers:nil
uploadProgress:^(NSProgress *uploadProgress) {
NSLog(@"Upload progress: %@", uploadProgress.localizedDescription);
} downloadProgress:^(NSProgress *downloadProgress) {
NSLog(@"Download progress: %@", downloadProgress.localizedDescription);
} success:^(NSURLSessionDataTask *task, id responseObject) {
// Handle successful response
NSLog(@"Success: %@", responseObject);
} failure:^(NSURLSessionDataTask *task, NSError *error) {
// Handle the error
NSLog(@"Failure: %@", error.localizedDescription);
}];
// Start the object download task
[task resume];2. Other underlying network library solutions
Taking libcurl as an example, libcurl / cURL at least 7.18.1 (March 30, 2008) compiled with an SSL/TLS toolkit with SNI support, curl has a --resolve method that can implement accessing HTTPS websites using specified IP addresses.
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);Where curlHost is like: {HTTPS domain name}:443:{IP address}
_hosts_list is a struct type hosts_list, which can set multiple mappings between IP and Host. The curl_easy_setopt method passes CURLOPT_RESOLVE to set this mapping in the HTTPS request. Then, SNI is implemented.
Summary
Solution | Applicable scenarios | Pros | Cons |
1. Replace URL 2. Set Host | HTTP scenarios | Simplest method | Only suitable for the plaintext HTTP protocol |
1. Replace URL 2. Set Host 3. Replace IP with the original domain name during certificate verification | Common HTTPS scenarios (non-SNI) | Simple integration, support for common system networking libraries, NSURLSession, NSURLConnection, AFHTTPSessionManager | Does not support SNI |
Custom NSURLProtocol | All scenarios | Based entirely on low-level system APIs | Based on a low-level network library. Development and maintenance costs are high. You must handle data transmission and reception, redirection, decoding, and caching. |
libcurl | All scenarios Cross-platform | Mature community and extensive documentation. Lets you set SNI. Strong cross-platform compatibility. | The C language API has a learning curve for iOS developers. You must handle issues such as cookies, redirection, and caching. |