feat(config): 添加配置引入、Unix socket 和多 server_name 支持

新增配置功能:
- include 机制:支持 glob 模式引入子配置文件,DAG-safe 循环检测
- unix_socket:Unix domain socket 权限和所有权配置
- server_names:多 server_name 支持(通配符和正则)
- location_type:proxy/static 的位置匹配类型配置
- location_name:命名 location 配置

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
xfy 2026-04-17 09:26:30 +08:00
parent 53eaec57ad
commit 1926bf34e0
3 changed files with 213 additions and 1 deletions

View File

@ -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 配置文件,解析并验证配置内容。

View File

@ -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")

137
internal/config/loader.go Normal file
View File

@ -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 collisionlisten 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)
}