https代理原理及实现

简介

http代理的实现比较简单,客户端请求发送给代理,代理拿着请求去请求远端服务器,然后将结果返回给客户端,而https无法沿用这个思路,原因是https消息内容是加密的,代理无法解析数据。
查询资料得知,可以利用一项叫http隧道的技术来实现代理。

http协议定义了CONNECT请求方式,该请求方式通常用于开启一个隧道

隧道建立

代理收到CONNECT请求,开始建立与目标服务器之间的代理。一旦建立完成,代理服务器就开始代理进出客户端的tcp流数据

20180806153348906069474.png
假如我通过代理访问 A 网站,浏览器首先通过 CONNECT 请求,让代理创建一条到 A 网站的 TCP 连接;一旦 TCP 连接建好,代理无脑转发后续流量即可。所以这种代理,理论上适用于任意基于 TCP 的应用层协议,HTTPS 网站使用的 TLS 协议当然也可以。这也是这种代理为什么被称为隧道的原因。对于 HTTPS 来说,客户端透过代理直接跟服务端进行 TLS 握手协商密钥,所以依然是安全的,下图中的抓包信息显示了这种场景
20180806153348908548991.png
可以看到,浏览器与代理进行 TCP 握手之后,发起了 CONNECT 请求,报文起始行如下:

CONNECT imququ.com:443 HTTP/1.1

对于 CONNECT 请求来说,只是用来让代理创建 TCP 连接,所以只需要提供服务器域名及端口即可,并不需要具体的资源路径。代理收到这样的请求后,需要与服务端建立 TCP 连接,并响应给浏览器这样一个 HTTP 报文:

HTTP/1.1 200 Connection Established

浏览器收到了这个响应报文,就可以认为到服务端的 TCP 连接已经打通,后续直接往这个 TCP 连接写协议数据即可.通过 Wireshark 的 Follow TCP Steam 功能,可以清楚地看到浏览器和代理之间的数据传递:
20180806153348917094637.png
可以看到,浏览器建立到服务端 TCP 连接产生的 HTTP 往返,完全是明文,这也是为什么 CONNECT 请求只需要提供域名和端口:如果发送了完整 URL、Cookie 等信息,会被中间人一览无余,降低了 HTTPS 的安全性。HTTP 代理承载的 HTTPS 流量,应用数据要等到 TLS 握手成功之后通过 Application Data 协议传输,中间节点无法得知用于流量加密的 master-secret,无法解密数据。而 CONNECT 暴露的域名和端口,对于普通的 HTTPS 请求来说,中间人一样可以拿到(IP 和端口很容易拿到,请求的域名可以通过 DNS Query 或者 TLS Client Hello 中的 Server Name Indication 拿到),所以这种方式并没有增加不安全性。

代码实现

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
func main() {
server := http.Server{
Addr: ":8889",
Handler: http.HandlerFunc(handleRequest),
}
server.ListenAndServe()
}
func handleRequest(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodConnect {
handleHTTPS(w, r)
} else {
handleHTTP(w, r)
}
}
func handleHTTPS(w http.ResponseWriter, r *http.Request) {
dest_conn, err := net.DialTimeout("tcp", r.URL.Host, 10*time.Second)
if err != nil {
//http.Error不会中断代码执行,需要手动return
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
//回应200 表示隧道已建立
w.WriteHeader(http.StatusOK)
//如果w实现了http.Hijacker 表明返回的hijacker可使用Hijack方法
hijacker, ok := w.(http.Hijacker)
if !ok {
http.Error(w, "Hijacking not supported", http.StatusInternalServerError)
return
}
//通过Hijack可以让调用方接管连接
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 copyHeader(dst, src http.Header) {
for k, vv := range src {
for _, v := range vv {
dst.Add(k, v)
}
}
}
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)
}

知识扩展

代理

  • 转发代理(隧道或网关)
http tunnel(隧道)
  • 反向代理
    通常用来控制保护后端服务器,该服务器可能用于负载均衡、认证、加密或缓存

参考资料

用不到 100 行的 Golang 代码实现 HTTP(S) 代理
HTTP 代理原理及实现
http://cizixs.com/2017/03/22/http-tunnel-proxy-and-golang-implementation