All Products
Search
Document Center

HTTPDNS:Use Webview in Android apps to access HTTPDNS

Last Updated:Jun 09, 2023

Important

  • This topic describes a solution that uses Webview in Android apps to access HTTPDNS. The code provided in this topic is for reference only. We recommend that you read this topic carefully and evaluate this solution before you implement it.

  • Android systems vary with service providers. Therefore, we recommend that you first implement the solution described in this topic in a test environment and check whether exceptions occur. If you have problems in the implementation of this solution, contact technical support.

  • This topic describes how to use Webview in Android apps to access HTTPDNS for IP address resolution. For more information about the resolution service provided by HTTPDNS, see the SDK for Android development manual.

Background information

HTTPDNS can be used in various scenarios to avoid DNS hijacking. However, no solutions are provided for scenarios where Webview is used. For the best practices in other scenarios, see Connect an Android app to an IP address over HTTPS and Use HTTPDNS and OkHttp to connect an Android app to an IP address.

You can still use HTTPDNS in WebView scenarios. You can even use the IP addresses resolved by HTTPDNS to access the server. This topic describes a solution that uses Webview in Android apps to access HTTPDNS.

Sample code

For the complete sample code, visit WebView+HTTPDNS Android Demo.

API for interception

void setWebViewClient (WebViewClient client);

WebView provides the setWebViewClient API. This API allows you to set the WebViewClient that will receive notifications and requests. You can intercept network requests by calling the shouldInterceptRequest method.

public class WebViewClient{
    // API < 21
    public WebResourceResponse shouldInterceptRequest(WebView view,
            String url) {
        ...
    }

   // API >= 21
    public WebResourceResponse shouldInterceptRequest(WebView view,
            WebResourceRequest request) {
        ...
    }
  ......

}

shouldInterceptRequest has two versions:

  • The following shouldInterceptRequest method is called when the API level is less than 21.

    public WebResourceResponse shouldInterceptRequest(WebView view, String url)

    Only the request URL is returned. You cannot obtain the request method, headers, and request body. If you call this method to intercept requests, WebView may fail to load all requested resources. Therefore, we recommend that you do not intercept requests on devices whose API level is less than 21.

    public WebResourceResponse shouldInterceptRequest(WebView view,
                                                      String url) {
      return super.shouldInterceptRequest(view, url);
    }
  • The following shouldInterceptRequest method is called when the API level is 21 or greater.

    public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request)

    The following code shows the structure of WebResourceRequest:

    public interface WebResourceRequest {
        Uri getUrl(); // The request URL.
        boolean isForMainFrame(); // Indicates whether the request is initiated by the primary MainFrame.
        boolean hasGesture(); // Indicates whether the request is triggered by a certain behavior (such as clicking).
        String getMethod(); // The method of the request.
        Map<String, String> getRequestHeaders(); // The headers of the request.
    }

When the API level is 21 or greater, the following information can be obtained:

  • The request URL.

  • The request method, such as POST and GET.

  • The request headers.

Usage

The following figure shows how requests are intercepted in Webview scenarios.

image..png
  1. Intercept only GET requests.

  2. Configure headers.

  3. Configure HTTPS certificates.

  4. Handle HTTPS requests with the SNI.

  5. Handle redirects.

  6. MIME&Encoding

Intercept only GET requests

WebResourceRequest does not encompass the method used to obtain the request body, so the shouldInterceptRequest method can be called to intercept only GET requests. The method cannot be used to intercept POST requests.

public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
    String scheme = request.getUrl().getScheme().trim();
    String method = request.getMethod();
    Map<String, String> headerFields = request.getRequestHeaders();
    // Requests with bodies cannot be intercepted. Only requests without the request body can be processed properly.
    if ((scheme.equalsIgnoreCase("http") || scheme.equalsIgnoreCase("https"))
        && method.equalsIgnoreCase("get")) {
      ......
    } else {
        return super.shouldInterceptRequest(view, reqeust);
    }

Configure headers

