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:
parent
53eaec57ad
commit
1926bf34e0
@ -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 配置文件,解析并验证配置内容。
|
||||
|
||||
@ -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
137
internal/config/loader.go
Normal 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 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)
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user