這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。![](https://raw.githubusercontent.com/studygolang/gctt-images/master/http-s-proxy/header.jpeg)我們的目標是實現一個能處理 HTTP 和 HTTPS 的[Proxy 伺服器](https://en.wikipedia.org/wiki/Proxy_server)。代理 HTTP 要求的過程其實就是一個解析請求、將該請求轉寄到目的伺服器、讀取目的伺服器響應並將其傳回原用戶端的過程。這個過程只需要內建的 HTTP 伺服器和用戶端([net/http](https://golang.org/pkg/net/http/))就能實現。HTTPS 的不同之處在於使用了名為 “HTTP CONNECT 隧道”的技術。首先,用戶端用 HTTP CONNECT 方法發送請求以建立到目的伺服器的隧道。當這個由兩個 TCP 串連組成的隧道就緒,用戶端就開始與目的伺服器的定期握手以建立安全的串連,之後就是發送請求與接收響應。## 認證我們的代理是一個 HTTPS 伺服器(當使用 `--proto https` 參數),因而需要認證和私密金鑰。我們使用自我簽署憑證。用如下指令碼產生:```bash#!/usr/bin/env bashcase `uname -s` in Linux*) sslConfig=/etc/ssl/openssl.cnf;; Darwin*) sslConfig=/System/Library/OpenSSL/openssl.cnf;;esacopenssl req \ -newkey rsa:2048 \ -x509 \ -nodes \ -keyout server.key \ -new \ -out server.pem \ -subj /CN=localhost \ -reqexts SAN \ -extensions SAN \ -config <(cat $sslConfig \ <(printf '[SAN]\nsubjectAltName=DNS:localhost')) \ -sha256 \ -days 3650```需要讓你的作業系統信任該認證。OS X 系統可以用 Keychain Access 來處理,參見 https://tosbourn.com/getting-os-x-to-trust-self-signed-ssl-certificates## HTTP我們用[內建的 HTTP 伺服器和用戶端](https://golang.org/pkg/net/http/)實現對 HTTP 的支援。“代理”在其中的角色是處理 HTTP 要求、轉寄該請求到目的伺服器並將響應返回到原用戶端。![](https://raw.githubusercontent.com/studygolang/gctt-images/master/http-s-proxy/http_proxy.png)## HTTP CONNECT 隧道假設用戶端與伺服器可能使用 HTTPS 或 WebSocket 方式與伺服器互動,用戶端會發現正在使用代理。在有些情境下是無法使用簡單的 HTTP 要求/響應流的,例如用戶端需要與伺服器建立安全連線(HTTPS)或想使用其他基於 TCP 串連的協議(如 WebSockets)的情況。此時,該 HTTP [CONNECT](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/CONNECT) 方法出場了。HTTP CONNECT 方法告知Proxy 伺服器與目的伺服器建立 TCP 串連,並在串連成功建立後代理起止於用戶端的 TCP 流。這種方式,Proxy 伺服器不會終止 SSL 串連,而是簡單地在用戶端和目的伺服器之間傳遞資料。所以用戶端和目的伺服器之間的串連是安全的。![](https://raw.githubusercontent.com/studygolang/gctt-images/master/http-s-proxy/http_connect_tunneling.png)## 實現```gopackage mainimport ( "crypto/tls" "flag" "io" "log" "net" "net/http" "time")func handleTunneling(w http.ResponseWriter, r *http.Request) { dest_conn, err := net.DialTimeout("tcp", r.Host, 10*time.Second) if err != nil { http.Error(w, err.Error(), http.StatusServiceUnavailable) return } w.WriteHeader(http.StatusOK) hijacker, ok := w.(http.Hijacker) if !ok { http.Error(w, "Hijacking not supported", http.StatusInternalServerError) return } client_conn, _, err := hijacker.Hijack() if err != nil { http.Error(w, err.Error(), http.StatusServiceUnavailable) } go transfer(dest_conn, client_conn) go transfer(client_conn, dest_conn)}func transfer(destination io.WriteCloser, source io.ReadCloser) { defer destination.Close() defer source.Close() io.Copy(destination, source)}func handleHTTP(w http.ResponseWriter, req *http.Request) { resp, err := http.DefaultTransport.RoundTrip(req) if err != nil { http.Error(w, err.Error(), http.StatusServiceUnavailable) return } defer resp.Body.Close() copyHeader(w.Header(), resp.Header) w.WriteHeader(resp.StatusCode) io.Copy(w, resp.Body)}func copyHeader(dst, src http.Header) { for k, vv := range src { for _, v := range vv { dst.Add(k, v) } }}func main() { var pemPath string flag.StringVar(&pemPath, "pem", "server.pem", "path to pem file") var keyPath string flag.StringVar(&keyPath, "key", "server.key", "path to key file") var proto string flag.StringVar(&proto, "proto", "https", "Proxy protocol (http or https)") flag.Parse() if proto != "http" && proto != "https" { log.Fatal("Protocol must be either http or https") } server := &http.Server{ Addr: ":8888", Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method == http.MethodConnect { handleTunneling(w, r) } else { handleHTTP(w, r) } }), // Disable HTTP/2. TLSNextProto: make(map[string]func(*http.Server, *tls.Conn, http.Handler)), } if proto == "http" { log.Fatal(server.ListenAndServe()) } else { log.Fatal(server.ListenAndServeTLS(pemPath, keyPath)) }}```> 以上展示的代碼並非生產層級的解決方案。缺少對 [hop-by-hop 頭資訊](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers#hbh)的處理,在兩個串連或由 `net/http` 暴露的服務連接埠之間複製資料的過程中沒有設定到期時間(更多資訊見:["The complete guide to Go net/http timeouts"](https://blog.cloudflare.com/the-complete-guide-to-golang-net-http-timeouts/))(譯註:中譯文[ Go net/http 逾時機制完全手冊](https://studygolang.com/articles/9339))。我們的伺服器在接收請求的時候,會在處理 HTTP 要求和 HTTP CONNECT 隧道請求之間二選一,通過如下代碼實現:```gohttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method == http.MethodConnect { handleTunneling(w, r) } else { handleHTTP(w, r) }})```處理 HTTP 要求的 handleHTTP 函數如其名所示,我們將重點放在處理隧道的 handleTunneling 函數上。handleTunneling 函數的第一部分設定到目的伺服器的串連:```godest_conn, err := net.DialTimeout("tcp", r.Host, 10*time.Second)if err != nil { http.Error(w, err.Error(), http.StatusServiceUnavailable) return}w.WriteHeader(http.StatusOK)```緊接的是由 HTTP 伺服器維護的劫持串連的部分:```gohijacker, ok := w.(http.Hijacker)if !ok { http.Error(w, "Hijacking not supported", http.StatusInternalServerError) return}client_conn, _, err := hijacker.Hijack()if err != nil { http.Error(w, err.Error(), http.StatusServiceUnavailable)}```[Hijacker 介面](https://golang.org/pkg/net/http/#Hijacker) 允許接管串連。之後由發起者負責管理該串連(HTTP 不再處理)。一旦我們有兩個 TCP 串連(用戶端到代理,代理到目的伺服器),就需要啟動隧道:```gogo transfer(dest_conn, client_conn)go transfer(client_conn, dest_conn)```兩個 goroutine 中資料朝兩個方向複製:從用戶端到目的伺服器及其反方向。## 測試可以在 Chrome 中使用如下配置來測試我們的代理:```go> Chrome --proxy-server=https://localhost:8888```或者用 [Curl](https://github.com/curl/curl):```> curl -Lv --proxy https://localhost:8888 --proxy-cacert server.pem https://google.com```> curl 需要原生支援 HTTPS-proxy(在 7.52.0 引入)。## HTTP/2我們的伺服器中,刻意移除對 HTTP/2 的支援,因為無法實現劫持。更多資訊參見 [#14797](https://github.com/golang/go/issues/14797#issuecomment-196103814).
via: https://medium.com/@mlowicki/http-s-proxy-in-golang-in-less-than-100-lines-of-code-6a51c2f2c38c
作者:Michał Łowicki 譯者:dongkui0712 校對:rxcai
本文由 GCTT 原創編譯,Go語言中文網 榮譽推出
本文由 GCTT 原創翻譯,Go語言中文網 首發。也想加入譯者行列,為開源做一些自己的貢獻嗎?歡迎加入 GCTT!
翻譯工作和譯文發表僅用於學習和交流目的,翻譯工作遵照 CC-BY-NC-SA 協議規定,如果我們的工作有侵犯到您的權益,請及時聯絡我們。
歡迎遵照 CC-BY-NC-SA 協議規定 轉載,敬請在本文中標註並保留原文/譯文連結和作者/譯者等資訊。
文章僅代表作者的知識和看法,如有不同觀點,請樓下排隊吐槽
1666 次點擊 ∙ 1 贊