public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
    ......

      URL url = new URL(request.getUrl().toString());
      conn = (HttpURLConnection) url.openConnection();
      // Obtain the IP address by calling the synchronization API.
      String ip = httpdns.getIpByHostAsync(url.getHost());
      if (ip != null) {
        // After the IP address is obtained by using HTTPDNS, replace the original value of the HOST field in the HTTP request URL with the IP address.
        Log.d(TAG, "Get IP: " + ip + " for host: " + url.getHost() + " from HTTPDNS successfully!");
        String newUrl = path.replaceFirst(url.getHost(), ip);
        conn = (HttpURLConnection) new URL(newUrl).openConnection();

        // Add the original headers.
        if (headers != null) {
          for (Map.Entry<String, String> field : headers.entrySet()) {
            conn.setRequestProperty(field.getKey(), field.getValue());
          }
        }
        // Set the Host header field of the HTTP request.
        conn.setRequestProperty("Host", url.getHost());
      } 
}

Configure HTTPS certificates

If the intercepted request is an HTTPS request, you must verify the certificate.

if (conn instanceof HttpsURLConnection) {
  final HttpsURLConnection httpsURLConnection = (HttpsURLConnection)conn;
  // Verify the certificate for an HTTPS request.
  httpsURLConnection.setHostnameVerifier(new HostnameVerifier() {
    @Override
    public boolean verify(String hostname, SSLSession session) {
      String host = httpsURLConnection.getRequestProperty("Host");
      if (null == host) {
        host = httpsURLConnection.getURL().getHost();
      }
      return HttpsURLConnection.getDefaultHostnameVerifier().verify(host, session);
    }
  });
}

Handle HTTPS requests with the SNI

If the requests have the SNI, you must customize an SSLSocket. For more information, see SNI.

TlsSniSocketFactory sslSocketFactory = new TlsSniSocketFactory((HttpsURLConnection) conn);
// Handle HTTPS requests with the SNI Create an SSLScoket.
((HttpsURLConnection) conn).setSSLSocketFactory(sslSocketFactory);

......
class TlsSniSocketFactory extends SSLSocketFactory {
        private final String TAG = "TlsSniSocketFactory";
        HostnameVerifier hostnameVerifier = HttpsURLConnection.getDefaultHostnameVerifier();
        private HttpsURLConnection conn;

        public TlsSniSocketFactory(HttpsURLConnection conn) {
            this.conn = conn;
        }
          ......

        @Override
        public Socket createSocket(Socket plainSocket, String host, int port, boolean autoClose) throws IOException {
            String peerHost = this.conn.getRequestProperty("Host");
            if (peerHost == null)
                peerHost = host;
            Log.i(TAG, "customized createSocket. host: " + peerHost);
            InetAddress address = plainSocket.getInetAddress();
            if (autoClose) {
                // we don't need the plainSocket
                plainSocket.close();
            }
            // create and connect SSL socket, but don't do hostname/certificate verification yet
            SSLCertificateSocketFactory sslSocketFactory = (SSLCertificateSocketFactory) SSLCertificateSocketFactory.getDefault(0);
            SSLSocket ssl = (SSLSocket) sslSocketFactory.createSocket(address, port);

            // enable TLSv1.1/1.2 if available
            ssl.setEnabledProtocols(ssl.getSupportedProtocols());

            // set up SNI before the handshake
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
                Log.i(TAG, "Setting SNI hostname");
                sslSocketFactory.setHostname(ssl, peerHost);
            } else {
                Log.d(TAG, "No documented SNI support on Android <4.2, trying with reflection");
                try {
                    java.lang.reflect.Method setHostnameMethod = ssl.getClass().getMethod("setHostname", String.class);
                    setHostnameMethod.invoke(ssl, peerHost);
                } catch (Exception e) {
                    Log.w(TAG, "SNI not useable", e);
                }
            }

            // verify hostname and certificate
            SSLSession session = ssl.getSession();

            if (!hostnameVerifier.verify(peerHost, session))
                throw new SSLPeerUnverifiedException("Cannot verify hostname: " + peerHost);

            Log.i(TAG, "Established " + session.getProtocol() + " connection with " + session.getPeerHost() +
                    " using " + session.getCipherSuite());

