// Package proxy 反向代理包,为 Lolly HTTP 服务器提供反向代理功能。 // // 该文件实现 Location 和 Refresh 响应头的改写功能。 // 当上游服务器返回重定向响应时,将 Location 头中的上游地址 // 改写为代理地址,确保客户端能正确访问。 // // 主要功能: // - default 模式:自动将上游地址替换为客户端原始 Host // - custom 模式:使用自定义规则(正则/前缀匹配)进行改写 // - off 模式:禁用改写 // - Refresh 头改写:支持 "N; url=URL" 格式 // - 变量展开:自定义规则中支持 Lolly 变量 // // 主要用途: // // 用于处理上游服务器返回的 3xx 重定向响应,确保客户端 // 收到的是代理地址而非内部上游地址。 // // 注意事项: // - 调用位置:必须在 modifyResponseHeaders 之前调用 // - default 模式使用前缀匹配,防止部分匹配问题 // // 作者:xfy package proxy import ( "regexp" "strings" "github.com/valyala/fasthttp" "rua.plus/lolly/internal/config" "rua.plus/lolly/internal/variable" ) // redirect_rewrite 模式常量,配置 redirect_rewrite.mode 字段。 const ( redirectModeDefault = "default" // 默认模式:自动替换上游地址为客户端 Host redirectModeOff = "off" // 关闭模式:不执行任何改写 redirectModeCustom = "custom" // 自定义模式:使用预编译规则进行改写 ) // compiledRule 预编译的改写规则。 // // 用于 custom 模式,在初始化时编译以避免运行时开销。 type compiledRule struct { pattern *regexp.Regexp // 正则表达式模式,nil 表示使用前缀匹配 replacement string // 替换模板,支持 Lolly 变量展开 exactMatch string // 前缀匹配字符串(非正则模式下使用) caseInsensitive bool // 正则匹配时是否忽略大小写(~* 前缀触发) } // RedirectRewriter Location 和 Refresh 响应头改写器。 // // 根据配置的模式对上游返回的重定向响应进行改写,确保客户端 // 收到的是代理地址而非内部上游服务器地址。 type RedirectRewriter struct { proxyPath string // 当前代理路径(如 "/api/"),用于 default 模式 mode string // 改写模式:"default" | "off" | "custom",空字符串视为 default rules []compiledRule // 预编译的改写规则,仅 custom 模式下使用 } // NewRedirectRewriter 创建重定向改写器实例。 // // 根据配置模式初始化改写器: // - nil 配置或未指定模式:默认启用 default 模式 // - custom 模式:预编译所有改写规则(正则/前缀匹配) // // 参数: // - cfg: 重定向改写配置,nil 时使用 default 模式 // - proxyPath: 当前代理路径(如 "/api/") // // 返回值: // - *RedirectRewriter: 改写器实例 // - error: 正则表达式编译失败时返回错误 func NewRedirectRewriter(cfg *config.RedirectRewriteConfig, proxyPath string) (*RedirectRewriter, error) { if cfg == nil { // 未配置时默认启用 default 模式 return &RedirectRewriter{ mode: redirectModeDefault, proxyPath: proxyPath, }, nil } rw := &RedirectRewriter{ mode: cfg.Mode, proxyPath: proxyPath, } // custom 模式:预编译规则 if cfg.Mode == redirectModeCustom { rules := make([]compiledRule, 0, len(cfg.Rules)) for _, rule := range cfg.Rules { cr := compiledRule{ replacement: rule.Replacement, } if strings.HasPrefix(rule.Pattern, "~") { // 正则模式 var patternStr string if strings.HasPrefix(rule.Pattern, "~*") { cr.caseInsensitive = true patternStr = rule.Pattern[2:] } else { patternStr = rule.Pattern[1:] } re, err := regexp.Compile(patternStr) if err != nil { return nil, err } cr.pattern = re } else { // 非正则:使用前缀匹配 cr.exactMatch = rule.Pattern } rules = append(rules, cr) } rw.rules = rules } return rw, nil } // Mode 返回当前改写模式。 // // 处理空字符串默认值,空字符串视为 "default" 模式。 // // 返回值: // - string: 当前模式("default"、"off" 或 "custom") func (r *RedirectRewriter) Mode() string { if r.mode == "" { return redirectModeDefault } return r.mode } // RewriteResponse 改写响应中的 Location 和 Refresh 头。 // // 处理逻辑: // - 仅对 3xx 状态码处理 Location 头(重定向响应) // - 所有状态码都处理 Refresh 头 // // 调用位置:必须在 modifyResponseHeaders 之前调用。 // // 参数: // - resp: 上游响应 // - ctx: FastHTTP 请求上下文,用于变量展开 // - targetURL: 实际选中的上游地址(用于 default 模式) // - originalClientHost: 客户端原始 Host(在 modifyRequestHeaders 改写前保存) func (r *RedirectRewriter) RewriteResponse(resp *fasthttp.Response, ctx *fasthttp.RequestCtx, targetURL string, originalClientHost string) { statusCode := resp.StatusCode() // 仅 3xx 状态码处理 Location 头 if statusCode >= 300 && statusCode < 400 { location := resp.Header.Peek("Location") if len(location) > 0 { rewritten := r.rewriteURL(string(location), ctx, targetURL, originalClientHost) if rewritten != "" { resp.Header.Set("Location", rewritten) } } } // 所有状态码都处理 Refresh 头 refresh := resp.Header.Peek("Refresh") if len(refresh) > 0 { rewritten := r.rewriteRefresh(string(refresh), ctx, targetURL, originalClientHost) if rewritten != "" { resp.Header.Set("Refresh", rewritten) } } } // RewriteRefreshOnly 仅改写 Refresh 头(用于缓存响应路径)。 // // 缓存响应中仅存储 2xx 状态码,不存在 Location 头, // 故跳过 Location 处理,仅处理 Refresh 头。 // // 参数: // - resp: 缓存响应 // - ctx: FastHTTP 请求上下文 // - targetURL: 上游地址 // - originalClientHost: 客户端原始 Host func (r *RedirectRewriter) RewriteRefreshOnly(resp *fasthttp.Response, ctx *fasthttp.RequestCtx, targetURL string, originalClientHost string) { refresh := resp.Header.Peek("Refresh") if len(refresh) > 0 { rewritten := r.rewriteRefresh(string(refresh), ctx, targetURL, originalClientHost) if rewritten != "" { resp.Header.Set("Refresh", rewritten) } } } // rewriteURL 改写单个 URL 值(Location 或 Refresh 头中的 URL 部分)。 // // 根据当前模式选择对应的改写逻辑: // - off: 原样返回 // - custom: 使用预编译规则改写 // - default: 动态替换上游地址为客户端 Host // // 参数: // - headerValue: 头中的 URL 值 // - ctx: FastHTTP 请求上下文 // - targetURL: 上游目标地址 // - originalClientHost: 客户端原始 Host // // 返回值: // - string: 改写后的 URL,未匹配时返回原始值 func (r *RedirectRewriter) rewriteURL(headerValue string, ctx *fasthttp.RequestCtx, targetURL string, originalClientHost string) string { if headerValue == "" { return "" } switch r.Mode() { case redirectModeOff: return headerValue case redirectModeCustom: return r.rewriteCustom(headerValue, ctx) case redirectModeDefault, "": return r.rewriteDefault(headerValue, ctx, targetURL, originalClientHost) default: return headerValue } } // rewriteDefault 使用 default 模式动态改写 URL。 // // 使用前缀匹配:如果 headerValue 以 targetURL 开头,且后面紧跟 // "/"、"?"、"#" 或字符串结束,则将 targetURL 部分替换为 // "$scheme://originalClientHost"。 // // 防止部分匹配问题:例如 "https://www.google.com" 不会 // 错误匹配 "https://www.google.com.hk"。 // // 参数: // - headerValue: 原始 URL 值 // - ctx: FastHTTP 请求上下文(用于判断 TLS 协议) // - targetURL: 上游目标地址 // - originalClientHost: 客户端原始 Host // // 返回值: // - string: 改写后的 URL,未匹配时返回原始值 func (r *RedirectRewriter) rewriteDefault(headerValue string, ctx *fasthttp.RequestCtx, targetURL string, originalClientHost string) string { if targetURL == "" { return headerValue } // 精确前缀匹配:headerValue 以 targetURL 开头,且后面是 / ? # 或结束 // 防止 "https://www.google.com" 匹配 "https://www.google.com.hk"(后者后面是 .hk) if strings.HasPrefix(headerValue, targetURL) { remaining := headerValue[len(targetURL):] // 检查剩余部分是否以合法分隔符开头 if len(remaining) == 0 || remaining[0] == '/' || remaining[0] == '?' || remaining[0] == '#' { // 使用客户端原始 host 构建 replacement scheme := protoHTTP if ctx.IsTLS() { scheme = protoHTTPS } replacement := scheme + "://" + originalClientHost return replacement + remaining } } return headerValue } // rewriteCustom 使用预编译的 custom 规则改写 URL。 // // 规则按定义顺序匹配,第一个成功的规则生效: // - 正则匹配(~ 或 ~* 前缀):支持大小写不敏感 // - 前缀匹配(无特殊前缀):使用 HasPrefix 精确前缀匹配 // // 替换模板支持 Lolly 变量展开。 // // 参数: // - headerValue: 原始 URL 值 // - ctx: FastHTTP 请求上下文 // // 返回值: // - string: 改写后的 URL,未匹配时返回原始值 func (r *RedirectRewriter) rewriteCustom(headerValue string, ctx *fasthttp.RequestCtx) string { vc := variable.NewContext(ctx) defer variable.ReleaseContext(vc) for _, rule := range r.rules { if rule.pattern != nil { // 正则匹配 if rule.caseInsensitive { // 大小写不敏感:先将 headerValue 转为小写匹配,但替换时保留原始值 lowerValue := strings.ToLower(headerValue) loc := rule.pattern.FindStringIndex(lowerValue) if loc != nil { expanded := vc.Expand(rule.replacement) result := headerValue[:loc[0]] + expanded + headerValue[loc[1]:] return result } } else { loc := rule.pattern.FindStringIndex(headerValue) if loc != nil { expanded := vc.Expand(rule.replacement) result := headerValue[:loc[0]] + expanded + headerValue[loc[1]:] return result } } } else if rule.exactMatch != "" { // 前缀匹配 if strings.HasPrefix(headerValue, rule.exactMatch) { expanded := vc.Expand(rule.replacement) suffix := strings.TrimPrefix(headerValue, rule.exactMatch) return expanded + suffix } } } return headerValue } // rewriteRefresh 改写 Refresh 响应头。 // // Refresh 头格式:`N; url=URL` 或 `N;url=URL`(无空格)或纯数字 `N`。 // 该方法提取 URL 部分进行改写,保持延迟值不变。 // // 参数: // - value: 原始 Refresh 头值 // - ctx: FastHTTP 请求上下文 // - targetURL: 上游目标地址 // - originalClientHost: 客户端原始 Host // // 返回值: // - string: 改写后的 Refresh 头值,URL 未变化时返回原始值 func (r *RedirectRewriter) rewriteRefresh(value string, ctx *fasthttp.RequestCtx, targetURL string, originalClientHost string) string { delay, url, valid := parseRefreshHeader(value) if !valid || url == "" { // 无法解析或无 URL,原样返回 return value } rewrittenURL := r.rewriteURL(url, ctx, targetURL, originalClientHost) if rewrittenURL == url { // URL 未变化,原样返回 return value } return delay + "; url=" + rewrittenURL } // parseRefreshHeader 解析 Refresh 头格式 // 格式:`N; url=URL` 或 `N;url=URL`(无空格)或纯数字 `N` // 返回:delay(N), url(URL), 是否有效 // 边缘处理:忽略引号、忽略多余参数 func parseRefreshHeader(value string) (delay string, url string, valid bool) { value = strings.TrimSpace(value) if value == "" { return "", "", false } // 查找 url= 部分 urlIdx := strings.Index(strings.ToLower(value), "url=") if urlIdx == -1 { // 纯数字格式,有效但无 URL return value, "", true } // 提取 delay 部分(url= 之前的部分) delay = strings.TrimSpace(value[:urlIdx]) // 去除 delay 末尾的分号 delay = strings.TrimSuffix(delay, ";") delay = strings.TrimSpace(delay) if delay == "" { return "", "", false } // 提取 URL 部分(url= 之后) url = strings.TrimSpace(value[urlIdx+4:]) if url == "" { return delay, "", true } // 去除 URL 两端的引号(如果有) if len(url) >= 2 { if (url[0] == '"' && url[len(url)-1] == '"') || (url[0] == '\'' && url[len(url)-1] == '\'') { url = url[1 : len(url)-1] } } return delay, url, true }