Whistle 實現原理 —— 從 0 開始實現一個抓包工具


導語 通過這篇文章可以大致了解 Whistle 的實現原理,並學習如何實現一個簡單的抓包調試工具。

項目 Github 地址:https://github.com/avwo/whistle

Whistle 是基於 Node.js 實現的跨平台 Web 抓包調試(HTTP)代理,主要功能:

  1. 實時抓包:支持 HTTP、HTTPS、HTTP2、WebSocket、TCP 等常見 Web 請求的抓包;

  2. 修改請求響應:與一般抓包調試工具采用斷點的方式不同,Whistle 采用類似系統 host 的配置規則方式;

  3. 擴展功能:支持通過 Node 編寫插件,或作為獨立 NPM 包引入項目兩種擴展方式。

 

 

本文將從最基本的概念開始逐步講解 Whistle 功能,包含以下內容:

  1. 什么是 HTTP 代理

  2. 實現簡單 HTTP 代理

  3. 完整 HTTP 代理架構(Whistle)

  4. 具體實現原理

  5. 參考資料

1. 什么是 HTTP 代理

 

 

代理是客戶端到服務端的中轉服務,其中:

  1. 不經過代理的請求:客戶端和服務端直接建立連接后,即可開始交換數據。

  2. 經過代理的請求:客戶端不與服務端直接建立連接,而是先跟代理建立連接后,將目標服務器的地址發送給代理,通過代理再跟服務端建立連接,這里如果代理服務為 HTTP Server,則稱為 HTTP 代理。

接下來看下客戶端如何將目標服務器地址傳給 HTTP 代理,以及 HTTP 代理如何跟目標服務器建立連接。

2. 實現簡單 HTTP 代理

先看一個用 Node.js 實現的最簡單 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 一定存在,完整實現參見 Whistle
const { host, port } = getHostPort(req.headers.host, defaultPort);
return {
hostname: host, // 指定請求域名,用於通過 DNS 獲取服務器 IP 及設置請求頭 host 字段
port, // 指定服務器端口
path: req.url || '/',
method: req.method,
headers: req.headers,
rejectUnauthorized: false, // 給 HTTPS 請求用的,HTTP 請求會自動忽略
};
};

// 簡單處理,出錯直接斷開,完整實現邏輯參考 Whistle
const handleClose = (req, res) => {
const destroy = (err) => { // 及時關閉無用的連接,防止內存泄露
req.destroy();
res && res.destroy();
};
res && res.on('error', destroy);
req.on('error', destroy);
req.once('close', destroy);
};


