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"`
|
Resolver ResolverConfig `yaml:"resolver"`
|
||||||
Performance PerformanceConfig `yaml:"performance"`
|
Performance PerformanceConfig `yaml:"performance"`
|
||||||
Shutdown ShutdownConfig `yaml:"shutdown"`
|
Shutdown ShutdownConfig `yaml:"shutdown"`
|
||||||
|
Include []IncludeConfig `yaml:"include"` // 配置引入,支持从其他文件引入配置片段
|
||||||
|
}
|
||||||
|
|
||||||
|
// IncludeConfig 配置引入配置。
|
||||||
|
//
|
||||||
|
// 用于从其他文件加载配置片段并合并到当前配置。
|
||||||
|
// 支持 glob 模式展开多个文件。
|
||||||
|
//
|
||||||
|
// 使用示例:
|
||||||
|
//
|
||||||
|
// include:
|
||||||
|
// - path: "conf.d/*.yaml"
|
||||||
|
type IncludeConfig struct {
|
||||||
|
Path string `yaml:"path"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// VariablesConfig 自定义变量配置。
|
// VariablesConfig 自定义变量配置。
|
||||||
@ -197,6 +211,9 @@ type ServerConfig struct {
|
|||||||
Security SecurityConfig `yaml:"security"`
|
Security SecurityConfig `yaml:"security"`
|
||||||
Compression CompressionConfig `yaml:"compression"`
|
Compression CompressionConfig `yaml:"compression"`
|
||||||
SSL SSLConfig `yaml:"ssl"`
|
SSL SSLConfig `yaml:"ssl"`
|
||||||
|
UnixSocket UnixSocketConfig `yaml:"unix_socket"` // Unix socket 配置
|
||||||
|
// 切片字段
|
||||||
|
ServerNames []string `yaml:"server_names"` // 支持多个 server_name
|
||||||
// time.Duration 字段(int64)
|
// time.Duration 字段(int64)
|
||||||
ReadTimeout time.Duration `yaml:"read_timeout"`
|
ReadTimeout time.Duration `yaml:"read_timeout"`
|
||||||
IdleTimeout time.Duration `yaml:"idle_timeout"`
|
IdleTimeout time.Duration `yaml:"idle_timeout"`
|
||||||
@ -281,6 +298,10 @@ type StaticConfig struct {
|
|||||||
// 默认为 false,启用后会验证符号链接指向的文件是否在允许的路径范围内
|
// 默认为 false,启用后会验证符号链接指向的文件是否在允许的路径范围内
|
||||||
// 防止通过符号链接访问敏感文件(如 /etc/passwd)
|
// 防止通过符号链接访问敏感文件(如 /etc/passwd)
|
||||||
SymlinkCheck bool `yaml:"symlink_check"`
|
SymlinkCheck bool `yaml:"symlink_check"`
|
||||||
|
|
||||||
|
// LocationType 位置匹配类型
|
||||||
|
// 可选值:exact、prefix、regex、regex_caseless、prefix_priority、named
|
||||||
|
LocationType string `yaml:"location_type"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ProxyConfig 反向代理配置,支持负载均衡和健康检查。
|
// ProxyConfig 反向代理配置,支持负载均衡和健康检查。
|
||||||
@ -328,6 +349,14 @@ type ProxyConfig struct {
|
|||||||
Timeout ProxyTimeout `yaml:"timeout"`
|
Timeout ProxyTimeout `yaml:"timeout"`
|
||||||
// 基本类型字段
|
// 基本类型字段
|
||||||
VirtualNodes int `yaml:"virtual_nodes"`
|
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 负载均衡配置
|
// BalancerByLuaConfig Lua 负载均衡配置
|
||||||
@ -1727,6 +1756,35 @@ type StreamProxySSLConfig struct {
|
|||||||
SessionReuse bool `yaml:"session_reuse"`
|
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 从文件加载配置。
|
// Load 从文件加载配置。
|
||||||
//
|
//
|
||||||
// 读取指定路径的 YAML 配置文件,解析并验证配置内容。
|
// 读取指定路径的 YAML 配置文件,解析并验证配置内容。
|
||||||
|
|||||||
@ -237,7 +237,6 @@ func GenerateConfigYAML(cfg *Config) ([]byte, error) {
|
|||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
|
|
||||||
buf.WriteString("# Lolly 配置文件\n")
|
buf.WriteString("# Lolly 配置文件\n")
|
||||||
// buf.WriteString("# 文档: https://github.com/xfy/lolly\n")
|
|
||||||
buf.WriteString("\n")
|
buf.WriteString("\n")
|
||||||
|
|
||||||
// mode 配置
|
// mode 配置
|
||||||
@ -255,6 +254,14 @@ func GenerateConfigYAML(cfg *Config) ([]byte, error) {
|
|||||||
buf.WriteString(" - # 服务器配置\n")
|
buf.WriteString(" - # 服务器配置\n")
|
||||||
fmt.Fprintf(&buf, " listen: \"%s\" # 监听地址\n", cfg.Servers[0].Listen)
|
fmt.Fprintf(&buf, " listen: \"%s\" # 监听地址\n", cfg.Servers[0].Listen)
|
||||||
fmt.Fprintf(&buf, " name: \"%s\" # 服务器名称(虚拟主机匹配)\n", cfg.Servers[0].Name)
|
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")
|
buf.WriteString(" # default: false # 虚拟主机模式下标记为默认服务器(接收未匹配的请求)\n")
|
||||||
fmt.Fprintf(&buf, " read_timeout: %ds # 读取超时(0 表示不限制)\n", int(cfg.Servers[0].ReadTimeout.Seconds()))
|
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()))
|
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(" # 反向代理配置\n")
|
||||||
buf.WriteString(" # proxy:\n")
|
buf.WriteString(" # proxy:\n")
|
||||||
buf.WriteString(" # - path: /api # 匹配路径前缀\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(" # targets: # 后端目标列表\n")
|
||||||
buf.WriteString(" # - url: http://backend1:8080\n")
|
buf.WriteString(" # - url: http://backend1:8080\n")
|
||||||
buf.WriteString(" # weight: 3 # 权重(加权轮询时有效)\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(" # # replacement: \"$scheme://$host/$1\" # 使用捕获组 $1\n")
|
||||||
buf.WriteString("\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 配置
|
// SSL 配置
|
||||||
buf.WriteString(" # SSL/TLS 配置\n")
|
buf.WriteString(" # SSL/TLS 配置\n")
|
||||||
buf.WriteString(" # ssl:\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