100行Golang代码实现HTTP(S)代理
我们的目标是实现一个HTTP和HTTPS的代理服务。HTTP的处理其实就是解析请求,把请求发送到目标服务器,然后读取响应再发送到客户端。我们需要的就是Golang内置的HTTP服务器和客户端(net/http)。HTTPS有点不一样,因为需要用到HTTP CONNECT隧道的技术。首先客户端通过使用HTTP CONNECT方法发送请求来建立客户端和服务器端的隧道。当这个由两个TCP链接组成的隧道建立好了之后,客户端开始和目标服务器TLS握手,来建立安全的链接,随后发送请求和接收响应。
证书
我们的代理将会是一个HTTPS服务器(当使用 —proto https的时候),所以我们需要证书和私钥。根据这篇文字的目的,我们就使用自签名的证书好了。通过下面的脚本可以生成证书:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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中,可以通过Keychain Access设置 — https://tosbourn.com/getting-os-x-to-trust-self-signed-ssl-certificates/.
HTTP
我们将使用内置的HTTP服务器和客户端来支持HTTP。代理的角色是处理请求,并发送到目标服务器,最后把响应发回给客户端。
HTTP CONNECT隧道
假设客户端想要通过HTTPS或者WebSoket来和服务器通信。客户端知道在使用代理。由于客户端需要和服务器端建立安全的链接(HTTPS)或者使用基于TCP的其他协议(WebSocket),所以简单的HTTP请求/响应流程就无法使用。这时,需要用到HTTP CONNECT方法。它可以告诉代理服务器和目标服务器建立TCP连接,然后从客户端代理TCP流。通过这种方式,代理服务器不会打断SSL,只是简单的在客户端和服务器端之间传输数据,并且它们之间可以建立安全的连接。
实现
1 | package main |
这里展示的代码不是生产环境可用的。比如,它缺少逐段传输的处理能力,在两个链接复制数据的时候设置超时时间,还有其他net/http包里的超时时间。— 这里是net/http所有的超时设置
当收到请求的时候,我们的代理服务器会有两种处理方式:处理HTTP或者处理HTTP CONNECT隧道。实现代码如下:1
2
3
4
5
6
7
8
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodConnect {
handleTunneling(w, r)
} else {
handleHTTP(w, r)
}
})
处理HTTP的方法handleHTTP是自解释的,所以我们重点关注一下处理隧道的代码。第一部分是关于处理和目标服务器的连接的。
1 | dest_conn, err := net.DialTimeout("tcp", r.Host, 10*time.Second) |
接下来,我们有一部分是劫持连接的代码:
1 | hijacker, ok := w.(http.Hijacker) |
Hijacker interface可以让我们接管连接。然后调用者需要对管理连接负责,(HTTP库将不在管理该连接)。
一旦我们拥有了两个TCP连接(客户端->代理, 代理->目标服务器),我们需要启动隧道:1
2go transfer(dest_conn, client_conn)
go transfer(client_conn, dest_conn)
在这两个goroutine中,数据有两个传输方向:一个是从客户端到服务器,一个是从服务器到客户端。
测试
测试我们的代理,你可以使用Chrome:1
> Chrome --proxy-server=https://localhost:8888
或者Curl:1
> curl -Lv --proxy https://localhost:8888 --proxy-cacert server.pem https://google.com
curl需要编译的时候加入HTTPS-proxy的支持
HTTP/2
在我们的代理服务中,HTTP/2的支持被故意的放弃了,因为劫持是不可能的。
具体看这个#14797
更新
请看https://medium.com/@mlowicki/https-proxies-support-in-go-1-10-b956fb501d6b
原文地址:https://medium.com/@mlowicki/http-s-proxy-in-golang-in-less-than-100-lines-of-code-6a51c2f2c38c