Android基于DNS攔截的Webview代理

背景

App的開發(fā)過程中,通常需要切換環(huán)境來進行調(diào)試。雖然我們可以通過切換App內(nèi)部域名的方式來達到切換環(huán)境的目的。但是對于M頁來說,并不能影響到M頁請求地址。
通常的處理方案,是手機的wifi連上電腦的代理,然后電腦配置對應的host,然后才能進行調(diào)試或者測試。
因此這里做一個可以在app內(nèi)部進行代理切換的功能。

配置域名

配置你需要的域名,這里給出實例代碼,實際中可以通過讀取自己頁面的配置來實現(xiàn)。

public class ProxyList {
    private static HashMap<String, String> mMap;
    public static HashMap<String, String> getList() {
        if (mMap == null) {
            synchronized (ProxyList.class) {
                if (mMap == null) {
                    mMap = new HashMap<String, String>();
                    // 可以從實際的配置中取,這是只是演示
                    mMap.put("host.com", "testhost.com");
                    mMap.put("ip.com", "192.168.1.1");
                }
            }
        }
        return mMap;
    }
}

上述代碼存在兩個問題
1、數(shù)據(jù)未put進去的時候,但是mMap已經(jīng)創(chuàng)建的時候,這時get方法得到的未空。
2、極小的概率會造成mMap為空。說明請看這篇文章http://blog.csdn.net/glory1234work2115/article/details/50814419吧。
最后這里還是把整個方法改成synchronized來解決了。最簡單的解決方式了。

okhttp的DNS攔截(可選)

因為本人搬磚的項目用的是okttp,因此這里給下okhttp攔截方法。將okhttp的Dns參數(shù)替換成自定義的MyDns,這樣連okhttp都會使用我們配置的dns。這個對webview是沒有影響的,因此可不配置。

public class MyDns implements Dns {
    @Override
    public List<InetAddress> lookup(String hostname) throws UnknownHostException {
        if (hostname == null) throw new UnknownHostException("hostname == null");
        if (BuildConfig.RELEASE) {
            return Dns.SYSTEM.lookup(hostname);
        }

        String ip = ProxyList.getList().get(hostname);
        ZLog.d("DNS LOG: start intercept:" + hostname);
        if (StringUtils.isNotEmpty(ip)){
            ZLog.d("DNS LOG:" + hostname + " is replaced by ip " + ip);
            return Arrays.asList(InetAddress.getAllByName(ip));
        } else {
            ZLog.d("DNS LOG:" + hostname + " is not replaced");
            return Arrays.asList(InetAddress.getAllByName(hostname));
        }
    }
}

webview資源攔截

使用WebviewClient的攔截資源請求的方法來實現(xiàn)。

//WebviewClient 中重寫這兩個方法,攔截資源
        @Override
        public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
            if (BuildConfig.RELEASE) {
                return super.shouldInterceptRequest(view, request);
            } else {
                return WebviewDnsInterceptUtil.getDnsInterceptRequest(view, request);
            }
        }

        @Override
        public WebResourceResponse shouldInterceptRequest(WebView view, String url) {
            if (BuildConfig.RELEASE) {
                return super.shouldInterceptRequest(view, url);
            } else {
                return WebviewDnsInterceptUtil.getDnsInterceptUrl(view, url);
            }
        }

然后就是攔截資源了
1、查看是否需要替換域名
2、建立urlconnection獲取資源返回資源,不同擔心,這里是子線程。
攔截資源代碼如下:

public class WebviewDnsInterceptUtil {

    @SuppressLint("NewApi")
    public static WebResourceResponse getDnsInterceptRequest(WebView view, WebResourceRequest request) {
        if (request != null && request.getUrl() != null && request.getMethod().equalsIgnoreCase("get")) {
            return getWebResourceFromUrl(request.getUrl().toString());
        }
        return null;
    }

