HTTP 代理的原理和實現
HTTP 代理可以說是每個開發者都繞不開的工具。幾乎每天都會使用,但你真的瞭解 HTTP 代理的原理嗎?
說明:這裏討論的 HTTP 代理是指 HTTP Proxy Server,具體是正向 HTTP 代理服務端的原理和實現。
想了解 HTTP 代理的原理,最嚴謹的方法是閱讀 RFC 文檔,但這同時也是最困難的方式。今天,我將介紹一種更直觀的學習技巧。
從名字上就可以看出,HTTP 代理基於 HTTP 協議,而 HTTP 協議運行在 TCP 之上。我們可以直接監聽某個 TCP 端口,模擬 HTTP 代理服務端,同時使用 curl
命令通過代理訪問網站,觀察具體數據,再決定如何實現這個 HTTP 代理服務端。
代理 HTTP 請求
步驟 1:使用 ncat
監聽端口 7788。
ncat -lvp 7788
步驟 2:執行 curl
命令,通過代理訪問百度的 HTTP 協議。
curl http://baidu.com/123?a=1 --proxy 127.0.0.1:7788
步驟 3:查看 ncat
的輸出:
Ncat: Connection from 127.0.0.1:58458.
GET http://baidu.com/123?a=1 HTTP/1.1
Host: baidu.com
User-Agent: curl/8.7.1
Accept: */*
Proxy-Connection: Keep-Alive
第一行是 ncat
輸出的信息,可以忽略。後面的內容完全是 HTTP 協議的數據。
由此可見,HTTP 代理實際上是一個 “傳話筒”:客戶端將完整的請求協議發送給 HTTP 代理,代理轉發給目標服務器,並將響應返回給客戶端。
接下來,我們用 Golang 實現這種代理功能。
使用 Golang 實現 HTTP 代理
1. 搭建 HTTP 服務
以下代碼使用 Golang 官方庫啓動 HTTP 服務,並通過一個 handler
處理所有請求:
package main
import (
"io"
"log"
"net/http"
)
func main() {
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
handleHttp(w, r)
})
err := http.ListenAndServe(":7788", handler)
if err != nil {
log.Fatal(err)
}
}
2. 實現代理邏輯
接下來實現代理的核心功能:
func handleHttp(w http.ResponseWriter, r *http.Request) {
// 向目標服務器發送請求
resp, err := http.DefaultTransport.RoundTrip(r)
if err != nil {
http.Error(w, err.Error(), http.StatusServiceUnavailable)
return
}
defer resp.Body.Close()
// 將響應頭和響應體寫入客戶端
for k, vv := range resp.Header {
for _, v := range vv {
w.Header().Add(k, v)
}
}
w.WriteHeader(resp.StatusCode)
_, _ = io.Copy(w, resp.Body)
}
測試結果
運行代碼後,再次執行 curl
命令:
curl http://baidu.com/123?a=1 --proxy 127.0.0.1:7788
輸出:
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<html><head>
<title>302 Found</title>
</head><body>
<h1>Found</h1>
<p>The document has moved <a href="http://www.baidu.com/search/error.html">here</a>.</p>
</body></html>
可以看到,HTTP 代理已經正確轉發了請求。但目前只支持 HTTP 請求,接下來我們繼續支持 HTTPS 協議。
代理 HTTPS 請求
步驟 1:使用 ncat
監聽端口 7788。
ncat -lvp 7788
步驟 2:執行 curl
命令,通過代理訪問百度的 HTTPS 協議。
curl https://baidu.com/123?a=1 --proxy 127.0.0.1:7788
步驟 3:查看 ncat
的輸出:
CONNECT baidu.com:443 HTTP/1.1
Host: baidu.com:443
User-Agent: curl/8.7.1
Proxy-Connection: Keep-Alive
在之前的 HTTP 請求中,我們能夠獲取到請求方法、路徑、參數等完整信息。然而,在當前的 HTTPS 請求中,僅能看到 CONNECT
方法以及目標服務器的主機名和端口,其餘信息完全無法獲取。
爲什麼會出現這樣的情況?我們需要參考 RFC 定義。
在 RFC 9110 章節 9.3.6 - CONNECT 方法: https://www.rfc-editor.org/rfc/rfc9110.html#name-connect
The CONNECT method requests that the recipient establish a tunnel to the destination origin server identified by the request target and, if successful, thereafter restrict its behavior to blind forwarding of data, in both directions, until the tunnel is closed. Tunnels are commonly used to create an end-to-end virtual connection, through one or more proxies, which can then be secured using TLS (Transport Layer Security[TLS13]).
翻譯如下:
CONNECT 方法請求接收者建立到由請求目標標識的目的地源服務器的隧道,如果成功,則將其行爲限制爲雙向盲目轉發數據,直到隧道關閉。隧道通常用於通過一個或多個代理創建端到端虛擬連接,然後可以使用 TLS(傳輸層安全性)保護該連接 [TLS13]。
使用 Golang 實現隧道功能
func handleTunnel(w http.ResponseWriter, r *http.Request) {
remoteConn, err := net.DialTimeout("tcp", r.Host, 5*time.Second)
if err != nil {
http.Error(w, err.Error(), http.StatusServiceUnavailable)
return
}
defer remoteConn.Close()
// 響應客戶端隧道建立成功
w.WriteHeader(http.StatusOK)
// 獲取客戶端連接
hijacker, ok := w.(http.Hijacker)
if !ok {
http.Error(w, "hijack not supported", http.StatusInternalServerError)
return
}
clientConn, _, err := hijacker.Hijack()
if err != nil {
http.Error(w, err.Error(), http.StatusServiceUnavailable)
return
}
defer clientConn.Close()
// 雙向拷貝數據
go bidirectionalCopy(remoteConn, clientConn)
}
輔助函數:
func bidirectionalCopy(conn1, conn2 net.Conn) {
done := make(chanstruct{})
gofunc() {
_, _ = io.Copy(conn1, conn2)
done <- struct{}{}
}()
gofunc() {
_, _ = io.Copy(conn2, conn1)
done <- struct{}{}
}()
<-done
<-done
}
完整代碼
最終實現如下:
package main
import (
"io"
"log"
"net"
"net/http"
"time"
)
func main() {
var handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodConnect {
handleTunnel(w, r)
} else {
handleHttp(w, r)
}
})
err := http.ListenAndServe(":7788", handler)
if err != nil {
log.Fatal(err)
}
}
func handleHttp(w http.ResponseWriter, r *http.Request) {
// 向下遊服務器發送請求
resp, err := http.DefaultTransport.RoundTrip(r)
if err != nil {
http.Error(w, err.Error(), http.StatusServiceUnavailable)
return
}
defer resp.Body.Close()
// 將響應頭和響應體寫入到客戶端
for k, vv := range resp.Header {
for _, v := range vv {
w.Header().Add(k, v)
}
}
w.WriteHeader(resp.StatusCode)
_, _ = io.Copy(w, resp.Body)
}
func handleTunnel(w http.ResponseWriter, r *http.Request) {
remoteConn, err := net.DialTimeout("tcp", r.Host, 5*time.Second)
if err != nil {
http.Error(w, err.Error(), http.StatusServiceUnavailable)
return
}
defer remoteConn.Close()
// 響應客戶端隧道建立成功
w.WriteHeader(http.StatusOK)
// 獲取客戶端長連接
hijacker, ok := w.(http.Hijacker)
if !ok {
http.Error(w, "hijack not supported", http.StatusInternalServerError)
return
}
centralConn, _, err := hijacker.Hijack()
if err != nil {
http.Error(w, err.Error(), http.StatusServiceUnavailable)
}
// 雙向拷貝數據
go bidirectionalCopy(remoteConn, centralConn)
}
func bidirectionalCopy(conn1, conn2 net.Conn) {
done := make(chanstruct{})
gofunc() {
_, _ = io.Copy(conn1, conn2)
done <- struct{}{}
}()
gofunc() {
_, _ = io.Copy(conn2, conn1)
done <- struct{}{}
}()
<-done
<-done
_ = conn1.Close()
_ = conn2.Close()
}
這裏我們只使用了不到 100 行 Golang 代碼就實現了一個簡單的 HTTP 代理服務器,進階用法還有很多,例如認證、攔截指定請求,修改請求和響應等等,你最希望看到什麼內容?請留言。
快來動手,用你最擅長的編程語言實現一個簡單的 HTTP 代理服務器吧~
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/y1-tRCR3KiTEAMyhTKEJdg