Files
rose_cat707 0d84b07f68
CI / docker (push) Successful in 1m58s
feat: Wormhole 内网穿透与反向代理网关
Go + Vue 管理台:Agent 隧道、域名反代、IP 安全、访客验证与监控大盘。
Gitea Actions CI 多架构镜像;纯 Go SQLite、无 CGO Docker 构建。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-23 17:52:43 +08:00

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
}