从零实现支持缓存+上游代理的HTTP/HTTPS中间人代理

一、项目背景

在日常开发和运维中,我们经常需要访问各类开源软件包、镜像文件,反复下载相同资源会浪费大量带宽和时间。本文将手把手教你用Go语言实现一个支持缓存的HTTP/HTTPS中间人代理服务器,核心功能包括:

  • 支持HTTP/HTTPS全量代理(中间人模式)
  • 自动缓存可复用资源(压缩包、二进制文件、静态资源等)
  • 配置上游HTTP代理转发请求
  • 高性能日志输出(基于Zap框架)
  • 解决Go net/http库默认重定向导致的CONNECT请求301错误

项目完整代码:https://gitee.com/impl/myproxy

二、核心技术点

  • Go net/http Hijack:劫持CONNECT请求,建立HTTPS隧道
  • TLS中间人代理:自签CA证书,动态生成目标域名证书
  • 文件缓存:基于文件扩展名的缓存策略,减少重复下载
  • 上游代理集成:所有对外请求通过指定代理转发
  • Zap日志框架:结构化日志输出,替代原生fmt.Printf

三、实现步骤

3.1 项目初始化

bash 复制代码
# 初始化Go模块
go mod init myproxy

# 安装依赖(Zap日志框架)
go get go.uber.org/zap

3.2 核心问题解决:CONNECT请求301重定向

问题现象

使用Go原生ServeMux处理CONNECT请求时,会触发net/http库的路径规范化逻辑,返回301 Moved Permanently重定向,导致HTTPS隧道建立失败。

解决方案:自定义Handler完全接管请求

绕过默认ServeMux,实现自定义http.Handler接口,在最顶层处理CONNECT请求:

go 复制代码
// 自定义Handler,完全接管所有请求
type ProxyHandler struct {
	cm *CacheManager
	tm *TLSManager
}

func (ph *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	logger.Debug("收到请求",
		zap.String("method", r.Method),
		zap.String("url", r.URL.String()),
		zap.String("remoteAddr", r.RemoteAddr))

	// 最顶层判断CONNECT,直接处理
	if r.Method == http.MethodConnect {
		handleConnect(w, r, ph.cm, ph.tm)
		return
	}

	// 其他请求走HTTP代理逻辑
	httpProxyHandler(ph.cm)(w, r)
}
CONNECT请求处理(关键修复)

立即劫持连接,发送标准200响应,避免默认逻辑干扰:

go 复制代码
// 独立的CONNECT处理函数
func handleConnect(w http.ResponseWriter, r *http.Request, cm *CacheManager, tm *TLSManager) {
	logger.Info("处理CONNECT请求",
		zap.String("host", r.URL.Host),
		zap.String("remoteAddr", r.RemoteAddr))

	// 立即劫持连接,绕过所有默认逻辑
	hj, ok := w.(http.Hijacker)
	if !ok {
		logger.Error("服务器不支持Hijack", zap.String("remoteAddr", r.RemoteAddr))
		http.Error(w, "服务器不支持Hijack", http.StatusInternalServerError)
		return
	}

	conn, bufrw, err := hj.Hijack()
	if err != nil {
		logger.Error("Hijack失败", zap.Error(err), zap.String("remoteAddr", r.RemoteAddr))
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}

	// 发送标准的200响应,建立隧道
	_, err = bufrw.WriteString("HTTP/1.1 200 Connection established\r\n\r\n")
	if err != nil {
		logger.Error("发送200响应失败", zap.Error(err), zap.String("remoteAddr", r.RemoteAddr))
		conn.Close()
		return
	}
	err = bufrw.Flush()
	if err != nil {
		logger.Error("刷新缓冲区失败", zap.Error(err), zap.String("remoteAddr", r.RemoteAddr))
		conn.Close()
		return
	}

	// 启动中间人代理,连接的关闭由handleHTTPSMitm负责
	go handleHTTPSMitm(cm, tm, conn)
}

3.3 集成上游代理

添加上游代理配置,所有对外请求通过指定代理转发:

go 复制代码
// 全局上游代理配置
var upstreamProxyURL *url.URL

// 创建带上游代理的HTTP客户端
func createProxyClient() *http.Client {
	transport := &http.Transport{
		TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
		Proxy:           http.ProxyURL(upstreamProxyURL), // 使用上游代理
	}

	return &http.Client{
		Timeout:   10 * time.Minute,
		Transport: transport,
	}
}

3.4 缓存功能实现

基于文件扩展名的缓存策略,自动缓存常用资源:

go 复制代码
// 判断URL是否可缓存
func (cm *CacheManager) isCacheableURL(urlStr string) bool {
	ext := filepath.Ext(urlStr)
	return cm.cacheableExt[strings.ToLower(ext)]
}

// 缓存命中逻辑
if req.Method == http.MethodGet && cm.hasCache(fullURL) {
	logger.Info("缓存命中", zap.String("url", fullURL))
	data, err := cm.readCache(fullURL)
	if err != nil {
		http.Error(rw, fmt.Sprintf("读取缓存失败: %v", err), http.StatusInternalServerError)
		req.Body.Close()
		continue
	}
	rw.Header().Set("X-Cache", "HIT")
	rw.WriteHeader(http.StatusOK)
	rw.Write(data)
	req.Body.Close()
	continue
}

