All Products
Search
Document Center

HTTPDNS:Handle issues related to cookies when you use WebView in iOS apps to connect to the server through IP addresses

Last Updated:Sep 15, 2022

You can use WebView in iOS apps to connect to the server through IP addresses to avoid DNS spoofing. This topic describes the issues that may occur in this scenario and provides solutions.

Notice

This topic focus on how to use IP addresses resolved by HTTPDNS. For more information about how to use HTTPDNS to resolve domain names to IP addresses, see SDK for iOS

WKWebView cannot use NSURLProtocol to intercept network requests

You can use the following solutions to handle this issue:

  • Use UIWebView instead of WKWebView.

  • Use WKWebView with a private API to register a custom protocol and intercept requests. The following code provides an example:

      // Register your protocol.
      [NSURLProtocol registerClass:[CustomProtocol class]];
      // Create WKWebview.
      WKWebViewConfiguration * config = [[WKWebViewConfiguration alloc] init];
      WKWebView * wkWebView = [[WKWebView alloc] initWithFrame:CGRectMake(0, 0,   [UIScreen mainScreen].bounds.size.width, [UIScreen mainScreen].bounds.size.height) configuration:config];
      [wkWebView loadRequest:webViewReq];
      [self.view addSubview:wkWebView];
      // Register the scheme.
      Class cls = NSClassFromString(@"WKBrowsingContextController");
      SEL sel = NSSelectorFromString(@"registerSchemeForCustomProtocol:");
      if ([cls respondsToSelector:sel]) {
          // Allow HTTP requests or HTTPS requests, and allow other schemes that meet the requirement of the URL loading system.
          [cls performSelector:sel withObject:@"http"];
          [cls performSelector:sel withObject:@"https"];
      }

    Private APIs used in solutions may result in compatibility issues. For example, the browsingContextController method and the unregisterSchemeForCustomProtocol method used to unregister schemes can be used only in iOS 8.4 and later. In iOS 8.0 to 8.3, you can obtain the results of WKBrowsingContextController only by generating dynamic strings and cannot unregister schemes. However, these issues can be easily fixed by using alternative solutions. You can also verify whether this solution works in the developer previews of new iOS versions. If the private API used in this example is removed in further iOS versions, you can use the official solution provided by Apple to handle this issue.

    Notice

    If you register the scheme by using - (void)viewDidLoad, an error may occur because the registration is not performed within the expected period of time. We recommend that you use +load to register the scheme. In this case, you can see Connect an iOS app to an IP address over HTTPS (including scenarios in which SNI is required) to handle NSURLProtocol-related issues that may occur.

Handle issues related to cookies when you use WebView to connect to the server through IP addresses

This section provides solutions to the following issue:

  • WKWebView is not optimal for cookie management. Developers wonder whether cookie management is optimized in WKWebVIew in iOS 11 and how to use the optimized cookie management feature.

  • When you use an IP address to connect to the server, the value of the Domain field in the cookies that are returned by the server is also an IP address. If the IP address is dynamic, the following issue may occur: The logon status verification of some HTML5 apps depends on cookies. However, requests in WKWebView do not automatically include cookies.

No cookies are obtained when WKWebView uses NSURLProtocol to intercept requests

iOS 11 provides a new API operation WKHTTPCookieStore that can be used to intercept the cookies of WKWebView.

The following code provides an example on how to use WKHTTPCookieStore:

   WKHTTPCookieStore *cookieStroe = self.webView.configuration.websiteDataStore.httpCookieStore;
   // get cookies
    [cookieStroe getAllCookies:^(NSArray<NSHTTPCookie *> * _Nonnull cookies) {
        NSLog(@"All cookies %@",cookies);
    }];

    // set cookie
    NSMutableDictionary *dict = [NSMutableDictionary dictionary];
    dict[NSHTTPCookieName] = @"userid";
    dict[NSHTTPCookieValue] = @"123";
    dict[NSHTTPCookieDomain] = @"xxxx.com";
    dict[NSHTTPCookiePath] = @"/";

    NSHTTPCookie *cookie = [NSHTTPCookie cookieWithProperties:dict];
    [cookieStroe setCookie:cookie completionHandler:^{
        NSLog(@"set cookie");
    }];

    // delete cookie
    [cookieStroe deleteCookie:cookie completionHandler:^{
        NSLog(@"delete cookie");
    }];

