Tuesday, January 12, 2016

Opening App Transport Security failures in Safari

One of my client projects is an iOS application which, like many iOS Applications, has an embedded web view, which it uses in this case to display help content from the web. Because this app targets iOS 7.x as a baseline, it uses UIWebView instead of, say, SFSafariViewController.

The help content is part of the company's website, and the web page footer has social media links, and some of these links, if you clicked on them, wouldn't load.

Initially, I thought this might be because of the target="_blank" attribute, which has been a problem with UIWebView in the past, so I built a UIWebViewDelegate that would inject a little bit of JavaScript to remove the targets. It didn't work. So I changed the JavaScript to set the target to "_self". Still didn't work:

- (void)webViewDidFinishLoad:(UIWebView *)webView
{
    NSString *injectedJavascript = @"function "
      "AppPrefix_Injected_suppressBlankTargets() {\n"
      "  var links = document.getElementsByTagName('a');\n"
      "  var hrefs = [];\n"
      "  for( index = 0; index < links.length; index++ ) {\n"
      "    var link = links.item( index );\n"
      "    if( link.getAttribute( 'target' ) == '_blank' ) {\n"
      "      link.setAttribute( 'target', '_self' );\n"
    "      hrefs.push( link.outerHTML );\n"
      "    }\n"
      "  }\n"
      "  return hrefs.join();\n"
      "}"
      "AppPrefix_Injected_suppressBlankTargets();";
    NSString *result = [webView 
      stringByEvaluatingJavaScriptFromString:injectedJavascript];
    DLog( "Injected javascript to suppress blank targets on links: %@", result );
}

Having failed on two assumptions, I realized that I should verify my assumption instead, and implemented webView:didFailLoadWithError, which quickly showed me that the actual problem was App Transport Security.

The social media links? Some of them are HTTP rather than HTTPS, and those ones won't load because App Transport Security doesn't like it. That left options.

Disable App Transport Security?
I don't like going this route until I have to.  I don't want to whitelist certain sites because the social media links might lead to other sites which would also need to be whitelisted and then eventually you just end up disabling app transport security anyway.

Open ATS Failures in Safari
What I ended up doing instead is listening for errors (webView:didFailLoadWithError) and then check the error. If it was an app transport security failure, pull the URL out of NSError's userInfo and then open that link in mobile safari instead.

It's fairly straightforward. Write a UIWebViewDelegate like this:

- (void)webView:(UIWebView *)webView didFailLoadWithError:(NSError *)error {
    NSURL *failedUrl = [self parseATSError:error];
    if( failedUrl != nil ) {
        DLog( "ATS Failure, opening in Safari: %@", failedUrl );
        [[UIApplication sharedApplication] openURL:failedUrl];
    } else {
        DLog( "Failed to load, with non-ATS error: %@", error );
    }
}

- (NSURL *)parseATSError:(NSError*)error {
    if( error == nil )
        return nil;
    if( ![error.domain isEqualToString:NSURLErrorDomain] )
        return nil;
    if( error.code != -1022 )
        return nil;
    NSString *url = error.userInfo[ NSURLErrorFailingURLStringErrorKey ];
    return [NSURL URLWithString:url];
}

This seems to work. So if you're using an UIWebView and running afoul of App Transport Security, maybe this will help you.

1 comment:

  1. Thank you for the thorough write up! I agree with you that opening the webpages, which are not whitelisted in ATS, in Safari is probably the best and unobtrusive (because of the back button on the Safari status bar) way to go about this issue.

    ReplyDelete