Android基于DNS攔截的Webview代理

背景

App的開(kāi)發(fā)過(guò)程中弟疆,通常需要切換環(huán)境來(lái)進(jìn)行調(diào)試钧排。雖然我們可以通過(guò)切換App內(nèi)部域名的方式來(lái)達(dá)到切換環(huán)境的目的括勺。但是對(duì)于M頁(yè)來(lái)說(shuō),并不能影響到M頁(yè)請(qǐng)求地址潜秋。
通常的處理方案蛔琅,是手機(jī)的wifi連上電腦的代理,然后電腦配置對(duì)應(yīng)的host峻呛,然后才能進(jìn)行調(diào)試或者測(cè)試罗售。
因此這里做一個(gè)可以在app內(nèi)部進(jìn)行代理切換的功能。

配置域名

配置你需要的域名钩述,這里給出實(shí)例代碼寨躁,實(shí)際中可以通過(guò)讀取自己頁(yè)面的配置來(lái)實(shí)現(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>();
                    // 可以從實(shí)際的配置中取牙勘,這是只是演示
                    mMap.put("host.com", "testhost.com");
                    mMap.put("ip.com", "192.168.1.1");
                }
            }
        }
        return mMap;
    }
}

上述代碼存在兩個(gè)問(wèn)題
1职恳、數(shù)據(jù)未put進(jìn)去的時(shí)候,但是mMap已經(jīng)創(chuàng)建的時(shí)候谜悟,這時(shí)get方法得到的未空话肖。
2北秽、極小的概率會(huì)造成mMap為空葡幸。說(shuō)明請(qǐng)看這篇文章http://blog.csdn.net/glory1234work2115/article/details/50814419吧。
最后這里還是把整個(gè)方法改成synchronized來(lái)解決了贺氓。最簡(jiǎn)單的解決方式了蔚叨。

okhttp的DNS攔截(可選)

因?yàn)楸救税岽u的項(xiàng)目用的是okttp,因此這里給下okhttp攔截方法辙培。將okhttp的Dns參數(shù)替換成自定義的MyDns蔑水,這樣連okhttp都會(huì)使用我們配置的dns。這個(gè)對(duì)webview是沒(méi)有影響的扬蕊,因此可不配置搀别。

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的攔截資源請(qǐng)求的方法來(lái)實(shí)現(xiàn)。

//WebviewClient 中重寫(xiě)這兩個(gè)方法尾抑,攔截資源
        @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獲取資源返回資源再愈,不同擔(dān)心榜苫,這里是子線程。
攔截資源代碼如下:

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 請(qǐng)求 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)絡(luò)請(qǐng)求及圖片請(qǐng)求
        if ((scheme.equalsIgnoreCase("http") || scheme.equalsIgnoreCase("https"))) {
            try {
                URL oldUrl = new URL(url);
                URLConnection connection = oldUrl.openConnection();
                // 獲取HttpDns域名解析結(jié)果 // 通過(guò)HTTPDNS獲取IP成功翎冲,進(jìn)行URL替換和HOST頭設(shè)置
                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(); // 設(shè)置HTTP請(qǐng)求頭Host域
                connection.setRequestProperty("Host", oldUrl.getHost());
                ZLog.d("ContentType a: " + connection.getContentType());
//有可能是text/html; charset=utf-8的形式垂睬,只需要第一個(gè)
                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;
    }
}

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

(補(bǔ)充)解決https證書(shū)錯(cuò)誤等問(wèn)題

