lolly/internal/server/router.go
xfy b62a3f12da feat(handler): add autoindex module for directory listing
Add nginx-like autoindex functionality with three output formats:
- HTML: styled directory listing with sortable columns
- JSON: structured API-friendly output
- XML: machine-readable format

Configuration options:
- auto_index: enable/disable directory listing
- auto_index_format: output format (html/json/xml)
- auto_index_localtime: use local time instead of GMT
- auto_index_exact_size: show exact bytes vs human-readable

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-30 15:11:34 +08:00

267 lines
8.0 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"
"rua.plus/lolly/internal/config"
"rua.plus/lolly/internal/handler"
"rua.plus/lolly/internal/loadbalance"
"rua.plus/lolly/internal/logging"
"rua.plus/lolly/internal/matcher"
"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)
}
}
}
// registerStaticHandlersWithLocationEngine 使用 LocationEngine 注册静态文件处理器。
func (s *Server) registerStaticHandlersWithLocationEngine(cfg *config.ServerConfig) {
for _, static := range cfg.Static {
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,
)
}
// 根据 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 {
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)
// 设置缓存过期时间
if static.Expires != "" {
staticHandler.SetExpires(static.Expires)
}
// 设置 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)
}
}