HTTP proxy
最近在用Golang做httpProxy,看到一篇不错的老外博客.所以翻译一遍以备后用
原文地址:https://medium.com/@mlowicki/http-s-proxy-in-golang-in-less-than-100-lines-of-code-6a51c2f2c38c
HTTP(S) Proxy在Golang中实现不超过一百行代码
本文的目标是为HTTP何HTTPS实施代理服务器.处理HTTP请求只是一个简单的解析请求:将http的请求传递给最终的目标服务器,读取响应并将其传递回客户端. 我们所需要的就是内置http server和client(net/http);HTTPS不同,因为它将使用HTTP CONNECT隧道技术.第一个客户端使用HTTP CONNECT方法发送请求以建立客户端和目标服务器之间的隧道. 当这种由两个TCP链接组成的隧道准备就绪后,客户端就会与目标服务器开始定义的TLS握手.以建立安全的连接并在随后发送请求并接收响应.
证书
我们的代理将是一个HTTPS服务器(当使用–proto https时),所以我们需要证书和私钥.为了本文的目的,我们使用自签名证书.可以使用以下脚本生成证书:
#!/usr/bin/env bash
case `uname -s` in
Linux*) sslConfig=/etc/ssl/openssl.cnf;;
Darwin*) sslConfig=/System/Library/OpenSSL/openssl.cnf;;
esac
openssl 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中,可以使用钥匙串访问完成 - https://tosbourn.com/getting-os-x-to-trust-self-signed-ssl-certificates/。
HTTP
为了支持HTTP,我们将使用内置的HTTP Server和Client。 代理的角色是处理HTTP请求,将此请求传递给目标服务器并将响应发送回客户端。
HTTP CONNECT 隧道
假设客户端想要使用HTTPS或WebSockets来与服务器通信.客户端会察觉到使用了代理。简单的http请求/响应流程不再支持,因为客户端需要建立与服务器的安全连接(HTTPS)或系统通过TCP连接(WebSockets)使用其他协议.实现的方法是使用HTTP CONNECT.它会告诉代理服务器建议与目标服务器的TCP连接,并完成代理到客户端和来自客户端的TCP流。这样代理服务器不会终止SSL,但是在客户端和目标服务之间传递数据,因为代理和客户端简历了安全的连接.
实现
package main
import (
"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))
}
}
提供的代码不是生产级解决方案。 它缺乏 处理逐跳报头,在两个连接之间复制数据或者通过net/http暴露数据时设置超时 - 在“The complete guide to Go net/http timeouts”中可以查看到更多内容。
我们的服务器在获取请求时将采取两种处理方式:处理HTTP或处理HTTP CONNECT隧道。 这是通过:
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodConnect {
handleTunneling(w, r)
} else {
handleHTTP(w, r)
}
})
handleHTTP不言自明是处理http的函数,所以让我们专注于处理隧道。 handleTunneling的第一部分是关于设置连接到目标服务器:
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)
接下来,我们需要劫持一部分由HTTP Server维持的连接:
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)
}
Hijacker接口允许接管连接。 之后,调用者负责管理这种连接(HTTP库不会负责)。
一旦我们拿到两个TCP连接(客户端→代理,代理→目标服务器)之后,我们需要设置隧道:
go transfer(dest_conn, client_conn)
go transfer(client_conn, dest_conn)
在两个例程中,数据被复制到两个方向:从客户端到目标服务器并向后(backward)。
测试
要测试我们的代理,您可以使用例如 Chrome:
> Chrome --proxy-server=https://localhost:8888
或者Curl
> curl -Lv --proxy https://localhost:8888 --proxy-cacert server.pem https://google.com
curl需要使用HTTPS代理支持(在7.52.0中引入)来构建。
HTTP/2
在我们的服务器HTTP / 2支持已被故意删除,因为劫持不支持的。 更多信息可以查看#14797。
更新
请阅读https://medium.com/@mlowicki/https-proxies-support-in-go-1-10-b956fb501d6b关于对Go 1.10中的HTTPS代理的支持。