4 Commits
v1.0.1 ... main

11 changed files with 455 additions and 154 deletions

View File

@@ -1,29 +1,35 @@
# AnyProxy 简介
AnyProxy 是一个简单的 HTTP/HTTPS 代理服务器。它可以帮助你转发和代理请求。
支持 GET / POST / PUT / DELETE / HEAD / OPTIONS 请求。
兼容 SSE 流式请求。
## 使用方法
1. **直接协议路径**
- 目标URL: `https://example.com/path`
代理URL: `http://AnyproxyIP/https/example.com/path`
- 目标URL: `http://example.com/path`
代理URL: `http://AnyproxyIP/http/example.com/path`
2. **完整URL路径**
- 目标URL: `https://example.com`
代理URL: `http://AnyproxyIP/proxy/https://example.com`
- 目标 URL: `https://example.com/path`
代理 URL: `http://AnyproxyIP/https/example.com/path`
- 目标 URL: `http://example.com/path`
代理 URL: `http://AnyproxyIP/http/example.com/path`
> 目标URL 必须以 `https://` 或 `http://` 开头。
2. **完整 URL 路径**
- 目标 URL: `https://example.com`
代理 URL: `http://AnyproxyIP/proxy/https://example.com`
> 目标 URL 必须以 `https://` 或 `http://` 开头。
> 访问根路径可以查看使用方式
## 安装
1. 下载对应平台的二进制Relase文件
1. 下载对应平台的二进制 Relase 文件
2. 运行二进制文件
3. (可选) 配置为系统服务
系统服务参考(Systemd)
~~~ ini
### 系统服务参考(Systemd)
```ini
# /etc/systemd/system/anyproxy.service
[Unit]
Description=AnyProxy Service
@@ -37,4 +43,15 @@ User=root
[Install]
WantedBy=multi-user.target
~~~
```
## 可选参数
| 参数 | 是否可选 | 默认值 | 数据类型 | 解释 |
| -------- | -------: | :---------------: | -------- | --------------------------------- |
| -port | 是 | 8080 | int | 代理服务器监听端口 |
| -debug | 是 | false | bool | 调试模式(等价于未指定 -log-level 时将日志等级提升为 debug |
| -log-level | 是 | warn | string | 日志等级: debug / info / warn / error |
| -log | 是 | (输出到 stderr | string | 日志文件路径(默认输出到 stderr |
| -grace | 是 | 10 | int | 优雅停机等待秒数 |
| -timeout | 是 | 0 | int | 单次上游请求超时秒0 = 不设置) |

1
go.mod
View File

@@ -17,6 +17,7 @@ require (
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/lmittmann/tint v1.1.2 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect

2
go.sum
View File

@@ -31,6 +31,8 @@ github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/lmittmann/tint v1.1.2 h1:2CQzrL6rslrsyjqLDwD11bZ5OpLBPU+g3G/r5LSfS8w=
github.com/lmittmann/tint v1.1.2/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=

34
internal/config/config.go Normal file
View File

@@ -0,0 +1,34 @@
package config
import (
"flag"
"fmt"
)
// Config 保存程序配置
type Config struct {
Port int
Debug bool
LogFile string
ShutdownGrace int // 优雅停机等待秒数
RequestTimeout int // 上游整体请求超时时间(秒)
LogLevel string // 日志等级: debug|info|warn|error
}
// Parse 解析命令行参数返回配置
func Parse() *Config {
cfg := &Config{}
flag.IntVar(&cfg.Port, "port", 8080, "代理服务器监听端口")
flag.BoolVar(&cfg.Debug, "debug", false, "调试模式 (debug level log)")
flag.StringVar(&cfg.LogFile, "log", "", "日志文件路径 (默认输出到 stderr)")
flag.IntVar(&cfg.ShutdownGrace, "grace", 10, "优雅停机等待秒数")
flag.IntVar(&cfg.RequestTimeout, "timeout", 0, "单次上游请求超时秒(0=不设置)")
flag.StringVar(&cfg.LogLevel, "log-level", "warn", "日志等级: debug|info|warn|error (默认 warn)")
flag.Parse()
// 兼容旧的 -debug 参数: 当 -debug 为 true 且未显式指定其它日志等级(仍为默认 warn) 时,提升为 debug
if cfg.Debug && cfg.LogLevel == "warn" { cfg.LogLevel = "debug" }
return cfg
}
func (c *Config) Addr() string { return fmt.Sprintf(":%d", c.Port) }

View File

@@ -0,0 +1,31 @@
package middleware
import (
"log/slog"
"time"
"github.com/gin-gonic/gin"
)
// Logger 使用 slog 输出结构化访问日志
func Logger(logger *slog.Logger) gin.HandlerFunc {
return func(c *gin.Context) {
start := time.Now()
path := c.Request.URL.Path
raw := c.Request.URL.RawQuery
c.Next()
if raw != "" { path = path + "?" + raw }
latency := time.Since(start)
status := c.Writer.Status()
logger.Info("HTTP请求",
"req_id", GetReqID(c),
"method", c.Request.Method,
"path", path,
"status", status,
"latency_ms", latency.Milliseconds(),
"size", c.Writer.Size(),
"ip", c.ClientIP(),
"ua", c.GetHeader("User-Agent"),
)
}
}

View File

@@ -0,0 +1,30 @@
package middleware
import (
"log/slog"
"net/http"
"runtime/debug"
"github.com/gin-gonic/gin"
)
// Recovery 捕获 panic 并记录堆栈信息
func Recovery(logger *slog.Logger) gin.HandlerFunc {
return func(c *gin.Context) {
defer func() {
if rcv := recover(); rcv != nil {
logger.Error("发生Panic",
"req_id", GetReqID(c),
"error", rcv,
"stack", string(debug.Stack()),
"path", c.Request.URL.Path,
)
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{
"error": "内部服务器错误",
"req_id": GetReqID(c),
})
}
}()
c.Next()
}
}

View File

@@ -0,0 +1,32 @@
package middleware
import (
"fmt"
"sync/atomic"
"github.com/gin-gonic/gin"
)
const RequestIDKey = "reqID"
var globalReqID atomic.Int64
// RequestID 生成自增的请求 ID 并注入上下文及响应头
func RequestID() gin.HandlerFunc {
return func(c *gin.Context) {
id := globalReqID.Add(1)
c.Set(RequestIDKey, id)
c.Writer.Header().Set("X-Request-ID", fmt.Sprintf("%d", id))
c.Next()
}
}
// GetReqID 从上下文中获取请求 ID
func GetReqID(c *gin.Context) int64 {
if v, ok := c.Get(RequestIDKey); ok {
if id, ok2 := v.(int64); ok2 {
return id
}
}
return 0
}

166
internal/proxy/proxy.go Normal file
View File

@@ -0,0 +1,166 @@
package proxy
import (
"bufio"
"errors"
"fmt"
"io"
"log/slog"
"mime"
"net/http"
"strings"
"sync/atomic"
"github.com/gin-gonic/gin"
"anyproxy/internal/middleware"
)
// 转发的总请求计数器
var totalForwarded atomic.Int64
// Proxy 封装具体的转发逻辑
type Proxy struct {
Client *http.Client
Log *slog.Logger
}
func New(client *http.Client, logger *slog.Logger) *Proxy {
return &Proxy{Client: client, Log: logger}
}
// HandleProxyPath 处理 /proxy/*path 形式的请求
func (p *Proxy) HandleProxyPath(c *gin.Context) {
urlStr, err := BuildFromProxyPath(c.Param("proxyPath"), c.Request.URL.Query())
if err != nil {
p.writeError(c, http.StatusBadRequest, err)
return
}
p.forward(c, urlStr)
}
// HandleProtocol 处理 /:protocol/*remainder 形式的请求
func (p *Proxy) HandleProtocol(c *gin.Context) {
urlStr, err := BuildFromProtocol(c.Param("protocol"), c.Param("remainder"), c.Request.URL.Query())
if err != nil {
p.writeError(c, http.StatusBadRequest, err)
return
}
p.forward(c, urlStr)
}
func (p *Proxy) writeError(c *gin.Context, code int, err error) {
c.JSON(code, gin.H{"error": err.Error(), "req_id": middleware.GetReqID(c)})
}
func (p *Proxy) forward(c *gin.Context, target string) {
reqID := middleware.GetReqID(c)
current := totalForwarded.Add(1)
p.Log.Debug("开始转发请求",
"req_id", reqID,
"count", current,
"method", c.Request.Method,
"target", target,
"uri", c.Request.RequestURI,
)
// 基于原始上下文创建上游请求(支持客户端断开时取消)
upReq, err := http.NewRequestWithContext(c.Request.Context(), c.Request.Method, target, c.Request.Body)
if err != nil {
p.Log.Error("创建上游请求失败", "req_id", reqID, "error", err)
p.writeError(c, http.StatusInternalServerError, errors.New("创建上游请求失败"))
return
}
upReq.Header = c.Request.Header.Clone()
// 仅在 SSE 时禁用压缩;稍后检测
resp, err := p.Client.Do(upReq)
if err != nil {
p.Log.Error("上游请求失败", "req_id", reqID, "error", err)
p.writeError(c, http.StatusBadGateway, errors.New("上游请求失败"))
return
}
defer resp.Body.Close()
mediaType, _, _ := mime.ParseMediaType(resp.Header.Get("Content-Type"))
isSSE := mediaType == "text/event-stream"
p.Log.Debug("上游响应", "req_id", reqID, "status", resp.StatusCode, "sse", isSSE)
// 复制上游响应头(最小化过滤)
for k, vs := range resp.Header {
for _, v := range vs { c.Header(k, v) }
}
if isSSE {
c.Writer.Header().Del("Content-Length")
c.Writer.Header().Del("Transfer-Encoding")
c.Header("Content-Type", "text/event-stream")
c.Header("Cache-Control", "no-cache")
c.Header("Connection", "keep-alive")
c.Header("X-Accel-Buffering", "no")
// 确保禁用上游压缩避免 SSE 事件被聚合
upReq.Header.Del("Accept-Encoding")
}
c.Status(resp.StatusCode)
if f, ok := c.Writer.(http.Flusher); ok { f.Flush() }
if !isSSE {
if _, err := io.Copy(c.Writer, resp.Body); err != nil {
p.Log.Error("写入响应体失败", "req_id", reqID, "error", err)
}
return
}
reader := bufio.NewReader(resp.Body)
w := c.Writer
flusher, _ := w.(http.Flusher)
for {
line, err := reader.ReadBytes('\n')
if len(line) > 0 {
if _, werr := w.Write(line); werr != nil {
p.Log.Warn("SSE写入失败", "req_id", reqID, "error", werr)
return
}
if flusher != nil { flusher.Flush() }
}
if err != nil {
if errors.Is(err, io.EOF) {
p.Log.Debug("SSE结束(EOF)", "req_id", reqID)
} else {
p.Log.Error("SSE读取失败", "req_id", reqID, "error", err)
}
return
}
}
}
// HelloPage 返回简单状态页面
func HelloPage(c *gin.Context) {
count := totalForwarded.Load()
// 推断外部可见协议与主机(支持反向代理常见头)
scheme := "http"
if c.Request.TLS != nil { scheme = "https" }
if xf := c.GetHeader("X-Forwarded-Proto"); xf != "" {
// 取第一个
scheme = strings.TrimSpace(strings.Split(xf, ",")[0])
}
host := c.Request.Host
if xfh := c.GetHeader("X-Forwarded-Host"); xfh != "" {
host = strings.TrimSpace(strings.Split(xfh, ",")[0])
}
base := scheme + "://" + host
str := fmt.Sprintf("AnyProxy 服务器正在运行... 已转发 %d 个请求", count)
str += "\n\n使用方法:\n"
str += "方式1 - 直接协议路径: \n"
str += fmt.Sprintf(" 目标URL: https://example.com/path --> 代理URL: %s/https/example.com/path\n", base)
str += fmt.Sprintf(" 目标URL: http://example.com/path --> 代理URL: %s/http/example.com/path\n\n", base)
str += "方式2 - 完整URL路径: \n"
str += fmt.Sprintf(" 目标URL: https://example.com --> 代理URL: %s/proxy/https://example.com\n", base)
str += fmt.Sprintf(" 目标URL: http://example.com --> 代理URL: %s/proxy/http://example.com\n\n", base)
str += "目标URL必须以 https:// 或 http:// 开头。\n\n"
str += fmt.Sprintf("本机访问基地址: %s\n", base)
c.String(200, str)
}

51
internal/proxy/url.go Normal file
View File

@@ -0,0 +1,51 @@
package proxy
import (
"errors"
"net/url"
"strings"
)
// normalizeURL 规范化URL格式处理缺少斜杠的情况
func normalizeURL(rawURL string) string {
if strings.HasPrefix(rawURL, "https:/") && !strings.HasPrefix(rawURL, "https://") {
return strings.Replace(rawURL, "https:/", "https://", 1)
}
if strings.HasPrefix(rawURL, "http:/") && !strings.HasPrefix(rawURL, "http://") {
return strings.Replace(rawURL, "http:/", "http://", 1)
}
return rawURL
}
// BuildFromProxyPath 构建 /proxy/*path 形式传入的 URL
func BuildFromProxyPath(pathPart string, originalQuery url.Values) (string, error) {
pathPart = strings.TrimPrefix(pathPart, "/")
if pathPart == "" { return "", errors.New("目标为空") }
pathPart = normalizeURL(pathPart)
return mergeQuery(pathPart, originalQuery)
}
// BuildFromProtocol 构建 /:protocol/*remainder 形式
func BuildFromProtocol(protocol, remainder string, originalQuery url.Values) (string, error) {
if protocol != "http" && protocol != "https" {
return "", errors.New("不支持的协议")
}
full := protocol + ":/" + remainder
full = normalizeURL(full)
return mergeQuery(full, originalQuery)
}
func mergeQuery(raw string, original url.Values) (string, error) {
parsed, err := url.Parse(raw)
if err != nil { return "", err }
// 合并 query
q := parsed.Query()
for k, vs := range original {
for _, v := range vs { q.Add(k, v) }
}
parsed.RawQuery = q.Encode()
if _, err := url.ParseRequestURI(parsed.String()); err != nil {
return "", err
}
return parsed.String(), nil
}

View File

@@ -0,0 +1,15 @@
package version
import "runtime/debug"
var (
Version = "1.1.0-rc"
GitCommit = ""
BuildInfo = ""
)
func init() {
if info, ok := debug.ReadBuildInfo(); ok {
BuildInfo = info.Main.Version
}
}

222
main.go
View File

@@ -1,166 +1,88 @@
package main
import (
"flag"
"fmt"
"context"
"io"
"log/slog"
"net/http"
"net/url"
"os"
"os/signal"
"strings"
"sync/atomic"
"syscall"
"time"
"github.com/gin-gonic/gin"
"github.com/lmittmann/tint"
"anyproxy/internal/config"
"anyproxy/internal/middleware"
"anyproxy/internal/proxy"
"anyproxy/internal/version"
)
// 全局请求计数器,使用原子操作确保线程安全
var requestCounter int64
func main() {
cfg := config.Parse()
port := flag.Int("port", 8080, "代理服务器监听的端口")
debug := flag.Bool("debug", false, "是否启用调试模式")
flag.Parse()
// 日志初始化设置 (支持显式日志等级)
levelVar := new(slog.LevelVar)
lvlStr := strings.ToLower(cfg.LogLevel)
switch lvlStr {
case "debug":
levelVar.Set(slog.LevelDebug)
case "info":
levelVar.Set(slog.LevelInfo)
case "warn", "warning":
levelVar.Set(slog.LevelWarn)
case "error", "err":
levelVar.Set(slog.LevelError)
default:
levelVar.Set(slog.LevelWarn) // 回退到默认 warn
}
var writer io.Writer = os.Stderr
if cfg.LogFile != "" {
f, err := os.OpenFile(cfg.LogFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
if err != nil { panic(err) }
writer = io.MultiWriter(os.Stderr, f)
}
h := tint.NewHandler(writer, &tint.Options{AddSource: true, Level: levelVar, TimeFormat: "2006-01-02 15:04:05"})
logger := slog.New(h)
slog.SetDefault(logger)
if *debug {
gin.SetMode(gin.DebugMode) // 启用调试模式
if cfg.Debug || lvlStr == "debug" { gin.SetMode(gin.DebugMode) } else { gin.SetMode(gin.ReleaseMode) }
// 可复用的 HTTP 客户端(保持连接复用)
transport := &http.Transport{Proxy: http.ProxyFromEnvironment, DisableCompression: true}
client := &http.Client{Transport: transport}
if cfg.RequestTimeout > 0 { client.Timeout = time.Duration(cfg.RequestTimeout) * time.Second }
p := proxy.New(client, logger)
r := gin.New()
r.Use(middleware.Recovery(logger), middleware.RequestID(), middleware.Logger(logger))
r.GET("/", proxy.HelloPage) // 欢迎页面
r.Any("/proxy/*proxyPath", p.HandleProxyPath) // 处理 /proxy/*path 形式的请求
r.Any(":protocol/*remainder", p.HandleProtocol) // 处理 /:protocol/*remainder 形式的请求
logger.Info("服务器启动", "addr", cfg.Addr(), "debug", cfg.Debug, "log_level", lvlStr, "version", version.Version, "commit", version.GitCommit)
// 优雅停机设置:监听系统信号,执行平滑关闭
srv := &http.Server{Addr: cfg.Addr(), Handler: r}
go func() {
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
logger.Error("服务器监听错误", "error", err)
}
}()
stop := make(chan os.Signal, 1)
signal.Notify(stop, syscall.SIGINT, syscall.SIGTERM)
<-stop
logger.Info("开始关闭 (收到退出信号)")
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(cfg.ShutdownGrace)*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
logger.Error("关闭出错", "error", err)
} else {
gin.SetMode(gin.ReleaseMode) // 在调试时暂时注释掉
}
r := gin.Default()
// 处理根路径
r.GET("/", HelloPage)
// 使用 "catch-all" 路由来捕获所有代理请求
// 这里我们使用 /proxy/* 前缀来避免与根路径冲突
r.Any("/proxy/*proxyPath", proxyHandler)
// 为了保持向后兼容我们也可以处理直接的URL请求
// 检查是否以协议开头的路径
r.Any("/:protocol/*remainder", protocolHandler)
fmt.Printf("HTTP 代理服务器启动,监听端口 :%d\n", *port)
if err := r.Run(fmt.Sprintf(":%d", *port)); err != nil {
fmt.Printf("启动服务器失败: %v\n", err)
logger.Info("关闭完成")
}
}
// normalizeURL 规范化URL格式处理缺少斜杠的情况
func normalizeURL(rawURL string) string {
// 处理 https:/example.com 或 http:/example.com 的情况
if strings.HasPrefix(rawURL, "https:/") && !strings.HasPrefix(rawURL, "https://") {
return strings.Replace(rawURL, "https:/", "https://", 1)
}
if strings.HasPrefix(rawURL, "http:/") && !strings.HasPrefix(rawURL, "http://") {
return strings.Replace(rawURL, "http:/", "http://", 1)
}
return rawURL
}
func proxyHandler(c *gin.Context) {
// 从路径参数中获取目标 URL
targetURLStr := c.Param("proxyPath")
// 移除前导斜杠
targetURLStr = strings.TrimPrefix(targetURLStr, "/")
// 规范化URL格式
targetURLStr = normalizeURL(targetURLStr)
// 检查 URL 合法性
if _, err := url.ParseRequestURI(targetURLStr); err != nil {
c.String(http.StatusBadRequest, "无效的目标 URL: %v", err)
return
}
// 执行代理请求
executeProxy(c, targetURLStr)
}
// protocolHandler 处理直接以协议开头的URL请求 (如 /https/example.com/path)
func protocolHandler(c *gin.Context) {
protocol := c.Param("protocol")
remainder := c.Param("remainder")
// 只处理 http 和 https 协议
if protocol != "http" && protocol != "https" {
c.String(http.StatusBadRequest, "不支持的协议: %s", protocol)
return
}
// 构建完整的URL
targetURLStr := protocol + ":/" + remainder
// 规范化URL格式
targetURLStr = normalizeURL(targetURLStr)
// 检查 URL 合法性
if _, err := url.ParseRequestURI(targetURLStr); err != nil {
c.String(http.StatusBadRequest, "无效的目标 URL: %v", err)
return
}
// 执行代理请求
executeProxy(c, targetURLStr)
}
// executeProxy 执行实际的代理请求
func executeProxy(c *gin.Context, targetURLStr string) {
// 增加请求计数器
atomic.AddInt64(&requestCounter, 1)
// 创建到目标服务器的请求
// 注意:我们直接将原始请求的 Body 传递过去
proxyReq, err := http.NewRequest(c.Request.Method, targetURLStr, c.Request.Body)
if err != nil {
c.String(http.StatusInternalServerError, "创建代理请求失败: %v", err)
return
}
// 复制原始请求的 Headers
proxyReq.Header = c.Request.Header
// 发送代理请求
client := &http.Client{}
resp, err := client.Do(proxyReq)
if err != nil {
c.String(http.StatusBadGateway, "请求目标服务器失败: %v", err)
return
}
defer resp.Body.Close()
// 复制目标服务器响应的 Headers 到原始响应
for key, values := range resp.Header {
for _, value := range values {
c.Header(key, value)
}
}
// 将目标服务器的响应状态码设置到原始响应
c.Status(resp.StatusCode)
// 将目标服务器的响应 Body 直接流式传输到客户端
// 使用 io.Copy 更高效,并能处理各种编码(如 chunked
_, err = io.Copy(c.Writer, resp.Body)
if err != nil {
// 如果在写入 body 时发生错误,记录下来
fmt.Printf("写入响应 Body 时出错: %v\n", err)
}
}
func HelloPage(c *gin.Context) {
// 获取当前的请求计数
count := atomic.LoadInt64(&requestCounter)
str := fmt.Sprintf("AnyProxy 服务器正在运行... 已转发 %d 个请求", count)
str += "\n\n使用方法:\n"
str += "方式1 - 直接协议路径: \n"
str += " 目标URL: https://example.com/path --> 代理URL: http://AnyproxyIP/https/example.com/path\n"
str += " 目标URL: http://example.com/path --> 代理URL: http://AnyproxyIP/http/example.com/path\n\n"
str += "方式2 - 完整URL路径: \n"
str += " 目标URL: https://example.com --> 代理URL: http://AnyproxyIP/proxy/https://example.com\n\n"
str += "目标URL必须以 https:// 或 http:// 开头。\n\n"
c.String(200, str)
}