HTTP 代理伺服器技術選型之旅

bestswifter發表於2017-12-13

好久不寫部落格了,在元旦到來前水一篇文章,聊聊我在實現代理伺服器的過程中遇到的一些坑,同時祝各位讀者新年快樂。

背景

長期以來,貼吧開發人員多,業務耦合大,需求變化頻繁,因此容易產生 bug。而我所負責的廣告相關業務,和 UI 密切相關,一旦因為某種原因(甚至是被別人改了程式碼)產生了 bug,必然大幅度影響廣告收入。

解決問題的一種方法在於頻繁的測試,既然避免不了程式碼層面的耦合,那總是可以通過定時的檢查來避免問題。所以我們維護了一組核心 case,密切關注最核心的功能。選擇核心 case 實際上是在覆蓋面和測試成本之間的權衡,然而多個 case 有不同的測試步驟,測試效率始終難以提高。

因此,我們的目標是建立一個代理伺服器,能夠在執行時把任何包(包括線上包)的資料改成我希望的樣子。換句話說,這個代理伺服器也可以理解為一個私服,它能夠獲得客戶端的請求資料並作出修改,也可以獲得服務端的響應資料並做修改。

代理伺服器工作模型

在早期版本中,我們選擇了簡單的 HTTP 協議。這種選擇對技術的要求最低,我們自己實現了一個代理伺服器,開啟 socket,監聽埠,然後將客戶端的請求傳送給伺服器,再把伺服器的返回資料傳回客戶端。這種模式也被稱為:“中間人模式”(MITM: Man In The Middle)。

雖然道理很簡單,但實現起來還是有些地方要注意。首先,當 socket 接受資料後,應該新開一個程式/執行緒 進行處理。既然涉及到新的程式/執行緒,就一定要注意它的釋放時機,否則會導致記憶體無限制增加。

其次,對於 socket 來說,它並沒有等待函式,也就是說我無從得知何時有資料可讀,因此這個艱鉅的任務就交給了 select。我們把需要監聽的 socket 物件作為引數傳入其中,函式會一直阻塞,直到有可讀、可寫的物件,或者達到超時時間。

Keep-Alive 欄位可以複用 TCP 連線,是一種常見的 HTTP 協議的優化方式,在 HTTP 1.1 中已經是預設選項。填寫這個欄位後,Server 返回的資料可能是分批次的,這樣能夠改善使用者體驗,但也會增加代理伺服器的實現難度。所以代理伺服器在作為客戶端,向真正伺服器請求資料時,應該刪除這個欄位。

由於整套流程都是自己實現,因此可以比較容易的 HOOK 住上下行資料並做修改。只有注意在接收到全部資料後再做修改即,整個流程可以用下圖簡單表示:

代理伺服器的工作模式

當時做完這一套東西以後,我在團隊內部做了一次分享, 感興趣的讀者可以去 images.bestswifter.com/Proxy 2.key 下載 PPT。

技術選型

短連線

由於長連線基於 TCP,不用每次新建連線,也省略了不必要的 HTTP 報文頭部,效率明顯優於 HTTP。所以各大公司基本上選擇了長連線作為實際生產環境下的連線方式。然而由於不熟悉 WebSocket 協議,並且我們依然支援短連線,所以代理伺服器最終選擇了 HTTP 協議。

要想實現這一點, 就得在應用啟動時,模擬後臺向客戶端傳送一段控制資訊,強制客戶端選擇 HTTP 請求。這樣一來,即使是線上包也可以走代理伺服器。

HTTPS

由於蘋果強制要求使用 HTTPS,雖然已經延期,但也是明年的趨勢。考慮到後續的使用,我們決定對之前實現的代理伺服器進行升級。由於 HTTPS 涉及到請求協議的解析,以及加密解密和證照管理,上述自研方案很難 hold 住。經過一番調研,最後選擇了一個比較知名的開源庫 mitmproxy

Mitmproxy

選擇這個庫最主要的理由是它直接支援 HTTPS,不過沒有中文文件,國內的使用相對來說比較少,所以在接入的時候可能會略花一點時間。

這是一個 python 庫, 首先要安裝 virtualenv,如果本地沒裝的話輸入:

sudo pip install virtualenv
複製程式碼

安裝好了以後,進入 mitmproxy/venv3.5/bin 資料夾輸入:

source ./active
複製程式碼

這樣就可以啟用 virtualenv 環境了。

Hook 指令碼

這個庫可以理解為命令列中可互動版本的 Charles,不過我並不打算用它的這個功能。因為我的需求主要是利用指令碼來 Hook 請求, 所以我選擇了 mitmdump 這個工具。使用它的時候可以指定指令碼:

mitmdump -s "xxx.py"
複製程式碼

指令碼也很簡單,我們可以重寫 requeest 或者 receive 函式:

def request(flow):
flow.response.content = "<p>hello world</p>"
複製程式碼

執行指令碼以後,把手機的代理設為本機 ip 地址,埠號改為 8080,然後用手機瀏覽器開啟 mitm.it/,如果一切配置順利,你會看到證照的安裝介面。

安裝好證照後,用手機訪問任何一個網站(包括 HTTPS),你應該都會看到一個小小的 hello world,至此所有的配置就完成了。

bug 修改

這個開源庫有一個很嚴重的 bug,在解析 multipart 型別的資料時可能會發生。它使用了 splitline 方法來分割換行符,然而如果資料中有 \n 的話,就會因此丟失。很不幸的是,很多 protobuf 編碼後的資料都有 \n,一旦丟失就會導致解析失敗。

如果你不幸遇到了和我一樣的坑,可以把相關程式碼改成我的版本:

for i in content.split(b"--" + boundary):
parts = i.split(b'\r\n\r\n', 2)
if len(parts) > 1 and parts[0][0:2] != b"--":
match = rx.search(parts[0])
if match:
key = match.group(1)
value = parts[1][0:len(parts[1])-2] # Remove last \r\n
r.append((key, value))
複製程式碼

More

到了這一步,基本上已經成功實現支援 HTTPS 的代理伺服器了。後續要處理的可能就是解析 protobuf,完善業務程式碼等等瑣碎的事情,只要小心謹慎,基本上不會有問題。

相關文章