- Fix gjson/gjson.go package comments and constant documentation - Fix internal/config/config.go constant documentation - Fix internal/utils/httperror.go variable documentation - Fix internal/matcher/matcher.go constant documentation - Fix internal/middleware/compression/compression.go constant documentation - Fix internal/middleware/limitrate/limitrate.go constant documentation - Fix internal/middleware/rewrite/rewrite.go constant documentation - Fix internal/middleware/security/access.go and auth.go constant documentation - Fix internal/ssl/client_verify.go constant documentation - Fix internal/variable/builtin.go and ssl.go constant documentation - Fix internal/lua/api_log.go HTTP and log level constant documentation - Fix internal/benchmark/tools/tools.go constant documentation - Include author attribution (xfy)
242 lines
6.8 KiB
Go
242 lines
6.8 KiB
Go
// Package rewrite 提供 URL 重写中间件,支持正则表达式匹配和多种重写标志。
|
||
//
|
||
// 该文件包含 URL 重写相关的核心功能,包括:
|
||
// - 正则表达式匹配和替换
|
||
// - 多种重写标志(last、redirect、permanent、break)
|
||
// - 变量展开支持
|
||
// - ReDoS 安全防护
|
||
//
|
||
// 主要用途:
|
||
//
|
||
// 用于实现类似 nginx rewrite 模块的 URL 重写功能,支持灵活的 URL 变换规则。
|
||
//
|
||
// 注意事项:
|
||
// - 重写规则按配置顺序执行,FlagLast 规则会重新从第一条规则开始匹配
|
||
// - 最大迭代次数限制防止无限循环
|
||
// - 正则表达式会进行安全性检查,防止 ReDoS 攻击
|
||
//
|
||
// 作者:xfy
|
||
package rewrite
|
||
|
||
import (
|
||
"fmt"
|
||
"regexp"
|
||
"strings"
|
||
|
||
"github.com/valyala/fasthttp"
|
||
"rua.plus/lolly/internal/config"
|
||
"rua.plus/lolly/internal/utils"
|
||
"rua.plus/lolly/internal/variable"
|
||
)
|
||
|
||
// MaxRewriteIterations URL重写最大迭代次数,防止无限循环。
|
||
const MaxRewriteIterations = 10
|
||
|
||
// Flag 重写标志类型。
|
||
type Flag int
|
||
|
||
const (
|
||
// FlagLast 继续匹配其他规则(nginx 行为:重新从第一条规则开始匹配)。
|
||
// 匹配到规则后会重新从第一条规则开始遍历,用于多规则链式重写。
|
||
FlagLast Flag = iota
|
||
// FlagRedirect 返回 302 临时重定向。
|
||
// 客户端收到 302 响应后重新请求新 URL,不会继续匹配后续规则。
|
||
FlagRedirect
|
||
// FlagPermanent 返回 301 永久重定向。
|
||
// 客户端收到 301 响应后永久重定向到新 URL,不会继续匹配后续规则。
|
||
FlagPermanent
|
||
// FlagBreak 停止匹配规则。
|
||
// 修改请求路径后终止重写流程,直接进入下一个处理器。
|
||
FlagBreak
|
||
)
|
||
|
||
// parseFlag 解析配置中的标志字符串为 Flag 枚举值。
|
||
//
|
||
// 将配置字符串转换为对应的 Flag 类型,用于控制重写后的行为。
|
||
//
|
||
// 参数:
|
||
// - s: 标志字符串,支持 "last"、"redirect"、"permanent"、"break"
|
||
//
|
||
// 返回值:
|
||
// - Flag: 对应的标志枚举值,无法识别时返回 FlagLast
|
||
func parseFlag(s string) Flag {
|
||
switch strings.ToLower(s) {
|
||
case "redirect":
|
||
return FlagRedirect
|
||
case "permanent":
|
||
return FlagPermanent
|
||
case "break":
|
||
return FlagBreak
|
||
default:
|
||
return FlagLast
|
||
}
|
||
}
|
||
|
||
// Rule 编译后的重写规则。
|
||
type Rule struct {
|
||
// pattern 正则匹配模式,用于匹配请求路径
|
||
pattern *regexp.Regexp
|
||
// replacement 替换字符串,支持 $1、$2 等捕获组和变量展开
|
||
replacement string
|
||
// flag 执行标志,控制重写后的行为(last/redirect/permanent/break)
|
||
flag Flag
|
||
}
|
||
|
||
// Middleware URL 重写中间件。
|
||
type Middleware struct {
|
||
// rules 编译后的规则列表,按配置顺序执行
|
||
rules []Rule
|
||
}
|
||
|
||
// New 创建 URL 重写中间件。
|
||
//
|
||
// 编译配置中的重写规则,验证正则表达式安全性后返回中间件实例。
|
||
//
|
||
// 参数:
|
||
// - rules: 重写规则配置列表,包含模式、替换和标志
|
||
//
|
||
// 返回值:
|
||
// - *Middleware: 编译后的重写中间件
|
||
// - error: 正则表达式无效或不安全时返回错误
|
||
func New(rules []config.RewriteRule) (*Middleware, error) {
|
||
compiled := make([]Rule, 0, len(rules))
|
||
for _, r := range rules {
|
||
// 验证正则表达式安全性,防止 ReDoS
|
||
if err := validateRegexSafety(r.Pattern); err != nil {
|
||
return nil, fmt.Errorf("unsafe regex pattern %q: %w", r.Pattern, err)
|
||
}
|
||
|
||
re, err := regexp.Compile(r.Pattern)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
compiled = append(compiled, Rule{
|
||
pattern: re,
|
||
replacement: r.Replacement,
|
||
flag: parseFlag(r.Flag),
|
||
})
|
||
}
|
||
return &Middleware{rules: compiled}, nil
|
||
}
|
||
|
||
// validateRegexSafety 验证正则表达式的安全性,防止 ReDoS 攻击。
|
||
//
|
||
// 检测可能导致灾难性回溯的危险模式,如嵌套量词。
|
||
// 限制模式最大长度 1000 字符。
|
||
//
|
||
// 参数:
|
||
// - pattern: 待验证的正则表达式字符串
|
||
//
|
||
// 返回值:
|
||
// - error: 检测到不安全模式时返回错误
|
||
func validateRegexSafety(pattern string) error {
|
||
// 限制模式长度
|
||
if len(pattern) > 1000 {
|
||
return fmt.Errorf("pattern too long (max 1000 chars)")
|
||
}
|
||
|
||
// 检测危险模式:嵌套量词
|
||
// 例如:(\w+)+, (\d+)+, (a+)+, (.+)+
|
||
dangerousPatterns := []string{
|
||
`(\w+)+`, `(\d+)+`, `(a+)+`, `(.+)+`,
|
||
`(\w*)*`, `(\d*)*`, `(a*)*`, `(.*)*`,
|
||
`(\w+)?+`, `(\d+)?+`,
|
||
}
|
||
|
||
for _, dangerous := range dangerousPatterns {
|
||
if strings.Contains(pattern, dangerous) {
|
||
return fmt.Errorf("potential catastrophic backtracking pattern detected")
|
||
}
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// Name 返回中间件名称。
|
||
func (m *Middleware) Name() string {
|
||
return "rewrite"
|
||
}
|
||
|
||
// Process 应用重写规则。
|
||
//
|
||
// 对请求路径执行正则匹配和替换,根据标志控制后续行为。
|
||
// 支持迭代重写(FlagLast 会重新从第一条规则开始匹配)。
|
||
//
|
||
// 参数:
|
||
// - next: 下一个请求处理器
|
||
//
|
||
// 返回值:
|
||
// - fasthttp.RequestHandler: 包装后的请求处理器
|
||
func (m *Middleware) Process(next fasthttp.RequestHandler) fasthttp.RequestHandler {
|
||
return func(ctx *fasthttp.RequestCtx) {
|
||
path := string(ctx.Path())
|
||
originalPath := path
|
||
|
||
// 全局迭代计数器,用于检测循环(每次重写都计入迭代)
|
||
iterationCount := 0
|
||
// 规则索引,支持FlagLast后重新开始匹配
|
||
ruleIndex := 0
|
||
|
||
for ruleIndex < len(m.rules) {
|
||
// 步骤1: 检查迭代次数是否超过限制(防止无限循环)
|
||
if iterationCount >= MaxRewriteIterations {
|
||
utils.SendError(ctx, utils.ErrInternalError)
|
||
return
|
||
}
|
||
|
||
rule := m.rules[ruleIndex]
|
||
|
||
if rule.pattern.MatchString(path) {
|
||
// 步骤2: 执行正则替换
|
||
newPath := rule.pattern.ReplaceAllString(path, rule.replacement)
|
||
|
||
// 步骤3: 对替换结果进行变量展开
|
||
vc := variable.NewContext(ctx)
|
||
newPath = vc.Expand(newPath)
|
||
variable.ReleaseContext(vc)
|
||
|
||
// 步骤4: 根据标志决定后续行为
|
||
switch rule.flag {
|
||
case FlagRedirect:
|
||
// 302 临时重定向
|
||
ctx.Redirect(newPath, fasthttp.StatusFound)
|
||
return
|
||
case FlagPermanent:
|
||
// 301 永久重定向
|
||
ctx.Redirect(newPath, fasthttp.StatusMovedPermanently)
|
||
return
|
||
case FlagBreak:
|
||
// 修改路径后停止匹配,直接进入处理器
|
||
ctx.Request.SetRequestURI(newPath)
|
||
next(ctx)
|
||
return
|
||
case FlagLast:
|
||
// 修改路径,并重新从第一条规则开始匹配(nginx兼容行为)
|
||
path = newPath
|
||
ctx.Request.SetRequestURI(path)
|
||
iterationCount++ // 每次FlagLast重写都增加计数
|
||
ruleIndex = 0 // 重新从第一条规则开始
|
||
continue
|
||
}
|
||
}
|
||
|
||
ruleIndex++
|
||
}
|
||
|
||
// 步骤5: 如果路径被修改过,需要重新设置
|
||
if path != originalPath {
|
||
ctx.Request.SetRequestURI(path)
|
||
}
|
||
|
||
next(ctx)
|
||
}
|
||
}
|
||
|
||
// Rules 返回编译后的规则列表(用于调试)。
|
||
//
|
||
// 返回值:
|
||
// - []Rule: 编译后的重写规则列表
|
||
func (m *Middleware) Rules() []Rule {
|
||
return m.rules
|
||
}
|