Use WKHTTPCookieStore provided by iOS 11 to resolve the issue that the first request on WKWebView does not include cookies

  • Problem description: The logon status verification of some HTML5 apps depends on cookies. However, requests in WKWebView do not automatically include cookies. For example, you use your local native framework to log on to a web page, obtain cookies, and use NSHTTPCookieStorage to store the cookies to your on-premises machine. However, when you use WKWebView to access the related web page, you are not automatically logged on to the web page. If the logon operation is also performed on WebView, this issue does not occur.

  • You can use the API of iOS 11 to resolve this issue. Each request that is sent to WKWebView includes cookies in WKHTTPCookieStore but may not include cookies in NSHTTPCookieStorage. Therefore, the first request that is sent to WKWebView does not include cookies.

  • Solution:

    Before you perform the -[WKWebView loadReques:] operation, copy the content in NSHTTPCookieStorage to WKHTTPCookieStore. This way, cookies on WKWebView are injected into WKHTTPCookieStore. The following sample code provides an example:

        [self copyNSHTTPCookieStorageToWKHTTPCookieStoreWithCompletionHandler:^{
                NSURL *url = [NSURL URLWithString:@"https://www.v2ex.com"];
                NSURLRequest *request = [NSURLRequest requestWithURL:url];
                [_webView loadRequest:request];
            }];

    - (void)copyNSHTTPCookieStorageToWKHTTPCookieStoreWithCompletionHandler:(nullable void (^)())theCompletionHandler; {
        NSArray *cookies = [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookies];
        WKHTTPCookieStore *cookieStroe = self.webView.configuration.websiteDataStore.httpCookieStore;
        if (cookies.count == 0) {
            !theCompletionHandler ?: theCompletionHandler();
            return;
        }
        for (NSHTTPCookie *cookie in cookies) {
            [cookieStroe setCookie:cookie completionHandler:^{
                if ([[cookies lastObject] isEqual:cookie]) {
                    !theCompletionHandler ?: theCompletionHandler();
                    return;
                }
            }];
        }
    }

    The preceding sample code shows how to use the API of iOS 11 to resolve this issue. If you use an iOS version earlier than iOS 11, use the API of iOS earlier than iOS 11.

Use the API of iOS earlier than iOS 11 to resolve the issue that the first request on WKWebView does not include cookies

All WKWebView objects use the same WKProcessPool instance to allow WKWebView objects to share cookies. Cookies are classified into session cookies and persistent cookies. However, the WKProcessPool instance of WKWebView is reset after the app kills processes and restarts. As a result, the persistent cookies and session cookies in WKProcessPool are lost. The WKProcessPool instance cannot be hosted on your on-premises machine. To resolve this issue, you can include cookies in the request header.

 WKWebView * webView = [WKWebView new]; 
 NSMutableURLRequest * request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"http://xxx.com/login"]]; 
 [request addValue:@"skey=skeyValue" forHTTPHeaderField:@"Cookie"]; 
 [webView loadRequest:request];

You can use the Domain field to obtain the cookie skey=skeyValue. The following example shows the custom class.

HTTPDNSCookieManager.h

#ifndef HTTPDNSCookieManager_h
#define HTTPDNSCookieManager_h

// Make sure that the specified URL matches the cookie rule.
typedef BOOL (^HTTPDNSCookieFilter)(NSHTTPCookie *, NSURL *);

@interface HTTPDNSCookieManager : NSObject

+ (instancetype)sharedInstance;

/**
 Specify a URL that matches the cookie rule.

 @param filter Specify the filter.
 */
- (void)setCookieFilter:(HTTPDNSCookieFilter)filter;

