一周后勘误: 我这里实现的严格来说还是 HTTP 代理,只不过是通过隧道的方式传输非 HTTP 的流量,这里是 HTTPS 流量。尽管它可以传输 HTTPS 流量,它也不算是 HTTPS 代理。
上次写了 基于 Gin 的 HTTP 代理 Demo 之后,对这方面还是蛮感兴趣的,所以就接着继续走下去。为了这个主题的内容,我斥巨资购入了一本二手的 《HTTP 权威指南》,因为我知道这本书里面有我想要的知识。在我还在大学的时候,我就看过这本书的前面关于 HTTP 协议的基本知识,当时正好也接触了 Fiddler,所以就利用 Fiddler 进行学习。抓取协议,了解各个字段的含义,尝试用JAVA的 TCP 来模拟,因此对于 HTTP 协议有了一个基本的认识。当时看到后面的章节,我就看到了关于代理和隧道的内容,不过当时显然是看不懂的,但是这颗种子已经在我心里埋下了。后来,我已经很少使用 Fiddler 了,但是我对于它的工作原理却一直很感兴趣,现在让我们从代理的角度来理解它吧。那么首先就是明白它的工作原理,所以最好的方式就是写一个 Demo 了。
所以,我觉得自己做一个 HTTP(HTTPS) 代理服务器的 Demo,对于这种广泛使用的软件,想要做得很好是需要很大的能力和精力的,但是做一个可以运行的 Demo 还是要轻松一点的。下面就让我们尝试在 100 行之内,使用 Gin 实现一个建议的 HTTP/HTTPS 代理服务器的 Demo吧。
注1:为什么是 100 行呢,因为我在快实现的时候,发现了一个老外写的相似的内容,100 行实现一个 HTTP 代理服务器。我也吸收了它的部分代码,就是关于建立 TCP 隧道之后的读写。他直接使用了 io.Copy
,而我最开始是使用的 Read
和 Write
方法,老实说自己来处理网络流的读写真的是麻烦(也做不好,没有考虑各种可能的异常情况),不建议这样来做。
注2:为什么使用 Gin 呢,如果你去搜索实现一个 HTTP 代理服务器,这基本上算是一个 Netty 的入门项目了(我发现很多人都是用 Netty 写这个)。因为我现在是主要使用 Go 语言了,所以我首选是用 Go 语言来实现,还有,我想要表明的是:任何 Web 框架都能作为 一个 HTTP 代理服务器。 当然了,通常来说使用 Netty 这种框架是最好的。但是,好不好和能不能是两回事,我想对于一个 Demo 来说,只要实现能不能就行了,而且你也会学习到一些你使用 Netty 无法了解到的知识。
一、代码和演示
Talk is cheap, show me your code.
让我们直入主题,上代码吧!
1.1 代码
package main
import (
"bufio"
"io"
"log"
"net"
"net/http"
"strings"
"github.com/gin-gonic/gin"
)
const (
RPOXY_SERVER = "CrazyDragonHttpProxy" // it is just a kidding, but Only HTTP!
TUNNEL_PACKET = "HTTP/1.1 200 Connection Established\r\nProxy-agent: CrazyDragonHttpProxy\r\n\r\n" // Don'e USE `` to surround a protocl strng, DAMN!!!
)
var proxyHttpClient = http.DefaultClient
func main() {
r := gin.Default()
r.NoRoute(routeProxy) // NO Route is every Route!!!
r.Run("localhost:8888") // I may be safer when in only run in localhost.
}
// Then I can process all routes
func routeProxy(c *gin.Context) {
req := c.Request
go func(req *http.Request) { // just print basic info. Remember you can't proxy youself.
log.Printf("Method: %s, Host: %s, URL: %s, Version: %s\n", req.Method, req.Host, req.URL.Path, req.Proto)
}(req)
if req.Method == http.MethodConnect {
httpsProxy(c, req) // create http tunnel to process https
} else {
httpProxy(c, req) // process plain http
}
}
func httpsProxy(c *gin.Context, req *http.Request) {
// established connect tunnel
address := req.URL.Host // it contains the port
tunnelConn, err := net.Dial("tcp", address)
if err != nil {
log.Println(err)
return
}
log.Printf("try to established Connect Tunnel to: %s has been successfully.\n", address)
tunnelrw := bufio.NewReadWriter(bufio.NewReader(tunnelConn), bufio.NewWriter(tunnelConn))
// c.Status(200)
// c.Writer.WriteHeaderNow()
// And We need to take over the http connection, Then make it become a TCP connection.
hj, ok := c.Writer.(http.Hijacker)
if !ok {
http.Error(c.Writer, "webserver doesn't support hijacking", http.StatusInternalServerError)
return
}
clientConn, bufrw, err := hj.Hijack()
if err != nil {
http.Error(c.Writer, err.Error(), http.StatusInternalServerError)
return
}
// 建立隧道之后,发送一个连接建立的响应(其实,只要状态码是 200 就可以了)
if _, err = clientConn.Write([]byte(TUNNEL_PACKET)); err != nil {
log.Printf("Response Failed: %v", err.Error())
} else {
log.Println("Response Success.")
}
// data flow direction: client <---> tunnel <---> server
defer clientConn.Close()
defer tunnelConn.Close()
done := make(chan struct{})
go transfer(bufrw, tunnelrw, done) // client --> proxy --> server
go transfer(tunnelrw, bufrw, done) //server --> proxy --> client
<-done
}
func httpProxy(c *gin.Context, req *http.Request) {
req.RequestURI = "" // Must create a new Req or empty this.
resp, err := proxyHttpClient.Do(req)
if err != nil {
log.Println(err)
return
}
defer resp.Body.Close()
c.Status(resp.StatusCode) // change the status code, default is 404 !!!
for k, v := range resp.Header {
c.Header(k, strings.Join(v, ",")) // write Header
}
c.Header("Server", RPOXY_SERVER) // haha, it just a kidding!!!
io.Copy(c.Writer, resp.Body) // and response data to client
}
// tunnel transfer data.
func transfer(from io.Reader, to io.Writer, ch chan<- struct{}) {
io.Copy(to, from)
ch <- struct{}{}
}
1.2 启动代理服务器 Demo
我已经把它交叉编译成 Windows 的可执行文件了,因为我是在镜像内开发的,所以要拿出来运行(或者可能要配置 Docker 的网络,不过那就变麻烦了。)
一定要注意是先启动代理,然后再配置系统代理,不然在配置系统代理到启动代理服务器的这段时间内,你是断网的。我相信,使用过代理上网的大部分人都遇到过代理服务器关闭了,但是系统代理没有关闭,导致自己上不了网,然后还看不懂浏览器的报错提示吧,哈哈!。
1.3 系统代理配置
http=127.0.0.1:8888;https=127.0.0.1:8888
因为我见 Fiddler 是指定了 HTTP 和 HTTPS 协议,所以我就复制它的来配置吧。
1.4 运行效果
我写这篇文章就是在开启了代理的情况下,所以我插入图片,可以看到 csdn 的链接,而且证明了它是没有什么问题的。
注意:如果你可能注意到了这里大量的 404 请求日志。我一开始也感觉到很困惑,不过在我一番探索之后发现。它是因为我的请求都是在 NotRoute 中处理的,它默认是绑定了 404 的 handler(打印日志时,应该是依赖了状态码)。但是你可以看见下面的 200,你能看到它的请求方法(不是 CONNECT)说明它是 HTTP 请求。在那里,我是手动设置了状态码。但是 HTTPS 请求,因为劫持之后就不是 HTTP 连接了,退化成了 TCP 连接了,所以我就改不了了(那个时候已经脱离 gin 或者说 http 服务器的控制了)。
实际上,我也可以改的,那就是不在隧道建立之后发送响应,而是提前发送:把这两行代码放开,然后建立连接之后的写入连接建立成功的那段代码注释掉就可以了。效果的话就像下图一样,不过我感觉这样不符合代理服务器实现的时序逻辑了,而且实际上返回的是 200 OK
而不是 200 Connection Established
。但是因为状态码才是最重要的,这个短语是给人看的,所以不是那么重要,不过为了合乎逻辑,我选择了后者,所以就让它显示 404 吧。
// c.Status(200)
// c.Writer.WriteHeaderNow()
不过这么多 404,看起来真的挺烦人的,还是来改一下吧,只把下面这一行放出来就行,我看源码这个应该是先写入一个缓冲区的,不是写入连接的,所以也不影响后续的读写,这样就只影响记录日志时的状态了。
c.Status(200)
这样就好多了,不过还是有一些 404,但是它们是 HTTP 的状态码了。这个情况还是不一样的,我去查了一下,这几个 URL 是和证书认证有关的,似乎是问我要认证证书的,我怎么会有这种东西呢,哈哈,索性就不管它们了。
二、HTTPS 代理时序图
强烈推荐阅读《HTTP权威指南》第 6 章和第 8 章,如果你也对这一块感兴趣的话,必然会大有收获了。下面是一个简单的 HTTPS 连接代理的时序图:
因为这里的代理是 HTTP 服务器,它是无法处理 HTTPS 连接的,没法进行 TLS 握手。所以客户端会使用 HTTP 协议发送一个 CONNECT 连接,代理会去连接服务器建立一个隧道(一个 TCP 连接)。如果建立成功,它就会向客户端响应一个连接已经建立的请求报文,然后客户端直接向代理发送 TCP 上的数据,代理虽然无法理解,但是可以转发它。这就相当于客户端到代理,代理到服务器都是一个 TCP 连接,它不需要管它们直接发送的是什么,只需要盲目的转发数据即可。从而实现了不同协议之间的通讯。这里不止可以传递 HTTPS 流量,其它类型的协议也是可以的,稍后我们会提到一个众所周知的协议。
三、使用 Gin 实现的技术难点
这里使用 Gin 来做,其实还是蛮方便的,第一个难点是如何处理所有的连接。这个在上一篇文章中已经介绍过了,就是通过处理 404 请求,没有路由就是等于全部的路由了。第二个难点是客户端和代理之间的 TCP 连接,刚开始的时候,它是一个 HTTP 连接,然后要在它上面传输 TCP 流量。这个可是难倒了我,我去看源码发现底层的 TCP 连接是不导出的变量,没有办法直接操作它。
不过,最后我还是找到了 hijacker
,这个方法我以前见过,不明白这玩意干嘛的,一眼而过。不过,现在它可真是我的大救星了,哈哈。看来,如果不了解一些其它的知识,是无法了解代码的用处的。这个接口被 ResponseWriters
实现,允许我们去接管底层的 TCP 连接。所以,你在我的代码里面可以看到,我调用了 Hijacke
方法,然后就在那上面转发 HTTPS 的流量了。
注:我认识 hijack
这个单词比见到 Hijack()
方法 可能要早,所以就更有意思了(这个单词的意思是 劫持,打劫
)。
这个 Hijack
还是很有趣的,你可能不明白为什么会提供一个这样的方法呢?让我们去看一看大名鼎鼎的 Gorilla/websocket
是怎么实现的吧!是的,没错,它就是依靠 Hijacke 实现的。这里你需要简单了解一下,WebSocket 也是通过一个 HTTP/HTTPS 请求建立的,然后它会劫持这个连接,获取底层的 TCP 连接,然后会返回一个 101 的状态码(Connection: Upgrade
),之后这个连接就是 WebSocket 连接,你就可以在连接上进行双向的数据传输了。
四、下一步展望
我这里的文字描述可能比较少,因为这个确实需要你有一点网络的知识了,特别是有代理的使用经验,会更有助于你理解的。那么下一步还能做什么呢?这个程序可以沿着这个思路往下继续走下去,我大致有两个想法:
如果尝试做一个抓包软件的话,需要解决 HTTPS 报文解密的问题,这个我也在考虑,不过这个东西的意义就不大了。因为抓包软件都蛮成熟的了,不过如果是为了再深入了解抓包软件解密的原理,也是蛮有意思的,这个我可能会尝试去做一下解密的这一块,稍微了解一下就行了。
然后,是下面这个上网行为分析,记录一下自己日常看了哪些网站,然后统计一下数据或者只是简单记录一下,我感觉更有意思一点或者说更实用一些吧,我还是对自己日常主要看哪些网站比较感兴趣的(我平时刷 B 站比较多一些,哈哈)。
五、站在巨人的肩膀上
用不到 100 行的 Golang 代码实现 HTTP(S) 代理
Go Hijack 黑科技
理解HTTP CONNECT通道
Http代理服务器—Netty版
Socks 5 协议解析
再看 io.Copy
一文了解 io.Copy 函数
神奇的 Golang-IO 包
PS: 据说引入了外链,会导致降低展现量。不过我就不明白了,如果不建立在他人的基础之上,哪能写出来什么东西呢?