一、项目背景
在日常开发和运维中,我们经常需要访问各类开源软件包、镜像文件,反复下载相同资源会浪费大量带宽和时间。本文将手把手教你用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跳过验证
七、总结
本文实现的代理服务器具备以下核心能力:
- 支持HTTP/HTTPS全量代理,解决了Go原生库的301重定向问题
- 集成上游代理,可通过代理访问外部资源
- 基于文件扩展名的缓存策略,减少重复下载
- 替换Zap日志框架,实现结构化、分级别的日志输出
- 跨系统的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 | 显示帮助 | - |