    public static WebResourceResponse getDnsInterceptUrl(WebView view, String url) {
        if (!TextUtils.isEmpty(url) && Uri.parse(url).getScheme() != null) {
            return getWebResourceFromUrl(url);
        }
        return null;
    }

//核心攔截方法
    private static WebResourceResponse getWebResourceFromUrl(String url) {

        String scheme = Uri.parse(url).getScheme().trim();

        ZLog.d("web log 請求 url: " + url);

        String ips = ProxyList.getList().get(Uri.parse(url).getHost());
        if (StringUtils.isEmpty(ips)) {
            ZLog.d("web log 不攔截:" + url);
            return null;
        }

        // HttpDns解析css文件的網(wǎng)絡請求及圖片請求
        if ((scheme.equalsIgnoreCase("http") || scheme.equalsIgnoreCase("https"))) {
            try {
                URL oldUrl = new URL(url);
                URLConnection connection = oldUrl.openConnection();
                // 獲取HttpDns域名解析結(jié)果 // 通過HTTPDNS獲取IP成功,進行URL替換和HOST頭設置
                ZLog.d("HttpDns ips are: " + ips + " for host: " + oldUrl.getHost());
                String ip;
                if (ips.contains(";")) {
                    ip = ips.substring(0, ips.indexOf(";"));
                } else {
                    ip = ips;
                }
                String newUrl = url.replaceFirst(oldUrl.getHost(), ip);
                ZLog.d("newUrl a is: " + newUrl);
                connection =  new URL(newUrl).openConnection(); // 設置HTTP請求頭Host域
                connection.setRequestProperty("Host", oldUrl.getHost());
                ZLog.d("ContentType a: " + connection.getContentType());
//有可能是text/html; charset=utf-8的形式,只需要第一個
                String[] strings = connection.getContentType().split(";");
                return new WebResourceResponse(strings[0], "UTF-8", connection.getInputStream());
            } catch (MalformedURLException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return null;
    }
}

然后測試,發(fā)現(xiàn)攔截成功了,域名會變成我們改的域名或者ip。

(補充)解決https證書錯誤等問題

測試環(huán)境下沒有真正的https證書,因此會產(chǎn)生異常。
這里的解決方案就是忽略證書錯誤。
修改后的代碼:

        // HttpDns解析css文件的網(wǎng)絡請求及圖片請求
        if ((scheme.equalsIgnoreCase("http") || scheme.equalsIgnoreCase("https"))) {
            HttpsURLConnection connection = null;
            try {
                URL oldUrl = new URL(url);
                // 獲取HttpDns域名解析結(jié)果 // 通過HTTPDNS獲取IP成功,進行URL替換和HOST頭設置
                ZLog.d("HttpDns ips are: " + ips + " for host: " + oldUrl.getHost());
                String ip;
                if (ips.contains(";")) {
                    ip = ips.substring(0, ips.indexOf(";"));
                } else {
                    ip = ips;
                }
                String newUrl = url.replaceFirst(oldUrl.getHost(), ip);
                ZLog.d("newUrl a is: " + newUrl);
                connection = (HttpsURLConnection) new URL(newUrl).openConnection(); // 設置HTTP請求頭Host域
                connection.setHostnameVerifier(getNullHostNameVerifier());
                connection.setSSLSocketFactory(getIgnoreSSLContext().getSocketFactory());
                connection.setRequestProperty("Host", oldUrl.getHost());
                ZLog.d("ContentType a: " + connection.getContentType());

                String encode = connection.getContentEncoding();
                if (encode == null) {
                    encode = "UTF-8";
                }
                if (connection.getContentType() != null) {
                    String[] strings = connection.getContentType().split(";");
                    return new WebResourceResponse(strings[0], encode, connection.getInputStream());
                } else {
                    return new WebResourceResponse("document", encode, connection.getInputStream());
                }
            } catch (Exception e) {
                ZLog.d(e.toString());
                return new WebResourceResponse("document", "UTF-8", null);
            } finally {
                if (connection != null) {
                    connection.disconnect();
                }
            }
        }
        return null;

兩個get方法:

    private static volatile SSLContext mIgnoreSSLContext;
    private static volatile HostnameVerifier mNullHostNameVerifier;

    public static SSLContext getIgnoreSSLContext () {
       if (mIgnoreSSLContext == null) {
           synchronized (WebviewDnsInterceptUtil.class) {
               if (mIgnoreSSLContext == null) {
                   try {
                       mIgnoreSSLContext = SSLContext.getInstance("TLS");
                       TrustManager[] trustAllCerts = new TrustManager[]{new X509TrustManager(){
                           public X509Certificate[] getAcceptedIssuers(){return null;}
                           public void checkClientTrusted(X509Certificate[] certs, String authType){}
                           public void checkServerTrusted(X509Certificate[] certs, String authType){}
                       }};
                       mIgnoreSSLContext.init(null, trustAllCerts, new SecureRandom());
                   } catch (Exception e) {
                       e.printStackTrace();
                   }
               }
           }
       }
       return mIgnoreSSLContext;
    }

    public static HostnameVerifier getNullHostNameVerifier() {
        if (mNullHostNameVerifier == null) {
            synchronized (WebviewDnsInterceptUtil.class) {
                mNullHostNameVerifier = new NullHostNameVerifier();
            }
        }
        return mNullHostNameVerifier;
    }

NullHostNameVerifier:

public class NullHostNameVerifier implements HostnameVerifier {

    @Override
    public boolean verify(String hostname, SSLSession session) {
        return true;
    }
}

總結(jié)

實現(xiàn)這個功能還是不難的,關(guān)鍵是要去想實現(xiàn)這個功能,多查查資料我們總能找到辦法去解決。當然,這里只是把功能的實現(xiàn)列出來,要實際用起來,還需要寫一些配置頁面,開關(guān)的攔截等。要做成完整的功能,還需要大家自己去實現(xiàn)了。

感悟

要肯想解決問題,才會去解決問題。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容