Background information
This topic describes a general approach for implementing IP direct connections in HTTPS scenarios, including those with Server Name Indication (SNI), on Android platforms.
If you're an Android developer using OkHttp
for network development, you can seamlessly implement IP direct connections as OkHttp
offers a customizable DNS service interface. This method is simpler and more flexible compared to the general approach. For integration details, see HTTPDNS + OkHttp Integration Guide for Android.
HTTPS
An SSL/TLS handshake is required to initiate an HTTPS request. The handshake involves the following steps:
The client sends a handshake request with a random number, supported algorithms, and other parameters.
The server selects an algorithm, then sends back its public key certificate and a random number.
The client verifies the server's certificate and encrypts a random number with the server's public key.
The server decrypts the random number using its private key.
Both parties generate a session ticket from the exchanged information, which serves as the encryption key for subsequent data transmission.
HTTPDNS comes into play during step 3, where the client must authenticate the server's certificate. This involves two critical checks:
The client uses a locally stored root certificate to validate the server's certificate chain, ensuring it's issued by a trusted CA.
The client verifies that the certificate's domain and any additional domains cover the host of the current request.
If both checks pass, the server is deemed trustworthy; otherwise, the connection should be terminated.
When HTTPDNS resolves a domain name, the host in the request URL is replaced with the IP address from HTTPDNS. This can lead to a domain mismatch during certificate verification, causing the SSL/TLS handshake to fail.
SNI
SNI (Server Name Indication) is an SSL/TLS extension that allows a server to host multiple domains and certificates. It operates as follows:
The client sends the domain name of the requested site before establishing an SSL connection.
The server responds with the appropriate certificate for the given domain name.
Most operating systems and browsers support SNI, which is also included in OpenSSL 0.9.8 and later.
When HTTPDNS is used, the server receives the resolved IP address instead of the domain name, which may result in the server returning a default or no certificate, leading to handshake failure.
For instance, when accessing resources on a CDN-accelerated site via HTTPS, the site may serve multiple domains, necessitating the use of SNI to specify the correct certificate for the domain.
HTTPS scenarios (non-SNI) solution
To address the domain mismatch issue, the following solution can be applied: Intercept the certificate verification process, substitute the IP address with the original domain name, and proceed with the verification.
Should a network request utilizing this solution encounter an SSL verification error
, for instance, the Android system error System.err: javax.net.ssl.SSLHandshakeException: java.security.cert.CertPathValidatorException: Trust anchor for certification path not found.
, verify if the scenario includes SNI (multiple HTTPS domain names on a single IP).
The following example is applicable to the HttpURLConnection interface.
try {
val url = "https://140.205.XX.XX/?sprefer=sypc00"
val connection = URL(url).openConnection() as HttpURLConnection
connection.setRequestProperty("Host", "m.taobao.com")
if (connection is HttpsURLConnection) {
connection.hostnameVerifier = HostnameVerifier { _, session ->
/*
* Description about this interface is provided in the 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 using HTTPDNS, the hostname set in the URL is not the hostname of the remote host (e.g., m.taobao.com), and does not match the domain bound to the certificate.
* Android HttpsURLConnection provides a callback interface for users to handle such customized scenarios.
* After confirming that the origin IP address returned by HTTPDNS matches the IP address carried by the session, you can use the callback method to replace the domain name to be verified with the original real domain name for verification.
*
*/
HttpsURLConnection.getDefaultHostnameVerifier().verify("m.taobao.com", session)
}
}
connection.connect()
} catch (e: java.lang.Exception) {
e.printStackTrace()
}
try {
String url = "https://140.205.XX.XX/?sprefer=sypc00";
HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
connection.setRequestProperty("Host", "m.taobao.com");
if (connection instanceof HttpsURLConnection) {
connection.setHostnameVerifier(new HostnameVerifier() {
/*
* Description about this interface is provided in the 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 using HTTPDNS, the hostname set in the URL is not the hostname of the remote host (e.g., m.taobao.com), and does not match the domain bound to the certificate.
* Android HttpsURLConnection provides a callback interface for users to handle such customized scenarios.
* After confirming that the origin IP address returned by HTTPDNS matches the IP address carried by the session, you can use the callback method to replace the domain name to be verified with the original real domain name for verification.
*
*/
@Override
public boolean verify(String hostname, SSLSession session) {
return HttpsURLConnection.getDefaultHostnameVerifier().verify("m.taobao.com", session);
}
});
}
connection.connect();
} catch (Exception e) {
e.printStackTrace();
}
HTTPS scenarios (SNI) solution
The HTTPDNS Android Demo provides sample code for implementing HTTPDNS in SNI scenarios using the HttpsURLConnection interface.
Customize the SSLSocketFactory to replace the domain name with the IP address from HTTPDNS during socket creation and configure SNI/HostName verification.
class TlsSniSocketFactory constructor(conn: HttpsURLConnection): SSLSocketFactory() {
private val mConn: HttpsURLConnection
init {
mConn = conn
}
override fun createSocket(plainSocket: Socket?, host: String?, port: Int, autoClose: Boolean): Socket {
var peerHost = mConn.getRequestProperty("Host")
if (peerHost == null) {
peerHost = host
}
val address = plainSocket!!.inetAddress
if (autoClose) {
// we don't need the plainSocket
plainSocket.close()
}
// create and connect SSL socket, but don't do hostname/certificate verification yet
val sslSocketFactory =
SSLCertificateSocketFactory.getDefault(0) as SSLCertificateSocketFactory
val ssl = sslSocketFactory.createSocket(address, R.attr.port) as SSLSocket
// enable TLSv1.1/1.2 if available
ssl.enabledProtocols = ssl.supportedProtocols
// set up SNI before the handshake
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
// setting sni hostname
sslSocketFactory.setHostname(ssl, peerHost)
} else {
// No documented SNI support on Android <4.2, trying with reflection
try {
val setHostnameMethod = ssl.javaClass.getMethod(
"setHostname",
String::class.java
)
setHostnameMethod.invoke(ssl, peerHost)
} catch (e: Exception) {
}
}
// verify hostname and certificate
val session = ssl.session
if (!HttpsURLConnection.getDefaultHostnameVerifier()
.verify(peerHost, session)
) throw SSLPeerUnverifiedException(
"Cannot verify hostname: $peerHost"
)
return ssl
}
}
public class TlsSniSocketFactory extends SSLSocketFactory {
private HttpsURLConnection mConn;
public TlsSniSocketFactory(HttpsURLConnection conn) {
mConn = conn;
}
@Override
public Socket createSocket(Socket plainSocket, String host, int port, boolean autoClose) throws IOException {
String peerHost = mConn.getRequestProperty("Host");
if (peerHost == null)
peerHost = host;
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) {
// setting sni hostname
sslSocketFactory.setHostname(ssl, peerHost);
} else {
// 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) {
}
}
// verify hostname and certificate
SSLSession session = ssl.getSession();
if (!HttpsURLConnection.getDefaultHostnameVerifier().verify(peerHost, session))
throw new SSLPeerUnverifiedException("Cannot verify hostname: " + peerHost);
return ssl;
}
}
For sites requiring SNI, requests often need redirection. The example also includes a method for handling such redirected requests.
fun recursiveRequest(path: String) {
var conn: HttpURLConnection? = null
try {
val url = URL(path)
conn = url.openConnection() as HttpURLConnection
// Obtain the IP address by calling the synchronization API
val httpdnsResult = HttpDns.getService(context, accountID)
.getHttpDnsResultForHostSync(url.host, RequestIpType.both)
var ip: String? = null
if (httpdnsResult.ips != null && httpdnsResult.ips.isNotEmpty()) {
ip = httpdnsResult.ips[0]
} else if (httpdnsResult.ipv6s != null && httpdnsResult.ipv6s.isNotEmpty()) {
ip = httpdnsResult.ipv6s[0]
}
if (!TextUtils.isEmpty(ip)) {
// After the IP address is obtained by using HTTPDNS, replace the value of the HOST field of the HTTP request header with the resolved IP address
val newUrl = path.replaceFirst(url.host.toRegex(), ip!!)
conn = URL(newUrl).openConnection() as HttpURLConnection
conn.connectTimeout = 30000
conn.readTimeout = 30000
conn.instanceFollowRedirects = false
// Set the Host header field of the HTTP request
conn.setRequestProperty("Host", url.host)
if (conn is HttpsURLConnection) {
val httpsURLConnection = conn
// Verify the certificate for an HTTPS request
httpsURLConnection.hostnameVerifier =
HostnameVerifier { _, session ->
var host = httpsURLConnection.getRequestProperty("Host")
if (null == host) {
host = httpsURLConnection.url.host
}
HttpsURLConnection.getDefaultHostnameVerifier().verify(host, session)
}
// Handle HTTPS requests with the SNI Create an SSLScoket
httpsURLConnection.sslSocketFactory = TlsSniSocketFactory(httpsURLConnection)
}
}
val code = conn.responseCode // Network block
if (code in 300..399) {
var location = conn.getHeaderField("Location")
if (location == null) {
location = conn.getHeaderField("location")
}
if (location != null) {
if (!(location.startsWith("http://") || location
.startsWith("https://"))
) {
// In some cases, the host is omitted, and only the path is returned. Therefore, you need to complete the URL
val originalUrl = URL(path)
location = (originalUrl.protocol + "://"
+ originalUrl.host + location)
}
recursiveRequest(location)
}
} else {
// Redirect finish.
val dis = DataInputStream(conn.inputStream)
var len: Int
val buff = ByteArray(4096)
val response = StringBuilder()
while (dis.read(buff).also { len = it } != -1) {
response.append(String(buff, 0, len))
}
Log.d(TAG, "Response: $response")
}
} catch (e: MalformedURLException) {
Log.w(TAG, "recursiveRequest MalformedURLException")
} catch (e: IOException) {
Log.w(TAG, "recursiveRequest IOException")
} catch (e: java.lang.Exception) {
Log.w(TAG, "Unknown exception")
} finally {
conn?.disconnect()
}
}
public void recursiveRequest(String path) {
HttpURLConnection conn = null;
try {
URL url = new URL(path);
conn = (HttpURLConnection) url.openConnection();
// Obtain the IP address by calling the synchronization API
HTTPDNSResult httpdnsResult = HttpDns.getService(context, accountID).getHttpDnsResultForHostSync(url.getHost(), RequestIpType.both);
String ip = null;
if (httpdnsResult.getIps() != null && httpdnsResult.getIps().length > 0) {
ip = httpdnsResult.getIps()[0];
} else if (httpdnsResult.getIpv6s() != null && httpdnsResult.getIpv6s().length > 0) {
ip = httpdnsResult.getIpv6s()[0];
}
if (!TextUtils.isEmpty(ip)) {
// After the IP address is obtained by using HTTPDNS, replace the value of the HOST field of the HTTP request header with the resolved IP address
String newUrl = path.replaceFirst(url.getHost(), ip);
conn = (HttpURLConnection) new URL(newUrl).openConnection();
conn.setConnectTimeout(30000);
conn.setReadTimeout(30000);
conn.setInstanceFollowRedirects(false);
// Set the Host header field of the HTTP request
conn.setRequestProperty("Host", url.getHost());
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 Create an SSLScoket
httpsURLConnection.setSSLSocketFactory(new TlsSniSocketFactory(httpsURLConnection));
}
}
int code = conn.getResponseCode();// Network block
if (code >= 300 && code < 400) {
String location = conn.getHeaderField("Location");
if (location == null) {
location = conn.getHeaderField("location");
}
if (location != null) {
if (!(location.startsWith("http://") || location
.startsWith("https://"))) {
// In some cases, the host is omitted, and only the path is returned. Therefore, you need to complete the URL
URL originalUrl = new URL(path);
location = originalUrl.getProtocol() + "://"
+ originalUrl.getHost() + location;
}
recursiveRequest(location);
}
} 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, "Unknown exception");
} finally {
if (conn != null) {
conn.disconnect();
}
}
}