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

在这里插入图片描述

在这里插入图片描述

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

在这里插入图片描述

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/204468.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

ant design vue3 处理 ant-card-head ant-tabs靠左边对齐之has选择器不生效

火狐浏览器是不支持has的。 解决方法&#xff1a;通过position来解决。

解析编程中的技术迷题:常见挑战与应对策略

前言 在数字化时代的浪潮中&#xff0c;编程已经成为了一项至关重要的技能。无论是在软件开发、数据分析、人工智能还是互联网领域&#xff0c;编程都扮演着不可或缺的角色。作为一种创造性的活动&#xff0c;编程不仅仅是代码的书写&#xff0c;更是一种解决问题和创新的思维方…

软件测试的测试文档怎么编写?

在软件测试中的流程中&#xff0c;测试文档也是一个重要的流程&#xff0c;所以测试人员也需要学习测试文档的编写和阅读。 一定义&#xff1a; 测试文档&#xff08;TestingDocumentation&#xff09;记录和描述了整个测试流程&#xff0c;它是整个测试活动中非常重要的文件。…

【JavaEE初阶】 详解HTTPS协议加密过程

文章目录 &#x1f334;HTTPS协议是什么&#xff1f;&#x1f384;运营商劫持事件&#x1f38b;HTTPS的工作过程&#x1f6a9;对称加密&#x1f6a9;非对称加密&#x1f6a9;引入证书&#x1f6a9;完整流程 &#x1f333;HTTPS加密总结⭕总结 &#x1f334;HTTPS协议是什么&…

基于Netty实现TCP通信

创建一个Maven项目添加下面依赖 <dependencies><!-- 日志依赖 --><dependency><groupId>org.slf4j</groupId><artifactId>slf4j-api</artifactId><version>1.7.32</version></dependency><dependency><g…

帮亲戚个忙,闲来有事用php写个58商铺出租转让信息抓取

最近亲戚想做点小超市生意&#xff0c;但是又不懂互联网&#xff0c;信息获取有点闭塞。知道我身在互联网大潮中&#xff0c;想让我帮忙看看网上有没有商铺转让的。心想&#xff0c;这不是小菜一碟&#xff0c;大显身手的时候来了&#xff0c;大概去58瞅了瞅&#xff0c;这玩意…

【问题定位】阅读Nacos服务注册与发现的源码解决服务注册异常

问题现象 本地服务启动&#xff0c;发现调用FeignClient的服务&#xff0c;跑的是sit的服务&#xff0c;而本地是uat的环境配置。 问题跟踪 feign.SynchronousMethodHandler#invoke&#xff0c;调用远程服务。 public Object invoke(Object[] argv) throws Throwable {Reque…

信号类型(通信)——高斯最小频率键控(GMSK)

系列文章目录 《信号类型&#xff08;通信&#xff09;——仿真》 《信号类型&#xff08;通信&#xff09;——QAM调制信号》 《信号类型&#xff08;通信&#xff09;——QPSK、OQPSK、IJF_OQPSK调制信号》 《信号类型&#xff08;通信&#xff09;——最小频移键控&…

【秒懂JDK,JRE,JVM的关系】

&#x1f320;作者&#xff1a;TheMythWS. &#x1f387;座右铭&#xff1a;不走心的努力都是在敷衍自己&#xff0c;让自己所做的选择&#xff0c;熠熠发光。 ​ JDK与JRE与JVM的关系 先用一张图来直观感受JDK JRE JVM之间的关系&#xff1a; JDK与JRE的关系 先说JDK和JRE…

卷轴模式:金融领域的新趋势

卷轴模式在金融领域逐渐崭露头角&#xff0c;成为一种新型的投资策略。这种模式基于完成任务或达成特定目标来获取积分&#xff0c;利用这些积分进行投资或获取现实物品。它不同于传统的资金盘&#xff0c;而是以一种更稳健的方式运作&#xff0c;避免了资金盘的风险。 一、卷轴…

替代升级虚拟化 | ZStack Cloud云平台助力中节能镇江公司核心业务上云

数字经济正加速推动各行各业的高质量升级发展&#xff0c;云计算是数字经济的核心底层基础设施。作为云基础软件企业&#xff0c;云轴科技ZStack 坚持自主创新&#xff0c;自研架构&#xff0c;产品矩阵可全面覆盖数据中心云基础设施&#xff0c;针对虚拟化资源实现纳管、替代和…

springboot自定义校验注解的实现

自定义校验注解的实现 通过谷粒商城项目学习了自定义校验器的实现一、编写自定义校验注解二、自定义注解的校验器三、关联自定义的校验器和自定义的校验注解总结 通过谷粒商城项目学习了自定义校验器的实现 近日在学习雷神的谷粒商城项目&#xff0c;其中有一个自定义校验的实…

网络字节序

字节序的概念和示例 CPU向内存保存数据的方式有2种&#xff0c;所以CPU解析数据的方式也分为2种。CPU保存和解析数据的方式叫字节序&#xff0c;分为小端字节序和大端字节序。 大端字节序&#xff1a;高位字节存放到低位地址。 小端字节序&#xff1a;高位字节存放到高位地址。…

2023年第三届中国高校大数据挑战赛思路及代码

比赛时间&#xff1a;2023.12.28 08:00 至 2023.12.31 20:00 赛题方向介绍 1、大数据统计分析方向 涉及内容包含&#xff1a;数据的清洗、数据的预测、数据之间的关联分析、综合评价、分类与判别等 2、文本或图象分析方向 涉及内容包含&#xff1a;计算机视觉基础、特征匹配…

基于YOLOv8深度学习的火焰烟雾检测系统【python源码+Pyqt5界面+数据集+训练代码】目标检测、深度学习实战

《博主简介》 小伙伴们好&#xff0c;我是阿旭。专注于人工智能、AIGC、python、计算机视觉相关分享研究。 ✌更多学习资源&#xff0c;可关注公-仲-hao:【阿旭算法与机器学习】&#xff0c;共同学习交流~ &#x1f44d;感谢小伙伴们点赞、关注&#xff01; 《------往期经典推…

[架构之路-253]:目标系统 - 设计方法 - 软件工程 - 软件设计 - 结构化设计的主要评估指标:高内聚(模块内部)、低耦合(模块之间)的含义

目录 前言&#xff1a; 一、软件工程中的软件设计种类&#xff1a;根据宏观到微观分 &#xff08;1&#xff09;软件架构设计&#xff08;层次划分、模块划分、职责分工&#xff09;&#xff1a; &#xff08;2&#xff09;软件高层设计、概要设计&#xff08;功能模块的接…

样品实验K-KAT348羧酸铋催化剂TDS说明书

样品实验K-KAT348羧酸铋催化剂TDS说明书 50克 100克 200克

【图像分类】基于深度学习的中草药分类系统的设计与实现(ResNet网络,附代码和数据集)

写在前面: 首先感谢兄弟们的关注和订阅,让我有创作的动力,在创作过程我会尽最大能力,保证作品的质量,如果有问题,可以私信我,让我们携手共进,共创辉煌。(专栏订阅用户订阅专栏后免费提供数据集和源码一份,超级VIP用户不在服务范围之内,不想订阅专栏的兄弟们可以私信…

【攻防世界-misc】来自银河的信号

1.下载并打开文件&#xff0c;是个音频软件 2.由于打开音频出现的声音类似于无线波&#xff0c;因此需要用RX-SSTV工具打开&#xff0c; RX-SSTV代表“接收图像慢扫描电视”的意思。慢扫描电视是一种通过无线电进行图像传输的技术&#xff0c;通常用于业余无线电领域。RX-SST…

数电实验-----触发器的原理与应用(Quartus II )

目录 触发器概述 1.基本RS触发器 2.同步触发器 &#xff08;1&#xff09;RS同步触发器 &#xff08;2&#xff09;D触发器 3.边沿触发器 &#xff08;1&#xff09;JK触发器 &#xff08;2&#xff09;T触发器 JK触发器的转换 &#xff08;1&#xff09;JK触发器转换…