Files
Havline/internal/api/settings_handler.go
T

271 lines
9.2 KiB
Go
Raw Normal View History

2026-09-23 09:40:59 +08:00
package api
import (
"encoding/json"
"fmt"
"net/http"
"strconv"
"strings"
2026-09-23 09:40:59 +08:00
"github.com/fonu/fonu/internal/notify"
"github.com/fonu/fonu/internal/service"
"github.com/fonu/fonu/internal/settings"
"github.com/fonu/fonu/internal/validate"
)
type SettingsHandler struct {
store *settings.Store
proxy *service.ProxyService
notify *notify.Service
}
func NewSettingsHandler(store *settings.Store, proxySvc *service.ProxyService, notifySvc *notify.Service) *SettingsHandler {
return &SettingsHandler{store: store, proxy: proxySvc, notify: notifySvc}
}
func (h *SettingsHandler) Get(w http.ResponseWriter, r *http.Request) {
values, err := h.store.List(r.Context())
if err != nil {
writeError(r, w, http.StatusInternalServerError, "读取设置失败", err)
return
}
notify.SanitizeSettings(values)
// 状态页访问码只回掩码:既不下发明文也不下发哈希
if values[settings.KeyStatusPageCodeHash] != "" {
values[settings.KeyStatusPageCode] = MaskedStatusPageCode
}
delete(values, settings.KeyStatusPageCodeHash)
2026-09-23 09:40:59 +08:00
writeJSON(w, http.StatusOK, values)
}
func (h *SettingsHandler) Put(w http.ResponseWriter, r *http.Request) {
var req map[string]string
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(r, w, http.StatusBadRequest, "请求格式无效")
return
}
allowed := map[string]bool{
settings.KeyTheme: true,
settings.KeyTimezone: true,
settings.KeyLogRetentionDays: true,
settings.KeyDDNSCheckInterval: true,
settings.KeyCertRenewThreshold: true,
settings.KeyRootDomain: true,
settings.KeyACMEEmail: true,
settings.KeyACMECA: true,
settings.KeyZeroSSLAPIKey: true,
settings.KeyACMEDNSResolvers: true,
settings.KeyLiteSSLEABKid: true,
settings.KeyLiteSSLEABHMAC: true,
settings.KeyNotifyWebhookURL: true,
settings.KeyNotifyType: true,
settings.KeyNotifyEmailJSON: true,
settings.KeyNotifySMTPPassword: true,
settings.KeyNotifyWebhookJSON: true,
settings.KeyNotifyWebhookSecret: true,
settings.KeyNotifyTelegramJSON: true,
settings.KeyNotifyTelegramToken: true,
settings.KeyNotifyOnDDNSIPChange: true,
settings.KeyNotifyOnDDNSFailure: true,
2026-09-23 09:40:59 +08:00
settings.KeyNotifyOnCertExpiry: true,
settings.KeyNotifyOnCertRenewSuccess: true,
settings.KeyNotifyOnIPFrequentAccess: true,
settings.KeyNotifyOnCertRenewFailure: true,
settings.KeyNotifyOnLoginFailure: true,
settings.KeyNotifyOnNginxReloadFailure: true,
settings.KeyNotifyIPFrequentThreshold: true,
settings.KeyNotifyIPFrequentWindowSec: true,
settings.KeyNotifyLoginFailureThreshold: true,
settings.KeyNotifyLoginFailureWindowSec: true,
settings.KeyNotifyOnResourceThreshold: true,
settings.KeyNotifyOnTunnelDown: true,
settings.KeyNotifyCPUThreshold: true,
settings.KeyNotifyMemoryThreshold: true,
settings.KeyNotifyDiskThreshold: true,
settings.KeyNotifyTunnelFailThreshold: true,
settings.KeyNotifyOnFrpsDown: true,
settings.KeyNotifyOnAgentUnreachable: true,
settings.KeyNotifyOnRouteDrift: true,
settings.KeyNotifyFrpsFailThreshold: true,
settings.KeyNotifyAgentFailThreshold: true,
settings.KeyNotifyDriftCooldownHours: true,
settings.KeyBackupAutoEnabled: true,
settings.KeyRouteHealthEnabled: true,
settings.KeyStatusPageEnabled: true,
settings.KeyMetricsHistoryEnabled: true,
settings.KeyStatusPageCode: true,
settings.KeyBackupKeepCount: true,
settings.KeyNotifyOnBackupFailure: true,
settings.KeyTrustedProxy: true,
settings.KeyGlobalIPBlacklist: true,
settings.KeyGlobalIPWhitelist: true,
settings.KeyChinaCIDRUpdateHours: true,
settings.KeyChinaCIDRSourceV4: true,
settings.KeyChinaCIDRSourceV6: true,
2026-09-23 09:40:59 +08:00
}
filtered := map[string]string{}
for key, value := range req {
if !allowed[key] {
continue
}
if err := h.validateSetting(key, value); err != nil {
writeError(r, w, http.StatusBadRequest, err.Error())
return
}
filtered[key] = value
}
// 状态页访问码走「掩码往返」:客户端提交明文,这里落 SHA-256;
// 空串 = 清除访问码,掩码值 = 保持不变。哈希字段不接受客户端直接写入。
if code, ok := filtered[settings.KeyStatusPageCode]; ok {
delete(filtered, settings.KeyStatusPageCode)
trimmed := strings.TrimSpace(code)
switch {
case trimmed == MaskedStatusPageCode:
case trimmed == "":
filtered[settings.KeyStatusPageCodeHash] = ""
default:
filtered[settings.KeyStatusPageCodeHash] = hashStatusPageCode(trimmed)
}
}
2026-09-23 09:40:59 +08:00
if h.notify != nil && h.notify.Store() != nil {
if in, ok := notify.ParseSaveInputFromMap(filtered); ok {
if err := h.notify.Store().Save(r.Context(), in); err != nil {
writeError(r, w, http.StatusBadRequest, err.Error())
return
}
for key := range filtered {
if isNotifySettingKey(key) {
delete(filtered, key)
}
}
}
}
if err := h.store.SetMany(r.Context(), filtered); err != nil {
writeError(r, w, http.StatusInternalServerError, "保存设置失败", err)
return
}
if h.proxy != nil && (filtered[settings.KeyTrustedProxy] != "" || filtered[settings.KeyGlobalIPBlacklist] != "" || filtered[settings.KeyGlobalIPWhitelist] != "") {
if err := h.proxy.ReloadAll(r.Context()); err != nil {
writeError(r, w, http.StatusInternalServerError, "设置已保存,但 Nginx 重载失败:"+err.Error(), err)
return
}
}
values, err := h.store.List(r.Context())
if err != nil {
writeError(r, w, http.StatusInternalServerError, "读取设置失败", err)
return
}
notify.SanitizeSettings(values)
// 状态页访问码只回掩码:既不下发明文也不下发哈希
if values[settings.KeyStatusPageCodeHash] != "" {
values[settings.KeyStatusPageCode] = MaskedStatusPageCode
}
delete(values, settings.KeyStatusPageCodeHash)
2026-09-23 09:40:59 +08:00
writeJSON(w, http.StatusOK, values)
}
func isNotifySettingKey(key string) bool {
switch key {
case settings.KeyNotifyType,
settings.KeyNotifyEmailJSON,
settings.KeyNotifySMTPPassword,
settings.KeyNotifyWebhookJSON,
settings.KeyNotifyWebhookSecret,
settings.KeyNotifyTelegramJSON,
settings.KeyNotifyTelegramToken,
settings.KeyNotifyOnDDNSIPChange,
settings.KeyNotifyOnDDNSFailure,
settings.KeyNotifyOnCertExpiry,
settings.KeyNotifyOnCertRenewSuccess,
settings.KeyNotifyOnIPFrequentAccess,
settings.KeyNotifyOnCertRenewFailure,
settings.KeyNotifyOnLoginFailure,
settings.KeyNotifyOnNginxReloadFailure,
settings.KeyNotifyIPFrequentThreshold,
settings.KeyNotifyIPFrequentWindowSec,
settings.KeyNotifyLoginFailureThreshold,
settings.KeyNotifyLoginFailureWindowSec,
settings.KeyNotifyOnResourceThreshold,
settings.KeyNotifyOnTunnelDown,
settings.KeyNotifyCPUThreshold,
settings.KeyNotifyMemoryThreshold,
settings.KeyNotifyDiskThreshold,
settings.KeyNotifyTunnelFailThreshold,
settings.KeyNotifyOnFrpsDown,
settings.KeyNotifyOnAgentUnreachable,
settings.KeyNotifyOnRouteDrift,
settings.KeyNotifyFrpsFailThreshold,
settings.KeyNotifyAgentFailThreshold,
settings.KeyNotifyDriftCooldownHours,
settings.KeyNotifyOnBackupFailure,
2026-09-23 09:40:59 +08:00
settings.KeyNotifyWebhookURL:
return true
default:
return false
}
}
func (h *SettingsHandler) validateSetting(key, value string) error {
switch key {
case settings.KeyNotifyEmailJSON, settings.KeyNotifyWebhookJSON, settings.KeyNotifyTelegramJSON:
if value != "" {
var raw map[string]any
if err := json.Unmarshal([]byte(value), &raw); err != nil {
return err
}
}
case settings.KeyNotifyType:
switch value {
case "", "email", "webhook", "telegram":
default:
return fmt.Errorf("无效的通知类型")
}
case settings.KeyNotifyIPFrequentThreshold:
if value != "" {
n, err := strconv.Atoi(value)
if err != nil || n < 10 || n > 10000 {
return fmt.Errorf("频繁访问阈值需在 10–10000 之间")
}
}
case settings.KeyNotifyIPFrequentWindowSec, settings.KeyNotifyLoginFailureWindowSec:
if value != "" {
n, err := strconv.Atoi(value)
if err != nil || n < 10 || n > 3600 {
return fmt.Errorf("统计窗口需在 10–3600 秒之间")
}
}
case settings.KeyBackupKeepCount:
if value != "" {
n, err := strconv.Atoi(value)
if err != nil || n < 1 || n > 60 {
return fmt.Errorf("备份保留份数需在 1–60 之间")
}
}
2026-09-23 09:40:59 +08:00
case settings.KeyNotifyLoginFailureThreshold:
if value != "" {
n, err := strconv.Atoi(value)
if err != nil || n < 3 || n > 100 {
return fmt.Errorf("登录失败阈值需在 3–100 之间")
}
}
case settings.KeyGlobalIPBlacklist, settings.KeyGlobalIPWhitelist:
var list []string
if value != "" && value != "[]" {
if err := json.Unmarshal([]byte(value), &list); err != nil {
return err
}
if err := validate.IPOrCIDRList(list); err != nil {
return err
}
}
case settings.KeyTrustedProxy:
if value != "" && value != "{}" {
var raw map[string]any
if err := json.Unmarshal([]byte(value), &raw); err != nil {
return err
}
}
}
return nil
}