Berdasarkan topik Alur Integrasi SDK Android, Anda telah mempelajari alur lengkap untuk mengimpor SDK Android, mengonfigurasinya, menguraikan alamat IP, menerapkannya ke pustaka jaringan, dan memverifikasi integrasi. Topik ini menjelaskan cara mengintegrasikan HTTPDNS dengan HttpURLConnection.
1. Pendahuluan
Dokumen ini menjelaskan cara mengintegrasikan HTTPDNS untuk menerapkan koneksi IP langsung pada permintaan HttpURLConnection di Android dalam skenario HTTPS, termasuk yang memerlukan Server Name Indication (SNI). Untuk informasi lebih lanjut mengenai prinsip dasarnya, lihat Cara kerja koneksi IP langsung dengan HTTPDNS.
Sebagian besar kerangka kerja pengembangan jaringan utama untuk Android telah beralih ke OkHttp. OkHttp secara native menyediakan antarmuka untuk layanan DNS kustom, yang memungkinkan penerapan koneksi IP langsung yang lebih sederhana dan elegan. Kami menyarankan Anda terlebih dahulu merujuk ke Praktik terbaik penggunaan HTTPDNS dengan OkHttp di Android untuk integrasi. Konten berikut menyediakan solusi alternatif menggunakan HttpURLConnection untuk skenario di mana Anda tidak dapat menggunakan OkHttp.
2. Solusi integrasi
Solusi ini bergantung pada apakah skenario Anda melibatkan SNI. Terdapat dua skenario:
Skenario HTTPS (SNI): Saat membuat
SSLSocket, kirimkan nama domain asli ke server melalui SNI. Anda juga harus menangani logika `HostnameVerifier` dengan benar.Skenario HTTPS (non-SNI): Anda dapat menggunakan antarmuka HostnameVerifier untuk mengembalikan alamat IP ke nama domain asli guna verifikasi selama validasi sertifikat.
Bagian berikut menjelaskan proses integrasi lengkap dan memberikan contoh untuk setiap skenario.
2.1 Skenario HTTPS (SNI)
Pada skenario di mana Anda telah menerapkan sertifikat multi-domain dan harus memberikan nama domain ke server melalui SNI sebelum handshake, Anda harus mengatur nama SNI yang benar saat membuat SSLSocket. Ini dilakukan selain menggunakan HostnameVerifier. Untuk melakukannya, Anda dapat membuat kustom SSLSocketFactory dan melakukan langkah-langkah berikut dalam metode createSocket()-nya:
Ganti nama domain dengan alamat IP yang diuraikan oleh HTTPDNS untuk membangun koneksi.
Panggil
SSLCertificateSocketFactorysistem atau kustom dan atur Hostname SNI menggunakansetHostname()sebelum handshake.Lakukan validasi sertifikat sendiri. Ubah nama domain yang digunakan untuk validasi dari alamat IP kembali ke nama domain asli.
Demo HTTPDNS Android resmi menyediakan contoh kode untuk menggunakan HTTPDNS dengan `HttpsURLConnection` dalam skenario SNI.
Contoh SSLSocketFactory kustom
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) {
// Kami tidak memerlukan plainSocket.
plainSocket.close()
}
// Buat dan hubungkan soket SSL, tetapi jangan lakukan verifikasi hostname atau sertifikat terlebih dahulu.
val sslSocketFactory =
SSLCertificateSocketFactory.getDefault(0) as SSLCertificateSocketFactory
val ssl = sslSocketFactory.createSocket(address, R.attr.port) as SSLSocket
// Aktifkan TLSv1.1 dan TLSv1.2 jika tersedia.
ssl.enabledProtocols = ssl.supportedProtocols
// Siapkan SNI sebelum handshake.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
// Atur hostname SNI.
sslSocketFactory.setHostname(ssl, peerHost)
} else {
// Tidak ada dukungan SNI yang didokumentasikan pada Android versi sebelum 4.2. Coba gunakan refleksi.
try {
val setHostnameMethod = ssl.javaClass.getMethod(
"setHostname",
String::class.java
)
setHostnameMethod.invoke(ssl, peerHost)
} catch (e: Exception) {
}
}
// Verifikasi hostname dan sertifikat.
val session = ssl.session
if (!HttpsURLConnection.getDefaultHostnameVerifier()
.verify(peerHost, session)
) throw SSLPeerUnverifiedException(
"Tidak dapat memverifikasi 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) {
// Kami tidak memerlukan plainSocket.
plainSocket.close();
}
// Buat dan hubungkan soket SSL, tetapi jangan lakukan verifikasi hostname atau sertifikat terlebih dahulu.
SSLCertificateSocketFactory sslSocketFactory = (SSLCertificateSocketFactory) SSLCertificateSocketFactory.getDefault(0);
SSLSocket ssl = (SSLSocket) sslSocketFactory.createSocket(address, port);
// Aktifkan TLSv1.1 dan TLSv1.2 jika tersedia.
ssl.setEnabledProtocols(ssl.getSupportedProtocols());
// Siapkan SNI sebelum handshake.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
// Atur hostname SNI.
sslSocketFactory.setHostname(ssl, peerHost);
} else {
// Tidak ada dukungan SNI yang didokumentasikan pada Android versi sebelum 4.2. Coba gunakan refleksi.
try {
java.lang.reflect.Method setHostnameMethod = ssl.getClass().getMethod("setHostname", String.class);
setHostnameMethod.invoke(ssl, peerHost);
} catch (Exception e) {
}
}
// Verifikasi hostname dan sertifikat.
SSLSession session = ssl.getSession();
if (!HttpsURLConnection.getDefaultHostnameVerifier().verify(peerHost, session))
throw new SSLPeerUnverifiedException("Tidak dapat memverifikasi hostname: " + peerHost);
return ssl;
}
}Contoh Penanganan Pengalihan
Permintaan dalam skenario SNI sering mengalami beberapa pengalihan HTTP 3xx. Contoh ini menunjukkan cara menggunakan HTTPDNS untuk menguraikan host baru selama pengalihan dan melanjutkan permintaan.
fun recursiveRequest(path: String) {
var conn: HttpURLConnection? = null
try {
val url = URL(path)
conn = url.openConnection() as HttpURLConnection
// Dapatkan alamat IP menggunakan API sinkron.
val httpdnsResult = HttpDns.getService(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)) {
// Jika alamat IP diperoleh dari HTTPDNS, ganti URL dan atur header Host.
val newUrl = path.replaceFirst(url.host.toRegex(), ip!!)
conn = URL(newUrl).openConnection() as HttpURLConnection
conn.connectTimeout = 30000
conn.readTimeout = 30000
conn.instanceFollowRedirects = false
// Atur bidang Host dalam header permintaan HTTP.
conn.setRequestProperty("Host", url.host)
if (conn is HttpsURLConnection) {
val httpsURLConnection = conn
// Untuk skenario HTTPS, lakukan validasi sertifikat.
httpsURLConnection.hostnameVerifier =
HostnameVerifier { _, session ->
var host = httpsURLConnection.getRequestProperty("Host")
if (null == host) {
host = httpsURLConnection.url.host
}
HttpsURLConnection.getDefaultHostnameVerifier().verify(host, session)
}
// Untuk skenario SNI, buat SSLSocket.
httpsURLConnection.sslSocketFactory = TlsSniSocketFactory(httpsURLConnection)
}
}
val code = conn.responseCode // Blok jaringan
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://"))
) {
// Kadang-kadang host dihilangkan dan hanya jalur yang dikembalikan. Dalam kasus ini, Anda harus melengkapi URL.
val originalUrl = URL(path)
location = (originalUrl.protocol + "://"
+ originalUrl.host + location)
}
recursiveRequest(location)
}
} else {
// pengalihan selesai.
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, "Tanggapan: $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, "pengecualian tidak dikenal")
} finally {
conn?.disconnect()
}
}public void recursiveRequest(String path) {
HttpURLConnection conn = null;
try {
URL url = new URL(path);
conn = (HttpURLConnection) url.openConnection();
// Dapatkan alamat IP menggunakan API sinkron.
HTTPDNSResult httpdnsResult = HttpDns.getService(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)) {
// Jika alamat IP diperoleh dari HTTPDNS, ganti URL dan atur header Host.
String newUrl = path.replaceFirst(url.getHost(), ip);
conn = (HttpURLConnection) new URL(newUrl).openConnection();
conn.setConnectTimeout(30000);
conn.setReadTimeout(30000);
conn.setInstanceFollowRedirects(false);
// Atur bidang Host dalam header permintaan HTTP.
conn.setRequestProperty("Host", url.getHost());
if (conn instanceof HttpsURLConnection) {
final HttpsURLConnection httpsURLConnection = (HttpsURLConnection) conn;
// Untuk skenario HTTPS, lakukan validasi sertifikat.
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);
}
});
// Untuk skenario SNI, buat SSLSocket.
httpsURLConnection.setSSLSocketFactory(new TlsSniSocketFactory(httpsURLConnection));
}
}
int code = conn.getResponseCode();// Blok jaringan
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://"))) {
// Kadang-kadang host dihilangkan dan hanya jalur yang dikembalikan. Dalam kasus ini, Anda harus melengkapi URL.
URL originalUrl = new URL(path);
location = originalUrl.getProtocol() + "://"
+ originalUrl.getHost() + location;
}
recursiveRequest(location);
}
} else {
// pengalihan selesai.
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, "Tanggapan: " + response.toString());
}
} catch (MalformedURLException e) {
Log.w(TAG, "recursiveRequest MalformedURLException");
} catch (IOException e) {
Log.w(TAG, "recursiveRequest IOException");
} catch (Exception e) {
Log.w(TAG, "pengecualian tidak dikenal");
} finally {
if (conn != null) {
conn.disconnect();
}
}
}2.2 Skenario HTTPS (non-SNI)
Untuk skenario yang hanya menggunakan sertifikat domain tunggal—artinya Anda tidak memiliki penerapan multi-domain atau tidak perlu menentukan nama domain dengan SNI—perubahan utamanya terletak pada verifikasi hostname. Prinsipnya adalah hook atau menerapkan sendiri HostnameVerifier selama proses validasi sertifikat. Hal ini memungkinkan Anda mengubah nama domain yang digunakan untuk verifikasi dari alamat IP kembali ke nama domain asli.
Penting Solusi ini hanya berlaku untuk skenario non-SNI. Jika aplikasi Anda menggunakan beberapa sertifikat atau beberapa nama domain, pastikan bahwa Anda tidak memerlukan SNI. Jika Anda mengalami kesalahan seperti SSLHandshakeException: java.security.cert.CertPathValidatorException: Trust anchor for certification path not found., pertama-tama periksa apakah situs target memerlukan dukungan SNI.Contoh berikut menggunakan HttpURLConnection untuk menunjukkan cara menggunakan antarmuka HostnameVerifier guna menyelesaikan validasi sertifikat.
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 ->
/*
* Dokumentasi resmi menjelaskan antarmuka ini sebagai berikut:
* "Ini adalah opsi verifikasi tambahan yang dapat disediakan oleh implementer.
* Antarmuka ini digunakan selama handshake jika hostname URL tidak cocok dengan
* hostname identifikasi peer."
*
* Saat Anda menggunakan HTTPDNS, hostname dalam URL bukanlah hostname jarak jauh, seperti m.taobao.com.
* Hal ini menyebabkan ketidaksesuaian dengan domain tempat sertifikat diterbitkan.
* HttpsURLConnection Android menyediakan antarmuka panggilan balik untuk menangani skenario khusus ini.
* Setelah Anda memastikan bahwa alamat IP asal dari HTTPDNS sesuai dengan informasi IP dalam sesi,
* gantilah nama domain yang akan diverifikasi dengan nama domain asli dalam metode panggilan balik.
*
*/
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() {
/*
* Dokumentasi resmi menjelaskan antarmuka ini sebagai berikut:
* "Ini adalah opsi verifikasi tambahan yang dapat disediakan oleh implementer.
* Antarmuka ini digunakan selama handshake jika hostname URL tidak cocok dengan
* hostname identifikasi peer."
*
* Saat Anda menggunakan HTTPDNS, hostname dalam URL bukanlah hostname jarak jauh, seperti m.taobao.com.
* Hal ini menyebabkan ketidaksesuaian dengan domain tempat sertifikat diterbitkan.
* HttpsURLConnection Android menyediakan antarmuka panggilan balik untuk menangani skenario khusus ini.
* Setelah Anda memastikan bahwa alamat IP asal dari HTTPDNS sesuai dengan informasi IP dalam sesi,
* gantilah nama domain yang akan diverifikasi dengan nama domain asli dalam metode panggilan balik.
*
*/
@Override
public boolean verify(String hostname, SSLSession session) {
return HttpsURLConnection.getDefaultHostnameVerifier().verify("m.taobao.com", session);
}
});
}
connection.connect();
} catch (Exception e) {
e.printStackTrace();
}3. Ringkasan
Tantangan penggunaan HTTPDNS dengan HTTPS
Validasi sertifikat memerlukan kecocokan nama domain.
SNI mengharuskan Anda memberikan nama domain ke server sebelum handshake.
Skenario SNI
Anda harus membuat
SSLSocketFactorykustom untuk mengatur Hostname SNI sebelum handshake. Anda juga harus menangani penggantian nama domain untuk validasi sertifikat dalamHostnameVerifier.
Skenario non-SNI
Anda cukup menggunakan
HostnameVerifieruntuk mengubah nama domain yang digunakan untuk validasi dari alamat IP kembali ke nama domain asli.
Utamakan menggunakan OkHttp
Jika Anda dapat menggunakan OkHttp dalam proyek Anda, kami menyarankan Anda mengikuti praktik terbaik dalam Praktik Terbaik HTTPDNS + OkHttp untuk Android. OkHttp secara native menyediakan antarmuka untuk DNS kustom, yang memungkinkan kode yang lebih ringkas dan fleksibilitas yang lebih baik.
Kini Anda mengetahui cara menerapkan koneksi IP langsung untuk HTTPS, termasuk SNI, menggunakan HTTPDNS dengan HttpURLConnection di Android. Topik ini membantu Anda menyelesaikan integrasi.