基于 Gin 的 HTTP 代理 Demo(2)

一周后勘误: 我这里实现的严格来说还是 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,而我最开始是使用的 ReadWrite 方法,老实说自己来处理网络流的读写真的是麻烦(也做不好,没有考虑各种可能的异常情况),不建议这样来做。

注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 的网络,不过那就变麻烦了。)

基于 Gin 的 HTTP 代理 Demo(2)_第1张图片

基于 Gin 的 HTTP 代理 Demo(2)_第2张图片

一定要注意是先启动代理,然后再配置系统代理,不然在配置系统代理到启动代理服务器的这段时间内,你是断网的。我相信,使用过代理上网的大部分人都遇到过代理服务器关闭了,但是系统代理没有关闭,导致自己上不了网,然后还看不懂浏览器的报错提示吧,哈哈!

基于 Gin 的 HTTP 代理 Demo(2)_第3张图片

1.3 系统代理配置

http=127.0.0.1:8888;https=127.0.0.1:8888 因为我见 Fiddler 是指定了 HTTP 和 HTTPS 协议,所以我就复制它的来配置吧。

基于 Gin 的 HTTP 代理 Demo(2)_第4张图片

1.4 运行效果

我写这篇文章就是在开启了代理的情况下,所以我插入图片,可以看到 csdn 的链接,而且证明了它是没有什么问题的。

基于 Gin 的 HTTP 代理 Demo(2)_第5张图片

注意:如果你可能注意到了这里大量的 404 请求日志。我一开始也感觉到很困惑,不过在我一番探索之后发现。它是因为我的请求都是在 NotRoute 中处理的,它默认是绑定了 404 的 handler(打印日志时,应该是依赖了状态码)。但是你可以看见下面的 200,你能看到它的请求方法(不是 CONNECT)说明它是 HTTP 请求。在那里,我是手动设置了状态码。但是 HTTPS 请求,因为劫持之后就不是 HTTP 连接了,退化成了 TCP 连接了,所以我就改不了了(那个时候已经脱离 gin 或者说 http 服务器的控制了)。

基于 Gin 的 HTTP 代理 Demo(2)_第6张图片

实际上,我也可以改的,那就是不在隧道建立之后发送响应,而是提前发送:把这两行代码放开,然后建立连接之后的写入连接建立成功的那段代码注释掉就可以了。效果的话就像下图一样,不过我感觉这样不符合代理服务器实现的时序逻辑了,而且实际上返回的是 200 OK 而不是 200 Connection Established。但是因为状态码才是最重要的,这个短语是给人看的,所以不是那么重要,不过为了合乎逻辑,我选择了后者,所以就让它显示 404 吧。

// c.Status(200)
// c.Writer.WriteHeaderNow()

基于 Gin 的 HTTP 代理 Demo(2)_第7张图片

不过这么多 404,看起来真的挺烦人的,还是来改一下吧,只把下面这一行放出来就行,我看源码这个应该是先写入一个缓冲区的,不是写入连接的,所以也不影响后续的读写,这样就只影响记录日志时的状态了。

c.Status(200)

这样就好多了,不过还是有一些 404,但是它们是 HTTP 的状态码了。这个情况还是不一样的,我去查了一下,这几个 URL 是和证书认证有关的,似乎是问我要认证证书的,我怎么会有这种东西呢,哈哈,索性就不管它们了。

基于 Gin 的 HTTP 代理 Demo(2)_第8张图片

二、HTTPS 代理时序图

强烈推荐阅读《HTTP权威指南》第 6 章和第 8 章,如果你也对这一块感兴趣的话,必然会大有收获了。下面是一个简单的 HTTPS 连接代理的时序图:

基于 Gin 的 HTTP 代理 Demo(2)_第9张图片

