在 HTTP 诸多的动词中,有一个不常用、不热门的,它是 CONNECT。
跟在它后面的资源,通常不是像 GET/POST 一样的资源路径,如:GET /path/to/file
。而是一个主机:端口
对,即TCP地址、四元组的一半、第三方,如:CONNECT github.com:443
。
如果该 HTTP 服务器支持 CONNECT 动词,比如淘宝的基于 nginx 修改后的 tengine 中的 proxy_connect
模块,
那么,它将连接到该地址。如果连接成功,它将返回200
状态码给客户端。
**此后,所有的流量数据完全透传。**这样就相当于在客户端和第三方之间打通了一个隧道(Tunnel)。 你可能在公司经常用SSH跳板机登录到服务器上去,这个跳板机也充当了隧道建立者的角色。为什么要用跳板机而不是直接登录?因为为了保证服务器的安全,只允许少量的IP可以访问。同时也提高了性能(iptables)。
这里我借用一下淘宝的 proxy_connect 模块的工作方式序列图,它非常形象地说明了数据的流向:
curl nginx (proxy_connect) github.com
| | |
(1) |-- CONNECT github.com:443 -->| |
| | |
| |----[ TCP connection ]--->|
| | |
(2) |<- HTTP/1.1 200 ---| |
| Connection Established | |
| | |
| |
========= CONNECT tunnel has been established. ===========
| |
| | |
| | |
| [ SSL stream ] | |
(3) |---[ GET / HTTP/1.1 ]----->| [ SSL stream ] |
| [ Host: github.com ] |---[ GET / HTTP/1.1 ]-->.
| | [ Host: github.com ] |
| | |
| | |
| | |
| | [ SSL stream ] |
| [ SSL stream ] |<--[ HTTP/1.1 200 OK ]---'
(4) |<--[ HTTP/1.1 200 OK ]------| [ < html page > ] |
| [ < html page > ] | |
| | |
注:
- 原文 establish 的过去分词错误拼写为 establesied,已修正
为了测试可用性,以及学习用Go语言编写网络程序,我试着完整地写了这样一个程序,不足 100 行(加上注释和空行)。
代码地址:https://gist.github.com/movsb/74e9a91b07e9f76e6c78224f8158f4ee
这段代码非常的简洁,仅两个函数。
main
函数建立了一个 HTTP 服务器,把请求的处理函数指向了tunnel
函数。
tunnel
函数是一个标准的 HTTP 处理器函数。有两个参数:一个请求,一个响应。
接下来分解这个函数的实现。
1 2 3 4 5 6 |
|
这段代码表示我们仅处理 CONNECT 动词,其它的动词通通返回404 未找到
。
1 2 3 4 5 6 7 8 9 10 |
|
这里是用户校验部分。由于客户端的授权是放在Proxy-Authorization
中的,而req.BasicAuth
依赖的是Authorization
,所以我们简单重新设置一下。
然后取得用户名和密码,并判断是否与预设一致。若不一致,返回错误。
1 2 |
|
这句话输出第三方的地址。
1 2 3 4 5 6 7 |
|
然后我们尝试连接到第三方,如果连接失败,返回错误。否则继续往下。
1 2 |
|
到这里就已经连接第三方成功了,我们应该告诉客户端。它是一个普通的200
状态码响应。
1 2 3 4 5 6 7 8 |
|
这一步非常关键。何为hijack
?我们知道,HTTP 是应用层协议,在它的下一层,是 TCP 网络层协议。
hijack
方法让我们可以从响应(Response)
中拿到这个 TCP 连接。非常关键的一个函数。
这个函数返回两个可读可写的对象。src
是TCP连接(如果是HTTPS服务器,则是TLS连接),bio
是对 src
包装的一个带缓冲的读写者。
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 |
|
这段是核心代码。创建了两个线程,然后调用io.Copy
进行全双工的双向数据拷贝(中继)。
从src
到dst
的前面多了一段对带缓冲对象的处理,因为带缓冲,可能有未读完的数据,所以先确保全部读走。目的是为了能直接使用src
。
不过,用bio
代替src
也是可以的,只是看上去效率应该会低一些。另外,如果是往bio
里面写数据,记得适时调用bio.Flush()
将数据刷走,否则可能会“假死”。
像下面这样运行并作为服务器:
1 2 3 4 5 6 |
|
可以在 cURL 中测试是否可以工作:
1
|
|
cURL 在目标为 HTTP 而非 HTTPS 时会使用 GET
去请求。-p
可以使其总是使用CONNECT
。
不出意外,服务器会打印出请求的第三方地址,cURL 会输出页面内容。
注:为了保证数据安全、防监听、插入广告,请在服务器上使用 HTTPS,勿使用 HTTP。即使用ListenAndServeTLS
代替ListenAndServe
。
为了查阅方便,附上完整代码:
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 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 |
|