Files
Havline/internal/settings/settings.go
T
mrlee fc16fb2dc2 fix(agent,acme,cert,web): 修 frps 安装 404 与 nginx 探测两处,证书颁发机构用 LiteSSL 替换 Buypass(v1.2.14)
- frps 安装/升级一直 404:frp 官方校验文件名始终是 frp_sha256_checksums.txt(不带版本号),代码却拼 frp_<版本>_sha256sums.txt;现按候选名依次尝试并在失败时列出尝试项。同一条 bug 也存在于本机下载 frpc 的路径,一并修复(SHA-256 校验逻辑未改)

- nginx 探测(agent):nginxPath() 不再回落到配置里的裸名字 nginx(否则「未安装」被误报为配置校验失败,且一键安装入口永不出现);vhost 目录探测不再要求被加载文件带 .conf 后缀(Debian 的 sites-enabled/default 不带,导致只剩主配置目录可试,报「均无法放置完整 server 配置」),失败信息补上具体原因

- 证书 CA:LiteSSL(亚数 TrustAsia,90 天,支持单域名/通配符/多域名)替换 Buypass。LiteSSL 目录 meta.externalAccountRequired=true,强制 EAB——在 FreeSSL 创建后填 Kid/HMAC 即可,HMAC 只回掩码;Buypass 保留常量与展示元数据但不再可选

- 前端申请证书弹窗的颁发机构卡片改为前端可控清单(原先后端 /api/certs/options 的结果不参与渲染,后端改了弹窗不变)

- 测试:internal/frp 校验文件候选名与 sha256sum 解析;internal/acme LiteSSL 目录/别名/通配符/Buypass 停用提示。gofmt / go build / go vet / go test / vue-tsc / vite build 全绿

- AgentVersion 0.12.4 → 0.12.7,双架构二进制已重编:VPS 上需点「升级 Agent」
2026-09-27 13:44:03 +08:00

