Files
anyproxy/main.go

251 lines
6.8 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package main
import (
"flag"
"fmt"
"io"
"log/slog"
"net/http"
"net/url"
"os"
"strings"
"sync/atomic"
"github.com/gin-gonic/gin"
)
// 全局请求计数器,使用原子操作确保线程安全
var requestCounter int64
func main() {
port := flag.Int("port", 8080, "代理服务器监听的端口")
debug := flag.Bool("debug", false, "是否启用调试模式")
logFile := flag.String("log", "", "日志文件路径,默认为标准输出")
flag.Parse()
// 配置slog
var logger *slog.Logger
// 根据调试模式设置日志级别
var logLevel slog.Level
if *debug {
logLevel = slog.LevelDebug
} else {
logLevel = slog.LevelInfo
}
// 创建处理器选项
opts := &slog.HandlerOptions{
Level: logLevel,
}
if *logFile != "" {
file, err := os.OpenFile(*logFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0666)
if err != nil {
slog.Error("无法打开日志文件", "error", err)
os.Exit(1)
}
logger = slog.New(slog.NewJSONHandler(file, opts))
} else {
logger = slog.New(slog.NewJSONHandler(os.Stdout, opts))
}
slog.SetDefault(logger)
if *debug {
gin.SetMode(gin.DebugMode) // 启用调试模式
} 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)
slog.Info("HTTP 代理服务器启动", "port", *port)
if err := r.Run(fmt.Sprintf(":%d", *port)); err != nil {
slog.Error("启动服务器失败", "error", err)
}
}
// 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
parsedURL, err := url.Parse(targetURLStr)
if err != nil {
c.String(http.StatusBadRequest, "无效的目标 URL: %v", err)
return
}
// 合并查询参数
originalQuery := c.Request.URL.Query()
targetQuery := parsedURL.Query()
for key, values := range originalQuery {
for _, value := range values {
targetQuery.Add(key, value)
}
}
parsedURL.RawQuery = targetQuery.Encode()
// 重新构建目标URL字符串
targetURLStr = parsedURL.String()
// 检查 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
parsedURL, err := url.Parse(targetURLStr)
if err != nil {
c.String(http.StatusBadRequest, "无效的目标 URL: %v", err)
return
}
// 合并查询参数
originalQuery := c.Request.URL.Query()
targetQuery := parsedURL.Query()
for key, values := range originalQuery {
for _, value := range values {
targetQuery.Add(key, value)
}
}
parsedURL.RawQuery = targetQuery.Encode()
// 重新构建目标URL字符串
targetURLStr = parsedURL.String()
// 检查 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) {
// 增加请求计数器
reqID := atomic.AddInt64(&requestCounter, 1)
slog.Debug("收到请求",
"reqID", reqID,
"method", c.Request.Method,
"uri", c.Request.RequestURI,
"target", targetURLStr)
// 创建到目标服务器的请求
// 注意:我们直接将原始请求的 Body 传递过去
proxyReq, err := http.NewRequest(c.Request.Method, targetURLStr, c.Request.Body)
if err != nil {
slog.Error("创建代理请求失败", "reqID", reqID, "error", err)
c.String(http.StatusInternalServerError, "创建代理请求失败: %v", err)
return
}
// 复制原始请求的 Headers
proxyReq.Header = c.Request.Header
// 发送代理请求
client := &http.Client{}
resp, err := client.Do(proxyReq)
if err != nil {
slog.Error("请求目标服务器失败", "reqID", reqID, "error", err)
c.String(http.StatusBadGateway, "请求目标服务器失败: %s", err.Error())
return
}
defer resp.Body.Close()
slog.Debug("收到响应",
"reqID", reqID,
"status_code", resp.StatusCode,
"status", resp.Status)
// 复制目标服务器响应的 Headers 到原始响应
for key, values := range resp.Header {
for _, value := range values {
c.Header(key, value)
}
}
// 将目标服务器的响应状态码设置到原始响应
c.Status(resp.StatusCode)
// 将目标服务器的响应 Body 直接流式传输到客户端
// 使用 io.Copy 更高效,并能处理各种编码(如 chunked
bytesCopied, err := io.Copy(c.Writer, resp.Body)
if err != nil {
slog.Error("写入响应 Body 时出错", "reqID", reqID, "error", err)
}
slog.Debug("响应写入完成", "reqID", reqID, "bytes_copied", bytesCopied)
}
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)
}