This topic describes how to connect an Android app to an IP address over Hypertext Transfer Protocol Secure (HTTPS). You can also specify the Server Name Indication (SNI) in HTTPS requests.
Overview
HTTPS is a transport protocol for secure communication over a computer network. It communicates over HTTP and uses SSL/TLS to encrypt data packets over a secure channel. 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: standard and SNI. Server Name Indication (SNI) is an extension to Secure Sockets Layer (SSL) and Transport Layer Security (TLS) that improves communication between servers and clients. SNI is mainly used when one server provides services for multiple domain names.
Standard scenarios
Developers can easily connect Android apps to an IP address over HTTPS by replacing the Host header value in the URL of the HTTPS request with the IP address. However, you need to replace the IP address with the original Host header value for certificate verification. The original Host header value indicates the original domain name.
SNI scenarios
In an SNI scenario (a single IP address with multiple HTTPS certificates), you can customize the `SSLSocketFactory` for the `HttpsURLConnection` interface. When creating a socket, use the API provided by the HTTPDNS Android SDK to replace the domain name with its resolved IP address. Then, configure the SNI and HostNameVerify settings.
If you use an IPv6 address to replace the Host in the request URL, you need to enclose the IPv6 address in square brackets "[]".
For example: https://[2400:3200:xxx:1]/path
OkHttp provides a custom DNS service interface that enables direct IP connections. If you use the OkHttp network framework for Android development, this solution is simpler and more versatile than the general solution. For more information, see Best practices for integrating the HTTPDNS Android SDK with the OkHttp framework.
If you use CDN services on the server side or plan to use CDN services in the future, please refer to the solution for HTTPS SNI scenarios.
Best practices
Solution for standard HTTPS scenarios
For the "domain mismatch" issue, you can use a solution that hooks into certificate verification, directly replacing the IP address with the original domain name before performing certificate verification.
This example is for HttpURLConnection
try {
// If you use an IPv6 address to replace the host in the request URL, enclose the IPv6 address in square brackets ([]).
// For example: https://[2400:3200:xxx:1]/path
String url = "https://124.239.XX.XX/?sprefer=sypc00";
HttpsURLConnection connection = (HttpsURLConnection) new URL(url).openConnection();
connection.setRequestProperty("Host", "m.taobao.com");
connection.setHostnameVerifier(new HostnameVerifier() {
/*
* The official documentation describes this interface as follows:
* 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 the HTTPDNS Android SDK, the hostname in the URL is not the remote
* hostname (for example, m.taobao.com) and does not match the domain of the certificate.
* Android HttpsURLConnection provides a callback interface to handle this custom scenario.
* After you confirm that the origin IP address from the HTTPDNS Android SDK matches the
* IP information in the session, you can replace the domain to be verified with the
* original domain name in the callback method.
*
*/
@Override
public boolean verify(String hostname, SSLSession session) {
return HttpsURLConnection.getDefaultHostnameVerifier().verify("m.taobao.com", session);
}
});
connection.connect();
} catch (Exception e) {
e.printStackTrace();
} finally {
}If you receive an SSL verification error when initiating a network request using this solution, such as the Android system error System.err: javax.net.ssl.SSLHandshakeException: java.security.cert.CertPathValidatorException: Trust anchor for certification path not found., please check if your application scenario is SNI (single IP with multiple HTTPS domain names).
Solution for SNI HTTPS scenarios
For the HttpsURLConnection interface, we provide sample code for use in SNI scenarios. For the complete code, please refer to the Demo project source code.
Customize SSLSocketFactory, replace the domain name with the IP address obtained from domain name resolution when creating createSocket, and configure SNI/HostNameVerify.
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;
}
}The following sample code provides an example on how to specify the SNI to redirect requests.
public void recursiveRequest(String path, String reffer) {
URL url = null;
try {
url = new URL(path);
conn = (HttpsURLConnection) url.openConnection();
String ip = dnsResolver.getIPV4ByHost(url.getHost());
// If you use an IPv6 address to replace the host in the request URL, enclose the IPv6 address in square brackets ([]).
// For example: https://[2400:3200:xxx:1]/path
if (ip != null) {
// The IP address is obtained using the HTTPDNS Android SDK. Replace the URL and set the Host header.
Log.d(TAG, "get IP: " + ip + " for host: " + url.getHost() + "from pdns resolver success!");
String newUrl = path.replaceFirst(url.getHost(), ip);
conn = (HttpsURLConnection) new URL(newUrl).openConnection();
// Set the Host field in the HTTP request header.
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() {
/*
* The official documentation describes this interface as follows:
* 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 the HTTPDNS Android SDK, the hostname in the URL is not the remote
* hostname (for example, m.taobao.com) and does not match the domain of the certificate.
* Android HttpsURLConnection provides a callback interface to handle this custom scenario.
* After you confirm that the origin IP address from the HTTPDNS Android SDK matches the
* IP information in the session, you can replace the domain to be verified with the
* original domain name in the callback method.
*
*/
@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)) {
// The case of the `Location` header is significant for temporary and permanent redirects.
String location = conn.getHeaderField("Location");
if (location == null) {
location = conn.getHeaderField("location");
}
if (!(location.startsWith("http://") || location.startsWith("https://"))) {
// Sometimes the host is omitted and only the path is returned. In this case, you must complete the URL.
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();
}
}
}This topic covers integrating the HTTPDNS Android SDK for standard and SNI HTTPS scenarios.
To learn how to use the domain name resolution service of the HTTPDNS Android SDK or resolve issues with integrating the SDK, see the Android SDK Developer Guide.
For the complete code to integrate the HTTPDNS Android SDK in HTTPS SNI scenarios, see the demo project source code.