このトピックでは、Hypertext Transfer Protocol Secure(HTTPS)を介して Android アプリを IP アドレスに接続する方法について説明します。 HTTPS リクエストでサーバー名表示(SNI)を指定することもできます。
概要
HTTPS は、コンピューターネットワーク上で安全な通信を行うためのトランスポートプロトコルです。HTTP 上で通信し、SSL/TLS を使用してセキュアチャネル経由でデータパケットを暗号化します。HTTPS の主な目的は、Web サイトサーバーを認証し、交換されるデータのプライバシーと整合性を保護することです。TLS は、トランスポートレイヤーの暗号化プロトコルであり、SSL の後継です。HTTPS には、標準と SNI の 2 つの一般的なシナリオがあります。サーバ名表示 (SNI) は、Secure Sockets Layer (SSL) と Transport Layer Security (TLS) の拡張機能であり、サーバーとクライアント間の通信を改善します。SNI は主に、1 つのサーバーが複数のドメイン名にサービスを提供する場合に使用されます。
標準シナリオ
開発者は、HTTPS リクエストの URL の Host ヘッダー値を IP アドレスに置き換えることで、Android アプリを HTTPS 経由で IP アドレスに簡単に接続できます。 ただし、証明書の検証のために、IP アドレスを元の Host ヘッダー値に置き換える必要があります。 元の Host ヘッダー値は、元のドメイン名を示します。
SNI シナリオ
SNI シナリオ (単一の IP アドレスに複数の HTTPS 証明書がある場合) では、`HttpsURLConnection` インターフェイスの `SSLSocketFactory` をカスタマイズできます。ソケットを作成するときに、HTTPDNS Android SDK が提供する API を使用して、ドメイン名を解決済みの IP アドレスに置き換えます。次に、SNI と HostNameVerify の設定を構成します。
リクエスト URL の Host を置き換えるために IPv6 アドレスを使用する場合は、IPv6 アドレスを角括弧「[]」で囲む必要があります。
例:https://[2400:3200:xxx:1]/path
OkHttp は、IP ダイレクト接続を可能にするカスタム DNS サービスインターフェイスを提供します。Android 開発に OkHttp ネットワークフレームワークを使用する場合、このソリューションは一般的なソリューションよりもシンプルで汎用性があります。詳細については、「HTTPDNS Android SDK と OkHttp フレームワークを統合するためのベストプラクティス」をご参照ください。
サーバー側で CDN サービスを使用している場合、または今後 CDN サービスを使用する予定の場合は、HTTPS SNI シナリオのソリューションを参照してください。
ベストプラクティス
標準 HTTPS シナリオのソリューション
「ドメイン不一致」の問題については、証明書の検証にフックするソリューションを使用して、証明書の検証を実行する前に IP アドレスを元のドメイン名に直接置き換えることができます。
この例は HttpURLConnection 用です
try {
// リクエスト URL のホストを IPv6 アドレスに置き換える場合は、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() {
/*
* 公式ドキュメントでは、このインターフェイスを次のように説明しています:
* これは、実装者が提供できる拡張検証オプションです。
* URL のホスト名がピアの ID ホスト名と一致しない場合、ハンドシェイク中に使用されます。
*
* HTTPDNS Android SDK を使用すると、URL のホスト名はリモートホスト名 (m.taobao.com など) ではなく、
* 証明書のドメインと一致しません。
* Android HttpsURLConnection は、このカスタムシナリオを処理するためのコールバックインターフェイスを提供します。
* HTTPDNS Android SDK からのオリジン 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 {
}このソリューションを使用してネットワークリクエストを開始したときに SSL 検証エラー(Android システムエラー System.err: javax.net.ssl.SSLHandshakeException: java.security.cert.CertPathValidatorException: Trust anchor for certification path not found. など)が発生した場合は、アプリケーションシナリオが SNI(単一の IP と複数の HTTPS ドメイン名)であるかどうかを確認してください。
SNI HTTPS シナリオのソリューション
HttpsURLConnection インターフェースの場合、SNI シナリオで使用するためのサンプルコードを提供します。 完全なコードについては、デモプロジェクトのソースコード を参照してください。
SSLSocketFactory をカスタマイズし、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 アドレスに置き換える場合は、IPv6 アドレスを角括弧 ([]) で囲みます。
// 例: https://[2400:3200:xxx:1]/path
if (ip != null) {
// IP アドレスは HTTPDNS Android SDK を使用して取得されます。URL を置き換え、Host ヘッダーを設定します。
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();
// 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() {
/*
* 公式ドキュメントでは、このインターフェイスを次のように説明しています:
* これは、実装者が提供できる拡張検証オプションです。
* URL のホスト名がピアの ID ホスト名と一致しない場合、ハンドシェイク中に使用されます。
*
* HTTPDNS Android SDK を使用すると、URL のホスト名はリモートホスト名 (m.taobao.com など) ではなく、
* 証明書のドメインと一致しません。
* Android HttpsURLConnection は、このカスタムシナリオを処理するためのコールバックインターフェイスを提供します。
* HTTPDNS Android SDK からのオリジン 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` ヘッダーの大文字と小文字の区別は、一時的および恒久的なリダイレクトにとって重要です。
String location = conn.getHeaderField("Location");
if (location == null) {
location = conn.getHeaderField("location");
}
if (!(location.startsWith("http://") || location.startsWith("https://"))) {
// ホストが省略され、パスのみが返される場合があります。この場合、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: " + 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();
}
}
}このドキュメントでは、標準および SNI HTTPS シナリオ向けの HTTPDNS Android SDK の統合について説明します。
HTTPDNS Android SDK の名前解決サービスの使用方法、または SDK の統合に関する問題の解決方法については、「Android SDK 開発者ガイド」をご参照ください。
HTTPS SNI シナリオで HTTPDNS Android SDK を統合するための完全なコードについては、「デモプロジェクトのソースコード」をご参照ください。