            return ssl;
        }
    }

Handle redirects.

When you attempt to intercept a GET request and the server returns an HTTP redirect response, check whether the request contains the Cookie header.

  • If the request contains the Cookie header, the request is not intercepted because cookies are saved by domain names and change after redirects.

  • If the request does not contain the Cookie header, the request is initiated again.

int code = conn.getResponseCode();
if (code >= 300 && code < 400) {
  if (the GET request contains the Cookie header) {
      // This request is not intercepted.
    return super.shouldInterceptRequest(view, request);
  }

  // Note that the Location key is case-sensitive. Different capitalization styles indicate different redirection modes. For example, the key may indicate that all subsequent requests are redirected to the specified IP address, or that only the current request is redirected to the specified IP address. The capitalization styles and corresponding redirection modes are determined by the server.
  String location = conn.getHeaderField("Location");
  if (location == null) {
    location = conn.getHeaderField("location");
  }
  if (!(location.startsWith("http://") || location
        .startsWith("https://"))) {
    // In some cases, the server returns only the path of the new URL, and you need to complete the URL by replacing the original domain name with the IP address.
    URL originalUrl = new URL(path);
    location = originalUrl.getProtocol() + "://"
      + originalUrl.getHost() + location;
  }
  Log.e(TAG, "code:" + code + "; location:" + location + ";path" + path);

  Initiate the GET request again.
} else {
  // redirect finish.
  Log.e(TAG, "redirect finish");
  ......
}

MIME&Encoding

WebResourceResponse must be returned when you intercept a request.

public WebResourceResponse(String mimeType, String encoding, InputStream data) ;

You must specify the following information to obtain WebResourceResponse:

  • The Multipurpose Internet Mail Extensions (MIME) type of the request.

  • The character encoding of the request.

  • The input stream of the request.

You can obtain the input stream by calling URLConnection.getInputStream(). You can call URLConnection.getContentType () to obtain the MIME type and encoding from the ContentType of a request.

text/html;charset=utf-8

You may fail to obtain the complete ContentType of some requests. In this case, you can determine whether to intercept the requests based on the following strategies:

String contentType = conn.getContentType();
String mime = getMime(contentType);
String charset = getCharset(contentType);

// Requests without the MIME type are not intercepted.
if (TextUtils.isEmpty(mime)) {
  return super.shouldInterceptRequest(view, request);
} else {
  if (!TextUtils.isEmpty(charset)) {
    // If you can obtain both the MIME type and Accept-Charset header of a request, you can intercept this request.
    return new WebResourceResponse(mime, charset, connection.getInputStream());
  } else {
    // If you cannot obtain the character encoding of a request, you can determine whether to intercept this request based on the requested resource.

    // When a binary resource is requested, character encoding is not required. You can intercept this request.
    if (isBinaryRes(mime)) {
      Log.e(TAG, "binary resource for " + mime);
      return new WebResourceResponse(mime, charset, connection.getInputStream());
    } else {
      // When a non-binary resource is requested, character encoding is required. Unencoded requests are not intercepted.
      Log.e(TAG, "non binary resource for " + mime);
      return super.shouldInterceptRequest(view, request);
    }
  }
}

private boolean isBinaryRes(String mime) {
  // The logic can be extended.
  if (mime.startsWith("image")
      || mime.startsWith("audio")
      || mime.startsWith("video")) {
    return true;
  } else {
    return false;
  }
}

Summary

Scenario

Summary

Applicable scenarios

  • The API level of the device is less than 21.

  • The requests are POST requests.

  • The MIME type of the requests cannot be obtained.

  • Encoded non-binary file requests cannot be obtained.

Applicable scenarios

Prerequisites

  • API Level >= 21

  • The requests are GET requests.

  • The MIME type and encoding of the requests can be obtained or encoded non-binary file requests can be obtained.

Scenarios:

  • The requests are common HTTP requests.

  • The requests are HTTPS requests.

  • The requests contain the SNI.

  • Redirected HTTP requests are not contained in the Cookie header.