/**
 Process and store cookies that are included in HTTP responses.

 @param headerFields HTTP Header Fields
 @ param URL Search for the URL that matches the cookies based on the matching rule.
 @ return Return the cookies that are added to the storage.
 */
- (NSArray<NSHTTPCookie *> *)handleHeaderFields:(NSDictionary *)headerFields forURL:(NSURL *)URL;

/**
 Match the local cookies and obtain the request cookies that match the URL.

 @ param URL Search for the URL that matches the cookies based on the matching rule.
 @ return Return the request cookies that match the URL.
 */
- (NSString *)getRequestCookieHeaderForURL:(NSURL *)URL;

/**
 Delete the stored cookies.

 @ param URL Search for the URL that matches the cookies based on the matching rule.
 @ return Return the number of cookies that are deleted.
 */
- (NSInteger)deleteCookieForURL:(NSURL *)URL;

@end

#endif /* HTTPDNSCookieManager_h */

HTTPDNSCookieManager.m
#import <Foundation/Foundation.h>
#import "HTTPDNSCookieManager.h"

@implementation HTTPDNSCookieManager
{
    HTTPDNSCookieFilter cookieFilter;
}

- (instancetype)init {
    if (self = [super init]) {
        /**
            In this example, you can check whether the value of the host field in the URL includes the value of the Domain field in cookies to determine whether the URL matches the cookies. This matching rule is simple.
            You can call the setCookieFilter operation to specify the cookie matching rule. 
            For example, you can specify the matching rule that the Domain field in cookies matches the suffix of the host field in the URL. You can also check whether the URL complies with the configuration of the Path field in cookies.
            For more information about the matching rule, see section 3.3 of RFC 2965.
         */
        cookieFilter = ^BOOL(NSHTTPCookie *cookie, NSURL *URL) {
            if ([URL.host containsString:cookie.domain]) {
                return YES;
            }
            return NO;
        };
    }
    return self;
}

+ (instancetype)sharedInstance {
    static id singletonInstance = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        if (!singletonInstance) {
            singletonInstance = [[super allocWithZone:NULL] init];
        }
    });
    return singletonInstance;
}

+ (id)allocWithZone:(struct _NSZone *)zone {
    return [self sharedInstance];
}

- (id)copyWithZone:(struct _NSZone *)zone {
    return self;
}

- (void)setCookieFilter:(HTTPDNSCookieFilter)filter {
    if (filter != nil) {
        cookieFilter = filter;
    }
}

- (NSArray<NSHTTPCookie *> *)handleHeaderFields:(NSDictionary *)headerFields forURL:(NSURL *)URL {
    NSArray *cookieArray = [NSHTTPCookie cookiesWithResponseHeaderFields:headerFields forURL:URL];
    if (cookieArray != nil) {
        NSHTTPCookieStorage *cookieStorage = [NSHTTPCookieStorage sharedHTTPCookieStorage];
        for (NSHTTPCookie *cookie in cookieArray) {
            if (cookieFilter(cookie, URL)) {
                NSLog(@"Add a cookie: %@", cookie);
                [cookieStorage setCookie:cookie];
            }
        }
    }
    return cookieArray;
}

- (NSString *)getRequestCookieHeaderForURL:(NSURL *)URL {
    NSArray *cookieArray = [self searchAppropriateCookies:URL];
    if (cookieArray != nil && cookieArray.count > 0) {
        NSDictionary *cookieDic = [NSHTTPCookie requestHeaderFieldsWithCookies:cookieArray];
        if ([cookieDic objectForKey:@"Cookie"]) {
            return cookieDic[@"Cookie"];
        }
    }
    return nil;
}

- (NSArray *)searchAppropriateCookies:(NSURL *)URL {
    NSMutableArray *cookieArray = [NSMutableArray array];
    NSHTTPCookieStorage *cookieStorage = [NSHTTPCookieStorage sharedHTTPCookieStorage];
    for (NSHTTPCookie *cookie in [cookieStorage cookies]) {
        if (cookieFilter(cookie, URL)) {
            NSLog(@"Search an appropriate cookie: %@", cookie);
            [cookieArray addObject:cookie];
        }
    }
    return cookieArray;
}

