All Products
Search
Document Center

HTTPDNS:Connect an Android app to an IP address over HTTPS

Last Updated:Sep 28, 2022

Notice

This topic describes how to connect an Android app to an IP address that is resolved from HTTPDNS in HTTPS scenarios, including scenarios in which Server Name Indication (SNI) is required. For more information about the resolution service provided by HTTPDNS, see the SDK for Android development manual.

Background information

This topic describes how to connect an Android app to an IP address that is resolved from HTTPDNS in HTTPS scenarios, including scenarios in which SNI is required. If you use OkHttp as the network development framework, you can elegantly connect your Android app to an IP address by means of a DNS service customization API provided by OkHttp. This solution is simpler and more versatile than the general solution. We recommend that you access HTTPDNS by referring to Use HTTPDNS and OkHttp to connect an Android app to an IP address.

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 sends 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 the 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 to the server is established, the domain name (hostname) 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. In consequence, 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 HTTPS, 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 as described in the preceding section. You can replace the IP address with the original domain name, and then implement certificate verification.

Notice

When you implement this method and initiate a network request, the Android system may report an SSL verification error, such as System.err: javax.net.ssl.SSLHandshakeException: java.security.cert.CertPathValidatorException: Trust anchor for certification path not found. In this case, you can check whether SNI is configured.

The following code provides an example on the HttpsURLConnection API:

try {
    String url = "https://140.205.XX.XX/?sprefer=sypc00";
    HttpsURLConnection connection = (HttpsURLConnection) new URL(url).openConnection();

    connection.setRequestProperty("Host", "m.taobao.com");
    connection.setHostnameVerifier(new HostnameVerifier() {

/*
 * Description about HttpsURLConnection from the Android official documentation:
 * This is an extended verification option that implementers can provide.
 * It is to be used during a handshake if the URL's hostname does not match the
 * peer's identification hostname.
 *
 * After you use HTTPDNS to perform the DNS resolution, the IP address in the new URL is not the hostname of a remote host that has passed the certificate verification. In the following example, the hostname of a certified remote host is used, which is m.taobao.com. 
 * HttpsURLConnection provides a callback that you can use to modify the hostname in the URL. 
 * You need to check whether the IP address returned by HTTPDNS is the same as the IP address carried by the session. If they are the same, you can use this callback to replace the IP address with the original domain name before you verify the certificate. 
 *
 */

    @Override
    public boolean verify(String hostname, SSLSession session) {
        return HttpsURLConnection.getDefaultHostnameVerifier().verify("m.taobao.com", session);
        return false;
    }
    });

    connection.connect();
} catch (Exception e) {
    e.printStackTrace();
} finally {
}

HTTPS requests in scenarios in which SNI is required

HTTPDNS Android Demo provides sample HttpsURLConnection code on how to use HTTPDNS in scenarios in which SNI is required.

You need to use SSLSocketFactory to perform custom DNS resolution and certificate verification. When you call the createSocket method, replace the domain name with the IP address that is resolved from the domain name, and specify the SNI and HostNameVerify configurations.

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

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

    @Override
    public Socket createSocket() throws IOException {
        return null;
    }

    @Override
    public Socket createSocket(String host, int port) throws IOException, UnknownHostException {
        return null;
    }

    @Override
    public Socket createSocket(String host, int port, InetAddress localHost, int localPort) throws IOException, UnknownHostException {
        return null;
    }

    @Override
    public Socket createSocket(InetAddress host, int port) throws IOException {
        return null;
    }

    @Override
    public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException {
        return null;
    }

    // TLS layer

    @Override
    public String[] getDefaultCipherSuites() {
        return new String[0];
    }

    @Override
    public String[] getSupportedCipherSuites() {
        return new String[0];
    }

    @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;
    }
}

In normal cases, you must redirect requests for a site that requires SNI support. The following example shows how to redirect requests:

public void recursiveRequest(String path, String reffer) {
    URL url = null;
    try {
        url = new URL(path);
        conn = (HttpsURLConnection) url.openConnection();
        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 = (HttpsURLConnection) new URL(newUrl).openConnection();
            // Set the Host header field of the HTTP request.
            conn.setRequestProperty("Host", url.getHost());
        }
        conn.setConnectTimeout(30000);
        conn.setReadTimeout(30000);
        conn.setInstanceFollowRedirects(false);
        TlsSniSocketFactory sslSocketFactory = new TlsSniSocketFactory(conn);
        conn.setSSLSocketFactory(sslSocketFactory);
        conn.setHostnameVerifier(new HostnameVerifier() {
            /*
             * Description about HttpsURLConnection from the Android official documentation:
             * This is an extended verification option that implementers can provide.
             * It is to be used during a handshake if the URL's hostname does not match the
             * peer's identification hostname.
             *
             * After you use HTTPDNS to perform the DNS resolution, the IP address in the new URL is not the hostname of a remote host that has passed the certificate verification. In the following example, the hostname of a certified remote host is used, which is m.taobao.com. 
             * HttpsURLConnection provides a callback that you can use to modify the hostname in the URL. 
             * You need to check whether the IP address returned by HTTPDNS is the same as the IP address carried by the session. If they are the same, you can use this callback to replace the IP address with the original domain name before you verify the certificate. 
             *
             */
            @Override
            public boolean verify(String hostname, SSLSession session) {
                String host = conn.getRequestProperty("Host");
                if (null == host) {
                    host = conn.getURL().getHost();
                }
                return HttpsURLConnection.getDefaultHostnameVerifier().verify(host, session);
            }
        });
        int code = conn.getResponseCode();// Network block
        if (needRedirect(code)) {
            // 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 their 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;
            }
            recursiveRequest(location, path);
        } else {
            // redirect finish.
            DataInputStream dis = new DataInputStream(conn.getInputStream());
            int len;
            byte[] buff = new byte[4096];
            StringBuilder response = new StringBuilder();
            while ((len = dis.read(buff)) != -1) {
                response.append(new String(buff, 0, len));
            }
            Log.d(TAG, "Response: " + response.toString());
        }
    } catch (MalformedURLException e) {
        Log.w(TAG, "recursiveRequest MalformedURLException");
    } catch (IOException e) {
        Log.w(TAG, "recursiveRequest IOException");
    } catch (Exception e) {
        Log.w(TAG, "unknow exception");
    } finally {
        if (conn != null) {
            conn.disconnect();
        }
    }
}

private boolean needRedirect(int code) {
    return code >= 300 && code < 400;
}