184 lines
5.6 KiB
Go
184 lines
5.6 KiB
Go
package config
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
)
|
|
|
|
const CurrentVersion = 1
|
|
const ConfigFormat = "json"
|
|
|
|
type Config struct {
|
|
Version int `json:"version"`
|
|
SetupReady bool `json:"setup_ready"`
|
|
Server ServerConfig `json:"server"`
|
|
Database DatabaseConfig `json:"database"`
|
|
Paths PathsConfig `json:"paths"`
|
|
ACME ACMEConfig `json:"acme"`
|
|
Notification NotificationConfig `json:"notification"`
|
|
}
|
|
|
|
type ServerConfig struct {
|
|
Host string `json:"host"`
|
|
Port int `json:"port"`
|
|
HTTPSMode string `json:"https_mode"`
|
|
}
|
|
|
|
type DatabaseConfig struct {
|
|
Driver string `json:"driver"`
|
|
DSN string `json:"dsn"`
|
|
MaxOpenConns int `json:"max_open_conns"`
|
|
MaxIdleConns int `json:"max_idle_conns"`
|
|
ConnMaxLifetimeMins int `json:"conn_max_lifetime_mins"`
|
|
}
|
|
|
|
type PathsConfig struct {
|
|
Install string `json:"install"`
|
|
Certificate string `json:"certificate"`
|
|
Config string `json:"config"`
|
|
Data string `json:"data"`
|
|
Log string `json:"log"`
|
|
Backup string `json:"backup"`
|
|
}
|
|
|
|
type ACMEConfig struct {
|
|
CA string `json:"ca"`
|
|
Environment string `json:"environment"`
|
|
AccountEmail string `json:"account_email"`
|
|
RenewalThresholdDays int `json:"renewal_threshold_days"`
|
|
}
|
|
|
|
type NotificationConfig struct {
|
|
HTTPEnabled bool `json:"http_enabled"`
|
|
SMTPEnabled bool `json:"smtp_enabled"`
|
|
TimeoutSecs int `json:"timeout_secs"`
|
|
RetryCount int `json:"retry_count"`
|
|
}
|
|
|
|
func Default(baseDir string) Config {
|
|
paths := DefaultPaths(baseDir)
|
|
return Config{
|
|
Version: CurrentVersion,
|
|
SetupReady: false,
|
|
Server: ServerConfig{Host: "127.0.0.1", Port: 8080, HTTPSMode: "direct"},
|
|
Database: DatabaseConfig{
|
|
Driver: "sqlite", DSN: filepath.Join(paths.Data, "scert.db"),
|
|
MaxOpenConns: 4, MaxIdleConns: 2, ConnMaxLifetimeMins: 30,
|
|
},
|
|
Paths: paths,
|
|
ACME: ACMEConfig{CA: "letsencrypt", Environment: "staging", RenewalThresholdDays: 6},
|
|
Notification: NotificationConfig{TimeoutSecs: 10, RetryCount: 3},
|
|
}
|
|
}
|
|
|
|
func DefaultPaths(baseDir string) PathsConfig {
|
|
if runtime.GOOS == "linux" {
|
|
return PathsConfig{
|
|
Install: "/scert", Certificate: "/ssl", Config: "/scert/conf",
|
|
Data: "/scert/data", Log: "/scert/log", Backup: "/scert/backups",
|
|
}
|
|
}
|
|
if strings.TrimSpace(baseDir) == "" {
|
|
baseDir = "."
|
|
}
|
|
install := filepath.Join(baseDir, "Scert")
|
|
return PathsConfig{
|
|
Install: install, Certificate: filepath.Join(install, "ssl"),
|
|
Config: filepath.Join(install, "config"), Data: filepath.Join(install, "data"),
|
|
Log: filepath.Join(install, "logs"), Backup: filepath.Join(install, "backups"),
|
|
}
|
|
}
|
|
|
|
func Load(path string) (Config, error) {
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return Config{}, fmt.Errorf("读取配置文件失败:%w", err)
|
|
}
|
|
var cfg Config
|
|
if err := json.Unmarshal(data, &cfg); err != nil {
|
|
return Config{}, fmt.Errorf("解析配置文件失败:%w", err)
|
|
}
|
|
if err := cfg.Validate(); err != nil {
|
|
return Config{}, err
|
|
}
|
|
return cfg, nil
|
|
}
|
|
|
|
func (c Config) Save(path string) error {
|
|
if err := c.Validate(); err != nil {
|
|
return err
|
|
}
|
|
if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
|
|
return fmt.Errorf("创建配置目录失败:%w", err)
|
|
}
|
|
data, err := json.MarshalIndent(c, "", " ")
|
|
if err != nil {
|
|
return fmt.Errorf("编码配置失败:%w", err)
|
|
}
|
|
data = append(data, '\n')
|
|
tmpPath := path + ".tmp"
|
|
if err := os.WriteFile(tmpPath, data, 0o600); err != nil {
|
|
return fmt.Errorf("写入临时配置文件失败:%w", err)
|
|
}
|
|
if err := os.Rename(tmpPath, path); err != nil {
|
|
_ = os.Remove(tmpPath)
|
|
return fmt.Errorf("替换配置文件失败:%w", err)
|
|
}
|
|
if err := os.Chmod(path, 0o600); err != nil {
|
|
return fmt.Errorf("设置配置文件权限失败:%w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (c Config) Validate() error {
|
|
if c.Version != CurrentVersion {
|
|
return fmt.Errorf("配置版本不兼容:%d", c.Version)
|
|
}
|
|
if strings.TrimSpace(c.Server.Host) == "" {
|
|
return fmt.Errorf("服务监听地址不能为空")
|
|
}
|
|
if c.Server.Port < 1 || c.Server.Port > 65535 {
|
|
return fmt.Errorf("服务监听端口无效:%d", c.Server.Port)
|
|
}
|
|
if c.Server.HTTPSMode != "direct" && c.Server.HTTPSMode != "proxy" {
|
|
return fmt.Errorf("HTTPS 模式无效:%s", c.Server.HTTPSMode)
|
|
}
|
|
if c.Database.Driver != "sqlite" && c.Database.Driver != "mysql" {
|
|
return fmt.Errorf("数据库驱动无效:%s", c.Database.Driver)
|
|
}
|
|
if strings.TrimSpace(c.Database.DSN) == "" {
|
|
return fmt.Errorf("数据库连接信息不能为空")
|
|
}
|
|
if c.Database.MaxOpenConns < 1 || c.Database.MaxIdleConns < 0 || c.Database.MaxIdleConns > c.Database.MaxOpenConns {
|
|
return fmt.Errorf("数据库连接池配置无效")
|
|
}
|
|
if c.Database.ConnMaxLifetimeMins < 0 {
|
|
return fmt.Errorf("数据库连接生命周期不能为负数")
|
|
}
|
|
for name, value := range map[string]string{
|
|
"安装目录": c.Paths.Install, "证书目录": c.Paths.Certificate, "配置目录": c.Paths.Config,
|
|
"数据目录": c.Paths.Data, "日志目录": c.Paths.Log, "备份目录": c.Paths.Backup,
|
|
} {
|
|
if strings.TrimSpace(value) == "" {
|
|
return fmt.Errorf("%s不能为空", name)
|
|
}
|
|
}
|
|
if c.ACME.CA != "letsencrypt" {
|
|
return fmt.Errorf("当前仅支持 Let's Encrypt")
|
|
}
|
|
if c.ACME.Environment != "staging" && c.ACME.Environment != "production" {
|
|
return fmt.Errorf("ACME 环境无效:%s", c.ACME.Environment)
|
|
}
|
|
if c.ACME.RenewalThresholdDays < 1 {
|
|
return fmt.Errorf("续期阈值必须大于 0")
|
|
}
|
|
if c.Notification.TimeoutSecs < 1 || c.Notification.RetryCount < 0 {
|
|
return fmt.Errorf("通知配置无效")
|
|
}
|
|
return nil
|
|
}
|