当前位置: 首页 > news >正文

基于 Gin 的 HTTPS 代理 Demo

上次写了 基于 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 mainimport ("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.DefaultClientfunc 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.Requestgo 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 tunneladdress := req.URL.Host // it contains the porttunnelConn, 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 <---> serverdefer clientConn.Close()defer tunnelConn.Close()done := make(chan struct{})go transfer(bufrw, tunnelrw, done) // client --> proxy --> servergo 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: 据说引入了外链,会导致降低展现量。不过我就不明白了,如果不建立在他人的基础之上,哪能写出来什么东西呢?

相关文章:

  • 【深入剖析K8s】容器技术基础(三):深入理解容器镜像 文件角度
  • Hive内置表生成函数
  • 什么是轻量应用服务器?可以从亚马逊云科技的优势入手了解
  • Unsupervised Skill Discovery via Recurrent Skill Training论文笔记
  • STM32-使用固件库新建工程
  • c 语言线程的使用
  • 【软件测试】“我“做了一年的功能点点点测试,感觉在浪费时间...
  • 肾合胶囊 | 冬不养肾春易病,若出现了这六大表现,小心是肾虚!
  • 使用Python+Redis实现文章投票网站后端功能
  • 机器学习探索计划——KNN算法流程的简易了解
  • 网络篇---第一篇
  • [pyqt5]pyqt5设置窗口背景图片后上面所有图片都会变成和背景图片一样
  • WPF绘图技术介绍
  • Python武器库开发-前端篇之CSS基本语法(三十)
  • 用JAVA编程解决数位和相等问题
  • 【140天】尚学堂高淇Java300集视频精华笔记(86-87)
  • 【Amaple教程】5. 插件
  • canvas 高仿 Apple Watch 表盘
  • CentOS 7 防火墙操作
  • crontab执行失败的多种原因
  • Docker 1.12实践:Docker Service、Stack与分布式应用捆绑包
  • Docker下部署自己的LNMP工作环境
  • golang 发送GET和POST示例
  • Joomla 2.x, 3.x useful code cheatsheet
  • js中的正则表达式入门
  • nfs客户端进程变D,延伸linux的lock
  • php中curl和soap方式请求服务超时问题
  • tensorflow学习笔记3——MNIST应用篇
  • XML已死 ?
  • 阿里云容器服务区块链解决方案全新升级 支持Hyperledger Fabric v1.1
  • 程序员该如何有效的找工作?
  • 高程读书笔记 第六章 面向对象程序设计
  • 汉诺塔算法
  • 排序(1):冒泡排序
  • 少走弯路,给Java 1~5 年程序员的建议
  • 移动互联网+智能运营体系搭建=你家有金矿啊!
  • 硬币翻转问题,区间操作
  • 用jQuery怎么做到前后端分离
  • nb
  • 好程序员大数据教程Hadoop全分布安装(非HA)
  • ​io --- 处理流的核心工具​
  • !! 2.对十份论文和报告中的关于OpenCV和Android NDK开发的总结
  • # 飞书APP集成平台-数字化落地
  • $con= MySQL有关填空题_2015年计算机二级考试《MySQL》提高练习题(10)
  • (2020)Java后端开发----(面试题和笔试题)
  • (C语言)求出1,2,5三个数不同个数组合为100的组合个数
  • (C语言)输入一个序列,判断是否为奇偶交叉数
  • (ResultSet.TYPE_SCROLL_INSENSITIVE,ResultSet.CONCUR_READ_ONLY)讲解
  • (待修改)PyG安装步骤
  • (二)斐波那契Fabonacci函数
  • (附源码)计算机毕业设计SSM智慧停车系统
  • (十五)使用Nexus创建Maven私服
  • (四)Controller接口控制器详解(三)
  • (循环依赖问题)学习spring的第九天
  • (译)计算距离、方位和更多经纬度之间的点