0d84b07f68
CI / docker (push) Successful in 1m58s
Go + Vue 管理台:Agent 隧道、域名反代、IP 安全、访客验证与监控大盘。 Gitea Actions CI 多架构镜像;纯 Go SQLite、无 CGO Docker 构建。 Co-authored-by: Cursor <cursoragent@cursor.com>
201 lines
5.1 KiB
Go
201 lines
5.1 KiB
Go
package config
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"os"
|
|
"time"
|
|
|
|
"gopkg.in/yaml.v3"
|
|
)
|
|
|
|
const DefaultDBPath = "./data/wormhole.db"
|
|
|
|
type Config struct {
|
|
Server ServerConfig `yaml:"server" json:"server"`
|
|
Database DatabaseConfig `yaml:"database" json:"database"`
|
|
Auth AuthConfig `yaml:"auth" json:"auth"`
|
|
Metrics MetricsConfig `yaml:"metrics" json:"metrics"`
|
|
}
|
|
|
|
type ServerConfig struct {
|
|
BridgeAddr string `yaml:"bridge_addr" json:"bridge_addr"`
|
|
HTTPAddr string `yaml:"http_addr" json:"http_addr"`
|
|
HTTPProxyPort int `yaml:"http_proxy_port" json:"http_proxy_port"`
|
|
HTTPSProxyPort int `yaml:"https_proxy_port" json:"https_proxy_port"`
|
|
TLSCertFile string `yaml:"tls_cert_file" json:"tls_cert_file"`
|
|
TLSKeyFile string `yaml:"tls_key_file" json:"tls_key_file"`
|
|
}
|
|
|
|
type DatabaseConfig struct {
|
|
Path string `yaml:"path" json:"path"`
|
|
}
|
|
|
|
type AuthConfig struct {
|
|
JWTSecret string `yaml:"jwt_secret" json:"jwt_secret"`
|
|
TokenTTL time.Duration `yaml:"token_ttl" json:"token_ttl"`
|
|
LoginMaxAttempts int `yaml:"login_max_attempts" json:"login_max_attempts"`
|
|
LoginWindow time.Duration `yaml:"login_window" json:"login_window"`
|
|
LoginLockout time.Duration `yaml:"login_lockout" json:"login_lockout"`
|
|
}
|
|
|
|
type MetricsConfig struct {
|
|
FlushInterval time.Duration `yaml:"flush_interval" json:"flush_interval"`
|
|
}
|
|
|
|
// SettingsDTO is the API/UI representation of runtime settings.
|
|
type SettingsDTO struct {
|
|
Server ServerConfig `json:"server"`
|
|
Auth AuthSettings `json:"auth"`
|
|
Metrics struct {
|
|
FlushIntervalSec int `json:"flush_interval_sec"`
|
|
} `json:"metrics"`
|
|
}
|
|
|
|
type AuthSettings struct {
|
|
JWTSecret string `json:"jwt_secret,omitempty"`
|
|
TokenTTLHours int `json:"token_ttl_hours"`
|
|
LoginMaxAttempts int `json:"login_max_attempts"`
|
|
LoginWindowMin int `json:"login_window_min"`
|
|
LoginLockoutMin int `json:"login_lockout_min"`
|
|
}
|
|
|
|
func Default() *Config {
|
|
secret, _ := randomSecret(32)
|
|
return &Config{
|
|
Server: ServerConfig{
|
|
BridgeAddr: ":8528",
|
|
HTTPAddr: ":8529",
|
|
HTTPProxyPort: 8081,
|
|
HTTPSProxyPort: 8443,
|
|
},
|
|
Database: DatabaseConfig{
|
|
Path: DefaultDBPath,
|
|
},
|
|
Auth: AuthConfig{
|
|
JWTSecret: secret,
|
|
TokenTTL: 24 * time.Hour,
|
|
LoginMaxAttempts: 5,
|
|
LoginWindow: 5 * time.Minute,
|
|
LoginLockout: 15 * time.Minute,
|
|
},
|
|
Metrics: MetricsConfig{
|
|
FlushInterval: 30 * time.Second,
|
|
},
|
|
}
|
|
}
|
|
|
|
func randomSecret(n int) (string, error) {
|
|
b := make([]byte, n)
|
|
if _, err := rand.Read(b); err != nil {
|
|
return "wormhole-change-me-in-production", err
|
|
}
|
|
return hex.EncodeToString(b), nil
|
|
}
|
|
|
|
func (c *Config) Normalize() {
|
|
if c.Server.BridgeAddr == "" {
|
|
c.Server.BridgeAddr = ":8528"
|
|
}
|
|
if c.Server.HTTPAddr == "" {
|
|
c.Server.HTTPAddr = ":8529"
|
|
}
|
|
if c.Server.HTTPProxyPort == 0 {
|
|
c.Server.HTTPProxyPort = 8081
|
|
}
|
|
if c.Server.HTTPSProxyPort == 0 {
|
|
c.Server.HTTPSProxyPort = 8443
|
|
}
|
|
if c.Database.Path == "" {
|
|
c.Database.Path = DefaultDBPath
|
|
}
|
|
if c.Auth.TokenTTL == 0 {
|
|
c.Auth.TokenTTL = 24 * time.Hour
|
|
}
|
|
if c.Auth.LoginMaxAttempts == 0 {
|
|
c.Auth.LoginMaxAttempts = 5
|
|
}
|
|
if c.Auth.LoginWindow == 0 {
|
|
c.Auth.LoginWindow = 5 * time.Minute
|
|
}
|
|
if c.Auth.LoginLockout == 0 {
|
|
c.Auth.LoginLockout = 15 * time.Minute
|
|
}
|
|
if c.Auth.JWTSecret == "" {
|
|
secret, _ := randomSecret(32)
|
|
c.Auth.JWTSecret = secret
|
|
}
|
|
if c.Metrics.FlushInterval == 0 {
|
|
c.Metrics.FlushInterval = 30 * time.Second
|
|
}
|
|
}
|
|
|
|
func (c *Config) ToDTO() SettingsDTO {
|
|
sec := int(c.Metrics.FlushInterval / time.Second)
|
|
if sec < 1 {
|
|
sec = 30
|
|
}
|
|
dto := SettingsDTO{
|
|
Server: c.Server,
|
|
Auth: AuthSettings{
|
|
TokenTTLHours: int(c.Auth.TokenTTL / time.Hour),
|
|
LoginMaxAttempts: c.Auth.LoginMaxAttempts,
|
|
LoginWindowMin: int(c.Auth.LoginWindow / time.Minute),
|
|
LoginLockoutMin: int(c.Auth.LoginLockout / time.Minute),
|
|
},
|
|
}
|
|
dto.Metrics.FlushIntervalSec = sec
|
|
return dto
|
|
}
|
|
|
|
func SettingsFromDTO(d SettingsDTO, existing *Config) *Config {
|
|
cfg := Default()
|
|
if existing != nil {
|
|
*cfg = *existing
|
|
}
|
|
cfg.Server = d.Server
|
|
cfg.Auth.TokenTTL = time.Duration(d.Auth.TokenTTLHours) * time.Hour
|
|
cfg.Auth.LoginMaxAttempts = d.Auth.LoginMaxAttempts
|
|
cfg.Auth.LoginWindow = time.Duration(d.Auth.LoginWindowMin) * time.Minute
|
|
cfg.Auth.LoginLockout = time.Duration(d.Auth.LoginLockoutMin) * time.Minute
|
|
if d.Auth.JWTSecret != "" {
|
|
cfg.Auth.JWTSecret = d.Auth.JWTSecret
|
|
}
|
|
if d.Metrics.FlushIntervalSec > 0 {
|
|
cfg.Metrics.FlushInterval = time.Duration(d.Metrics.FlushIntervalSec) * time.Second
|
|
}
|
|
cfg.Normalize()
|
|
return cfg
|
|
}
|
|
|
|
func (c *Config) Summary() string {
|
|
return fmt.Sprintf(
|
|
"http_addr=%s bridge_addr=%s http_proxy_port=%d https_proxy_port=%d db=%s",
|
|
c.Server.HTTPAddr,
|
|
c.Server.BridgeAddr,
|
|
c.Server.HTTPProxyPort,
|
|
c.Server.HTTPSProxyPort,
|
|
c.Database.Path,
|
|
)
|
|
}
|
|
|
|
// Load reads config from a YAML file (tests only).
|
|
func Load(path string) (*Config, error) {
|
|
if path == "" {
|
|
return nil, fmt.Errorf("config path required")
|
|
}
|
|
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
cfg := Default()
|
|
if err := yaml.Unmarshal(data, cfg); err != nil {
|
|
return nil, err
|
|
}
|
|
cfg.Normalize()
|
|
return cfg, nil
|
|
}
|