因为这里的代理是 HTTP 服务器,它是无法处理 HTTPS 连接的,没法进行 TLS 握手。所以客户端会使用 HTTP 协议发送一个 CONNECT 连接,代理会去连接服务器建立一个隧道(一个 TCP 连接)。如果建立成功,它就会向客户端响应一个连接已经建立的请求报文,然后客户端直接向代理发送 TCP 上的数据,代理虽然无法理解,但是可以转发它。这就相当于客户端到代理,代理到服务器都是一个 TCP 连接,它不需要管它们直接发送的是什么,只需要盲目的转发数据即可。从而实现了不同协议之间的通讯。这里不止可以传递 HTTPS 流量,其它类型的协议也是可以的,稍后我们会提到一个众所周知的协议。

三、使用 Gin 实现的技术难点

这里使用 Gin 来做,其实还是蛮方便的,第一个难点是如何处理所有的连接。这个在上一篇文章中已经介绍过了,就是通过处理 404 请求,没有路由就是等于全部的路由了。第二个难点是客户端和代理之间的 TCP 连接,刚开始的时候,它是一个 HTTP 连接,然后要在它上面传输 TCP 流量。这个可是难倒了我,我去看源码发现底层的 TCP 连接是不导出的变量,没有办法直接操作它。

基于 Gin 的 HTTP 代理 Demo(2)_第10张图片

不过,最后我还是找到了 hijacker,这个方法我以前见过,不明白这玩意干嘛的,一眼而过。不过,现在它可真是我的大救星了,哈哈。看来,如果不了解一些其它的知识,是无法了解代码的用处的。这个接口被 ResponseWriters 实现,允许我们去接管底层的 TCP 连接。所以,你在我的代码里面可以看到,我调用了 Hijacke 方法,然后就在那上面转发 HTTPS 的流量了。

注:我认识 hijack 这个单词比见到 Hijack() 方法 可能要早,所以就更有意思了(这个单词的意思是 劫持,打劫)。

基于 Gin 的 HTTP 代理 Demo(2)_第11张图片

这个 Hijack 还是很有趣的,你可能不明白为什么会提供一个这样的方法呢?让我们去看一看大名鼎鼎的 Gorilla/websocket 是怎么实现的吧!是的,没错,它就是依靠 Hijacke 实现的。这里你需要简单了解一下,WebSocket 也是通过一个 HTTP/HTTPS 请求建立的,然后它会劫持这个连接,获取底层的 TCP 连接,然后会返回一个 101 的状态码(Connection: Upgrade),之后这个连接就是 WebSocket 连接,你就可以在连接上进行双向的数据传输了。

基于 Gin 的 HTTP 代理 Demo(2)_第12张图片

四、下一步展望

我这里的文字描述可能比较少,因为这个确实需要你有一点网络的知识了,特别是有代理的使用经验,会更有助于你理解的。那么下一步还能做什么呢?这个程序可以沿着这个思路往下继续走下去,我大致有两个想法:

基于 Gin 的 HTTP 代理 Demo(2)_第13张图片
如果尝试做一个抓包软件的话,需要解决 HTTPS 报文解密的问题,这个我也在考虑,不过这个东西的意义就不大了。因为抓包软件都蛮成熟的了,不过如果是为了再深入了解抓包软件解密的原理,也是蛮有意思的,这个我可能会尝试去做一下解密的这一块,稍微了解一下就行了。
然后,是下面这个上网行为分析,记录一下自己日常看了哪些网站,然后统计一下数据或者只是简单记录一下,我感觉更有意思一点或者说更实用一些吧,我还是对自己日常主要看哪些网站比较感兴趣的(我平时刷 B 站比较多一些,哈哈)。

五、站在巨人的肩膀上

用不到 100 行的 Golang 代码实现 HTTP(S) 代理
Go Hijack 黑科技
理解HTTP CONNECT通道
Http代理服务器—Netty版
Socks 5 协议解析
再看 io.Copy
一文了解 io.Copy 函数
神奇的 Golang-IO 包

PS: 据说引入了外链,会导致降低展现量。不过我就不明白了,如果不建立在他人的基础之上,哪能写出来什么东西呢?

你可能感兴趣的:(网络编程,代理服务器,gin,https,网络协议)