From 1a2c7bd06d03ba375915ea745197222b4421d8a7 Mon Sep 17 00:00:00 2001 From: wood chen Date: Tue, 27 May 2025 08:18:40 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0302=E8=B7=B3=E8=BD=AC?= =?UTF-8?q?=E6=94=AF=E6=8C=81=EF=BC=8C=E6=9B=B4=E6=96=B0=E7=9B=B8=E5=85=B3?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=E5=92=8C=E5=A4=84=E7=90=86=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/config/types.go | 21 +++++ internal/handler/proxy.go | 33 +++++--- internal/handler/redirect.go | 129 ++++++++++++++++++++++++++++++ web/app/dashboard/config/page.tsx | 8 +- 4 files changed, 178 insertions(+), 13 deletions(-) create mode 100644 internal/handler/redirect.go diff --git a/internal/config/types.go b/internal/config/types.go index cc4ac41..a422dc6 100644 --- a/internal/config/types.go +++ b/internal/config/types.go @@ -13,6 +13,7 @@ type PathConfig struct { DefaultTarget string `json:"DefaultTarget"` // 默认目标URL ExtensionMap []ExtRuleConfig `json:"ExtensionMap"` // 扩展名映射规则 ExtRules []ExtensionRule `json:"-"` // 内部使用,存储处理后的扩展名规则 + RedirectMode bool `json:"RedirectMode"` // 是否使用302跳转模式 } // ExtensionRule 表示一个扩展名映射规则(内部使用) @@ -21,6 +22,7 @@ type ExtensionRule struct { Target string // 目标服务器 SizeThreshold int64 // 最小阈值 MaxSize int64 // 最大阈值 + RedirectMode bool // 是否使用302跳转模式 } type CompressionConfig struct { @@ -39,6 +41,7 @@ type ExtRuleConfig struct { Target string `json:"Target"` // 目标服务器 SizeThreshold int64 `json:"SizeThreshold"` // 最小阈值 MaxSize int64 `json:"MaxSize"` // 最大阈值 + RedirectMode bool `json:"RedirectMode"` // 是否使用302跳转模式 } // 处理扩展名映射的方法 @@ -55,6 +58,7 @@ func (p *PathConfig) ProcessExtensionMap() { Target: rule.Target, SizeThreshold: rule.SizeThreshold, MaxSize: rule.MaxSize, + RedirectMode: rule.RedirectMode, } // 处理扩展名列表 @@ -87,3 +91,20 @@ func (p *PathConfig) GetProcessedExtTarget(ext string) (string, bool) { return "", false } + +// GetProcessedExtRule 获取扩展名对应的完整规则信息,包括RedirectMode +func (p *PathConfig) GetProcessedExtRule(ext string) (*ExtensionRule, bool) { + if p.ExtRules == nil { + return nil, false + } + + for _, rule := range p.ExtRules { + for _, e := range rule.Extensions { + if e == ext { + return &rule, true + } + } + } + + return nil, false +} diff --git a/internal/handler/proxy.go b/internal/handler/proxy.go index 1b56e4a..27816df 100644 --- a/internal/handler/proxy.go +++ b/internal/handler/proxy.go @@ -45,14 +45,15 @@ var hopHeadersBase = map[string]bool{ type ErrorHandler func(w http.ResponseWriter, r *http.Request, err error) type ProxyHandler struct { - pathMap map[string]config.PathConfig - prefixTree *prefixMatcher // 添加前缀匹配树 - client *http.Client - startTime time.Time - config *config.Config - auth *authManager - errorHandler ErrorHandler - Cache *cache.CacheManager + pathMap map[string]config.PathConfig + prefixTree *prefixMatcher // 添加前缀匹配树 + client *http.Client + startTime time.Time + config *config.Config + auth *authManager + errorHandler ErrorHandler + Cache *cache.CacheManager + redirectHandler *RedirectHandler // 添加302跳转处理器 } // 前缀匹配器结构体 @@ -166,10 +167,11 @@ func NewProxyHandler(cfg *config.Config) *ProxyHandler { return nil }, }, - startTime: time.Now(), - config: cfg, - auth: newAuthManager(), - Cache: cacheManager, + startTime: time.Now(), + config: cfg, + auth: newAuthManager(), + Cache: cacheManager, + redirectHandler: NewRedirectHandler(), // 初始化302跳转处理器 errorHandler: func(w http.ResponseWriter, r *http.Request, err error) { log.Printf("[Error] %s %s -> %v from %s", r.Method, r.URL.Path, err, utils.GetRequestSource(r)) w.WriteHeader(http.StatusInternalServerError) @@ -236,6 +238,13 @@ func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { return } + // 检查是否需要进行302跳转 + if h.redirectHandler != nil && h.redirectHandler.HandleRedirect(w, r, pathConfig, decodedPath) { + // 如果进行了302跳转,直接返回,不继续处理 + collector.RecordRequest(r.URL.Path, http.StatusFound, time.Since(start), 0, utils.GetClientIP(r), r) + return + } + // 使用统一的路由选择逻辑 targetBase, usedAltTarget := utils.GetTargetURL(h.client, r, pathConfig, decodedPath) diff --git a/internal/handler/redirect.go b/internal/handler/redirect.go new file mode 100644 index 0000000..2053b04 --- /dev/null +++ b/internal/handler/redirect.go @@ -0,0 +1,129 @@ +package handler + +import ( + "log" + "net/http" + "net/url" + "path/filepath" + "proxy-go/internal/config" + "proxy-go/internal/utils" + "strings" +) + +// RedirectHandler 处理302跳转逻辑 +type RedirectHandler struct{} + +// NewRedirectHandler 创建新的跳转处理器 +func NewRedirectHandler() *RedirectHandler { + return &RedirectHandler{} +} + +// HandleRedirect 处理302跳转请求 +func (rh *RedirectHandler) HandleRedirect(w http.ResponseWriter, r *http.Request, pathConfig config.PathConfig, targetPath string) bool { + // 检查是否需要进行302跳转 + shouldRedirect, targetURL := rh.shouldRedirect(r, pathConfig, targetPath) + + if !shouldRedirect { + return false + } + + // 执行302跳转 + rh.performRedirect(w, r, targetURL) + return true +} + +// shouldRedirect 判断是否应该进行302跳转,并返回目标URL +func (rh *RedirectHandler) shouldRedirect(r *http.Request, pathConfig config.PathConfig, targetPath string) (bool, string) { + // 获取文件扩展名 + ext := strings.ToLower(filepath.Ext(targetPath)) + if ext != "" { + ext = ext[1:] // 去掉点号 + } + + // 首先检查扩展名规则是否有302跳转配置 + if rule, found := pathConfig.GetProcessedExtRule(ext); found && rule.RedirectMode { + // 使用扩展名规则的目标URL进行302跳转 + targetURL := rh.buildTargetURL(rule.Target, targetPath, r.URL.RawQuery) + return true, targetURL + } + + // 检查通配符规则 + if rule, found := pathConfig.GetProcessedExtRule("*"); found && rule.RedirectMode { + // 使用通配符规则的目标URL进行302跳转 + targetURL := rh.buildTargetURL(rule.Target, targetPath, r.URL.RawQuery) + return true, targetURL + } + + // 检查默认目标是否配置为302跳转 + if pathConfig.RedirectMode { + // 使用默认目标URL进行302跳转 + targetURL := rh.buildTargetURL(pathConfig.DefaultTarget, targetPath, r.URL.RawQuery) + return true, targetURL + } + + return false, "" +} + +// buildTargetURL 构建完整的目标URL +func (rh *RedirectHandler) buildTargetURL(baseURL, targetPath, rawQuery string) string { + // URL 解码,然后重新编码,确保特殊字符被正确处理 + decodedPath, err := url.QueryUnescape(targetPath) + if err != nil { + // 如果解码失败,使用原始路径 + decodedPath = targetPath + } + + // 重新编码路径,保留 '/' + parts := strings.Split(decodedPath, "/") + for i, part := range parts { + parts[i] = url.PathEscape(part) + } + encodedPath := strings.Join(parts, "/") + + // 构建完整URL + targetURL := baseURL + encodedPath + + // 添加查询参数 + if rawQuery != "" { + targetURL = targetURL + "?" + rawQuery + } + + return targetURL +} + +// performRedirect 执行302跳转 +func (rh *RedirectHandler) performRedirect(w http.ResponseWriter, r *http.Request, targetURL string) { + // 设置302跳转响应头 + w.Header().Set("Location", targetURL) + w.Header().Set("Proxy-Go-Redirect", "1") + + // 添加缓存控制头,避免浏览器缓存跳转响应 + w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate") + w.Header().Set("Pragma", "no-cache") + w.Header().Set("Expires", "0") + + // 设置状态码为302 + w.WriteHeader(http.StatusFound) + + // 记录跳转日志 + clientIP := utils.GetClientIP(r) + log.Printf("[Redirect] %s %s -> 302 %s (%s) from %s", + r.Method, r.URL.Path, targetURL, clientIP, utils.GetRequestSource(r)) +} + +// IsRedirectEnabled 检查路径配置是否启用了任何形式的302跳转 +func (rh *RedirectHandler) IsRedirectEnabled(pathConfig config.PathConfig) bool { + // 检查默认目标是否启用跳转 + if pathConfig.RedirectMode { + return true + } + + // 检查扩展名规则是否有启用跳转的 + for _, rule := range pathConfig.ExtRules { + if rule.RedirectMode { + return true + } + } + + return false +} diff --git a/web/app/dashboard/config/page.tsx b/web/app/dashboard/config/page.tsx index 80e0152..d19be50 100644 --- a/web/app/dashboard/config/page.tsx +++ b/web/app/dashboard/config/page.tsx @@ -34,6 +34,7 @@ interface ExtRuleConfig { Target: string; // 目标服务器 SizeThreshold: number; // 最小阈值(字节) MaxSize: number; // 最大阈值(字节) + RedirectMode?: boolean; // 是否使用302跳转模式 } interface PathMapping { @@ -41,6 +42,7 @@ interface PathMapping { ExtensionMap?: ExtRuleConfig[] // 只支持新格式 SizeThreshold?: number // 保留全局阈值字段(向后兼容) MaxSize?: number // 保留全局阈值字段(向后兼容) + RedirectMode?: boolean // 是否使用302跳转模式 } interface CompressionConfig { @@ -77,6 +79,7 @@ export default function ConfigPage() { const [newPathData, setNewPathData] = useState({ path: "", defaultTarget: "", + redirectMode: false, extensionMap: {} as Record, sizeThreshold: 0, maxSize: 0, @@ -281,6 +284,7 @@ export default function ConfigPage() { setNewPathData({ path: "", defaultTarget: "", + redirectMode: false, extensionMap: {}, sizeThreshold: 0, maxSize: 0, @@ -327,6 +331,7 @@ export default function ConfigPage() { setNewPathData({ path: "", defaultTarget: "", + redirectMode: false, extensionMap: {}, sizeThreshold: 0, maxSize: 0, @@ -375,6 +380,7 @@ export default function ConfigPage() { setNewPathData({ path: "", defaultTarget: "", + redirectMode: false, extensionMap: {}, sizeThreshold: 0, maxSize: 0, @@ -1109,4 +1115,4 @@ const convertBytesToUnit = (bytes: number): { value: number, unit: 'B' | 'KB' | value: Number((bytes / Math.pow(k, i)).toFixed(2)), unit: sizes[i] } -} \ No newline at end of file +}