このトピックでは、Android アプリを HTTPS 経由で IP アドレスに接続する方法について説明します。 HTTPS リクエストでサーバ名表示 (SNI) を指定することもできます。
概要
HTTPS は HTTP の拡張です。コンピュータネットワークを介した安全な通信を実装するために使用されます。トランスポート層セキュリティ (TLS)、または以前は Secure Sockets Layer (SSL) が、セキュアチャネルの確立とデータの暗号化に使用されます。 HTTPS は、Web サイトサーバの身元認証を提供します。また、交換されるデータの機密性と整合性を保証します。 HTTPS リクエストには、一般的な HTTPS リクエストと SNI を使用した HTTPS リクエストが含まれます。 SNI は TLS または SSL プロトコルの拡張であり、クライアントが接続しようとするホスト名を示します。これにより、サーバは同じ IP アドレスで複数のドメイン名をホストできます。
一般的な HTTPS リクエスト
開発者は、HTTPS リクエストの URL 内の Host ヘッダー値を IP アドレスに置き換えることで、Android アプリを HTTPS 経由で IP アドレスに簡単に接続できます。ただし、証明書の検証のために、IP アドレスを元の Host ヘッダー値に置き換える必要があります。元の Host ヘッダー値は、元のドメイン名を示します。
SNI を使用した HTTPS リクエスト
SNI が指定されている場合、Web サイトは共有 IP アドレスでホストされている間も、独自の HTTPS 証明書を使用できます。この場合、開発者は HttpsURLConnection の SSLSocketFactory を使用して、必要な Web サイトへの接続を確立する必要があります。 createSocket メソッドを呼び出すときは、Alibaba Cloud Public DNS SDK for Android の API 操作を使用して、ドメイン名を IP アドレスに置き換える必要があります。この IP アドレスは、必要な Web サイトのドメイン名から解決されます。また、SNI と HostNameVerify の構成も指定する必要があります。
リクエスト URL のホストを IPv6 アドレスに置き換えるには、アドレスを角かっこ [] で囲む必要があります。
例: https://[2400:3200:xxx:1]/path
OkHttp は、カスタム DNS 解決を実行できる Dns 操作を提供します。その後、Android アプリを IP アドレスに接続できます。 Android 開発者で OkHttp を使用している場合は、OkHttp を Alibaba Cloud Public DNS SDK for Android と一緒に使用することをお勧めします。詳細については、「OkHttp を使用する Android アプリで Alibaba Cloud Public DNS SDK にアクセスするためのベストプラクティス」をご参照ください。
サーバーで Alibaba Cloud CDN サービスを使用している、または使用する予定の場合は、SNI を使用した HTTPS リクエストのソリューションを参照してください。
ソリューション
一般的な HTTPS リクエスト
HTTPS リクエストの URL で IP アドレスを使用した後、IP アドレスが指す Web サイトが証明書の検証に合格しない場合があります。この場合、フックを使用して証明書の検証を実装できます。 IP アドレスを元のドメイン名に置き換えてから、Web サイトの証明書を確認できます。
次の例では、HttpURLConnection が使用されています。
try {
// リクエスト URL のホストを IPv6 アドレスに置き換えるには、アドレスを角かっこ [] で囲む必要があります。
// 例: 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() {
/*
* Android 公式ドキュメントからの API 操作の説明:
* これは、実装者が提供できる拡張検証オプションです。
* URL のホスト名がピアの識別ホスト名と一致しない場合、ハンドシェイク中に使用されます。
*
* Alibaba Cloud Public DNS SDK for Android を使用して DNS 解決を実行した後、新しい URL の IP アドレスは、証明書の検証に合格したリモートホストのホスト名ではありません。次の例では、認定されたリモートホストのホスト名である m.taobao.com が使用されています。
* HttpsURLConnection は、URL 内のホスト名を変更するために使用できるコールバックを提供します。
* Alibaba Cloud Public DNS SDK for Android を使用して返された IP アドレスが、セッションによって伝送された IP アドレスと同じであるかどうかを確認する必要があります。同じである場合、このコールバックを使用して、証明書を検証する前に IP アドレスを元のドメイン名に置き換えることができます。
*
*/
@Override
public boolean verify(String hostname, SSLSession session) {
return HttpsURLConnection.getDefaultHostnameVerifier().verify("m.taobao.com", session);
}
});
connection.connect();
} catch (Exception e) {
e.printStackTrace();
} finally {
}
このメソッドを実装してネットワークリクエストを開始すると、Android で SSL 検証エラー
が報告される場合があります。たとえば、System.err: javax.net.ssl.SSLHandshakeException: java.security.cert.CertPathValidatorException: Trust anchor for certification path not found.
などです。この場合、SNI が指定されているかどうかを確認できます。
SNI を使用した HTTPS リクエスト
HTTPS リクエストで SNI が指定されている場合に HttpsURLConnection を呼び出すための完全なコードについては、デモプロジェクトのソースコードを参照してください。
SSLSocketFactory を使用して、カスタム DNS 解決と証明書の検証を実行する必要があります。 createSocket メソッドを呼び出すときは、ドメイン名をドメイン名から解決された IP アドレスに置き換え、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 レイヤー
@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, "カスタマイズされた createSocket。 host: " + peerHost);
InetAddress address = plainSocket.getInetAddress();
if (autoClose) {
// plainSocket は不要
plainSocket.close();
}
// SSL ソケットを作成して接続しますが、ホスト名/証明書の検証はまだ行いません
SSLCertificateSocketFactory sslSocketFactory = (SSLCertificateSocketFactory) SSLCertificateSocketFactory.getDefault(0);
SSLSocket ssl = (SSLSocket) sslSocketFactory.createSocket(address, port);
// 使用可能な場合は TLSv1.1/1.2 を有効にします
ssl.setEnabledProtocols(ssl.getSupportedProtocols());
// ハンドシェイクの前に SNI を設定します
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
Log.i(TAG, "SNI ホスト名を設定しています");
sslSocketFactory.setHostname(ssl, peerHost);
} else {
Log.d(TAG, "Android <4.2 では SNI のサポートに関するドキュメントがありません。リフレクションで試行しています");
try {
java.lang.reflect.Method setHostnameMethod = ssl.getClass().getMethod("setHostname", String.class);
setHostnameMethod.invoke(ssl, peerHost);
} catch (Exception e) {
Log.w(TAG, "SNI は使用できません", e);
}
}
// ホスト名と証明書を検証します
SSLSession session = ssl.getSession();
if (!hostnameVerifier.verify(peerHost, session)) {
throw new SSLPeerUnverifiedException("ホスト名を確認できません: " + peerHost);
}
Log.i(TAG, session.getProtocol() + " 接続を " + session.getPeerHost() +
" と " + session.getCipherSuite() + " を使用して確立しました");
return ssl;
}
}
次のサンプルコードは、SNI を指定してリクエストをリダイレクトする方法の例を示しています。
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());
// リクエスト URL のホストを IPv6 アドレスに置き換えるには、アドレスを角かっこ [] で囲む必要があります。
// 例: https://[2400:3200:xxx:1]/path。
if (ip != null) {
// Alibaba Cloud Public DNS SDK for Android を使用して DNS 解決を実行し、IP アドレスを取得します。 URL 内の元のドメイン名を IP アドレスに置き換え、Host ヘッダーを構成します。
Log.d(TAG, "IP を取得しました: " + ip + " for host: " + url.getHost() + " pdns リゾルバーから正常に取得できました!");
String newUrl = path.replaceFirst(url.getHost(), ip);
conn = (HttpsURLConnection) new URL(newUrl).openConnection();
// HTTP リクエストの Host ヘッダーを設定します。
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() {
/*
* Android 公式ドキュメントからの API 操作の説明:
* これは、実装者が提供できる拡張検証オプションです。
* URL のホスト名がピアの識別ホスト名と一致しない場合、ハンドシェイク中に使用されます。
*
* Alibaba Cloud Public DNS SDK for Android を使用して DNS 解決を実行した後、新しい URL の IP アドレスは、証明書の検証に合格したリモートホストのホスト名ではありません。次の例では、認定されたリモートホストのホスト名である m.taobao.com が使用されています。
* HttpsURLConnection は、URL 内のホスト名を変更するために使用できるコールバックを提供します。
* Alibaba Cloud Public DNS SDK for Android を使用して返された IP アドレスが、セッションによって伝送された IP アドレスと同じであるかどうかを確認する必要があります。同じである場合、このコールバックを使用して、証明書を検証する前に IP アドレスを元のドメイン名に置き換えることができます。
*
*/
@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();// ネットワークブロック
if (needRedirect(code)) {
// Location キーは大文字と小文字が区別されることに注意してください。大文字化のスタイルが異なると、異なるリダイレクトモードが示されます。たとえば、キーは、後続のすべてのリクエストが指定された IP アドレスにリダイレクトされること、または現在のリクエストのみが指定された IP アドレスにリダイレクトされることを示している場合があります。大文字化のスタイルとそれに対応するリダイレクトモードは、サーバーによって決定されます。
String location = conn.getHeaderField("Location");
if (location == null) {
location = conn.getHeaderField("location");
}
if (!(location.startsWith("http://") || location.startsWith("https://"))) {
// 場合によっては、サーバーは新しい URL のパスのみを返し、元のドメイン名を IP アドレスに置き換えることによって URL を完成させる必要があります。
URL originalUrl = new URL(path);
location = originalUrl.getProtocol() + "://"+originalUrl.getHost() + location;
}
recursiveRequest(location, path);
} else {
// リダイレクト完了。
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.toString());
}
} catch (MalformedURLException e) {
Log.w(TAG, "recursiveRequest MalformedURLException");
} catch (IOException e) {
Log.w(TAG, "recursiveRequest IOException");
} catch (Exception e) {
Log.w(TAG, "不明な例外");
} finally {
if (conn != null) {
conn.disconnect();
}
}
}
このトピックでは、Android アプリを HTTPS 経由で IP アドレスに接続するための参照のみを提供しています。
Alibaba Cloud Public DNS SDK for Android と DNS 解決サービスの使用方法の詳細については、「Android 開発者ガイド用 SDK」をご参照ください。
HTTPS リクエストで SNI が指定されている場合に Alibaba Cloud Public DNS SDK for Android を統合するための完全なコードについては、デモプロジェクトのソースコードを参照してください。