1. 程式人生 > 其它 >HTTP 代理原理及實現

HTTP 代理原理及實現

普通代理

第一種 Web 代理原理特別簡單:

下面這張圖片來自於《HTTP 權威指南》,直觀地展示了上述行為:

假如我通過代理訪問 A 網站,對於 A 來說,它會把代理當做客戶端,完全察覺不到真正客戶端的存在,這實現了隱藏客戶端 IP 的目的。當然代理也可以修改 HTTP 請求頭部,通過X-Forwarded-IP這樣的自定義頭部告訴服務端真正的客戶端 IP。但伺服器無法驗證這個自定義頭部真的是由代理新增,還是客戶端修改了請求頭,所以從 HTTP 頭部欄位獲取 IP 時,需要格外小心。

給瀏覽器顯式的指定代理,需要手動修改瀏覽器或作業系統相關設定,或者指定 PAC(Proxy Auto-Configuration,自動配置代理)檔案自動設定,還有些瀏覽器支援 WPAD(Web Proxy Autodiscovery Protocol,Web 代理自動發現協議)。顯式指定瀏覽器代理這種方式一般稱之為正向代理,瀏覽器啟用正向代理後,會對 HTTP 請求報文做一些修改,來規避老舊代理伺服器的一些問題,

還有一種情況是訪問 A 網站時,實際上訪問的是代理,代理收到請求報文後,再向真正提供服務的伺服器發起請求,並將響應轉發給瀏覽器。這種情況一般被稱之為反向代理,它可以用來隱藏伺服器 IP 及埠。一般使用反向代理後,需要通過修改 DNS 讓域名解析到代理伺服器 IP,這時瀏覽器無法察覺到真正伺服器的存在,當然也就不需要修改配置了。反向代理是 Web 系統最為常見的一種部署方式,例如本部落格就是使用 Nginx 的proxy_pass功能將瀏覽器請求轉發到背後的 Node.js 服務。

瞭解完第一種代理的基本原理後,我們用 Node.js 實現一下它。只包含核心邏輯的程式碼如下:

以上程式碼執行後,會在本地8888

埠開啟 HTTP 代理服務,這個服務從請求報文中解析出請求 URL 和其他必要引數,新建到服務端的請求,並把代理收到的請求轉發給新建的請求,最後再把服務端響應返回給瀏覽器。修改瀏覽器的 HTTP 代理為127.0.0.1:8888後再訪問 HTTP 網站,代理可以正常工作。

但是,使用我們這個代理服務後,HTTPS 網站完全無法訪問,這是為什麼呢?答案很簡單,這個代理提供的是 HTTP 服務,根本沒辦法承載 HTTPS 服務。那麼是否把這個代理改為 HTTPS 就可以了呢?顯然也不可以,因為這種代理的本質是中間人,而 HTTPS 網站的證書認證機制是中間人劫持的剋星。普通的 HTTPS 服務中,服務端不驗證客戶端的證書,中間人可以作為客戶端與服務端成功完成 TLS 握手;但是中間人沒有證書私鑰,無論如何也無法偽造成服務端跟客戶端建立 TLS 連線。當然如果你擁有證書私鑰,代理證書對應的 HTTPS 網站當然就沒問題了。

HTTP 抓包神器 Fiddler 的工作原理也是在本地開啟 HTTP 代理服務,通過讓瀏覽器流量走這個代理,從而實現顯示和修改 HTTP 包的功能。如果要讓 Fiddler 解密 HTTPS 包的內容,需要先將它自帶的根證書匯入到系統受信任的根證書列表中。一旦完成這一步,瀏覽器就會信任 Fiddler 後續的「偽造證書」,從而在瀏覽器和 Fiddler、Fiddler 和服務端之間都能成功建立 TLS 連線。而對於 Fiddler 這個節點來說,兩端的 TLS 流量都是可以解密的。

如果我們不匯入根證書,Fiddler 的 HTTP 代理還能代理 HTTPS 流量麼?實踐證明,不匯入根證書,Fiddler 只是無法解密 HTTPS 流量,HTTPS 網站還是可以正常訪問。這是如何做到的,這些 HTTPS 流量是否安全呢?這些問題將在下一節揭曉。

隧道代理

第二種 Web 代理的原理也很簡單:

下面這張圖片同樣來自於《HTTP 權威指南》,直觀地展示了上述行為:

假如我通過代理訪問 A 網站,瀏覽器首先通過 CONNECT 請求,讓代理建立一條到 A 網站的 TCP 連線;一旦 TCP 連線建好,代理無腦轉發後續流量即可。所以這種代理,理論上適用於任意基於 TCP 的應用層協議,HTTPS 網站使用的 TLS 協議當然也可以。這也是這種代理為什麼被稱為隧道的原因。對於 HTTPS 來說,客戶端透過代理直接跟服務端進行 TLS 握手協商金鑰,所以依然是安全的,下圖中的抓包資訊顯示了這種場景:

