- internal/utils:ClientIP 按序枚举 CDN/代理头(含 RFC 7239 Forwarded),仅在可信代理来源时采信,否则回退直连 IP;RemoteIP 取直连地址;RandomString 生成安全随机串 - 新增 server.trusted_proxies 配置(IP/CIDR,ConfigVersion 2→3 自动补全),启动时同步应用到 gin 与 utils - 初始管理员密码生成改用 utils.RandomString,原密码测试迁至 utils
290 lines
7.7 KiB
Go
290 lines
7.7 KiB
Go
package config
|
||
|
||
import (
|
||
_ "embed"
|
||
"errors"
|
||
"fmt"
|
||
"io/fs"
|
||
"log/slog"
|
||
"net"
|
||
"os"
|
||
"path/filepath"
|
||
"strings"
|
||
"time"
|
||
|
||
"github.com/goccy/go-yaml"
|
||
)
|
||
|
||
//go:embed config.default.yaml
|
||
var defaultConfigYAML []byte
|
||
|
||
type Config struct {
|
||
Version int `yaml:"version"`
|
||
Server ServerConfig `yaml:"server"`
|
||
Log LogConfig `yaml:"log"`
|
||
Static StaticConfig `yaml:"static"`
|
||
API APIConfig `yaml:"api"`
|
||
Auth AuthConfig `yaml:"auth"`
|
||
Database DatabaseConfig `yaml:"database"`
|
||
}
|
||
|
||
type ServerConfig struct {
|
||
Host string `yaml:"host"`
|
||
Port int `yaml:"port"`
|
||
Sock string `yaml:"sock"`
|
||
Mode string `yaml:"mode"`
|
||
TrustedProxies []string `yaml:"trusted_proxies"`
|
||
}
|
||
|
||
// TCPEnabled 是否启用 TCP 监听(port 为 0 表示不启用)。
|
||
func (s ServerConfig) TCPEnabled() bool {
|
||
return s.Port > 0
|
||
}
|
||
|
||
// SockEnabled 是否启用 unix socket 监听(sock 留空表示不启用)。
|
||
func (s ServerConfig) SockEnabled() bool {
|
||
return strings.TrimSpace(s.Sock) != ""
|
||
}
|
||
|
||
type LogConfig struct {
|
||
Level string `yaml:"level"`
|
||
AccessLog bool `yaml:"access_log"`
|
||
}
|
||
|
||
type StaticConfig struct {
|
||
Dir string `yaml:"dir"`
|
||
}
|
||
|
||
type APIConfig struct {
|
||
Prefix string `yaml:"prefix"`
|
||
CORS CORSConfig `yaml:"cors"`
|
||
}
|
||
|
||
type CORSConfig struct {
|
||
Enabled bool `yaml:"enabled"`
|
||
AllowOrigins []string `yaml:"allow_origins"`
|
||
AllowMethods []string `yaml:"allow_methods"`
|
||
AllowHeaders []string `yaml:"allow_headers"`
|
||
AllowCredentials bool `yaml:"allow_credentials"`
|
||
MaxAge string `yaml:"max_age"`
|
||
}
|
||
|
||
type AuthConfig struct {
|
||
Secret string `yaml:"secret"`
|
||
TokenTTL string `yaml:"token_ttl"`
|
||
}
|
||
|
||
type DatabaseConfig struct {
|
||
Driver string `yaml:"driver"` // sqlite3 / mysql
|
||
ConnectTimeout string `yaml:"connect_timeout"` // 建立连接超时
|
||
SQLite SQLiteConfig `yaml:"sqlite"`
|
||
MySQL MySQLConfig `yaml:"mysql"`
|
||
}
|
||
|
||
type SQLiteConfig struct {
|
||
Path string `yaml:"path"`
|
||
}
|
||
|
||
type MySQLConfig struct {
|
||
Host string `yaml:"host"`
|
||
Port int `yaml:"port"`
|
||
User string `yaml:"user"`
|
||
Password string `yaml:"password"`
|
||
Database string `yaml:"database"`
|
||
Charset string `yaml:"charset"`
|
||
MaxOpenConns int `yaml:"max_open_conns"`
|
||
MaxIdleConns int `yaml:"max_idle_conns"`
|
||
ConnMaxLifetime string `yaml:"conn_max_lifetime"`
|
||
}
|
||
|
||
func defaultConfig() *Config {
|
||
return &Config{
|
||
Server: ServerConfig{
|
||
Host: "0.0.0.0",
|
||
Port: 8080,
|
||
Sock: "web.sock",
|
||
Mode: "release",
|
||
TrustedProxies: []string{},
|
||
},
|
||
Log: LogConfig{
|
||
Level: "info",
|
||
AccessLog: true,
|
||
},
|
||
Static: StaticConfig{
|
||
Dir: "./dist",
|
||
},
|
||
API: APIConfig{
|
||
Prefix: "/api",
|
||
CORS: CORSConfig{
|
||
Enabled: true,
|
||
AllowOrigins: []string{"http://localhost:5173"},
|
||
AllowMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"},
|
||
AllowHeaders: []string{"Origin", "Content-Type", "Accept", "Authorization"},
|
||
AllowCredentials: false,
|
||
MaxAge: "12h",
|
||
},
|
||
},
|
||
Auth: AuthConfig{
|
||
TokenTTL: "24h",
|
||
},
|
||
Database: DatabaseConfig{
|
||
Driver: "sqlite3",
|
||
ConnectTimeout: "10s",
|
||
SQLite: SQLiteConfig{
|
||
Path: "./data/rill.db",
|
||
},
|
||
MySQL: MySQLConfig{
|
||
Host: "127.0.0.1",
|
||
Port: 3306,
|
||
User: "root",
|
||
Password: "",
|
||
Database: "rill",
|
||
Charset: "utf8mb4",
|
||
MaxOpenConns: 10,
|
||
MaxIdleConns: 5,
|
||
ConnMaxLifetime: "1h",
|
||
},
|
||
},
|
||
}
|
||
}
|
||
|
||
// LoadConfig 读取 YAML 配置文件,未指定的配置项使用默认值;文件不存在时自动生成默认配置文件。
|
||
func LoadConfig(path string) (*Config, error) {
|
||
cfg := defaultConfig()
|
||
|
||
data, err := os.ReadFile(path)
|
||
switch {
|
||
case err == nil:
|
||
data = applyConfigUpgrade(path, data)
|
||
case errors.Is(err, fs.ErrNotExist):
|
||
if err := generateConfig(path); err != nil {
|
||
return nil, err
|
||
}
|
||
data = defaultConfigYAML
|
||
default:
|
||
return nil, fmt.Errorf("读取配置文件 %s 失败: %w", path, err)
|
||
}
|
||
|
||
if err := yaml.Unmarshal(data, cfg); err != nil {
|
||
return nil, fmt.Errorf("解析配置文件 %s 失败: %w", path, err)
|
||
}
|
||
|
||
if err := cfg.validate(); err != nil {
|
||
return nil, err
|
||
}
|
||
return cfg, nil
|
||
}
|
||
|
||
func generateConfig(path string) error {
|
||
if dir := filepath.Dir(path); dir != "." {
|
||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||
return fmt.Errorf("创建配置目录 %s 失败: %w", dir, err)
|
||
}
|
||
}
|
||
if err := os.WriteFile(path, defaultConfigYAML, 0o644); err != nil {
|
||
return fmt.Errorf("生成默认配置文件 %s 失败: %w", path, err)
|
||
}
|
||
slog.Info("配置文件不存在,已生成默认配置", "path", path)
|
||
return nil
|
||
}
|
||
|
||
func (c *Config) validate() error {
|
||
switch c.Server.Mode {
|
||
case "debug", "release", "test":
|
||
default:
|
||
return fmt.Errorf("server.mode 无效: %q(可选: debug/release/test)", c.Server.Mode)
|
||
}
|
||
if c.Server.Port < 0 || c.Server.Port > 65535 {
|
||
return fmt.Errorf("server.port 无效: %d(0 表示不启用 TCP)", c.Server.Port)
|
||
}
|
||
if !c.Server.TCPEnabled() && !c.Server.SockEnabled() {
|
||
return fmt.Errorf("server.port 与 server.sock 至少需要启用一个")
|
||
}
|
||
for _, proxy := range c.Server.TrustedProxies {
|
||
value := strings.TrimSpace(proxy)
|
||
if value == "" || net.ParseIP(value) != nil {
|
||
continue
|
||
}
|
||
if _, _, err := net.ParseCIDR(value); err != nil {
|
||
return fmt.Errorf("server.trusted_proxies 无效: %q(需为 IP 或 CIDR)", proxy)
|
||
}
|
||
}
|
||
if _, err := parseLogLevel(c.Log.Level); err != nil {
|
||
return err
|
||
}
|
||
if _, err := time.ParseDuration(c.API.CORS.MaxAge); err != nil {
|
||
return fmt.Errorf("api.cors.max_age 无效: %w", err)
|
||
}
|
||
if _, err := time.ParseDuration(c.Auth.TokenTTL); err != nil {
|
||
return fmt.Errorf("auth.token_ttl 无效: %w", err)
|
||
}
|
||
if err := c.validateDatabase(); err != nil {
|
||
return err
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func (c *Config) validateDatabase() error {
|
||
if _, err := time.ParseDuration(c.Database.ConnectTimeout); err != nil {
|
||
return fmt.Errorf("database.connect_timeout 无效: %w", err)
|
||
}
|
||
|
||
switch c.Database.Driver {
|
||
case "sqlite3":
|
||
if strings.TrimSpace(c.Database.SQLite.Path) == "" {
|
||
return fmt.Errorf("database.sqlite.path 不能为空")
|
||
}
|
||
case "mysql":
|
||
db := c.Database.MySQL
|
||
if strings.TrimSpace(db.Host) == "" {
|
||
return fmt.Errorf("database.mysql.host 不能为空")
|
||
}
|
||
if db.Port <= 0 || db.Port > 65535 {
|
||
return fmt.Errorf("database.mysql.port 无效: %d", db.Port)
|
||
}
|
||
if strings.TrimSpace(db.Database) == "" {
|
||
return fmt.Errorf("database.mysql.database 不能为空")
|
||
}
|
||
if db.MaxOpenConns < 0 || db.MaxIdleConns < 0 {
|
||
return fmt.Errorf("database.mysql 连接池大小不能为负数")
|
||
}
|
||
if _, err := time.ParseDuration(db.ConnMaxLifetime); err != nil {
|
||
return fmt.Errorf("database.mysql.conn_max_lifetime 无效: %w", err)
|
||
}
|
||
default:
|
||
return fmt.Errorf("database.driver 无效: %q(可选: sqlite3/mysql)", c.Database.Driver)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func (c *Config) LogLevel() slog.Level {
|
||
level, _ := parseLogLevel(c.Log.Level)
|
||
return level
|
||
}
|
||
|
||
func (c *Config) CORSMaxAge() time.Duration {
|
||
maxAge, _ := time.ParseDuration(c.API.CORS.MaxAge)
|
||
return maxAge
|
||
}
|
||
|
||
// TokenTTLDuration 登录凭证有效期,配置无效时返回 0。
|
||
func (c *Config) TokenTTLDuration() time.Duration {
|
||
ttl, _ := time.ParseDuration(c.Auth.TokenTTL)
|
||
return ttl
|
||
}
|
||
|
||
func parseLogLevel(level string) (slog.Level, error) {
|
||
switch level {
|
||
case "debug":
|
||
return slog.LevelDebug, nil
|
||
case "info":
|
||
return slog.LevelInfo, nil
|
||
case "warn":
|
||
return slog.LevelWarn, nil
|
||
case "error":
|
||
return slog.LevelError, nil
|
||
default:
|
||
return slog.LevelInfo, fmt.Errorf("log.level 无效: %q(可选: debug/info/warn/error)", level)
|
||
}
|
||
}
|