- (NSInteger)deleteCookieForURL:(NSURL *)URL {
    int delCount = 0;
    NSHTTPCookieStorage *cookieStorage = [NSHTTPCookieStorage sharedHTTPCookieStorage];
    for (NSHTTPCookie *cookie in [cookieStorage cookies]) {
        if (cookieFilter(cookie, URL)) {
            NSLog(@"Delete a cookie: %@", cookie);
            [cookieStorage deleteCookie:cookie];
            delCount++;
        }
    }
    return delCount;
}

@end

The following code provides an example on how to sent requests:

 WKWebView * webView = [WKWebView new]; 
 NSMutableURLRequest * request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"http://xxx.com/login"]]; 
NSString *value = [[HTTPDNSCookieManager sharedInstance] getRequestCookieHeaderForURL:url];
[request setValue:value forHTTPHeaderField:@"Cookie"];
 [webView loadRequest:request];

The following code provides an example on how to receive and process a request:

    NSURLSessionTask *task = [session dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
        if (!error) {
            NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
            // Parse the HTTP response header and store cookies.
            [[HTTPDNSCookieManager sharedInstance] handleHeaderFields:[httpResponse allHeaderFields] forURL:url];
        }
    }];
    [task resume];

Configure document.cookie to resolve the cookie-related issue that occurs when you send Asynchronous JavaScript and XML (AJAX) or iframe requests to web pages that use the same domain name.

WKUserContentController* userContentController = [WKUserContentController new]; 
 WKUserScript * cookieScript = [[WKUserScript alloc] initWithSource: @"document.cookie = 'skey=skeyValue';" injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:NO]; 
 [userContentController addUserScript:cookieScript];

Logon failure caused by dynamic IP addresses in cookies

If cookies are generated during the client logon session and multiple IP addresses are specified for the related domain name, cookies that correspond to the domain name are read when the domain name is used to access the client and cookies that correspond to an IP address are read when the IP address is used to access the client. In this case, if different IP addresses are specified for the domain name that you used to access the client two consecutive times, the logon session becomes invalid.

If the WebView page in an app needs to use the logon session that is stored in system cookies, all local network requests of the app can share the logon session of cookies when a domain name is used to access a web page. However, when the local network requests use HTTPDNS, the requests need to use an IP address to access the client. As a result, WebView that still uses domain names to access web pages cannot read the logon session that is stored in system cookies. The key in the system cookies corresponds to the IP address. After an IP address-based direct connection is established, the server returns cookies that include a dynamic IP address. As a result, the logon fails.

After an IP address is used to access a web page, the cookies that are returned by the server also include the IP address. Therefore, when you use a domain name to access a web page, local cookies cannot be used or another IP address that corresponds to the same domain name may be used to access the web page. In this case, the cookies that are used are different. As a result, the logon fails.

The following two methods are available to solve this issue:

  • Change the storage mode of cookies to ensure that cookies are stored based on domain names.

  • Make sure that only a single IP address is returned for the domain name.

We recommend that you use the first method because you cannot use the scheduling capability of DNS in the second method.

Use iOS 11 API WKHTTPCookieStore to resolve the cookie management issues of WKWebView

Each time the server returns cookies, use a domain name to replace the IP address that is included in cookies before cookies are saved. This way, local cookies are matched even if an IP address is used to access a web page each time a network request is received. This is because the value of the host field is manually changed to a domain name that corresponds to the IP address.

The following code provides an example on how to use a domain name to replace the IP address included in cookies after a request is successful or a web page is loaded:

