このトピックでは、WebView シナリオで Android 用 HTTPDNS SDK を統合するためのベストプラクティスについて説明します。
概要
WebView は、HTML と JavaScript で記述された Web ページを解析して表示する、Android が提供する UI コントロールです。Android は、WebView でのネットワークリクエストをインターセプトし、カスタムロジックを挿入できる API を提供します。これらの API を使用して、ネットワークリクエストをインターセプトし、リクエスト URL からホストを取得し、Android 用 HTTPDNS SDK を呼び出してホストを解決できます。解決された IP アドレスを使用して、ネットワークリクエスト用の新しい URL を作成できます。これらのプラクティスは、HTTP、HTTPS、およびサーバ名表示 (SNI) のシナリオに適用されます。次の前提条件を満たす必要があります。
デバイスは Android API レベル 21 以降で実行されます。
Cookie ヘッダーのない HTTP リダイレクト リクエスト
GET リクエスト
WebView シナリオで Android 用 HTTPDNS SDK を統合するためのベストプラクティスを示す完全なサンプルコードについては、「サンプルプロジェクトのソースコード」をご参照ください。
ベストプラクティス
public void setWebViewClient(WebViewClient client);WebView は、通知とリクエストを受信するために WebViewClient を設定できる setWebViewClient API を提供します。shouldInterceptRequest メソッドを呼び出すことで、ネットワークリクエストをインターセプトできます。
public class WebViewClient{
// API < 21
public WebResourceResponse shouldInterceptRequest(WebView view,String url){
.....
}
// API >= 21
public WebResourceResponse shouldInterceptRequest(WebView view,WebResourceRequest request) {
.....
}
......
}Android SDK が提供する shouldInterceptRequest メソッドには、API レベルごとに異なるバージョンがあります。
API レベルが 21 未満の場合は、次の shouldInterceptRequest メソッドが呼び出されます。
public WebResourceResponse shouldInterceptRequest(WebView view,String url)リクエスト URL のみが返されます。 リクエスト メソッド、ヘッダー、または リクエスト 本文を取得することはできません。 このメソッドを呼び出して リクエスト をインターセプトすると、WebView が リクエスト されたすべてのリソースの読み込みに失敗する可能性があります。 したがって、API レベルが 21 未満のデバイスでは リクエスト をインターセプトしないことをお勧めします。
public WebResourceResponse shouldInterceptRequest(WebView view,String url) {
return super.shouldInterceptRequest(view, url);
}API レベルが 21 以上の場合は、次の shouldInterceptRequest メソッドが呼び出されます。
public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
String scheme = request.getUrl().getScheme().trim();
String method = request.getMethod();
Map<String, String> headerFields = request.getRequestHeaders();
// ボディのないリクエストのみが正しく処理されます。
if ((scheme.equalsIgnoreCase("http") || scheme.equalsIgnoreCase("https"))&& method.equalsIgnoreCase("get")) {
......
} else {
return super.shouldInterceptRequest(view, reqeust);
}
}WebResourceRequest はリクエスト本文を取得するメソッドを提供していないため、shouldInterceptRequest メソッドは GET リクエストのみをインターセプトできます。 POST リクエストをインターセプトするために使用することはできません。
実装
WebResourceResponse コールバックを提供する
shouldInterceptRequest メソッドの呼び出し時には、WebResourceResponse を返す必要があります:
public WebResourceResponse(String mimeType, String encoding, InputStream data) ;WebResourceResponse オブジェクトを作成するには、リクエストの MIME タイプ、リクエストのエンコーディング、およびリクエストの入力ストリームを提供する必要があります。
リクエストの入力ストリームは URLConnection.getInputStream() を呼び出すことで取得でき、MIME タイプと encoding は、URLConnection.getContentType() を呼び出してリクエストのコンテンツタイプから取得できます。
すべてのリクエストが完全な contentType 情報を返すわけではありません。この場合は、次の戦略を使用します。
String contentType = conn.getContentType();
String mime = getMime(contentType);
String charset = getCharset(contentType);
// MIME タイプのないリクエストはインターセプトしません。
if (TextUtils.isEmpty(mime)) {
return super.shouldInterceptRequest(view, request);
} else {
if (!TextUtils.isEmpty(charset)) {
// リクエストの MIME タイプと Accept-Charset ヘッダーの両方を取得できる場合、このリクエストをインターセプトします。
return new WebResourceResponse(mime, charset, connection.getInputStream());
} else {
// リクエストの文字エンコーディングを取得できない場合、リクエストされたリソースに基づいて、このリクエストをインターセプトするかどうかを判断します。
// バイナリリソースがリクエストされた場合、文字エンコーディングは不要なため、このリクエストをインターセプトします。
if (isBinaryRes(mime)) {
Log.e(TAG, "binary resource for " + mime);
return new WebResourceResponse(mime, charset, connection.getInputStream());
}else {
// 非バイナリリソースがリクエストされた場合、文字エンコーディングが必要です。エンコードされていないリクエストはインターセプトしません。
Log.e(TAG, "non binary resource for " + mime);
return super.shouldInterceptRequest(view, request);
}
}
}ホストヘッダーを設定する
public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
......
URL url = new URL(request.getUrl().toString());
conn = (HttpURLConnection) url.openConnection();
// Android 用 HTTPDNS SDK の API を呼び出して IP アドレスを取得します。
String ip = null;
String[] ipv4Array = mDNSResolver.getIpv4ByHostFromCache(url.getHost(),true);
if (ipv4Array != null && ipv4Array.length > 0) {
ip = ipv4Array[0];
}
if (ip != null) {
//Log.d(TAG, "get IP: " + ip + " for host: " + url.getHost() + "from pdns resolver success!");
String newUrl = path.replaceFirst(url.getHost(), ip);
conn = (HttpURLConnection) new URL(newUrl).openConnection();
for (Map.Entry<String, String> field : headers.entrySet()) {
// HTTP リクエストのヘッダー情報を設定します。
conn.setRequestProperty(field.getKey(), field.getValue( ));
}
}
// conn.setRequestProperty("Host", url.getHost());
}
}ユースケース
リダイレクト
サーバーからの GET リクエストをインターセプトした結果、リダイレクトが発生した場合は、元のリクエストに Cookie が含まれているかどうかを確認します。元のリクエストヘッダーに Cookie が含まれている場合は、リダイレクト後に Cookie が変更される可能性があるため、リクエストをインターセプトしないでください。元のリクエストヘッダーに Cookie が含まれていない場合は、2 番目のリクエストを送信する必要があります。
int code = conn.getResponseCode();
if (code >= 300 && code < 400) {
if (the GET request contains the Cookie header) {
// このリクエストはインターセプトされません。
return super.shouldInterceptRequest(view, request);
}
// Location キーでは大文字と小文字が区別される点にご注意ください。 大文字と小文字の使い分けは、異なるリダイレクトモードを示します。 たとえば、後続のすべてのリクエストを指定の IP アドレスにリダイレクトするか、現在のリクエストのみをリダイレクトするかなどです。 大文字と小文字の使い分けと、それに対応するリダイレクトモードはサーバーによって決定されます。
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;
}
Log.e(TAG, "code:" + code + "; location:" + location + ";path" + path);
Initiate the GET request again.
} else {
// リダイレクト終了。
Log.e(TAG, "redirect finish");
......
}HTTPS 証明書の検証
インターセプトされたリクエストが HTTPS リクエストの場合、証明書を検証する必要があります:
if (conn instanceof HttpsURLConnection) {
final HttpsURLConnection httpsURLConnection = (HttpsURLConnection)conn;
// HTTPS リクエストの証明書を検証します。
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);
}
});
}HTTPS+SNI
HTTPS リクエストに SNI シナリオが含まれる場合は、SSLSocket をカスタマイズする必要があります。詳細については、「Android における HTTPS (SNI を含む) シナリオの IP 直接接続ソリューション」をご参照ください。
このトピックのベストプラクティスは、WebView を使用して Android 用 HTTPDNS SDK を統合するシナリオにのみ適用されます。
ドメイン名の名前解決のために Android 用 HTTPDNS SDK を使用する方法、および潜在的な統合の問題を解決する方法については、「Android SDK 開発者ガイド」をご参照ください。
WebView シナリオで Android 用 HTTPDNS SDK を統合するためのベストプラクティスを示す完全なサンプルプロジェクトについては、「デモプロジェクトのソースコード」をご参照ください。