測(cè)試環(huán)境下沒(méi)有真正的https證書(shū)钳枕,因此會(huì)產(chǎn)生異常。
這里的解決方案就是忽略證書(shū)錯(cuò)誤赏壹。
修改后的代碼:

        // HttpDns解析css文件的網(wǎng)絡(luò)請(qǐng)求及圖片請(qǐng)求
        if ((scheme.equalsIgnoreCase("http") || scheme.equalsIgnoreCase("https"))) {
            HttpsURLConnection connection = null;
            try {
                URL oldUrl = new URL(url);
                // 獲取HttpDns域名解析結(jié)果 // 通過(guò)HTTPDNS獲取IP成功么伯,進(jìn)行URL替換和HOST頭設(shè)置
                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(); // 設(shè)置HTTP請(qǐng)求頭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;

兩個(gè)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é)

實(shí)現(xiàn)這個(gè)功能還是不難的,關(guān)鍵是要去想實(shí)現(xiàn)這個(gè)功能卡儒,多查查資料我們總能找到辦法去解決田柔。當(dāng)然,這里只是把功能的實(shí)現(xiàn)列出來(lái)骨望,要實(shí)際用起來(lái)硬爆,還需要寫(xiě)一些配置頁(yè)面,開(kāi)關(guān)的攔截等擎鸠。要做成完整的功能缀磕,還需要大家自己去實(shí)現(xiàn)了。

感悟

要肯想解決問(wèn)題劣光,才會(huì)去解決問(wèn)題袜蚕。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市绢涡,隨后出現(xiàn)的幾起案子牲剃,更是在濱河造成了極大的恐慌,老刑警劉巖雄可,帶你破解...
    沈念sama閱讀 222,681評(píng)論 6 517
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件凿傅,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡数苫,警方通過(guò)查閱死者的電腦和手機(jī)聪舒,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,205評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)虐急,“玉大人箱残,你說(shuō)我怎么就攤上這事≈褂酰” “怎么了被辑?”我有些...
    開(kāi)封第一講書(shū)人閱讀 169,421評(píng)論 0 362
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)赏殃。 經(jīng)常有香客問(wèn)我敷待,道長(zhǎng),這世上最難降的妖魔是什么仁热? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 60,114評(píng)論 1 300
  • 正文 為了忘掉前任榜揖,我火速辦了婚禮勾哩,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘举哟。我一直安慰自己思劳,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,116評(píng)論 6 398
  • 文/花漫 我一把揭開(kāi)白布妨猩。 她就那樣靜靜地躺著潜叛,像睡著了一般。 火紅的嫁衣襯著肌膚如雪壶硅。 梳的紋絲不亂的頭發(fā)上威兜,一...
    開(kāi)封第一講書(shū)人閱讀 52,713評(píng)論 1 312
  • 那天,我揣著相機(jī)與錄音庐椒,去河邊找鬼椒舵。 笑死,一個(gè)胖子當(dāng)著我的面吹牛约谈,可吹牛的內(nèi)容都是我干的笔宿。 我是一名探鬼主播,決...
    沈念sama閱讀 41,170評(píng)論 3 422
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼棱诱,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼泼橘!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起迈勋,我...
    開(kāi)封第一講書(shū)人閱讀 40,116評(píng)論 0 277
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤炬灭,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后粪躬,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體担败,經(jīng)...
    沈念sama閱讀 46,651評(píng)論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,714評(píng)論 3 342
  • 正文 我和宋清朗相戀三年镰官,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片吗货。...
    茶點(diǎn)故事閱讀 40,865評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡泳唠,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出宙搬,到底是詐尸還是另有隱情笨腥,我是刑警寧澤,帶...
    沈念sama閱讀 36,527評(píng)論 5 351
  • 正文 年R本政府宣布勇垛,位于F島的核電站脖母,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏闲孤。R本人自食惡果不足惜谆级,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,211評(píng)論 3 336
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧肥照,春花似錦脚仔、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,699評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至吕朵,卻和暖如春猎醇,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背努溃。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,814評(píng)論 1 274
  • 我被黑心中介騙來(lái)泰國(guó)打工姑食, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人茅坛。 一個(gè)月前我還...
    沈念sama閱讀 49,299評(píng)論 3 379
  • 正文 我出身青樓音半,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親贡蓖。 傳聞我的和親對(duì)象是個(gè)殘疾皇子曹鸠,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,870評(píng)論 2 361

推薦閱讀更多精彩內(nèi)容