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
#!/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中,可以通过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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
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包里的超时时间。— 这里是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
2
3
4
5
6
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)

接下来,我们有一部分是劫持连接的代码:

1
2
3
4
5
6
7
8
9
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 interface可以让我们接管连接。然后调用者需要对管理连接负责,(HTTP库将不在管理该连接)。

一旦我们拥有了两个TCP连接(客户端->代理, 代理->目标服务器),我们需要启动隧道:

1
2
go 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

加载评论框需要科学上网