lolly/internal/server/router.go
xfy 8f3c304837 fix(lua): register Lua routes in multi-server mode
Add registerLuaRoutes method for router-based route registration
and call it in startMultiServerMode to fix Lua routes not working
when multiple servers are configured.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 13:41:26 +08:00

390 lines
12 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 server
import (
"strings"
"time"
"github.com/valyala/fasthttp"
"rua.plus/lolly/internal/config"
"rua.plus/lolly/internal/handler"
"rua.plus/lolly/internal/loadbalance"
"rua.plus/lolly/internal/logging"
"rua.plus/lolly/internal/lua"
"rua.plus/lolly/internal/matcher"
"rua.plus/lolly/internal/middleware"
"rua.plus/lolly/internal/middleware/errorintercept"
"rua.plus/lolly/internal/proxy"
)
// createProxyForConfig 创建代理实例并配置健康检查。
// 返回创建的代理实例,如果创建失败则返回 nil。
func (s *Server) createProxyForConfig(proxyCfg *config.ProxyConfig) *proxy.Proxy {
// 转换目标
targets := make([]*loadbalance.Target, len(proxyCfg.Targets))
for j, t := range proxyCfg.Targets {
failTimeout := t.FailTimeout
if t.MaxFails > 0 && failTimeout == 0 {
failTimeout = 10 * time.Second
}
targets[j] = loadbalance.NewTargetFromConfig(
t.URL, t.Weight,
int64(t.MaxConns), int64(t.MaxFails), failTimeout,
t.Backup, t.Down, t.ProxyURI,
)
}
// 传递 Transport 配置和 Lua 引擎
p, err := proxy.NewProxy(proxyCfg, targets, &s.config.Performance.Transport, s.luaEngine)
if err != nil {
logging.Error().Msg("Failed to create proxy: " + err.Error())
return nil
}
// 设置 DNS 解析器(如果已配置)
if s.resolver != nil {
p.SetResolver(s.resolver)
if err := p.Start(); err != nil {
logging.Error().Err(err).Msg("Failed to start proxy")
}
}
// 启动健康检查
if proxyCfg.HealthCheck.Interval > 0 {
hc := proxy.NewHealthChecker(targets, &proxyCfg.HealthCheck)
hc.Start()
s.healthCheckers = append(s.healthCheckers, hc)
// 设置被动健康检查
p.SetHealthChecker(hc)
}
// 保存代理实例用于缓存统计
s.proxies = append(s.proxies, p)
return p
}
// registerProxyRoutesWithLocationEngine 使用 LocationEngine 注册代理路由。
//
// 根据配置为 LocationEngine 注册代理路径,创建代理处理器和健康检查器。
// 支持通过 LocationType 配置不同的匹配方式。
func (s *Server) registerProxyRoutesWithLocationEngine(serverCfg *config.ServerConfig) {
for i := range serverCfg.Proxy {
proxyCfg := &serverCfg.Proxy[i]
p := s.createProxyForConfig(proxyCfg)
if p == nil {
continue
}
// 根据 LocationType 注册路由
locType := proxyCfg.LocationType
if locType == "" {
locType = matcher.LocationTypePrefix
}
switch locType {
case matcher.LocationTypeExact:
_ = s.locationEngine.AddExact(proxyCfg.Path, p.ServeHTTP, proxyCfg.Internal)
case matcher.LocationTypePrefixPriority:
_ = s.locationEngine.AddPrefixPriority(proxyCfg.Path, p.ServeHTTP, proxyCfg.Internal)
case matcher.LocationTypeRegex, matcher.LocationTypeRegexCaseless:
caseInsensitive := locType == matcher.LocationTypeRegexCaseless
_ = s.locationEngine.AddRegex(proxyCfg.Path, p.ServeHTTP, caseInsensitive, proxyCfg.Internal)
case matcher.LocationTypeNamed:
if proxyCfg.LocationName != "" {
_ = s.locationEngine.AddNamed(proxyCfg.LocationName, p.ServeHTTP)
}
case matcher.LocationTypePrefix:
_ = s.locationEngine.AddPrefix(proxyCfg.Path, p.ServeHTTP, proxyCfg.Internal)
default:
_ = s.locationEngine.AddPrefix(proxyCfg.Path, p.ServeHTTP, proxyCfg.Internal)
}
}
}
// configureStaticHandler 配置静态文件处理器。
// 返回配置好的 StaticHandler由调用者执行路由注册。
func (s *Server) configureStaticHandler(static *config.StaticConfig, cfg *config.ServerConfig) *handler.StaticHandler {
path := static.Path
if path == "" {
path = "/"
}
staticHandler := handler.NewStaticHandler(
static.Root,
path,
static.Index,
true, // useSendfile
)
// 设置 alias与 root 互斥)
if static.Alias != "" {
staticHandler.SetAlias(static.Alias)
}
if s.fileCache != nil {
staticHandler.SetFileCache(s.fileCache)
// 设置默认缓存 TTL (5s)
staticHandler.SetCacheTTL(5 * time.Second)
}
if cfg.Compression.GzipStatic {
// extensions: 源文件类型,为空使用默认值
// GzipStaticExtensions: 预压缩文件扩展名(如 .br, .gz
staticHandler.SetGzipStatic(true, nil, cfg.Compression.GzipStaticExtensions)
}
// 设置符号链接安全检查
staticHandler.SetSymlinkCheck(static.SymlinkCheck)
// 设置 internal 限制
staticHandler.SetInternal(static.Internal)
// 设置缓存过期时间
if static.Expires != "" {
staticHandler.SetExpires(static.Expires)
}
// 设置目录列表
if static.AutoIndex {
staticHandler.SetAutoIndex(
static.AutoIndex,
static.AutoIndexFormat,
static.AutoIndexLocaltime,
static.AutoIndexExactSize,
)
}
return staticHandler
}
// registerStaticHandlersWithLocationEngine 使用 LocationEngine 注册静态文件处理器。
func (s *Server) registerStaticHandlersWithLocationEngine(cfg *config.ServerConfig) {
for _, static := range cfg.Static {
staticHandler := s.configureStaticHandler(&static, cfg)
path := static.Path
if path == "" {
path = "/"
}
// 根据 LocationType 注册路由
locType := static.LocationType
if locType == "" {
locType = matcher.LocationTypePrefix
}
switch locType {
case matcher.LocationTypeExact:
_ = s.locationEngine.AddExact(path, staticHandler.Handle, static.Internal)
case matcher.LocationTypePrefixPriority:
_ = s.locationEngine.AddPrefixPriority(path, staticHandler.Handle, static.Internal)
case matcher.LocationTypePrefix:
_ = s.locationEngine.AddPrefix(path, staticHandler.Handle, static.Internal)
default:
_ = s.locationEngine.AddPrefix(path, staticHandler.Handle, static.Internal)
}
}
}
// registerProxyRoutes 注册代理路由。
//
// 根据配置为路由器注册代理路径,创建代理处理器和健康检查器。
// 支持 GET、POST、PUT、DELETE、HEAD 等 HTTP 方法。
//
// 参数:
// - router: 路由器实例,用于注册路由规则
// - serverCfg: 服务器配置,包含代理目标、负载均衡、健康检查等设置
//
// 注意事项:
// - 代理目标初始状态默认为健康
// - 健康检查根据配置自动启动
func (s *Server) registerProxyRoutes(router *handler.Router, serverCfg *config.ServerConfig) {
for i := range serverCfg.Proxy {
proxyCfg := &serverCfg.Proxy[i]
p := s.createProxyForConfig(proxyCfg)
if p == nil {
continue
}
// 使用前缀匹配(通配符)注册代理路由
// path: / 匹配所有子路径如 /sorry/index
// path: /api/ 匹配 /api/* 所有子路径
routePath := proxyCfg.Path
// 确保通配符路由格式正确
if !strings.HasSuffix(routePath, "/") && routePath != "/" {
routePath += "/"
}
wildcardPath := routePath + "{path:*}"
router.GET(wildcardPath, p.ServeHTTP)
router.POST(wildcardPath, p.ServeHTTP)
router.PUT(wildcardPath, p.ServeHTTP)
router.DELETE(wildcardPath, p.ServeHTTP)
router.HEAD(wildcardPath, p.ServeHTTP)
}
}
// registerStaticHandlers 注册静态文件处理器。
//
// 为路由器注册静态文件服务,支持多个静态目录、文件缓存和预压缩文件。
//
// 参数:
// - router: 路由器实例,用于注册路由规则
// - cfg: 服务器配置,包含静态文件和压缩设置
func (s *Server) registerStaticHandlers(router *handler.Router, cfg *config.ServerConfig) {
for _, static := range cfg.Static {
staticHandler := s.configureStaticHandler(&static, cfg)
path := static.Path
if path == "" {
path = "/"
}
// 设置 try_files 配置
if len(static.TryFiles) > 0 {
// 注意tryFilesPass 需要路由器支持,当前实现传入 nil
// 如果 tryFilesPass 为 true需要额外处理
staticHandler.SetTryFiles(static.TryFiles, static.TryFilesPass, router)
}
// 注册路由:确保路径以 / 结尾
routePath := path
if !strings.HasSuffix(routePath, "/") {
routePath += "/"
}
router.GET(routePath+"{filepath:*}", staticHandler.Handle)
router.HEAD(routePath+"{filepath:*}", staticHandler.Handle)
}
}
// registerLuaRoutes 使用 Router 注册 Lua 路由。
//
// 遍历 Lua 配置中的脚本,为带有 Route 配置的脚本创建 LuaRouteHandler
// 并注册到 Router。
//
// 参数:
// - router: 路由器实例
// - serverCfg: 服务器配置
//
// 注意事项:
// - 只有设置了 Route 字段的脚本才会被注册
// - 支持 exact、prefix、regex 匹配类型router 模式下统一使用路径匹配)
func (s *Server) registerLuaRoutes(router *handler.Router, serverCfg *config.ServerConfig) {
if s.luaEngine == nil || serverCfg.Lua == nil || !serverCfg.Lua.Enabled {
return
}
for _, script := range serverCfg.Lua.Scripts {
if script.Route == "" {
continue
}
timeout := script.Timeout
if timeout == 0 {
timeout = 30 * time.Second
}
luaHandler := lua.NewLuaRouteHandler(s.luaEngine, script.Path, timeout)
handler := s.wrapRoutedHandler(luaHandler.ServeHTTP)
// Router 模式下,根据 routeType 注册不同路由
routePath := script.Route
routeType := script.RouteType
if routeType == "" {
routeType = "prefix"
}
switch routeType {
case "exact":
// 精确匹配:只注册该路径
router.GET(routePath, handler)
router.POST(routePath, handler)
router.PUT(routePath, handler)
router.DELETE(routePath, handler)
router.HEAD(routePath, handler)
default:
// 前缀匹配:注册带通配符的路径
if !strings.HasSuffix(routePath, "/") && routePath != "/" {
routePath += "/"
}
wildcardPath := routePath + "{path:*}"
router.GET(wildcardPath, handler)
router.POST(wildcardPath, handler)
router.PUT(wildcardPath, handler)
router.DELETE(wildcardPath, handler)
router.HEAD(wildcardPath, handler)
}
}
}
// registerLuaRoutesWithLocationEngine 使用 LocationEngine 注册 Lua 路由。
//
// 遍历 Lua 配置中的脚本,为带有 Route 配置的脚本创建 LuaRouteHandler
// 并注册到 LocationEngine。
//
// 参数:
// - serverCfg: 服务器配置,包含 Lua 脚本配置
//
// 注意事项:
// - 只有设置了 Route 字段的脚本才会被注册
// - 路由脚本不经过完整中间件链,只应用 accesslog 和 errorintercept
// - 支持 exact、prefix、prefix_priority、regex、regex_caseless 匹配类型
func (s *Server) registerLuaRoutesWithLocationEngine(serverCfg *config.ServerConfig) {
if s.luaEngine == nil || serverCfg.Lua == nil || !serverCfg.Lua.Enabled {
return
}
for _, script := range serverCfg.Lua.Scripts {
if script.Route == "" {
continue
}
timeout := script.Timeout
if timeout == 0 {
timeout = 30 * time.Second
}
luaHandler := lua.NewLuaRouteHandler(s.luaEngine, script.Path, timeout)
handler := s.wrapRoutedHandler(luaHandler.ServeHTTP)
routeType := script.RouteType
if routeType == "" {
routeType = matcher.LocationTypePrefix
}
switch routeType {
case matcher.LocationTypeExact:
_ = s.locationEngine.AddExact(script.Route, handler, false)
case matcher.LocationTypePrefixPriority:
_ = s.locationEngine.AddPrefixPriority(script.Route, handler, false)
case matcher.LocationTypeRegex:
_ = s.locationEngine.AddRegex(script.Route, handler, false, false)
case matcher.LocationTypeRegexCaseless:
_ = s.locationEngine.AddRegex(script.Route, handler, true, false)
default:
_ = s.locationEngine.AddPrefix(script.Route, handler, false)
}
}
}
// wrapRoutedHandler 为路由处理器包装基础中间件链。
//
// 路由处理器(如 LuaRouteHandler需要基础的访问日志和错误页面处理
// 但不需要完整的中间件链(如认证、限流等)。
//
// 参数:
// - handler: 原始请求处理器
//
// 返回值:
// - fasthttp.RequestHandler: 包装后的处理器
func (s *Server) wrapRoutedHandler(handler fasthttp.RequestHandler) fasthttp.RequestHandler {
var chain []middleware.Middleware
if s.accessLogMiddleware != nil {
chain = append(chain, s.accessLogMiddleware)
}
if s.errorPageManager != nil && s.errorPageManager.IsConfigured() {
chain = append(chain, errorintercept.New(s.errorPageManager))
}
if len(chain) == 0 {
return handler
}
return middleware.NewChain(chain...).Apply(handler)
}