diff --git a/internal/config/config.go b/internal/config/config.go index 76d2d17..3220455 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -82,6 +82,20 @@ type Config struct { Resolver ResolverConfig `yaml:"resolver"` Performance PerformanceConfig `yaml:"performance"` Shutdown ShutdownConfig `yaml:"shutdown"` + Include []IncludeConfig `yaml:"include"` // 配置引入,支持从其他文件引入配置片段 +} + +// IncludeConfig 配置引入配置。 +// +// 用于从其他文件加载配置片段并合并到当前配置。 +// 支持 glob 模式展开多个文件。 +// +// 使用示例: +// +// include: +// - path: "conf.d/*.yaml" +type IncludeConfig struct { + Path string `yaml:"path"` } // VariablesConfig 自定义变量配置。 @@ -197,6 +211,9 @@ type ServerConfig struct { Security SecurityConfig `yaml:"security"` Compression CompressionConfig `yaml:"compression"` SSL SSLConfig `yaml:"ssl"` + UnixSocket UnixSocketConfig `yaml:"unix_socket"` // Unix socket 配置 + // 切片字段 + ServerNames []string `yaml:"server_names"` // 支持多个 server_name // time.Duration 字段(int64) ReadTimeout time.Duration `yaml:"read_timeout"` IdleTimeout time.Duration `yaml:"idle_timeout"` @@ -281,6 +298,10 @@ type StaticConfig struct { // 默认为 false,启用后会验证符号链接指向的文件是否在允许的路径范围内 // 防止通过符号链接访问敏感文件(如 /etc/passwd) SymlinkCheck bool `yaml:"symlink_check"` + + // LocationType 位置匹配类型 + // 可选值:exact、prefix、regex、regex_caseless、prefix_priority、named + LocationType string `yaml:"location_type"` } // ProxyConfig 反向代理配置,支持负载均衡和健康检查。 @@ -328,6 +349,14 @@ type ProxyConfig struct { Timeout ProxyTimeout `yaml:"timeout"` // 基本类型字段 VirtualNodes int `yaml:"virtual_nodes"` + + // LocationType 位置匹配类型 + // 可选值:exact、prefix_priority、regex、regex_caseless、prefix、named + LocationType string `yaml:"location_type"` + + // LocationName 位置名称 + // 仅当 LocationType 为 named 时使用,用于命名位置块 + LocationName string `yaml:"location_name"` } // BalancerByLuaConfig Lua 负载均衡配置 @@ -1727,6 +1756,35 @@ type StreamProxySSLConfig struct { SessionReuse bool `yaml:"session_reuse"` } +// UnixSocketConfig Unix socket 特定配置。 +// +// 用于配置服务器监听 Unix domain socket 时的文件权限和所有权。 +// +// 注意事项: +// - Mode 为 socket 文件权限,默认 0666 +// - User 为 socket 文件所有者用户名 +// - Group 为 socket 文件所属用户组 +// +// 使用示例: +// +// unix_socket: +// mode: 0660 +// user: "www-data" +// group: "www-data" +type UnixSocketConfig struct { + // Mode 文件权限 + // Unix socket 文件的访问权限,默认 0666 + Mode int `yaml:"mode"` + + // User 文件所有者 + // Unix socket 文件的所有者用户名 + User string `yaml:"user"` + + // Group 文件组 + // Unix socket 文件的所属用户组 + Group string `yaml:"group"` +} + // Load 从文件加载配置。 // // 读取指定路径的 YAML 配置文件,解析并验证配置内容。 diff --git a/internal/config/defaults.go b/internal/config/defaults.go index ab43fe7..1a47962 100644 --- a/internal/config/defaults.go +++ b/internal/config/defaults.go @@ -237,7 +237,6 @@ func GenerateConfigYAML(cfg *Config) ([]byte, error) { var buf bytes.Buffer buf.WriteString("# Lolly 配置文件\n") - // buf.WriteString("# 文档: https://github.com/xfy/lolly\n") buf.WriteString("\n") // mode 配置 @@ -255,6 +254,14 @@ func GenerateConfigYAML(cfg *Config) ([]byte, error) { buf.WriteString(" - # 服务器配置\n") fmt.Fprintf(&buf, " listen: \"%s\" # 监听地址\n", cfg.Servers[0].Listen) fmt.Fprintf(&buf, " name: \"%s\" # 服务器名称(虚拟主机匹配)\n", cfg.Servers[0].Name) + buf.WriteString(" # server_names: # 多个 server_name(支持通配符和正则)\n") + buf.WriteString(" # - \"example.com\" # 精确匹配\n") + buf.WriteString(" # - \"*.example.com\" # 前缀通配(匹配 xxx.example.com)\n") + buf.WriteString(" # - \"~^www\\.\" # 正则匹配(以 www. 开头)\n") + buf.WriteString(" # unix_socket: # Unix socket 配置(监听 Unix 域套接字)\n") + buf.WriteString(" # mode: 0666 # 文件权限\n") + buf.WriteString(" # user: \"\" # 文件所有者(空表示当前用户)\n") + buf.WriteString(" # group: \"\" # 文件组(空表示当前组)\n") buf.WriteString(" # default: false # 虚拟主机模式下标记为默认服务器(接收未匹配的请求)\n") fmt.Fprintf(&buf, " read_timeout: %ds # 读取超时(0 表示不限制)\n", int(cfg.Servers[0].ReadTimeout.Seconds())) fmt.Fprintf(&buf, " write_timeout: %ds # 写入超时(0 表示不限制)\n", int(cfg.Servers[0].WriteTimeout.Seconds())) @@ -327,6 +334,8 @@ func GenerateConfigYAML(cfg *Config) ([]byte, error) { buf.WriteString(" # 反向代理配置\n") buf.WriteString(" # proxy:\n") buf.WriteString(" # - path: /api # 匹配路径前缀\n") + buf.WriteString(" # location_type: \"prefix\" # 匹配类型(有效值: exact, prefix_priority, regex, regex_caseless, prefix, named)\n") + buf.WriteString(" # location_name: \"\" # 命名 location 名称(仅 location_type=named 时使用,如 @fallback)\n") buf.WriteString(" # targets: # 后端目标列表\n") buf.WriteString(" # - url: http://backend1:8080\n") buf.WriteString(" # weight: 3 # 权重(加权轮询时有效)\n") @@ -389,6 +398,14 @@ func GenerateConfigYAML(cfg *Config) ([]byte, error) { buf.WriteString(" # # replacement: \"$scheme://$host/$1\" # 使用捕获组 $1\n") buf.WriteString("\n") + // include 配置 + buf.WriteString("# 配置文件拆分(include 机制)\n") + buf.WriteString("# include:\n") + buf.WriteString("# - path: \"conf.d/*.yaml\" # 相对路径 + glob 模式\n") + buf.WriteString("# - path: \"sites/example.yaml\" # 单个文件引入\n") + buf.WriteString("# 支持循环检测和深度限制(最大 10 层)\n") + buf.WriteString("\n") + // SSL 配置 buf.WriteString(" # SSL/TLS 配置\n") buf.WriteString(" # ssl:\n") diff --git a/internal/config/loader.go b/internal/config/loader.go new file mode 100644 index 0000000..9cb63ea --- /dev/null +++ b/internal/config/loader.go @@ -0,0 +1,137 @@ +package config + +import ( + "fmt" + "os" + "path/filepath" + + "gopkg.in/yaml.v3" +) + +const maxIncludeDepth = 10 + +// ConfigLoader 配置加载器 +type ConfigLoader struct { + baseDir string + loadedFiles map[string]bool // 所有已加载文件(用于跳过重复处理) + stack map[string]bool // 当前调用栈(用于 DAG 循环检测) + depth int +} + +// NewConfigLoader 构造函数 +func NewConfigLoader(mainConfigPath string) *ConfigLoader { + absPath, err := filepath.Abs(mainConfigPath) + if err != nil { + absPath = mainConfigPath + } + + return &ConfigLoader{ + baseDir: filepath.Dir(absPath), + loadedFiles: make(map[string]bool), + stack: make(map[string]bool), + depth: 0, + } +} + +// Load 加载配置(含 DAG-safe 循环检测) +func (l *ConfigLoader) Load(path string) (*Config, error) { + // 深度限制 + if l.depth > maxIncludeDepth { + return nil, fmt.Errorf("include depth exceeds maximum (%d)", maxIncludeDepth) + } + + absPath, err := filepath.Abs(path) + if err != nil { + return nil, fmt.Errorf("resolve path failed: %w", err) + } + + // DAG-safe 循环检测 + // 使用 stack 检测真正的循环(当前调用栈中的文件) + // 使用 loadedFiles 跳过已处理的文件(允许 DAG 共享子配置) + if l.stack[absPath] { + return nil, fmt.Errorf("circular include detected: '%s' is in current include chain", absPath) + } + + // 如果文件已处理过,跳过(不报错) + if l.loadedFiles[absPath] { + return &Config{}, nil // 返回空配置,跳过重复处理 + } + + l.stack[absPath] = true // 加入调用栈 + l.loadedFiles[absPath] = true // 标记已处理 + l.depth++ + + // 加载文件 + data, err := os.ReadFile(absPath) + if err != nil { + return nil, fmt.Errorf("read file failed: %w", err) + } + + var cfg Config + if err := yaml.Unmarshal(data, &cfg); err != nil { + return nil, fmt.Errorf("parse yaml failed: %w", err) + } + + // 处理 include + for _, inc := range cfg.Include { + files, err := l.expandGlob(inc.Path) + if err != nil { + return nil, err + } + + for _, f := range files { + subCfg, err := l.Load(f) // 递归 + if err != nil { + return nil, fmt.Errorf("include %s: %w", f, err) + } + + if err := l.merge(&cfg, subCfg, f); err != nil { + return nil, err + } + } + } + + // 清理调用栈 + delete(l.stack, absPath) + l.depth-- + return &cfg, nil +} + +// merge 合并配置 +func (l *ConfigLoader) merge(dst, src *Config, srcPath string) error { + // Server name collision(listen collision 由 validate.go 处理) + for _, newServer := range src.Servers { + for _, existing := range dst.Servers { + if newServer.Name == existing.Name { + return fmt.Errorf("server name collision: '%s'", newServer.Name) + } + } + } + dst.Servers = append(dst.Servers, src.Servers...) + + // Stream collision + for _, newStream := range src.Stream { + for _, existing := range dst.Stream { + if newStream.Listen == existing.Listen { + return fmt.Errorf("stream listen collision: '%s'", newStream.Listen) + } + } + } + dst.Stream = append(dst.Stream, src.Stream...) + + return nil +} + +// expandGlob 展开 glob 模式 +func (l *ConfigLoader) expandGlob(pattern string) ([]string, error) { + absPattern := l.resolvePath(pattern) + return filepath.Glob(absPattern) +} + +// resolvePath 解析路径 +func (l *ConfigLoader) resolvePath(path string) string { + if filepath.IsAbs(path) { + return path + } + return filepath.Join(l.baseDir, path) +}