Android Webview場景下防止dns劫持的探索
背景
阿裏雲HTTPDNS是避免dns劫持的一種有效手段,在許多特殊場景如HTTPS/SNI、okhttp等都有最佳實踐,但在webview場景下卻一直沒完美的解決方案。
攔截方案是目前已知的一種在webview上應用httpdns的可行方案,本文從攔截方案的基本原理出發,嚐試分析該方案背後存在的局限,並給出一些可行性上的建議。
基本原理
攔截方案是指通過對webview
進行配置WebViewClient
來做到對網絡請求的攔截:
void setWebViewClient (WebViewClient client);
攔截方案的的調用流程如下圖所示:
Webview
相關的網絡請求由係統的chromium網絡庫發起,Webview
調用loadUrl
方法時,chromium網絡庫會構造URLRequest
實例,經過c層到java層,最終請求參數會回調給上層WebViewClient
的shouldInterceptRequest
方法,而我們的目標是在shouldInterceptRequest
方法中通過HTTPDNS進行URL中域名到ip的替換,並且構造和返回合法的WebResourceResponse
,讓webview在避免dns劫持的同時,也能正常地進行展示。
局限
首先,當Android API < 21時,WebViewClient
提供的攔截API如下:
public WebResourceResponse shouldInterceptRequest(WebView view, String url);
此時shouldInterceptRequest
隻能拿到URL,而請求方法、頭部等這些信息是拿不到的,強行攔截會造成請求信息的丟失,由此可知局限1:
局限1:Android API < 21隻能攔截網絡請求的URL,請求方法、請求頭等無法攔截;
其次,對於Android API >= 21的情況,其攔截API為:
public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request);
第2個參數變成了WebResourceRequest
,其結構如下:
public interface WebResourceRequest {
Uri getUrl();
boolean isForMainFrame();
boolean isRedirect();
boolean hasGesture();
String getMethod();
Map<String, String> getRequestHeaders();
}
相比之下WebResourceRequest
能給的多了Method
,Header
以及是否重定向
,但是沒有Body
,也無法預知該請求是否可能攜帶body,對於帶body的符合協議但非標的Get請求一樣無法攔截,因此局限2:
局限2:Android API >= 21無法攔截body,攔截方案隻能正常處理不帶body的請求;
接下來,我們看下要構造的WebResourceResponse
,這個類需要我們提供MIME
,encoding
和InputStream
。其中,MIME
和encoding
可以通過解析響應頭的content-type
來獲得:
content-type:text/html;charset=UTF-8
但是,對於js/css/image
等類型的資源請求是沒有charset
的,強行攔截會因為編碼問題造成js/css/image
的加載/顯示異常,因此局限3:
局限3:
MIME
和encoding
可通過解析響應頭的content-type
來獲得,但有時會拿不到;
可行性
看完了上麵的各種局限,是不是攔截方案就完全行不可行呢?其實也不盡然,我們來看下麵一段代碼:
@SuppressLint("NewApi")
@Override
public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
final String scheme = request.getUrl().getScheme().trim();
final String url = request.getUrl().toString();
final Map<String, String> headerFields = request.getRequestHeaders();
// #1 隻攔截get方法
if (request.getMethod().equalsIgnoreCase("get") && (scheme.equalsIgnoreCase("http") || scheme.equalsIgnoreCase("https"))) {
try {
final URL oldUrl = new URL(url);
HttpURLConnection conn;
// #2 通過httpdns替換ip
final String ip = mService.getIpByHostAsync(oldUrl.getHost());
if (TextUtils.isEmpty(ip)) {
final String host = oldUrl.getHost();
final String newUrl = url.replaceFirst(host, ip);
// #3 設置HTTP請求頭Host域
conn = (HttpURLConnection) new URL(newUrl).openConnection();
conn.setRequestProperty("Host", host);
// #4 設置HTTP請求header
for (String header : headerFields.keySet()) {
conn.setRequestProperty(header, headerFields.get(header));
}
// #5 處理https場景
if (conn instanceof HttpsURLConnection) {
((HttpsURLConnection) conn).setHostnameVerifier(new HostnameVerifier() {
@Override
public boolean verify(String hostname, SSLSession session) {
return HttpsURLConnection.getDefaultHostnameVerifier().verify(host, session);
}
});
}
// #6 拿到MINE和encoding
final String contentType = conn.getContentType();
final String mine = getMine(contentType);
final String encoding = getEncoding(contentType);
// #7 MINE和encoding拿不到的情況下,不攔截
if (TextUtils.isEmpty(mine) || TextUtils.isEmpty(encoding)) {
return super.shouldInterceptRequest(view, request);
}
return new WebResourceResponse(mine, encoding, conn.getInputStream());
}
} catch (Exception e) {
e.printStackTrace();
}
}
return super.shouldInterceptRequest(view, request);
}
值得注意的是,如果webview中承載的內容是app為自身業務打造的,可控的,那就完全可以通過開發規範來繞開部分局限,也能在一定程度上通過httpdns來改善webview上的dns被劫持的狀況。
最後更新:2017-09-13 14:33:03