對於 CONNECT 請求來說,只是用來讓代理建立 TCP 連線,所以只需要提供伺服器域名及埠即可,並不需要具體的資源路徑。代理收到這樣的請求後,需要與服務端建立 TCP 連線,並響應給瀏覽器這樣一個 HTTP 報文:

瀏覽器收到了這個響應報文,就可以認為到服務端的 TCP 連線已經打通,後續直接往這個 TCP 連線寫協議資料即可。通過 Wireshark 的 Follow TCP Steam 功能,可以清楚地看到瀏覽器和代理之間的資料傳遞:

可以看到,瀏覽器建立到服務端 TCP 連線產生的 HTTP 往返,完全是明文,這也是為什麼 CONNECT 請求只需要提供域名和埠:如果傳送了完整 URL、Cookie 等資訊,會被中間人一覽無餘,降低了 HTTPS 的安全性。HTTP 代理承載的 HTTPS 流量,應用資料要等到 TLS 握手成功之後通過 Application Data 協議傳輸,中間節點無法得知用於流量加密的 master-secret,無法解密資料。而 CONNECT 暴露的域名和埠,對於普通的 HTTPS 請求來說,中間人一樣可以拿到(IP 和埠很容易拿到,請求的域名可以通過 DNS Query 或者 TLS Client Hello 中的 Server Name Indication 拿到),所以這種方式並沒有增加不安全性。

瞭解完原理後,再用 Node.js 實現一個支援 CONNECT 的代理也很簡單。核心程式碼如下:

以上程式碼執行後,會在本地8888埠開啟 HTTP 代理服務,這個服務從 CONNECT 請求報文中解析出域名和埠,建立到服務端的 TCP 連線,並和 CONNECT 請求中的 TCP 連線串起來,最後再響應一個 Connection Established 響應。修改瀏覽器的 HTTP 代理為127.0.0.1:8888後再訪問 HTTPS 網站,代理可以正常工作。

最後,將兩種代理的實現程式碼合二為一,就可以得到全功能的 Proxy 程式了,全部程式碼在 50 行以內(當然異常什麼的基本沒考慮,這是我部落格程式碼的一貫風格):

需要注意的是,大部分瀏覽器顯式配置了代理之後,只會讓 HTTPS 網站走隧道代理,這是因為建立隧道需要耗費一次往返,能不用就儘量不用。但這並不代表 HTTP 請求不能走隧道代理,我們用 Node.js 寫段程式驗證下(先執行前面的代理服務)

這段程式碼執行完,結果如下:

可以看到,通過 CONNECT 讓代理開啟到目標伺服器的 TCP 連線,用來承載 HTTP 流量也是完全沒問題的。

最後,HTTP 的認證機制可以跟代理配合使用,使得必須輸入正確的使用者名稱和密碼才能使用代理,這部分內容比較簡單,這裡略過。

我們知道 TLS 有三大功能:內容加密、身份認證和資料完整性。其中內容加密依賴於金鑰協商機制;資料完整性依賴於 MAC(Message authentication code)校驗機制;而身份認證則依賴於證書認證機制。一般作業系統或瀏覽器會維護一個受信任根證書列表,包含在列表之中的證書,或者由列表中的證書籤發的證書都會被客戶端信任。

提供 HTTPS 服務的證書可以自己生成,然後手動加入到系統根證書列表中。但是對外提供服務的 HTTPS 網站,不可能要求每個使用者都手動匯入你的證書,所以更常見的做法是向 CA(Certificate Authority,證書頒發機構)申請。根據證書的不同級別,CA 會進行不同級別的驗證,驗證通過後 CA 會用他們的證書籤髮網站證書,這個過程通常是收費的(有免費的證書,最近免費的Let's Encrypt也很火,這裡不多介紹)。由於 CA 使用的證書都是由廣泛內建在各系統中的根證書籤發,所以從 CA 獲得的網站證書會被絕大部分客戶端信任。

通過 CA 申請證書很簡單,本文為了方便演示,採用自己簽發證書的偷懶辦法。現在廣泛使用的證書是 x509.v3 格式,使用以下命令可以建立:

第二行命令執行後,需要填寫一些證書資訊。需要注意的是Common Name一定要填寫後續提供 HTTPS 服務的域名或 IP。例如你打算在本地測試,Common Name可以填寫127.0.0.1。證書建立好之後,再將public.crt新增到系統受信任根證書列表中。為了確保新增成功,可以用瀏覽器驗證一下:

接著,可以改造之前的 Node.js 程式碼了,需要改動的地方不多:

可以看到,除了將http.createServer換成https.createServer,增加證書相關配置之外,這段程式碼沒有任何改變。這也是引入 TLS 層的妙處,應用層不需要任何改動,就能獲得諸多安全特性。

執行服務後,只需要將瀏覽器的代理設定為HTTPS 127.0.0.1:8888即可,功能照舊。這樣改造,只是將瀏覽器到代理之間的流量升級為了 HTTPS,代理自身邏輯、與服務端的通訊方式,都沒有任何變化。

最後,還是寫段 Node.js 程式碼驗證下這個 HTTPS 代理服務:

轉自https://zhuanlan.zhihu.com/p/452035456