- (void)updateWKHTTPCookieStoreDomainFromIP:(NSString *)IP toHost:(NSString *)host {
    WKHTTPCookieStore *cookieStroe = self.webView.configuration.websiteDataStore.httpCookieStore;
    [cookieStroe getAllCookies:^(NSArray<NSHTTPCookie *> * _Nonnull cookies) {
        [[cookies copy] enumerateObjectsUsingBlock:^(NSHTTPCookie * _Nonnull cookie, NSUInteger idx, BOOL * _Nonnull stop) {
            if ([cookie.domain isEqualToString:IP]) {
                NSMutableDictionary<NSHTTPCookiePropertyKey, id> *dict = [NSMutableDictionary dictionaryWithDictionary:cookie.properties];
                dict[NSHTTPCookieDomain] = host;
                NSHTTPCookie *newCookie = [NSHTTPCookie cookieWithProperties:[dict copy]];
                [cookieStroe setCookie:newCookie completionHandler:^{
                    [self logCookies];
                    [cookieStroe deleteCookie:cookie
                            completionHandler:^{
                                [self logCookies];
                            }];
                }];
            }
        }];
    }];
}

iOS 11 also provides operations shown by the following code to help you determine the period of time when data in cookies can be replaced:

@protocol WKHTTPCookieStoreObserver <NSObject>
@optional
- (void)cookiesDidChangeInCookieStore:(WKHTTPCookieStore *)cookieStore;
@end

//WKHTTPCookieStore
/*! @abstract Adds a WKHTTPCookieStoreObserver object with the cookie store.
 @param observer The observer object to add.
 @discussion The observer is not retained by the receiver. It is your responsibility
 to unregister the observer before it becomes invalid.
 */
- (void)addObserver:(id<WKHTTPCookieStoreObserver>)observer;

/*! @abstract Removes a WKHTTPCookieStoreObserver object from the cookie store.
 @param observer The observer to remove.
 */
- (void)removeObserver:(id<WKHTTPCookieStoreObserver>)observer;

The following code provides an example on how to use the operations:

@interface WebViewController ()<WKHTTPCookieStoreObserver>
- (void)viewDidLoad {
    [super viewDidLoad];
    [NSURLProtocol registerClass:[WebViewURLProtocol class]];
    NSHTTPCookieStorage *cookieStorage = [NSHTTPCookieStorage sharedHTTPCookieStorage];
    [cookieStorage setCookieAcceptPolicy:NSHTTPCookieAcceptPolicyAlways];
    WKHTTPCookieStore *cookieStroe = self.webView.configuration.websiteDataStore.httpCookieStore;
    [cookieStroe addObserver:self];

    [self.view addSubview:self.webView];
    //... ...
}

#pragma mark -
#pragma mark - WKHTTPCookieStoreObserver Delegate Method

- (void)cookiesDidChangeInCookieStore:(WKHTTPCookieStore *)cookieStore {
    [self updateWKHTTPCookieStoreDomainFromIP:CYLIP toHost:CYLHOST];
}

The usage of the -updateWKHTTPCookieStoreDomainFromIP method is described in the preceding section.

To use this solution, you must maintain the mapping between IP addresses and hosts for clients. The mapping helps you identify a host based on an IP address. The maintenance cost of the mapping may be high. The following solution is a general solution, which is used to handle issues in iOS earlier than iOS 11.

Manually manage cookies in requests intercepted by NSURLProtocol

Step 1: Include the original URL in the request header when you replace an IP address with a domain name.

+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request {
    NSMutableURLRequest *mutableReq = [request mutableCopy];
    NSString *originalUrl = mutableReq.URL.absoluteString;
    NSURL *url = [NSURL URLWithString:originalUrl];
    // Call an API operation to obtain the IP address asynchronously.
    NSString *ip = [[HttpDnsService sharedInstance] getIpByHostAsync:url.host];
    if (ip) {
        NSRange hostFirstRange = [originalUrl rangeOfString:url.host];
        if (NSNotFound != hostFirstRange.location) {
            NSString *newUrl = [originalUrl stringByReplacingCharactersInRange:hostFirstRange withString:ip];
            mutableReq.URL = [NSURL URLWithString:newUrl];
            [mutableReq setValue:url.host forHTTPHeaderField:@"host"];
            // Include originalUrl in the request header and save the original URL.
            [mutableReq addValue:originalUrl forHTTPHeaderField:@"originalUrl"];
        }
    }
    NSURLRequest *postRequestIncludeBody = [mutableReq cyl_getPostRequestIncludeBody];
    return postRequestIncludeBody;
}