3.5 替换Zap日志框架

替代原生fmt.Printf,实现结构化日志输出:

go 复制代码
// 初始化Zap日志
func initLogger() {
	config := zap.NewProductionConfig()
	config.EncoderConfig = zapcore.EncoderConfig{
		TimeKey:        "time",
		LevelKey:       "level",
		NameKey:        "logger",
		CallerKey:      "caller",
		FunctionKey:    zapcore.OmitKey,
		MessageKey:     "msg",
		StacktraceKey:  "stacktrace",
		LineEnding:     zapcore.DefaultLineEnding,
		EncodeLevel:    zapcore.CapitalLevelEncoder,
		EncodeTime:     zapcore.ISO8601TimeEncoder,
		EncodeDuration: zapcore.SecondsDurationEncoder,
		EncodeCaller:   zapcore.ShortCallerEncoder,
	}
	config.Level = zap.NewAtomicLevelAt(zap.InfoLevel)

	var err error
	logger, err = config.Build()
	if err != nil {
		panic(fmt.Sprintf("初始化日志失败: %v", err))
	}
	defer logger.Sync()
}

四、CA证书配置

4.1 生成CA证书

启动代理时会自动生成CA证书(路径:./proxy_ca/ca.crt),需要将证书导入客户端信任列表。

4.2 不同系统证书导入

Ubuntu/Debian
bash 复制代码
sudo cp ca.crt /usr/local/share/ca-certificates/
sudo update-ca-certificates
AnolisOS/CentOS/RHEL
bash 复制代码
dnf install -y ca-certificates
cp ca.crt /etc/pki/ca-trust/source/anchors/
update-ca-trust extract
临时测试(跳过证书验证)
bash 复制代码
curl -x http://192.168.56.1:8080 --proxy-insecure -O https://github.com/labring/sealos/releases/download/v5.1.1/sealos_5.1.1_linux_amd64.tar.gz

五、运行与测试

5.1 编译运行

bash 复制代码
# 编译
go build -o myproxy main.go

# 运行(指定上游代理)
./myproxy -l 0.0.0.0:8080 -u http://192.168.56.1:10811

5.2 客户端测试

bash 复制代码
# 下载文件(自动缓存)
curl -x http://192.168.56.1:8080 -O https://github.com/labring/sealos/releases/download/v5.1.1/sealos_5.1.1_linux_amd64.tar.gz

# 验证缓存(第二次下载会命中缓存)
curl -x http://192.168.56.1:8080 -O https://github.com/labring/sealos/releases/download/v5.1.1/sealos_5.1.1_linux_amd64.tar.gz

六、关键问题排查

6.1 CONNECT请求301重定向

  • 原因:Go net/http默认路径规范化导致
  • 解决:自定义Handler完全接管请求,绕过ServeMux

6.2 use of closed network connection

  • 原因:handleConnect中defer conn.Close()提前关闭连接
  • 解决:移除defer,由handleHTTPSMitm负责连接关闭

6.3 tls: bad record MAC

  • 原因:客户端不信任代理CA证书
  • 解决:导入CA证书或使用--proxy-insecure跳过验证

七、总结

本文实现的代理服务器具备以下核心能力:

  1. 支持HTTP/HTTPS全量代理,解决了Go原生库的301重定向问题
  2. 集成上游代理,可通过代理访问外部资源
  3. 基于文件扩展名的缓存策略,减少重复下载
  4. 替换Zap日志框架,实现结构化、分级别的日志输出
  5. 跨系统的CA证书配置方案,适配Ubuntu/AnolisOS等发行版

该代理可广泛应用于开发环境、内网镜像加速、资源缓存等场景,完整代码已开源至Gitee,可直接部署使用。

附:命令行参数说明

参数 简写 说明 默认值
--listen-addr -l 监听地址 0.0.0.0:8080
--cache-dir -d 缓存目录 ./proxy_cache
--ca-dir -c CA证书目录 ./proxy_ca
--upstream-proxy -u 上游代理地址
--help -h 显示帮助 -
相关推荐
灰子学技术3 小时前
Envoy与Istio HTTP流量故障转移机制介绍
网络·网络协议·http·云原生·istio
全栈前端老曹14 小时前
【Redis】Redis 持久化机制 RDB 与 AOF
前端·javascript·数据库·redis·缓存·node.js·全栈
生命因何探索16 小时前
Redis-持久化
数据库·redis·缓存
果粒蹬i21 小时前
【HarmonyOS】RN of HarmonyOS实战开发项目+TanStack缓存策略
缓存·华为·harmonyos
thginWalker21 小时前
Redis的常用命令
数据库·redis·缓存
Re.不晚1 天前
Redis核心原理底层机制——持久化【RDB与AOF】
数据库·redis·缓存
seeInfinite1 天前
LLM面试相关汇总
数据库·redis·缓存
竟未曾年少轻狂1 天前
Spring Boot 项目集成 Redis
java·spring boot·redis·缓存·消息队列·wpf·redis集群
运维有小邓@1 天前
基于证书的身份验证:入门指南
网络协议·https·ssl