diff --git a/docs/plan.md b/docs/plan.md index 2dafa01..5fccb65 100644 --- a/docs/plan.md +++ b/docs/plan.md @@ -127,10 +127,6 @@ lolly -v # 显示版本 # 构建测试 make build -# 配置解析测试 -./lolly -t -c configs/lolly.yaml -# 输出:配置有效 - # 版本显示 ./lolly -v ``` @@ -324,7 +320,7 @@ func LogAccess(r *http.Request, status int, size int64, duration time.Duration) ```bash # 启动服务器 -./lolly -c configs/lolly.yaml +./lolly -c lolly.yaml # 静态文件测试 curl http://localhost:8080/index.html diff --git a/go.mod b/go.mod index 6770e73..58cbc52 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,5 @@ module rua.plus/lolly go 1.26.1 + +require gopkg.in/yaml.v3 v3.0.1 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..a62c313 --- /dev/null +++ b/go.sum @@ -0,0 +1,4 @@ +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/config/config.go b/internal/config/config.go new file mode 100644 index 0000000..65e2ba6 --- /dev/null +++ b/internal/config/config.go @@ -0,0 +1,314 @@ +// Package config 提供 YAML 配置文件的解析、验证和默认配置生成功能。 +package config + +import ( + "errors" + "fmt" + "os" + "time" + + "gopkg.in/yaml.v3" +) + +// Config 根配置结构,支持单服务器和多虚拟主机两种模式。 +type Config struct { + Server ServerConfig `yaml:"server"` // 单服务器模式配置 + Servers []ServerConfig `yaml:"servers"` // 多虚拟主机模式配置 + Logging LoggingConfig `yaml:"logging"` // 日志配置 + Performance PerformanceConfig `yaml:"performance"` // 性能配置 + Monitoring MonitoringConfig `yaml:"monitoring"` // 监控配置 +} + +// ServerConfig 服务器配置,包含监听地址、静态文件、代理、SSL 等设置。 +type ServerConfig struct { + Listen string `yaml:"listen"` // 监听地址,如 ":8080" + Name string `yaml:"name"` // 服务器名称,用于虚拟主机匹配 + Static StaticConfig `yaml:"static"` // 静态文件服务配置 + Proxy []ProxyConfig `yaml:"proxy"` // 反向代理规则列表 + SSL SSLConfig `yaml:"ssl"` // SSL/TLS 配置 + Security SecurityConfig `yaml:"security"` // 安全配置 + Rewrite []RewriteRule `yaml:"rewrite"` // URL 重写规则 + Compression CompressionConfig `yaml:"compression"` // 响应压缩配置 +} + +// StaticConfig 静态文件服务配置。 +type StaticConfig struct { + Root string `yaml:"root"` // 静态文件根目录 + Index []string `yaml:"index"` // 索引文件列表,默认 ["index.html", "index.htm"] +} + +// ProxyConfig 反向代理配置,支持负载均衡和健康检查。 +type ProxyConfig struct { + Path string `yaml:"path"` // 匹配路径前缀 + Targets []ProxyTarget `yaml:"targets"` // 后端目标列表 + LoadBalance string `yaml:"load_balance"` // 负载均衡算法:round_robin, weighted_round_robin, least_conn, ip_hash + HealthCheck HealthCheckConfig `yaml:"health_check"` // 健康检查配置 + Timeout ProxyTimeout `yaml:"timeout"` // 超时配置 + Headers ProxyHeaders `yaml:"headers"` // 请求/响应头修改 + Cache ProxyCacheConfig `yaml:"cache"` // 代理缓存配置 +} + +// ProxyTarget 后端目标配置。 +type ProxyTarget struct { + URL string `yaml:"url"` // 后端地址,如 "http://backend1:8080" + Weight int `yaml:"weight"` // 权重,用于加权轮询算法 +} + +// HealthCheckConfig 健康检查配置。 +type HealthCheckConfig struct { + Interval time.Duration `yaml:"interval"` // 检查间隔 + Path string `yaml:"path"` // 健康检查路径 + Timeout time.Duration `yaml:"timeout"` // 检查超时时间 +} + +// ProxyTimeout 代理超时配置。 +type ProxyTimeout struct { + Connect time.Duration `yaml:"connect"` // 连接超时 + Read time.Duration `yaml:"read"` // 读取超时 + Write time.Duration `yaml:"write"` // 写入超时 +} + +// ProxyHeaders 代理请求/响应头配置。 +type ProxyHeaders struct { + SetRequest map[string]string `yaml:"set_request"` // 设置请求头 + SetResponse map[string]string `yaml:"set_response"` // 设置响应头 + Remove []string `yaml:"remove"` // 移除的头部 +} + +// ProxyCacheConfig 代理缓存配置。 +type ProxyCacheConfig struct { + Enabled bool `yaml:"enabled"` // 是否启用缓存 + MaxAge time.Duration `yaml:"max_age"` // 缓存有效期 + CacheLock bool `yaml:"cache_lock"` // 缓存锁,防止击穿 + StaleWhileRevalidate time.Duration `yaml:"stale_while_revalidate"` // 过期缓存复用时间 +} + +// SSLConfig SSL/TLS 配置。 +type SSLConfig struct { + Cert string `yaml:"cert"` // 证书文件路径 + Key string `yaml:"key"` // 私钥文件路径 + CertChain string `yaml:"cert_chain"` // 证书链文件路径 + Protocols []string `yaml:"protocols"` // TLS 版本,默认 ["TLSv1.2", "TLSv1.3"] + Ciphers []string `yaml:"ciphers"` // 加密套件(仅 TLS 1.2 有效) + OCSPStapling bool `yaml:"ocsp_stapling"` // OCSP Stapling 支持 + HSTS HSTSConfig `yaml:"hsts"` // HSTS 配置 +} + +// HSTSConfig HTTP Strict Transport Security 配置。 +type HSTSConfig struct { + MaxAge int `yaml:"max_age"` // 过期时间(秒),默认 31536000(1年) + IncludeSubDomains bool `yaml:"include_sub_domains"` // 包含子域名,默认 true + Preload bool `yaml:"preload"` // 加入 HSTS 预加载列表 +} + +// SecurityConfig 安全配置,包含访问控制、限流、认证和安全头部。 +type SecurityConfig struct { + Access AccessConfig `yaml:"access"` // IP 访问控制 + RateLimit RateLimitConfig `yaml:"rate_limit"` // 速率限制 + Auth AuthConfig `yaml:"auth"` // 认证配置 + Headers SecurityHeaders `yaml:"headers"` // 安全头部 +} + +// AccessConfig IP 访问控制配置。 +type AccessConfig struct { + Allow []string `yaml:"allow"` // 允许的 IP/CIDR 列表 + Deny []string `yaml:"deny"` // 拒绝的 IP/CIDR 列表 + Default string `yaml:"default"` // 默认动作:allow 或 deny +} + +// RateLimitConfig 速率限制配置。 +type RateLimitConfig struct { + RequestRate int `yaml:"request_rate"` // 每秒请求数限制 + Burst int `yaml:"burst"` // 突发流量上限 + ConnLimit int `yaml:"conn_limit"` // 连接数限制 + Key string `yaml:"key"` // 限流 key 来源:ip, header +} + +// AuthConfig 认证配置。 +type AuthConfig struct { + Type string `yaml:"type"` // 认证类型:basic + RequireTLS bool `yaml:"require_tls"` // 强制 HTTPS,默认 true + Algorithm string `yaml:"algorithm"` // 哈希算法:bcrypt, argon2id + Users []User `yaml:"users"` // 用户列表 + Realm string `yaml:"realm"` // 认证域 + MinPasswordLength int `yaml:"min_password_length"` // 密码最小长度 +} + +// User 认证用户配置。 +type User struct { + Name string `yaml:"name"` // 用户名 + Password string `yaml:"password"` // 密码哈希 +} + +// SecurityHeaders 安全头部配置。 +type SecurityHeaders struct { + XFrameOptions string `yaml:"x_frame_options"` // X-Frame-Options: DENY, SAMEORIGIN + XContentTypeOptions string `yaml:"x_content_type_options"` // X-Content-Type-Options: nosniff + ContentSecurityPolicy string `yaml:"content_security_policy"` // Content-Security-Policy + ReferrerPolicy string `yaml:"referrer_policy"` // Referrer-Policy + PermissionsPolicy string `yaml:"permissions_policy"` // Permissions-Policy +} + +// RewriteRule URL 重写规则。 +type RewriteRule struct { + Pattern string `yaml:"pattern"` // 匹配模式(正则表达式) + Replacement string `yaml:"replacement"` // 替换目标 + Flag string `yaml:"flag"` // 标志:last, redirect, permanent, break +} + +// CompressionConfig 响应压缩配置。 +type CompressionConfig struct { + Type string `yaml:"type"` // 压缩类型:gzip, brotli, both + Level int `yaml:"level"` // 压缩级别:1-9 + MinSize int `yaml:"min_size"` // 最小压缩大小(字节) + Types []string `yaml:"types"` // 可压缩的 MIME 类型 +} + +// LoggingConfig 日志配置。 +type LoggingConfig struct { + Access AccessLogConfig `yaml:"access"` // 访问日志 + Error ErrorLogConfig `yaml:"error"` // 错误日志 +} + +// AccessLogConfig 访问日志配置。 +type AccessLogConfig struct { + Path string `yaml:"path"` // 日志文件路径 + Format string `yaml:"format"` // 日志格式 +} + +// ErrorLogConfig 错误日志配置。 +type ErrorLogConfig struct { + Path string `yaml:"path"` // 日志文件路径 + Level string `yaml:"level"` // 日志级别:debug, info, warn, error +} + +// PerformanceConfig 性能配置。 +type PerformanceConfig struct { + GoroutinePool GoroutinePoolConfig `yaml:"goroutine_pool"` // Goroutine 池 + FileCache FileCacheConfig `yaml:"file_cache"` // 文件缓存 + Transport TransportConfig `yaml:"transport"` // HTTP Transport +} + +// GoroutinePoolConfig Goroutine 池配置。 +type GoroutinePoolConfig struct { + Enabled bool `yaml:"enabled"` // 是否启用 + MaxWorkers int `yaml:"max_workers"` // 最大 worker 数 + MinWorkers int `yaml:"min_workers"` // 最小 worker 数(预热) + IdleTimeout time.Duration `yaml:"idle_timeout"` // 空闲超时 +} + +// FileCacheConfig 文件缓存配置。 +type FileCacheConfig struct { + MaxEntries int64 `yaml:"max_entries"` // 最大缓存条目数 + MaxSize int64 `yaml:"max_size"` // 内存上限(字节) + Inactive time.Duration `yaml:"inactive"` // 未访问淘汰时间 + LRUEviction bool `yaml:"lru_eviction"` // 启用 LRU 淘汰 +} + +// TransportConfig HTTP Transport 配置。 +type TransportConfig struct { + MaxIdleConns int `yaml:"max_idle_conns"` // 最大空闲连接数 + MaxIdleConnsPerHost int `yaml:"max_idle_conns_per_host"` // 每主机最大空闲连接 + IdleConnTimeout time.Duration `yaml:"idle_conn_timeout"` // 空闲连接超时 + MaxConnsPerHost int `yaml:"max_conns_per_host"` // 每主机最大连接数 +} + +// MonitoringConfig 监控配置。 +type MonitoringConfig struct { + Status StatusConfig `yaml:"status"` // 状态端点配置 +} + +// StatusConfig 状态监控端点配置。 +type StatusConfig struct { + Path string `yaml:"path"` // 端点路径 + Allow []string `yaml:"allow"` // 允许访问的 IP 列表 +} + +// Load 从文件加载配置。 +func Load(path string) (*Config, error) { + data, err := os.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("读取配置文件失败: %w", err) + } + + var cfg Config + if err := yaml.Unmarshal(data, &cfg); err != nil { + return nil, fmt.Errorf("解析配置文件失败: %w", err) + } + + if err := Validate(&cfg); err != nil { + return nil, fmt.Errorf("配置验证失败: %w", err) + } + + return &cfg, nil +} + +// LoadFromString 从 YAML 字符串加载配置。 +func LoadFromString(yamlStr string) (*Config, error) { + var cfg Config + if err := yaml.Unmarshal([]byte(yamlStr), &cfg); err != nil { + return nil, fmt.Errorf("解析配置失败: %w", err) + } + + if err := Validate(&cfg); err != nil { + return nil, fmt.Errorf("配置验证失败: %w", err) + } + + return &cfg, nil +} + +// Save 保存配置到文件。 +func Save(cfg *Config, path string) error { + data, err := yaml.Marshal(cfg) + if err != nil { + return fmt.Errorf("序列化配置失败: %w", err) + } + + if err := os.WriteFile(path, data, 0644); err != nil { + return fmt.Errorf("写入配置文件失败: %w", err) + } + + return nil +} + +// HasServers 检查是否为多虚拟主机模式。 +func (c *Config) HasServers() bool { + return len(c.Servers) > 0 +} + +// HasDefaultServer 检查是否有默认服务器配置。 +func (c *Config) HasDefaultServer() bool { + return c.Server.Listen != "" +} + +// GetDefaultServer 获取默认服务器配置(用于 fallback)。 +func (c *Config) GetDefaultServer() *ServerConfig { + if c.HasDefaultServer() { + return &c.Server + } + return nil +} + +// Validate 配置验证入口。 +func Validate(cfg *Config) error { + // 至少需要一种服务器配置 + if !cfg.HasDefaultServer() && !cfg.HasServers() { + return errors.New("至少需要配置 server 或 servers") + } + + // 验证默认服务器 + if cfg.HasDefaultServer() { + if err := validateServer(&cfg.Server, true); err != nil { + return err + } + } + + // 验证所有虚拟主机 + for i := range cfg.Servers { + if err := validateServer(&cfg.Servers[i], false); err != nil { + return fmt.Errorf("servers[%d]: %w", i, err) + } + } + + return nil +} \ No newline at end of file diff --git a/internal/config/defaults.go b/internal/config/defaults.go new file mode 100644 index 0000000..86c0d7a --- /dev/null +++ b/internal/config/defaults.go @@ -0,0 +1,247 @@ +// Package config 提供 YAML 配置文件的解析、验证和默认配置生成功能。 +package config + +import ( + "bytes" + "fmt" + "time" + + "gopkg.in/yaml.v3" +) + +// DefaultConfig 返回带默认值的配置结构体。 +func DefaultConfig() *Config { + return &Config{ + Server: ServerConfig{ + Listen: ":8080", + Name: "localhost", + Static: StaticConfig{ + Root: "/var/www/html", + Index: []string{"index.html", "index.htm"}, + }, + SSL: SSLConfig{ + Protocols: []string{"TLSv1.2", "TLSv1.3"}, + HSTS: HSTSConfig{ + MaxAge: 31536000, + IncludeSubDomains: true, + Preload: false, + }, + }, + Security: SecurityConfig{ + Headers: SecurityHeaders{ + XFrameOptions: "DENY", + XContentTypeOptions: "nosniff", + ReferrerPolicy: "strict-origin-when-cross-origin", + }, + Auth: AuthConfig{ + RequireTLS: true, + Algorithm: "bcrypt", + Realm: "Restricted Area", + }, + }, + Compression: CompressionConfig{ + Type: "gzip", + Level: 6, + MinSize: 1024, + Types: []string{ + "text/html", + "text/css", + "text/javascript", + "application/json", + "application/javascript", + }, + }, + }, + Logging: LoggingConfig{ + Access: AccessLogConfig{ + Format: "$remote_addr - $request - $status - $body_bytes_sent", + }, + Error: ErrorLogConfig{ + Level: "info", + }, + }, + Performance: PerformanceConfig{ + FileCache: FileCacheConfig{ + MaxEntries: 10000, + MaxSize: 256 * 1024 * 1024, // 256MB + Inactive: 20 * time.Second, + LRUEviction: true, + }, + Transport: TransportConfig{ + MaxIdleConns: 100, + MaxIdleConnsPerHost: 32, + IdleConnTimeout: 90 * time.Second, + }, + }, + Monitoring: MonitoringConfig{ + Status: StatusConfig{ + Path: "/_status", + Allow: []string{"127.0.0.1"}, + }, + }, + } +} + +// GenerateConfigYAML 生成带注释的默认配置 YAML。 +func GenerateConfigYAML(cfg *Config) ([]byte, error) { + // 手动构建带注释的 YAML + var buf bytes.Buffer + + buf.WriteString("# Lolly 配置文件\n") + buf.WriteString("# 文档: https://github.com/xfy/lolly\n") + buf.WriteString("\n") + + // server 配置 + buf.WriteString("# 服务器配置(单服务器模式)\n") + buf.WriteString("server:\n") + buf.WriteString(fmt.Sprintf(" listen: \"%s\" # 监听地址\n", cfg.Server.Listen)) + buf.WriteString(fmt.Sprintf(" name: \"%s\" # 服务器名称(虚拟主机匹配)\n", cfg.Server.Name)) + buf.WriteString("\n") + + // static 配置 + buf.WriteString(" # 静态文件服务配置\n") + buf.WriteString(" static:\n") + buf.WriteString(fmt.Sprintf(" root: \"%s\" # 静态文件根目录\n", cfg.Server.Static.Root)) + buf.WriteString(" index: # 索引文件\n") + for _, idx := range cfg.Server.Static.Index { + buf.WriteString(fmt.Sprintf(" - \"%s\"\n", idx)) + } + buf.WriteString("\n") + + // proxy 配置示例 + buf.WriteString(" # 反向代理配置(示例)\n") + buf.WriteString(" # proxy:\n") + buf.WriteString(" # - path: /api # 匹配路径前缀\n") + buf.WriteString(" # targets: # 后端目标列表\n") + buf.WriteString(" # - url: http://backend1:8080\n") + buf.WriteString(" # weight: 3 # 权重(加权轮询)\n") + buf.WriteString(" # - url: http://backend2:8080\n") + buf.WriteString(" # weight: 1\n") + buf.WriteString(" # load_balance: weighted_round_robin # 负载均衡算法\n") + buf.WriteString(" # health_check: # 健康检查\n") + buf.WriteString(" # interval: 10s\n") + buf.WriteString(" # path: /health\n") + buf.WriteString(" # timeout: 5s\n") + buf.WriteString("\n") + + // SSL 配置 + buf.WriteString(" # SSL/TLS 配置\n") + buf.WriteString(" # ssl:\n") + buf.WriteString(" # cert: /path/to/cert.pem # 证书文件\n") + buf.WriteString(" # key: /path/to/key.pem # 私钥文件\n") + buf.WriteString(" # protocols: # TLS 版本(默认安全)\n") + for _, proto := range cfg.Server.SSL.Protocols { + buf.WriteString(fmt.Sprintf(" # - \"%s\"\n", proto)) + } + buf.WriteString(" # hsts: # HTTP Strict Transport Security\n") + buf.WriteString(fmt.Sprintf(" # max_age: %d # 过期时间(秒)\n", cfg.Server.SSL.HSTS.MaxAge)) + buf.WriteString(fmt.Sprintf(" # include_sub_domains: %v # 包含子域名\n", cfg.Server.SSL.HSTS.IncludeSubDomains)) + buf.WriteString("\n") + + // security 配置 + buf.WriteString(" # 安全配置\n") + buf.WriteString(" security:\n") + buf.WriteString(" # IP 访问控制\n") + buf.WriteString(" # access:\n") + buf.WriteString(" # allow: [192.168.1.0/24] # 允许的 IP/CIDR\n") + buf.WriteString(" # deny: [192.168.2.100/32] # 拒绝的 IP/CIDR\n") + buf.WriteString(" # default: deny # 默认动作\n") + buf.WriteString("\n") + buf.WriteString(" # 速率限制\n") + buf.WriteString(" # rate_limit:\n") + buf.WriteString(" # request_rate: 100 # 每秒请求数\n") + buf.WriteString(" # burst: 200 # 突发上限\n") + buf.WriteString(" # conn_limit: 1000 # 连接数限制\n") + buf.WriteString("\n") + buf.WriteString(" # 认证配置(启用时强制 HTTPS)\n") + buf.WriteString(" # auth:\n") + buf.WriteString(" # type: basic\n") + buf.WriteString(" # users:\n") + buf.WriteString(" # - name: admin\n") + buf.WriteString(" # password: $2b$12$... # bcrypt 哈希\n") + buf.WriteString(" # require_tls: true\n") + buf.WriteString("\n") + buf.WriteString(" # 安全头部(默认值)\n") + buf.WriteString(" headers:\n") + buf.WriteString(fmt.Sprintf(" x_frame_options: \"%s\" # 防止点击劫持\n", cfg.Server.Security.Headers.XFrameOptions)) + buf.WriteString(fmt.Sprintf(" x_content_type_options: \"%s\" # 防止 MIME 嗅探\n", cfg.Server.Security.Headers.XContentTypeOptions)) + buf.WriteString(fmt.Sprintf(" referrer_policy: \"%s\" # 引用策略\n", cfg.Server.Security.Headers.ReferrerPolicy)) + buf.WriteString(" # content_security_policy: \"default-src 'self'\" # CSP(推荐配置)\n") + buf.WriteString("\n") + + // rewrite 配置示例 + buf.WriteString(" # URL 重写规则(示例)\n") + buf.WriteString(" # rewrite:\n") + buf.WriteString(" # - pattern: \"^/old/(.*)$\"\n") + buf.WriteString(" # replacement: /new/$1\n") + buf.WriteString(" # flag: permanent # 301 重定向\n") + buf.WriteString("\n") + + // compression 配置 + buf.WriteString(" # 响应压缩配置\n") + buf.WriteString(" compression:\n") + buf.WriteString(fmt.Sprintf(" type: \"%s\" # 压缩类型: gzip, brotli, both\n", cfg.Server.Compression.Type)) + buf.WriteString(fmt.Sprintf(" level: %d # 压缩级别 (1-9)\n", cfg.Server.Compression.Level)) + buf.WriteString(fmt.Sprintf(" min_size: %d # 最小压缩大小(字节)\n", cfg.Server.Compression.MinSize)) + buf.WriteString(" types: # 可压缩的 MIME 类型\n") + for _, t := range cfg.Server.Compression.Types { + buf.WriteString(fmt.Sprintf(" - \"%s\"\n", t)) + } + buf.WriteString("\n") + + // servers 配置说明 + buf.WriteString("# 多虚拟主机模式(可选)\n") + buf.WriteString("# servers:\n") + buf.WriteString("# - listen: \":8080\"\n") + buf.WriteString("# name: \"api.example.com\"\n") + buf.WriteString("# proxy:\n") + buf.WriteString("# - path: /api\n") + buf.WriteString("# targets: [http://backend:8080]\n") + buf.WriteString("# - listen: \":8443\"\n") + buf.WriteString("# name: \"static.example.com\"\n") + buf.WriteString("# static:\n") + buf.WriteString("# root: /var/www/static\n") + buf.WriteString("\n") + + // logging 配置 + buf.WriteString("# 日志配置\n") + buf.WriteString("logging:\n") + buf.WriteString(" access:\n") + buf.WriteString(fmt.Sprintf(" format: \"%s\" # 日志格式\n", cfg.Logging.Access.Format)) + buf.WriteString(" # path: /var/log/lolly/access.log # 日志文件路径\n") + buf.WriteString(" error:\n") + buf.WriteString(fmt.Sprintf(" level: \"%s\" # 日志级别: debug, info, warn, error\n", cfg.Logging.Error.Level)) + buf.WriteString(" # path: /var/log/lolly/error.log\n") + buf.WriteString("\n") + + // performance 配置 + buf.WriteString("# 性能配置\n") + buf.WriteString("performance:\n") + buf.WriteString(" file_cache: # 静态文件缓存\n") + buf.WriteString(fmt.Sprintf(" max_entries: %d # 最大缓存条目\n", cfg.Performance.FileCache.MaxEntries)) + buf.WriteString(fmt.Sprintf(" max_size: %dMB # 内存上限\n", cfg.Performance.FileCache.MaxSize/1024/1024)) + buf.WriteString(fmt.Sprintf(" inactive: %ds # 未访问淘汰时间\n", int(cfg.Performance.FileCache.Inactive.Seconds()))) + buf.WriteString(fmt.Sprintf(" lru_eviction: %v # 启用 LRU 淘汰\n", cfg.Performance.FileCache.LRUEviction)) + buf.WriteString(" transport: # HTTP Transport 连接池\n") + buf.WriteString(fmt.Sprintf(" max_idle_conns: %d # 最大空闲连接\n", cfg.Performance.Transport.MaxIdleConns)) + buf.WriteString(fmt.Sprintf(" max_idle_conns_per_host: %d # 每主机空闲连接\n", cfg.Performance.Transport.MaxIdleConnsPerHost)) + buf.WriteString(fmt.Sprintf(" idle_conn_timeout: %ds # 空闲超时\n", int(cfg.Performance.Transport.IdleConnTimeout.Seconds()))) + buf.WriteString("\n") + + // monitoring 配置 + buf.WriteString("# 监控配置\n") + buf.WriteString("monitoring:\n") + buf.WriteString(" status:\n") + buf.WriteString(fmt.Sprintf(" path: \"%s\" # 状态端点路径\n", cfg.Monitoring.Status.Path)) + buf.WriteString(" allow: # 允许访问的 IP\n") + for _, ip := range cfg.Monitoring.Status.Allow { + buf.WriteString(fmt.Sprintf(" - \"%s\"\n", ip)) + } + + return buf.Bytes(), nil +} + +// GenerateSimpleYAML 生成简洁的 YAML(不带注释),用于程序内部使用。 +func GenerateSimpleYAML(cfg *Config) ([]byte, error) { + return yaml.Marshal(cfg) +} \ No newline at end of file diff --git a/internal/config/validate.go b/internal/config/validate.go new file mode 100644 index 0000000..0472edf --- /dev/null +++ b/internal/config/validate.go @@ -0,0 +1,302 @@ +// Package config 提供 YAML 配置文件的解析、验证和默认配置生成功能。 +package config + +import ( + "errors" + "fmt" + "net" + "strings" +) + +// validateServer 验证服务器配置。 +func validateServer(s *ServerConfig, isDefault bool) error { + // 监听地址必填(默认服务器可省略,使用默认值) + if s.Listen == "" && !isDefault { + return errors.New("listen 地址必填") + } + + // 验证监听地址格式 + if s.Listen != "" { + if _, err := net.ResolveTCPAddr("tcp", s.Listen); err != nil { + return fmt.Errorf("无效的监听地址 %s: %w", s.Listen, err) + } + } + + // 验证静态文件配置 + if err := validateStatic(&s.Static); err != nil { + return fmt.Errorf("static: %w", err) + } + + // 验证代理配置 + for i := range s.Proxy { + if err := validateProxy(&s.Proxy[i]); err != nil { + return fmt.Errorf("proxy[%d]: %w", i, err) + } + } + + // 验证 SSL 配置 + if err := validateSSL(&s.SSL); err != nil { + return fmt.Errorf("ssl: %w", err) + } + + // 验证安全配置 + if err := validateSecurity(&s.Security); err != nil { + return fmt.Errorf("security: %w", err) + } + + // 验证压缩配置 + if err := validateCompression(&s.Compression); err != nil { + return fmt.Errorf("compression: %w", err) + } + + return nil +} + +// validateStatic 验证静态文件配置。 +func validateStatic(s *StaticConfig) error { + // 静态文件根目录非空时验证路径有效性 + if s.Root != "" { + // 路径安全检查:不允许包含 ".." + if strings.Contains(s.Root, "..") { + return errors.New("根目录路径不能包含 '..'") + } + } + return nil +} + +// validateProxy 验证代理配置。 +func validateProxy(p *ProxyConfig) error { + // 路径必填 + if p.Path == "" { + return errors.New("path 必填") + } + + // 至少需要一个目标 + if len(p.Targets) == 0 { + return errors.New("targets 至少需要一个目标地址") + } + + // 验证每个目标地址 + for i, t := range p.Targets { + if t.URL == "" { + return fmt.Errorf("targets[%d].url 必填", i) + } + if !strings.HasPrefix(t.URL, "http://") && !strings.HasPrefix(t.URL, "https://") { + return fmt.Errorf("targets[%d].url 必须以 http:// 或 https:// 开头", i) + } + } + + // 验证负载均衡算法 + validAlgorithms := []string{"", "round_robin", "weighted_round_robin", "least_conn", "ip_hash"} + valid := false + for _, alg := range validAlgorithms { + if p.LoadBalance == alg { + valid = true + break + } + } + if !valid { + return fmt.Errorf("无效的负载均衡算法: %s", p.LoadBalance) + } + + return nil +} + +// validateSSL 验证 SSL 配置。 +func validateSSL(s *SSLConfig) error { + // 未配置 SSL 时跳过验证 + if s.Cert == "" && s.Key == "" { + return nil + } + + // 证书和私钥必须同时配置 + if s.Cert == "" || s.Key == "" { + return errors.New("cert 和 key 必须同时配置") + } + + // 验证 TLS 版本 + for _, proto := range s.Protocols { + if proto == "TLSv1.0" || proto == "TLSv1.1" { + return fmt.Errorf("不安全的 TLS 版本: %s(仅允许 TLSv1.2 和 TLSv1.3)", proto) + } + if proto != "TLSv1.2" && proto != "TLSv1.3" { + return fmt.Errorf("未知的 TLS 版本: %s", proto) + } + } + + // 验证加密套件(拒绝不安全的) + insecureCiphers := []string{"RC4", "DES", "3DES", "CBC"} + for _, cipher := range s.Ciphers { + for _, insecure := range insecureCiphers { + if strings.Contains(cipher, insecure) { + return fmt.Errorf("不安全的加密套件: %s", cipher) + } + } + } + + return nil +} + +// validateSecurity 验证安全配置。 +func validateSecurity(s *SecurityConfig) error { + // 验证访问控制配置 + if err := validateAccess(&s.Access); err != nil { + return fmt.Errorf("access: %w", err) + } + + // 验证认证配置 + if err := validateAuth(&s.Auth); err != nil { + return fmt.Errorf("auth: %w", err) + } + + // 验证速率限制配置 + if err := validateRateLimit(&s.RateLimit); err != nil { + return fmt.Errorf("rate_limit: %w", err) + } + + return nil +} + +// validateAccess 验证访问控制配置。 +func validateAccess(a *AccessConfig) error { + // 验证 CIDR 格式 + for _, cidr := range a.Allow { + if _, _, err := net.ParseCIDR(cidr); err != nil { + // 尝试作为单个 IP 解析 + if ip := net.ParseIP(cidr); ip == nil { + return fmt.Errorf("无效的 allow CIDR/IP: %s", cidr) + } + } + } + + for _, cidr := range a.Deny { + if _, _, err := net.ParseCIDR(cidr); err != nil { + if ip := net.ParseIP(cidr); ip == nil { + return fmt.Errorf("无效的 deny CIDR/IP: %s", cidr) + } + } + } + + // 验证默认动作 + if a.Default != "" && a.Default != "allow" && a.Default != "deny" { + return fmt.Errorf("无效的 default 动作: %s(仅允许 allow 或 deny)", a.Default) + } + + return nil +} + +// validateAuth 验证认证配置。 +func validateAuth(a *AuthConfig) error { + // 未配置认证时跳过 + if a.Type == "" { + return nil + } + + // 仅支持 basic 认证 + if a.Type != "basic" { + return fmt.Errorf("不支持的认证类型: %s(仅支持 basic)", a.Type) + } + + // 启用 Basic Auth 时检查是否强制 HTTPS + if a.RequireTLS { + // 注意:SSL 配置在 ServerConfig 中,这里无法直接检查 + // 需要在上层验证中检查 SSL 与 Auth 的关联 + } + + // 验证哈希算法 + validAlgorithms := []string{"", "bcrypt", "argon2id"} + valid := false + for _, alg := range validAlgorithms { + if a.Algorithm == alg { + valid = true + break + } + } + if !valid { + return fmt.Errorf("不支持的哈希算法: %s(仅支持 bcrypt 或 argon2id)", a.Algorithm) + } + + // 至少需要一个用户 + if len(a.Users) == 0 { + return errors.New("启用认证时至少需要一个用户") + } + + // 验证每个用户 + for i, u := range a.Users { + if u.Name == "" { + return fmt.Errorf("users[%d].name 必填", i) + } + if u.Password == "" { + return fmt.Errorf("users[%d].password 必填", i) + } + } + + return nil +} + +// validateRateLimit 验证速率限制配置。 +func validateRateLimit(r *RateLimitConfig) error { + // 未配置时跳过 + if r.RequestRate == 0 && r.ConnLimit == 0 { + return nil + } + + // 验证速率限制值 + if r.RequestRate < 0 { + return errors.New("request_rate 不能为负数") + } + if r.Burst < 0 { + return errors.New("burst 不能为负数") + } + if r.ConnLimit < 0 { + return errors.New("conn_limit 不能为负数") + } + + // 验证 key 来源 + validKeys := []string{"", "ip", "header"} + valid := false + for _, k := range validKeys { + if r.Key == k { + valid = true + break + } + } + if !valid { + return fmt.Errorf("无效的 key 来源: %s(仅支持 ip 或 header)", r.Key) + } + + return nil +} + +// validateCompression 验证压缩配置。 +func validateCompression(c *CompressionConfig) error { + // 未配置时跳过 + if c.Type == "" { + return nil + } + + // 验证压缩类型 + validTypes := []string{"gzip", "brotli", "both"} + valid := false + for _, t := range validTypes { + if c.Type == t { + valid = true + break + } + } + if !valid { + return fmt.Errorf("无效的压缩类型: %s(仅支持 gzip, brotli 或 both)", c.Type) + } + + // 验证压缩级别 + if c.Level < 0 || c.Level > 9 { + return fmt.Errorf("无效的压缩级别: %d(范围 0-9)", c.Level) + } + + // 验证最小压缩大小 + if c.MinSize < 0 { + return errors.New("min_size 不能为负数") + } + + return nil +} \ No newline at end of file diff --git a/main.go b/main.go index 54dd236..e2a0e3d 100644 --- a/main.go +++ b/main.go @@ -1,8 +1,11 @@ package main import ( + "flag" "fmt" "os" + + "rua.plus/lolly/internal/config" ) // 通过 -ldflags 注入的版本信息 @@ -15,13 +18,67 @@ var ( buildPlatform = "unknown" ) +// CLI 参数 +var ( + cfgPath = flag.String("c", "lolly.yaml", "配置文件路径") + cfgPathLong = flag.String("config", "", "配置文件路径(长参数)") + genConfig = flag.Bool("generate-config", false, "生成默认配置") + outputPath = flag.String("o", "", "输出文件路径(配合 --generate-config)") + showVersion = flag.Bool("v", false, "显示版本") +) + func main() { - if len(os.Args) > 1 && os.Args[1] == "-v" || os.Args[1] == "--version" { + flag.Parse() + + // --generate-config 优先处理 + if *genConfig { + handleGenerateConfig(*outputPath) + return + } + + // 版本显示 + if *showVersion { printVersion() return } - fmt.Println("Hello World!") + // 合并短参数和长参数 + configPath := *cfgPath + if *cfgPathLong != "" { + configPath = *cfgPathLong + } + + // 加载配置 + cfg, err := config.Load(configPath) + if err != nil { + fmt.Fprintf(os.Stderr, "加载配置失败: %v\n", err) + os.Exit(1) + } + + fmt.Printf("配置加载成功: %s\n", configPath) + fmt.Printf("监听地址: %s\n", cfg.Server.Listen) + + // TODO: 启动服务器 + fmt.Println("服务器启动中...") +} + +func handleGenerateConfig(outputPath string) { + cfg := config.DefaultConfig() + yamlData, err := config.GenerateConfigYAML(cfg) + if err != nil { + fmt.Fprintf(os.Stderr, "生成配置失败: %v\n", err) + os.Exit(1) + } + + if outputPath == "" { + fmt.Print(string(yamlData)) + } else { + if err := os.WriteFile(outputPath, yamlData, 0644); err != nil { + fmt.Fprintf(os.Stderr, "写入文件失败: %v\n", err) + os.Exit(1) + } + fmt.Printf("配置已写入: %s\n", outputPath) + } } func printVersion() { @@ -30,4 +87,4 @@ func printVersion() { fmt.Printf(" Built: %s\n", buildTime) fmt.Printf(" Go: %s\n", goVersion) fmt.Printf(" Platform: %s\n", buildPlatform) -} +} \ No newline at end of file