209 lines
7.1 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package settings
import (
"context"
"database/sql"
"strconv"
"strings"
)
const (
KeyTheme = "theme"
KeyTimezone = "timezone"
KeyLogRetentionDays = "log_retention_days"
KeyDDNSCheckInterval = "ddns_check_interval_minutes"
KeyCertRenewThreshold = "cert_renew_threshold_days"
KeyRootDomain = "root_domain"
KeyACMEEmail = "acme_email"
KeyACMECA = "acme_ca"
KeyZeroSSLAPIKey = "zerossl_api_key"
// KeyACMEDNSResolvers DNS-01 传播检查用的递归解析器(逗号分隔;system = 用系统解析器)
KeyACMEDNSResolvers = "acme_dns_resolvers"
// LiteSSL 强制 EAB:kid 与 hmac 在 FreeSSL 的「证书自动化 → EAB 管理」创建
KeyLiteSSLEABKid = "litessl_eab_kid"
KeyLiteSSLEABHMAC = "litessl_eab_hmac"
KeyNotifyWebhookURL = "notify_webhook_url"
KeyNotifyType = "notify_type"
KeyNotifyEmailJSON = "notify_email_json"
KeyNotifySMTPPassword = "notify_smtp_password"
KeyNotifyWebhookJSON = "notify_webhook_json"
KeyNotifyWebhookSecret = "notify_webhook_secret"
KeyNotifyTelegramJSON = "notify_telegram_json"
KeyNotifyTelegramToken = "notify_telegram_token"
KeyNotifyOnDDNSError = "notify_on_ddns_error" // legacy, migrated to KeyNotifyOnDDNSFailure
KeyNotifyOnCertError = "notify_on_cert_error" // legacy, migrated to KeyNotifyOnCertExpiry
KeyNotifyOnNginxError = "notify_on_nginx_error" // legacy, ignored
KeyNotifyOnDDNSIPChange = "notify_on_ddns_ip_change"
KeyNotifyOnDDNSFailure = "notify_on_ddns_failure"
KeyNotifyOnCertExpiry = "notify_on_cert_expiry"
KeyNotifyOnCertRenewSuccess = "notify_on_cert_renew_success"
KeyNotifyOnIPFrequentAccess = "notify_on_ip_frequent_access"
KeyNotifyOnCertRenewFailure = "notify_on_cert_renew_failure"
KeyNotifyOnLoginFailure = "notify_on_login_failure"
KeyNotifyOnNginxReloadFailure = "notify_on_nginx_reload_failure"
KeyNotifyIPFrequentThreshold = "notify_ip_frequent_threshold"
KeyNotifyIPFrequentWindowSec = "notify_ip_frequent_window_sec"
KeyNotifyLoginFailureThreshold = "notify_login_failure_threshold"
KeyNotifyLoginFailureWindowSec = "notify_login_failure_window_sec"
KeyNotifyOnResourceThreshold = "notify_on_resource_threshold"
KeyNotifyOnTunnelDown = "notify_on_tunnel_down"
KeyNotifyCPUThreshold = "notify_cpu_threshold"
KeyNotifyMemoryThreshold = "notify_memory_threshold"
KeyNotifyDiskThreshold = "notify_disk_threshold"
KeyNotifyTunnelFailThreshold = "notify_tunnel_fail_threshold"
KeyNotifyOnFrpsDown = "notify_on_frps_down"
KeyNotifyOnAgentUnreachable = "notify_on_agent_unreachable"
KeyNotifyOnRouteDrift = "notify_on_route_drift"
KeyNotifyFrpsFailThreshold = "notify_frps_fail_threshold"
KeyNotifyAgentFailThreshold = "notify_agent_fail_threshold"
KeyNotifyDriftCooldownHours = "notify_drift_cooldown_hours"
KeyBackupAutoEnabled = "backup_auto_enabled"
KeyBackupKeepCount = "backup_keep_count"
KeyRouteHealthEnabled = "route_health_enabled"
KeyStatusPageEnabled = "status_page_enabled"
KeyMetricsHistoryEnabled = "metrics_history_enabled"
// KeyStatusPageCodeHash 存访问码的 SHA-256;明文只在保存请求里出现一次
KeyStatusPageCodeHash = "status_page_code_hash"
// KeyStatusPageCode 只是「保存请求」里用的字段名,不会被直接写库
KeyStatusPageCode = "status_page_code"
KeyNotifyOnBackupFailure = "notify_on_backup_failure"
KeyTrustedProxy = "trusted_proxy_json"
KeyGlobalIPBlacklist = "global_ip_blacklist"
KeyGlobalIPWhitelist = "global_ip_whitelist"
KeyChinaCIDRUpdateHours = "china_cidr_update_interval_hours"
KeyChinaCIDRSourceV4 = "china_cidr_source_url_v4"
KeyChinaCIDRSourceV6 = "china_cidr_source_url_v6"
KeyChinaCIDRUpdatedAt = "china_cidr_updated_at"
KeyChinaCIDRCountV4 = "china_cidr_count_v4"
KeyChinaCIDRCountV6 = "china_cidr_count_v6"
KeyChinaCIDRLastError = "china_cidr_last_error"
KeyFRPEnabled = "frp_enabled"
KeyFRPServerAddr = "frp_server_addr"
KeyFRPServerPort = "frp_server_port"
KeyFRPAuthToken = "frp_auth_token"
KeyFRPTLSEnabled = "frp_tls_enabled"
KeyFRPCustomDomains = "frp_custom_domains"
KeyFRPLastError = "frp_last_error"
KeyFRPStartedAt = "frp_started_at"
KeyNginxGlobalMode = "nginx_global_mode"
)
var Defaults = map[string]string{
KeyTheme: "system",
KeyTimezone: "Asia/Shanghai",
KeyLogRetentionDays: "30",
KeyDDNSCheckInterval: "5",
KeyCertRenewThreshold: "30",
KeyACMECA: "letsencrypt",
KeyChinaCIDRUpdateHours: "24",
KeyNginxGlobalMode: "auto",
}
type Store struct {
db *sql.DB
}
func NewStore(db *sql.DB) *Store {
return &Store{db: db}
}
func (s *Store) Get(ctx context.Context, key string) (string, error) {
var value string
err := s.db.QueryRowContext(ctx, `SELECT value FROM settings WHERE key = ?`, key).Scan(&value)
if err == sql.ErrNoRows {
if def, ok := Defaults[key]; ok {
return def, nil
}
return "", nil
}
return value, err
}
func (s *Store) Set(ctx context.Context, key, value string) error {
_, err := s.db.ExecContext(ctx, `
INSERT INTO settings(key, value) VALUES (?, ?)
ON CONFLICT(key) DO UPDATE SET value = excluded.value
`, key, value)
return err
}
func (s *Store) GetBool(ctx context.Context, key string) (bool, error) {
raw, err := s.Get(ctx, key)
if err != nil {
return false, err
}
switch strings.ToLower(strings.TrimSpace(raw)) {
case "1", "true", "yes", "on":
return true, nil
default:
return false, nil
}
}
func (s *Store) SetBool(ctx context.Context, key string, value bool) error {
if value {
return s.Set(ctx, key, "true")
}
return s.Set(ctx, key, "false")
}
func (s *Store) SetInt(ctx context.Context, key string, value int) error {
return s.Set(ctx, key, strconv.Itoa(value))
}
func (s *Store) GetInt(ctx context.Context, key string) (int, error) {
raw, err := s.Get(ctx, key)
if err != nil {
return 0, err
}
if raw == "" {
if def, ok := Defaults[key]; ok {
raw = def
}
}
n, err := strconv.Atoi(raw)
if err != nil {
return 0, err
}
return n, nil
}
func (s *Store) List(ctx context.Context) (map[string]string, error) {
rows, err := s.db.QueryContext(ctx, `SELECT key, value FROM settings`)
if err != nil {
return nil, err
}
defer rows.Close()
out := map[string]string{}
for k, v := range Defaults {
out[k] = v
}
for rows.Next() {
var key, value string
if err := rows.Scan(&key, &value); err != nil {
return nil, err
}
out[key] = value
}
return out, rows.Err()
}
func (s *Store) SetMany(ctx context.Context, values map[string]string) error {
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer func() { _ = tx.Rollback() }()
for key, value := range values {
if _, err := tx.ExecContext(ctx, `
INSERT INTO settings(key, value) VALUES (?, ?)
ON CONFLICT(key) DO UPDATE SET value = excluded.value
`, key, value); err != nil {
return err
}
}
return tx.Commit()
}