一探究竟!Whistle攔截HTTPS是如何實現(xiàn)的?

導(dǎo)語?|?本文主要介紹Whistle的實現(xiàn)原理,通過這篇文章讀者可以了解Whistle的具體實現(xiàn)過程,并且自己也可以實現(xiàn)一個簡單的抓包調(diào)試工具。
項目Github地址:https://github.com/avwo/whistle
Whistle是基于Node實現(xiàn)的跨平臺Web抓包調(diào)試(HTTP)代理,主要功能:
實時抓包:支持HTTP、HTTPS、HTTP2、WebSocket、TCP等常見Web請求
修改請求響應(yīng):與一般抓包調(diào)試工具采用斷點的方式不同,Whistle采用類似系統(tǒng)host的配置規(guī)則方式
擴展功能:支持通過Node編寫插件,或作為獨立NPM包引入項目兩種擴展方式

本文將從最基本的概念開始逐步實現(xiàn)Whistle功能,包含以下內(nèi)容:
什么是HTTP代理
實現(xiàn)簡單HTTP代理
完整HTTP代理架構(gòu)(Whistle)
各個模塊的實現(xiàn)原理
參考資料
一、什么是HTTP代理

代理是客戶端到服務(wù)端的中轉(zhuǎn)服務(wù),其中:
不經(jīng)過代理的請求:客戶端和服務(wù)端直接建立連接后,即可開始交換數(shù)據(jù)
經(jīng)過代理的請求:客戶端不與服務(wù)端直接建立連接,而是先跟代理建立連接后,將目標(biāo)服務(wù)器的地址發(fā)送給代理,通過代理再跟服務(wù)端建立連接,這里如果代理服務(wù)為HTTP Server,則稱為HTTP代理。
接下來看下客戶端如何將目標(biāo)服務(wù)器地址傳給HTTP代理,以及HTTP代理如何跟目標(biāo)服務(wù)器建立連接。
二、實現(xiàn)簡單HTTP代理
先看一個用Node實現(xiàn)的最簡單HTTP代理:
const http = require('http');const { connect } = require('net');/****************** 工具方法 ******************/const getHostPort = (host, defaultPort) => {let port = defaultPort || 80;const index = host.indexOf(':');if (index !== -1) {port = host.substring(index + 1);host = host.substring(0, index);}return {host, port};};const getOptions = (req, defaultPort) => {// 這里假定 host 一定存在,完整實現(xiàn)參見 Whistleconst { host, port } = getHostPort(req.headers.host, defaultPort);return {hostname: host, // 指定請求域名,用于通過 DNS 獲取服務(wù)器 IP 及設(shè)置請求頭 host 字段port, // 指定服務(wù)器端口path: req.url || '/',method: req.method,headers: req.headers,rejectUnauthorized: false, // 給 HTTPS 請求用的,HTTP 請求會自動忽略};};// 簡單處理,出錯直接斷開,完整實現(xiàn)邏輯參考 Whistleconst handleClose = (req, res) => {const destroy = (err) => { // 及時關(guān)閉無用的連接,防止內(nèi)存泄露req.destroy();res && res.destroy();};res && res.on('error', destroy);req.on('error', destroy);req.once('close', destroy);};/****************** 服務(wù)代碼 ******************/const server = http.createServer();// 處理 HTTP 請求server.on('request', (req, res) => {// 與服務(wù)端建立連接,透傳客戶端請求及服務(wù)端響應(yīng)內(nèi)容const client = http.request(getOptions(req), (svrRes) => {res.writeHead(svrRes.statusCode, svrRes.headers);svrRes.pipe(res);});req.pipe(client);handleClose(res, client);});// 隧道代理:處理 HTTPS、HTTP2、WebSocket、TCP 等請求server.on('connect', (req, socket) => {// 與服務(wù)端建立連接,透傳客戶端請求及服務(wù)端響應(yīng)內(nèi)容const client = connect(getHostPort(req.url), () => {socket.write('HTTP/1.1 200 Connection Established\r\n\r\n');socket.pipe(client).pipe(socket);});handleClose(socket, client);});server.listen(8080);
上述代碼實現(xiàn)了一個具有轉(zhuǎn)發(fā)請求功能的HTTP代理,從代碼可知HTTP代理就是一個普通的HTTP Server,并監(jiān)聽request和connect這兩個事件,客戶端會通過這兩個事件將目標(biāo)服務(wù)器地址傳過來,其中:
request:一般普通HTTP會通過該事件將目標(biāo)服務(wù)器地址傳過來。
connect:一般非HTTP請求,如HTTPS、HTTP/2、WebSocket、TCP等會通過該事件將目標(biāo)服務(wù)器地址傳過來,觸發(fā)該事件的代理請求也叫隧道代理。
可以在事件里面的req.url或req.headers.host獲取目標(biāo)服務(wù)器的地址(host:port),再跟該服務(wù)器地址建立連接并將結(jié)果通過HTTP響應(yīng)的方式返回給客戶端,這里只是實現(xiàn)代理的最基本功能,完整的HTTP除了請求轉(zhuǎn)發(fā),至少應(yīng)該還有:
查看實時抓包;
解析HTTPS請求;
修改請求響應(yīng)內(nèi)容;
擴展功能。
下面以Whistle為例看下如何用Node.js實現(xiàn)一個完整的HTTP代理。
三、完整HTTP代理架構(gòu)(Whistle)

主要分五個模塊:
請求接入模塊
隧道代理模塊
處理HTTP請求模塊
規(guī)則管理模塊
插件管理模塊
四、具體實現(xiàn)原理
下面分別看下這五個模塊具體是怎么實現(xiàn)的。
(一)請求接入模塊

所有請求先會經(jīng)過請求接入模塊,Whistle支持四種請求接入方式:
HTTP&HTTPS直接請求:相當(dāng)于配hosts或DNS的方式,將請求轉(zhuǎn)發(fā)到Whistle;
HTTP代理:Whistle默認(rèn)接入方式,即配系統(tǒng)代理或通過瀏覽器插件配 HTTP代理的方式;
HTTPS代理:在HTTP代理之上對代理請求進(jìn)行了加密,即HTTPS Server,可以通過指定證書轉(zhuǎn)成HTTP代理請求;
Socks5代理:利用npm包socksv5轉(zhuǎn)成普通的TCP請求,并將TCP請求轉(zhuǎn)成隧道代理請求。
實現(xiàn)原理:將所有請求都轉(zhuǎn)成HTTP代理的隧道代理請求或HTTP請求,再解析隧道代理請求轉(zhuǎn)成HTTP請求。
如何將普通tcp請求轉(zhuǎn)成隧道代理請求參見:lack-proxy
下面看下如何從隧道代理請求解析出HTTP請求。
(二)隧道代理模塊

關(guān)鍵點(HTTP請求也可以走隧道代理):
通過匹配的全局規(guī)則判斷是否要解析隧道代理請求,如果不解析,則當(dāng)成普通TCP請求處理;
如果需要,則通過socket.once('data', handler)?讀取請求點第一幀數(shù)據(jù);
將第一幀數(shù)據(jù)轉(zhuǎn)成字符串,通過正則/^(\w+)\s+(\S+)\s+HTTP\/1.\d$/mi是否是HTTP請求?如果是HTTP請求,再判斷下是否是CONNECT請求,即隧道代理請求(隧道代理請求也可以代理隧道代理請求),如果是,則轉(zhuǎn)回隧道代理方法處理,如果不是,則轉(zhuǎn)到HTTP請求模塊處理;
如果不是HTTP請求,則當(dāng)成HTTPS請求處理,這里需要用到中間人的方式將HTTPS請求轉(zhuǎn)成HTTP請求;
Whistle會先按以下順序獲取請求證書:
通過匹配的插件獲取(可以通過規(guī)則 sniCallback://plugin 指定加載證書的插件);
通過啟動參數(shù)-z certDir指定目錄或~/.WhistleAppData/custom_certs 加載的自定義證書;
如果沒有上述兩種自動證書,Whistle會自動生成一個默認(rèn)的證書。
獲取到證書后,再利用該證書啟動一個HTTPS Server,將HTTPS請求轉(zhuǎn)成HTTP請求交給HTTP請求模塊處理。
(三)HTTP請求處理模塊

HTTP 請求處理可以分兩個階段:
請求階段:
匹配全局規(guī)則;
如果規(guī)則里類似whistle.xx的規(guī)則,執(zhí)行對應(yīng)插件鉤子,獲取插件規(guī)則并跟匹配的全局規(guī)則合并;
執(zhí)行規(guī)則、記錄狀態(tài)并請求到指定服務(wù)。
響應(yīng)階段:
執(zhí)行匹配插件的鉤子,獲取插件規(guī)則并跟匹配的全局規(guī)則合并;
執(zhí)行規(guī)則、記錄狀態(tài)并請求返回客戶端。
(四)規(guī)則管理
與傳統(tǒng)抓包調(diào)試代理采用斷點修改請求響應(yīng)數(shù)據(jù)不同,Whistle采用配置規(guī)則的方式修改請求響應(yīng),采用配置方式的好處是操作簡單,且可以將操作持久化存儲及共享給他人,先看幾個例子:

Whistle的規(guī)則管理主要兩個功能:解析規(guī)則、匹配規(guī)則。
解析規(guī)則
Whistle有兩類規(guī)則:
全局規(guī)則(公共規(guī)則),所有請求都會嘗試匹配的規(guī)則,由以下規(guī)則組成:
界面Rules配置的規(guī)則;
插件根目錄rules.txt配置文件;
文檔:https://github.com/whistle-plugins/whistle.autosave/blob/master/rules.txt? ?
界面或插件rules.txt通過@url方式引入的遠(yuǎn)程規(guī)則(要單獨一行,Whistle會定時更新遠(yuǎn)程規(guī)則)。

插件規(guī)則(私有規(guī)則),即進(jìn)入插件的請求(匹配的全局規(guī)則里有whistle.xxx協(xié)議)才會匹配到的規(guī)則,由以下規(guī)則組成:
文檔:https://wproxy.org/whistle/plugins.html
插件reqRulesServer等hooks動態(tài)返回;
插件根目錄_rules.txt等文件配置的靜態(tài)規(guī)則。
匹配規(guī)則
Whistle規(guī)則的完整結(jié)構(gòu)為:

文檔:https://wproxy.org/whistle/mode.html
(五)插件管理

Whistle插件的功能很多,不僅具備Node的所有能力,且可以操作Whistle的所有規(guī)則(理論上可以基于插件實現(xiàn)一個Whistle),主要用來做以下事情:
鑒權(quán)功能
提供UI交互界面
作為請求Server(直接響應(yīng)或轉(zhuǎn)發(fā)并修改請求響應(yīng))
統(tǒng)計請求信息(查看上報/打點數(shù)據(jù)等)
設(shè)置規(guī)則(動態(tài)、靜態(tài)、全局及私有規(guī)則)
獲取抓包數(shù)據(jù)
編解碼請求響應(yīng)數(shù)據(jù)流(pipe stream功能)
擴展界面右鍵菜單(如:分享抓包數(shù)據(jù))
保存并同步Rules&Values數(shù)據(jù)
自定義HTTPS請求的證書
比如:
whistle.script:實現(xiàn)通過自定義腳本動態(tài)設(shè)置規(guī)則
whistle.vase:提供靈活強大的mock能力
whistle.inspect:方便快速注入vConsole、eruda等頁面調(diào)試工具
whistle.sni-callback:自定義證書插件
其它插件例子參見:https://github.com/whistle-plugins
Whistle是如何實現(xiàn)插件功能?主要遵循以下三個設(shè)計原則:
完備性
確保所有功能點都可擴展,如:請求鑒權(quán)、生成證書、獲取抓包、設(shè)置規(guī)則、請求處理等。
穩(wěn)定性
插件內(nèi)部異常不影響其它功能,Whistle的每個插件獨立進(jìn)程,插件與Whistle之間通過HTTP協(xié)議交互。
Whistle是使用npm包pfork來啟動插件進(jìn)程,進(jìn)程間的交換是直接通過Node的http模塊實現(xiàn)的),方便開發(fā)者利用http的生態(tài)開發(fā)插件。
易用性
方便用戶開發(fā)及使用。
開發(fā):結(jié)構(gòu)簡單 (npm包) + 腳手架lack;
使用:安裝npm包即可,用法跟內(nèi)置協(xié)議一樣,且可內(nèi)置交互界面。
有關(guān)插件的更多細(xì)節(jié)參見:https://wproxy.org/whistle/plugins.html
事實上,Whistle除了支持插件擴展,還可以同時作為獨立模塊引入項目使用;除了本地開發(fā)使用,也可以基于Whistle開發(fā)出支持多人使用的開發(fā)聯(lián)調(diào)協(xié)作工具,比如后面會給大家介紹其實現(xiàn)原理的:
基于Whistle實現(xiàn)的多人多環(huán)境遠(yuǎn)程抓包調(diào)試工具
Nohost:https://github.com/Tencent/nohost
基于Whistle和Nohost實現(xiàn)的分布式遠(yuǎn)程抓包調(diào)試工具TDE等
TDE目前只在騰訊內(nèi)部使用,后續(xù)后逐步對外開源。
參考資料:
1.Github倉庫:https://github.com/avwo/whistle
2.官方插件倉庫:https://github.com/whistle-plugins
3.詳細(xì)文檔:https://wproxy.org/whistle/
?作者簡介
吳文斌(avenwu)
騰訊前端高級工程師
騰訊前端高級工程師,Whistle、Nohost作者,目前主要負(fù)責(zé)團隊的Node服務(wù)框架及效率工具的開發(fā)維護工作。
?推薦閱讀
它來了,關(guān)于Golang并發(fā)編程的超詳細(xì)教程!
有的放矢,遠(yuǎn)程操控中實時音視頻的優(yōu)化之道