/****************** 服務代碼 ******************/
const server = http.createServer();
// 處理 HTTP 請求
server.on('request', (req, res) => {
// 與服務端建立連接,透傳客戶端請求及服務端響應內容
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) => {
// 與服務端建立連接,透傳客戶端請求及服務端響應內容
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);

上述代碼實現了一個具有轉發請求功能的 HTTP 代理,從代碼可知 HTTP 代理就是一個普通的 HTTP Server,並監聽 request 和 connect 這兩個事件,客戶端會通過這兩個事件將目標服務器地址傳過來,其中:

  1. request:一般普通 HTTP 會通過該事件將目標服務器地址傳過來。

  2. connect:一般非 HTTP 請求,如 HTTPS、HTTP/2、WebSocket、TCP 等會通過該事件將目標服務器地址傳過來,觸發該事件的代理請求也叫隧道代理

可以在事件里面的 req.url 或 req.headers.host 獲取目標服務器的地址(host:port),再跟該服務器地址建立連接並將結果通過 HTTP 響應的方式返回給客戶端,這里只是實現代理的最基本功能,完整的 HTTP 除了請求轉發,至少應該還有:

  1. 查看實時抓包;

  2. 解析 HTTPS 請求;

  3. 修改請求響應內容;

  4. 擴展功能。

下面以 Whistle 為例看下如何用 Node.js 實現一個完整的 HTTP 代理。

3. 完整 HTTP 代理架構(Whistle)

 

 

主要分五個模塊:

  1. 請求接入模塊

  2. 隧道代理模塊

  3. 處理 HTTP 請求模塊

  4. 規則管理模塊

  5. 插件管理模塊

4. 具體實現原理

下面分別看下這五個模塊具體是怎么實現的。

4.1 請求接入模塊

 

 

所有請求先會經過請求接入模塊,Whistle 支持四種請求接入方式:

  1. HTTP & HTTPS 直接請求:相當於配 hosts 或 DNS 的方式,將請求轉發到 Whistle;

  2. HTTP 代理:Whistle 默認接入方式,即配系統代理或通過瀏覽器插件配 HTTP 代理的方式;

  3. HTTPS 代理:在 HTTP 代理之上對代理請求進行了加密,即 HTTPS Server,可以通過指定證書轉成 HTTP 代理請求;

  4. Socks5 代理:利用 npm 包 socksv5 轉成普通的 TCP 請求,並將 TCP 請求轉成隧道代理請求。

基實現原理是:將所有請求都轉成 HTTP 代理的 隧道代理請求 或 HTTP 請求,再解析 隧道代理請求 轉成 HTTP 請求。

如何將普通 tcp 請求轉成隧道代理請求參見: lack-proxy

下面看下如何從 隧道代理請求 解析出 HTTP 請求。

4.2 隧道代理模塊

 

 

關鍵點(HTTP 請求也可以走隧道代理):

  1. 通過匹配的全局規則判斷是否要解析隧道代理請求,如果不解析,則當成普通 TCP 請求處理;

  2. 如果需要,則通過 socket.once('data', handler) 讀取請求點第一幀數據;

  3. 將第一幀數據轉成字符串,通過正則 /^(\w+)\s+(\S+)\s+HTTP\/1.\d$/mi 是否是 HTTP 請求?如果是 HTTP 請求,再判斷下是否是 CONNECT 請求,即隧道代理請求(隧道代理請求也可以代理隧道代理請求),如果是,則轉回隧道代理方法處理,如果不是,則轉到 HTTP 請求模塊處理;

  4. 如果不是 HTTP 請求,則當成 HTTPS 請求處理,這里需要用到中間人的方式將 HTTPS 請求轉成 HTTP 請求;

  5. Whistle 會先按以下順序獲取請求證書:

    • 通過匹配的插件獲取(可以通過規則 sniCallback://plugin 指定加載證書的插件);

    • 通過啟動參數 -z certDir 指定目錄或 ~/.WhistleAppData/custom_certs 加載的自定義證書;

    • 如果沒有上述兩種自動證書,Whistle 會自動生成一個默認的證書。

  6. 獲取到證書后,再利用該證書啟動一個 HTTPS Server,將 HTTPS 請求轉成 HTTP 請求交給 HTTP 請求模塊處理。

4.3 HTTP 請求處理模塊

 

 

HTTP 請求處理可以分兩個階段:

  1. 請求階段:

    • 匹配全局規則;

    • 如果規則里類似 whistle.xxx 的規則,執行對應插件鈎子,獲取插件規則並跟匹配的全局規則合並;

    • 執行規則、記錄狀態並請求到指定服務。

  2. 響應階段:

    • 執行匹配插件的鈎子,獲取插件規則並跟匹配的全局規則合並;

    • 執行規則、記錄狀態並請求返回客戶端。

4.4 規則管理

與傳統抓包調試代理 采用斷點修改請求響應數據不同,Whistle 采用配置規則的方式修改請求響應,采用配置方式的好處是操作簡單,且可以將操作持久化存儲及共享給他人,先看幾個例子:

 

 

Whistle 的規則管理主要兩個功能:

  1. 解析規則

  2. 匹配規則

解析規則

Whistle 有兩類規則:

  1. 全局規則(公共規則),所有請求都會嘗試匹配的規則,由以下規則組成:

  2. 插件規則(私有規則),即進入插件的請求(匹配的全局規則里有 whistle.xxx 協議)才會匹配到的規則,由以下規則組成:

    文檔:https://wproxy.org/whistle/plugins.html

    • 插件 reqRulesServer 等 hooks 動態返回;

    • 插件根目錄 _rules.txt 等文件配置的靜態規則;

匹配規則

Whistle 規則的完整結構為:

 

 

文檔:https://wproxy.org/whistle/mode.html

4.5 插件管理

 

 

Whistle 插件的功能很多,不僅具備 Node 的所有能力,且可以操作 Whistle 的所有規則(理論上可以基於插件實現一個 Whistle),主要用來做以下事情:

  1. 鑒權功能

  2. 提供 UI 交互界面

  3. 作為請求 Server(直接響應或轉發並修改請求響應)

  4. 統計請求信息(查看上報 / 打點數據等)

  5. 設置規則(動態,靜態,全局及私有規則)

  6. 獲取抓包數據

  7. 編解碼請求響應數據流(pipe stream 功能)

  8. 擴展界面右鍵菜單(如:分享抓包數據)

  9. 保存並同步 Rules & Values 數據

  10. 自定義 HTTPS 請求的證書

比如:

  1. whistle.script:實現通過自定義腳本動態設置規則

  2. whistle.vase:提供靈活強大的 mock 能力

  3. whistle.inspect:方便快速注入 vConsole、eruda 等頁面調試工具

  4. whistle.sni-callback:自定義證書插件

其它插件例子參見:https://github.com/whistle-plugins

Whistle 是如何實現插件功能?主要遵循以下三個設計原則:

  1. 完備性:

    確保所有功能點都可擴展,如:請求鑒權、生成證書、獲取抓包、設置規則、請求處理等。

  2. 穩定性:

    插件內部異常不影響其它功能,Whistle 的每個插件獨立進程,插件與 Whistle 之間通過 HTTP 協議交互。

    Whistle 是使用 npm 包 pfork 來啟動插件進程,進程間的交換是直接通過 Node 的 http 模塊實現的),方便開發者利用 http 的生態開發插件。

  3. 易用性:

    方便用戶開發及使用。

  4. 開發:結構簡單 (npm 包) + 腳手架 lack

    使用:安裝 npm 包即可,用法跟內置協議一樣,且可內置交互界面。

有關插件的更多細節參見:https://wproxy.org/whistle/plugins.html

事實上,Whistle 除了支持插件擴展,還可以同時作為獨立模塊引入項目使用;除了本地開發使用,也可以基於 Whistle 開發出支持多人使用的開發聯調協作工具,比如后面會給大家介紹其實現原理的:

  1. 基於 Whistle 實現的多人多環境遠程抓包調試工具。

    Nohost:https://github.com/Tencent/nohost

  2. 基於 Whistle 和 Nohost 實現的分布式遠程抓包調試工具 TDE 等等。

    TDE 目前只在騰訊內部使用,后續后逐步對外開源。

5. 參考資料

  1. Github 倉庫:https://github.com/avwo/whistle

  2. 官方插件倉庫:https://github.com/whistle-plugins

  3. 詳細文檔:https://wproxy.org/whistle/

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM