閱讀415 返回首頁    go 阿裏雲 go 技術社區[雲棲]


Android Webview場景下防止dns劫持的探索

背景

阿裏雲HTTPDNS是避免dns劫持的一種有效手段,在許多特殊場景如HTTPS/SNIokhttp等都有最佳實踐,但在webview場景下卻一直沒完美的解決方案。

攔截方案是目前已知的一種在webview上應用httpdns的可行方案,本文從攔截方案的基本原理出發,嚐試分析該方案背後存在的局限,並給出一些可行性上的建議。

基本原理

攔截方案是指通過對webview進行配置WebViewClient來做到對網絡請求的攔截:

void setWebViewClient (WebViewClient client);

攔截方案的的調用流程如下圖所示:

Webview相關的網絡請求由係統的chromium網絡庫發起,Webview調用loadUrl方法時,chromium網絡庫會構造URLRequest實例,經過c層到java層,最終請求參數會回調給上層WebViewClientshouldInterceptRequest方法,而我們的目標是在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能給的多了MethodHeader以及是否重定向,但是沒有Body,也無法預知該請求是否可能攜帶body,對於帶body的符合協議但非標的Get請求一樣無法攔截,因此局限2:

局限2:Android API >= 21無法攔截body,攔截方案隻能正常處理不帶body的請求;

接下來,我們看下要構造的WebResourceResponse,這個類需要我們提供MIMEencodingInputStream。其中,MIMEencoding可以通過解析響應頭的content-type來獲得:

content-type:text/html;charset=UTF-8

但是,對於js/css/image等類型的資源請求是沒有charset的,強行攔截會因為編碼問題造成js/css/image的加載/顯示異常,因此局限3:

局限3MIMEencoding可通過解析響應頭的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

  上一篇:go  阿裏雲前端周刊 - 第 24 期
  下一篇:go  iOS 防 DNS 汙染方案調研 --- WebView 業務場景