Step 2: After the data is obtained, manually manage cookies.

- (void)handleCookiesFromResponse:(NSURLResponse *)response {
    NSString *originalURLString = [self.request valueForHTTPHeaderField:@"originalUrl"];
    if ([response isKindOfClass:[NSHTTPURLResponse class]]) {
        NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
        NSDictionary<NSString *, NSString *> *allHeaderFields = httpResponse.allHeaderFields;
        if (originalURLString && originalURLString.length > 0) {
            NSArray *cookies = [NSHTTPCookie cookiesWithResponseHeaderFields:allHeaderFields forURL: [[NSURL alloc] initWithString:originalURLString]];
            if (cookies && cookies.count > 0) {
                NSURL *originalURL = [NSURL URLWithString:originalURLString];
                [[NSHTTPCookieStorage sharedHTTPCookieStorage] setCookies:cookies forURL:originalURL mainDocumentURL:nil];
            }
        }
    }
}

- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task willPerformHTTPRedirection:(NSHTTPURLResponse *)response newRequest:(NSURLRequest *)newRequest completionHandler:(void (^)(NSURLRequest *))completionHandler {
    NSString *location = response.allHeaderFields[@"Location"];
    NSURL *url = [[NSURL alloc] initWithString:location];
    NSMutableURLRequest *mRequest = [newRequest mutableCopy];
    mRequest.URL = url;
    if (location && location.length > 0) {
        if ([[newRequest.HTTPMethod lowercaseString] isEqualToString:@"post"]) {
            // Redirect the POST request to a GET request.
            mRequest.HTTPMethod = @"GET";
            mRequest.HTTPBody = nil;
        }
        [mRequest setValue:nil forHTTPHeaderField:@"host"];
        // Include the cookies in the request. 
        [self handleCookiesFromResponse:response];
        [XXXURLProtocol removePropertyForKey:XXXURLProtocolHandledKey inRequest:mRequest];
        completionHandler(mRequest);
    } else{
       completionHandler(mRequest);
    }
}

Step 3: Before the request is sent, include the cookies in the request.

+ (void)handleCookieWithRequest:(NSMutableURLRequest *)request {
    NSString* originalURLString = [request valueForHTTPHeaderField:@"originalUrl"];
    if (!originalURLString || originalURLString.length == 0) {
        return;
    }
    NSArray *cookies = [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookies];
    if (cookies && cookies.count >0) {
        NSDictionary *cookieHeaders = [NSHTTPCookie requestHeaderFieldsWithCookies:cookies];
        NSString *cookieString = [cookieHeaders objectForKey:@"Cookie"];
        [request addValue:cookieString forHTTPHeaderField:@"Cookie"];
    }
}

+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request {
    NSMutableURLRequest *mutableReq = [request mutableCopy];
//...
     [self handleCookieWithRequest:mutableReq];
    return [mutableReq copy];
}

References:

Handle issues related to the cookies of HTTP 302 requests

The preceding cookie-related solutions cannot be used to resolve issues that occur when the HTTP 302 response code is returned for requests. For example, if the first request is to access http://www.a.com, you can include cookies in the request header to resolve the cookie-related issue. When the web page for the request is redirected to http://www.b.com, the request may fail because the request does not include cookies. The callback function is called each time the system redirects the user to another web page.

 - (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler;

Therefore, you can use the callback function to intercept a request for which the HTTP 302 response code is returned, copy the request, include cookies in the request header, and then call loadRequest. This solution cannot resolve the cookie-related issue that occurs when cross-origin iframe for HTTP requests are initiated. -[WKWebView loadRequest:] is suitable only for loading mainframe requests.

References

Libraries:

Demos: