CI / verify (push) Canceled after 0s
- 三个新事件(默认关闭):公网服务端掉线、公网 agent 不可达、配置漂移;连续失败阈值与漂移提醒间隔均可配,达标与恢复各报一次 - 避免误报:只在「用户要服务端运行」时判定掉线;ConnectionState=unknown 按健康处理;没配 agent 的服务端不参与可达性判定 - internal/monitor 改 Source 接口(Routes/Servers/Agents/Drift),frp 适配在 internal/app/monitor_source.go;「连续 N 次」抽成共用 failureTransition,通知一律在锁外调用 - 通知图标分类补齐;通知方式标签改「Webhook / IM 机器人」;新增 6 个设置项;补 serverDown/serverLabel/stateOr 测试 - agent 无改动,AgentVersion 保持 0.12.2
555 lines
21 KiB
Go
555 lines
21 KiB
Go
package notify
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/fonu/fonu/internal/secret"
|
|
"github.com/fonu/fonu/internal/settings"
|
|
)
|
|
|
|
type Store struct {
|
|
settings *settings.Store
|
|
secret *secret.Box
|
|
}
|
|
|
|
func NewStore(settingsStore *settings.Store, secretBox *secret.Box) *Store {
|
|
return &Store{settings: settingsStore, secret: secretBox}
|
|
}
|
|
|
|
func (s *Store) Load(ctx context.Context) (Config, error) {
|
|
cfg, _, err := s.load(ctx, false)
|
|
return cfg, err
|
|
}
|
|
|
|
func (s *Store) LoadRuntime(ctx context.Context) (RuntimeConfig, error) {
|
|
cfg, runtime, err := s.load(ctx, true)
|
|
if err != nil {
|
|
return RuntimeConfig{}, err
|
|
}
|
|
return cfg.Runtime(runtime.smtpPassword, runtime.webhookSecret, runtime.telegramToken), nil
|
|
}
|
|
|
|
type runtimeSecrets struct {
|
|
smtpPassword string
|
|
webhookSecret string
|
|
telegramToken string
|
|
}
|
|
|
|
func (s *Store) load(ctx context.Context, decrypt bool) (Config, runtimeSecrets, error) {
|
|
cfg := Config{
|
|
Email: EmailConfig{Port: 587, TLS: true},
|
|
Webhook: WebhookConfig{Provider: WebhookBark, Server: DefaultBarkServer},
|
|
Telegram: TelegramConfig{},
|
|
}
|
|
secrets := runtimeSecrets{}
|
|
|
|
typeRaw, _ := s.settings.Get(ctx, settings.KeyNotifyType)
|
|
cfg.Type = NotifyType(strings.TrimSpace(typeRaw))
|
|
|
|
emailRaw, _ := s.settings.Get(ctx, settings.KeyNotifyEmailJSON)
|
|
if emailRaw != "" {
|
|
_ = json.Unmarshal([]byte(emailRaw), &cfg.Email)
|
|
}
|
|
if cfg.Email.Port <= 0 {
|
|
cfg.Email.Port = 587
|
|
}
|
|
|
|
webhookRaw, _ := s.settings.Get(ctx, settings.KeyNotifyWebhookJSON)
|
|
if webhookRaw != "" {
|
|
_ = json.Unmarshal([]byte(webhookRaw), &cfg.Webhook)
|
|
}
|
|
if cfg.Webhook.Server == "" && cfg.Webhook.Provider == WebhookBark {
|
|
cfg.Webhook.Server = DefaultBarkServer
|
|
}
|
|
|
|
telegramRaw, _ := s.settings.Get(ctx, settings.KeyNotifyTelegramJSON)
|
|
if telegramRaw != "" {
|
|
_ = json.Unmarshal([]byte(telegramRaw), &cfg.Telegram)
|
|
}
|
|
|
|
cfg.IPFrequentThreshold = intSetting(s.settings, ctx, settings.KeyNotifyIPFrequentThreshold, DefaultIPFrequentThreshold)
|
|
cfg.IPFrequentWindowSec = intSetting(s.settings, ctx, settings.KeyNotifyIPFrequentWindowSec, DefaultIPFrequentWindowSec)
|
|
|
|
cfg.LoginFailureThreshold = intSetting(s.settings, ctx, settings.KeyNotifyLoginFailureThreshold, DefaultLoginFailureThreshold)
|
|
cfg.LoginFailureWindowSec = intSetting(s.settings, ctx, settings.KeyNotifyLoginFailureWindowSec, DefaultLoginFailureWindowSec)
|
|
cfg.CPUThreshold = intSetting(s.settings, ctx, settings.KeyNotifyCPUThreshold, DefaultCPUThreshold)
|
|
cfg.MemoryThreshold = intSetting(s.settings, ctx, settings.KeyNotifyMemoryThreshold, DefaultMemoryThreshold)
|
|
cfg.DiskThreshold = intSetting(s.settings, ctx, settings.KeyNotifyDiskThreshold, DefaultDiskThreshold)
|
|
cfg.TunnelFailThreshold = intSetting(s.settings, ctx, settings.KeyNotifyTunnelFailThreshold, DefaultTunnelFailThreshold)
|
|
cfg.FrpsFailThreshold = intSetting(s.settings, ctx, settings.KeyNotifyFrpsFailThreshold, DefaultFrpsFailThreshold)
|
|
cfg.AgentFailThreshold = intSetting(s.settings, ctx, settings.KeyNotifyAgentFailThreshold, DefaultAgentFailThreshold)
|
|
cfg.DriftCooldownHours = intSetting(s.settings, ctx, settings.KeyNotifyDriftCooldownHours, DefaultDriftCooldownHours)
|
|
|
|
if hasNotifyEventSettings(ctx, s.settings) {
|
|
cfg.OnDDNSIPChange = flag(s.settings, ctx, settings.KeyNotifyOnDDNSIPChange, true)
|
|
cfg.OnDDNSFailure = flag(s.settings, ctx, settings.KeyNotifyOnDDNSFailure, true)
|
|
cfg.OnCertExpiry = flag(s.settings, ctx, settings.KeyNotifyOnCertExpiry, true)
|
|
cfg.OnCertRenewSuccess = flag(s.settings, ctx, settings.KeyNotifyOnCertRenewSuccess, true)
|
|
cfg.OnCertRenewFailure = flag(s.settings, ctx, settings.KeyNotifyOnCertRenewFailure, true)
|
|
cfg.OnIPFrequentAccess = flag(s.settings, ctx, settings.KeyNotifyOnIPFrequentAccess, false)
|
|
cfg.OnLoginFailure = flag(s.settings, ctx, settings.KeyNotifyOnLoginFailure, false)
|
|
cfg.OnNginxReloadFailure = flag(s.settings, ctx, settings.KeyNotifyOnNginxReloadFailure, true)
|
|
// 资源与隧道告警默认关闭:升级后不改变现有用户收到的通知
|
|
cfg.OnResourceThreshold = flag(s.settings, ctx, settings.KeyNotifyOnResourceThreshold, false)
|
|
cfg.OnTunnelDown = flag(s.settings, ctx, settings.KeyNotifyOnTunnelDown, false)
|
|
cfg.OnFrpsDown = flag(s.settings, ctx, settings.KeyNotifyOnFrpsDown, false)
|
|
cfg.OnAgentUnreachable = flag(s.settings, ctx, settings.KeyNotifyOnAgentUnreachable, false)
|
|
cfg.OnRouteDrift = flag(s.settings, ctx, settings.KeyNotifyOnRouteDrift, false)
|
|
} else {
|
|
legacyDDNS := flag(s.settings, ctx, settings.KeyNotifyOnDDNSError, true)
|
|
legacyCert := flag(s.settings, ctx, settings.KeyNotifyOnCertError, true)
|
|
cfg.OnDDNSFailure = legacyDDNS
|
|
cfg.OnCertExpiry = legacyCert
|
|
cfg.OnCertRenewFailure = legacyCert
|
|
cfg.OnDDNSIPChange = true
|
|
cfg.OnCertRenewSuccess = true
|
|
cfg.OnNginxReloadFailure = true
|
|
cfg.OnIPFrequentAccess = false
|
|
cfg.OnLoginFailure = false
|
|
}
|
|
|
|
smtpEnc, _ := s.settings.Get(ctx, settings.KeyNotifySMTPPassword)
|
|
if smtpEnc != "" {
|
|
cfg.Email.HasPassword = true
|
|
if decrypt && s.secret != nil {
|
|
plain, err := s.secret.Decrypt(smtpEnc)
|
|
if err != nil {
|
|
// 单个通道的密文损坏(例如更换过加密密钥)不应让整份配置加载失败:
|
|
// 仅标记该通道不可用并继续解析其它通道,避免 Telegram/Webhook 一起失效
|
|
cfg.Email.HasPassword = false
|
|
} else {
|
|
secrets.smtpPassword = plain
|
|
}
|
|
}
|
|
}
|
|
|
|
webhookEnc, _ := s.settings.Get(ctx, settings.KeyNotifyWebhookSecret)
|
|
if webhookEnc != "" {
|
|
cfg.Webhook.HasSecret = true
|
|
if decrypt && s.secret != nil {
|
|
plain, err := s.secret.Decrypt(webhookEnc)
|
|
if err != nil {
|
|
// 同上:只让该通道不可用,不影响其它通道
|
|
cfg.Webhook.HasSecret = false
|
|
} else {
|
|
secrets.webhookSecret = plain
|
|
}
|
|
}
|
|
}
|
|
|
|
telegramEnc, _ := s.settings.Get(ctx, settings.KeyNotifyTelegramToken)
|
|
if telegramEnc != "" {
|
|
cfg.Telegram.HasBotToken = true
|
|
if decrypt && s.secret != nil {
|
|
plain, err := s.secret.Decrypt(telegramEnc)
|
|
if err != nil {
|
|
// 同上:只让该通道不可用,不影响其它通道
|
|
cfg.Telegram.HasBotToken = false
|
|
} else {
|
|
secrets.telegramToken = plain
|
|
}
|
|
}
|
|
}
|
|
|
|
if cfg.Type == "" {
|
|
legacyURL, _ := s.settings.Get(ctx, settings.KeyNotifyWebhookURL)
|
|
if strings.TrimSpace(legacyURL) != "" {
|
|
cfg.Type = NotifyTypeWebhook
|
|
cfg.Webhook.Provider = WebhookCustom
|
|
cfg.Webhook.URL = strings.TrimSpace(legacyURL)
|
|
}
|
|
}
|
|
|
|
return cfg, secrets, nil
|
|
}
|
|
|
|
func flag(store *settings.Store, ctx context.Context, key string, fallback bool) bool {
|
|
raw, err := store.Get(ctx, key)
|
|
if err != nil || raw == "" {
|
|
return fallback
|
|
}
|
|
return raw == "1" || strings.EqualFold(raw, "true")
|
|
}
|
|
|
|
func intSetting(store *settings.Store, ctx context.Context, key string, fallback int) int {
|
|
raw, err := store.Get(ctx, key)
|
|
if err != nil || strings.TrimSpace(raw) == "" {
|
|
return fallback
|
|
}
|
|
n, err := strconv.Atoi(strings.TrimSpace(raw))
|
|
if err != nil || n <= 0 {
|
|
return fallback
|
|
}
|
|
return n
|
|
}
|
|
|
|
func hasNotifyEventSettings(ctx context.Context, store *settings.Store) bool {
|
|
keys := []string{
|
|
settings.KeyNotifyOnDDNSIPChange,
|
|
settings.KeyNotifyOnDDNSFailure,
|
|
settings.KeyNotifyOnCertExpiry,
|
|
settings.KeyNotifyOnCertRenewSuccess,
|
|
settings.KeyNotifyOnIPFrequentAccess,
|
|
settings.KeyNotifyOnCertRenewFailure,
|
|
settings.KeyNotifyOnLoginFailure,
|
|
settings.KeyNotifyOnNginxReloadFailure,
|
|
settings.KeyNotifyOnResourceThreshold,
|
|
settings.KeyNotifyOnTunnelDown,
|
|
settings.KeyNotifyOnFrpsDown,
|
|
settings.KeyNotifyOnAgentUnreachable,
|
|
settings.KeyNotifyOnRouteDrift,
|
|
}
|
|
for _, key := range keys {
|
|
raw, err := store.Get(ctx, key)
|
|
if err == nil && raw != "" {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (s *Store) Save(ctx context.Context, in SaveInput) error {
|
|
if err := validateSaveInput(in); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := s.settings.Set(ctx, settings.KeyNotifyType, string(in.Type)); err != nil {
|
|
return err
|
|
}
|
|
if err := s.settings.SetBool(ctx, settings.KeyNotifyOnDDNSIPChange, in.OnDDNSIPChange); err != nil {
|
|
return err
|
|
}
|
|
if err := s.settings.SetBool(ctx, settings.KeyNotifyOnDDNSFailure, in.OnDDNSFailure); err != nil {
|
|
return err
|
|
}
|
|
if err := s.settings.SetBool(ctx, settings.KeyNotifyOnCertExpiry, in.OnCertExpiry); err != nil {
|
|
return err
|
|
}
|
|
if err := s.settings.SetBool(ctx, settings.KeyNotifyOnCertRenewSuccess, in.OnCertRenewSuccess); err != nil {
|
|
return err
|
|
}
|
|
if err := s.settings.SetBool(ctx, settings.KeyNotifyOnIPFrequentAccess, in.OnIPFrequentAccess); err != nil {
|
|
return err
|
|
}
|
|
if err := s.settings.SetBool(ctx, settings.KeyNotifyOnCertRenewFailure, in.OnCertRenewFailure); err != nil {
|
|
return err
|
|
}
|
|
if err := s.settings.SetBool(ctx, settings.KeyNotifyOnLoginFailure, in.OnLoginFailure); err != nil {
|
|
return err
|
|
}
|
|
if err := s.settings.SetBool(ctx, settings.KeyNotifyOnNginxReloadFailure, in.OnNginxReloadFailure); err != nil {
|
|
return err
|
|
}
|
|
if err := s.settings.SetBool(ctx, settings.KeyNotifyOnResourceThreshold, in.OnResourceThreshold); err != nil {
|
|
return err
|
|
}
|
|
if err := s.settings.SetBool(ctx, settings.KeyNotifyOnTunnelDown, in.OnTunnelDown); err != nil {
|
|
return err
|
|
}
|
|
if err := s.settings.SetInt(ctx, settings.KeyNotifyCPUThreshold, clampThreshold(in.CPUThreshold, DefaultCPUThreshold)); err != nil {
|
|
return err
|
|
}
|
|
if err := s.settings.SetInt(ctx, settings.KeyNotifyMemoryThreshold, clampThreshold(in.MemoryThreshold, DefaultMemoryThreshold)); err != nil {
|
|
return err
|
|
}
|
|
if err := s.settings.SetInt(ctx, settings.KeyNotifyDiskThreshold, clampThreshold(in.DiskThreshold, DefaultDiskThreshold)); err != nil {
|
|
return err
|
|
}
|
|
if err := s.settings.SetInt(ctx, settings.KeyNotifyTunnelFailThreshold, clampThreshold(in.TunnelFailThreshold, DefaultTunnelFailThreshold)); err != nil {
|
|
return err
|
|
}
|
|
if err := s.settings.SetBool(ctx, settings.KeyNotifyOnFrpsDown, in.OnFrpsDown); err != nil {
|
|
return err
|
|
}
|
|
if err := s.settings.SetBool(ctx, settings.KeyNotifyOnAgentUnreachable, in.OnAgentUnreachable); err != nil {
|
|
return err
|
|
}
|
|
if err := s.settings.SetBool(ctx, settings.KeyNotifyOnRouteDrift, in.OnRouteDrift); err != nil {
|
|
return err
|
|
}
|
|
if err := s.settings.SetInt(ctx, settings.KeyNotifyFrpsFailThreshold, clampThreshold(in.FrpsFailThreshold, DefaultFrpsFailThreshold)); err != nil {
|
|
return err
|
|
}
|
|
if err := s.settings.SetInt(ctx, settings.KeyNotifyAgentFailThreshold, clampThreshold(in.AgentFailThreshold, DefaultAgentFailThreshold)); err != nil {
|
|
return err
|
|
}
|
|
if err := s.settings.SetInt(ctx, settings.KeyNotifyDriftCooldownHours, clampThreshold(in.DriftCooldownHours, DefaultDriftCooldownHours)); err != nil {
|
|
return err
|
|
}
|
|
threshold := in.IPFrequentThreshold
|
|
if threshold <= 0 {
|
|
threshold = DefaultIPFrequentThreshold
|
|
}
|
|
windowSec := in.IPFrequentWindowSec
|
|
if windowSec <= 0 {
|
|
windowSec = DefaultIPFrequentWindowSec
|
|
}
|
|
if err := s.settings.SetInt(ctx, settings.KeyNotifyIPFrequentThreshold, threshold); err != nil {
|
|
return err
|
|
}
|
|
if err := s.settings.SetInt(ctx, settings.KeyNotifyIPFrequentWindowSec, windowSec); err != nil {
|
|
return err
|
|
}
|
|
loginThreshold := in.LoginFailureThreshold
|
|
if loginThreshold <= 0 {
|
|
loginThreshold = DefaultLoginFailureThreshold
|
|
}
|
|
loginWindowSec := in.LoginFailureWindowSec
|
|
if loginWindowSec <= 0 {
|
|
loginWindowSec = DefaultLoginFailureWindowSec
|
|
}
|
|
if err := s.settings.SetInt(ctx, settings.KeyNotifyLoginFailureThreshold, loginThreshold); err != nil {
|
|
return err
|
|
}
|
|
if err := s.settings.SetInt(ctx, settings.KeyNotifyLoginFailureWindowSec, loginWindowSec); err != nil {
|
|
return err
|
|
}
|
|
|
|
email := in.Email
|
|
email.HasPassword = email.HasPassword || strings.TrimSpace(in.SMTPPassword) != "" && in.SMTPPassword != MaskedSecret
|
|
emailJSON, err := json.Marshal(email)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := s.settings.Set(ctx, settings.KeyNotifyEmailJSON, string(emailJSON)); err != nil {
|
|
return err
|
|
}
|
|
|
|
webhook := in.Webhook
|
|
secretValue := strings.TrimSpace(in.WebhookSecret)
|
|
if secretValue != "" && secretValue != MaskedSecret {
|
|
webhook.HasSecret = true
|
|
} else if providerUsesKey(webhook.Provider) {
|
|
if strings.TrimSpace(webhook.Key) != "" && webhook.Key != MaskedSecret {
|
|
webhook.HasSecret = true
|
|
}
|
|
}
|
|
webhookJSON, err := json.Marshal(webhook)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := s.settings.Set(ctx, settings.KeyNotifyWebhookJSON, string(webhookJSON)); err != nil {
|
|
return err
|
|
}
|
|
|
|
telegram := in.Telegram
|
|
if strings.TrimSpace(in.TelegramToken) != "" && in.TelegramToken != MaskedSecret {
|
|
telegram.HasBotToken = true
|
|
}
|
|
telegramJSON, err := json.Marshal(telegram)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := s.settings.Set(ctx, settings.KeyNotifyTelegramJSON, string(telegramJSON)); err != nil {
|
|
return err
|
|
}
|
|
|
|
if in.Type == NotifyTypeWebhook && webhook.Provider == WebhookCustom {
|
|
if err := s.settings.Set(ctx, settings.KeyNotifyWebhookURL, strings.TrimSpace(webhook.URL)); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if err := s.saveSecret(ctx, settings.KeyNotifySMTPPassword, in.SMTPPassword, in.Email.HasPassword); err != nil {
|
|
return err
|
|
}
|
|
|
|
webhookPlain := secretValue
|
|
if webhookPlain == "" || webhookPlain == MaskedSecret {
|
|
if providerUsesKey(webhook.Provider) {
|
|
if strings.TrimSpace(webhook.Key) != "" && webhook.Key != MaskedSecret {
|
|
webhookPlain = strings.TrimSpace(webhook.Key)
|
|
}
|
|
}
|
|
}
|
|
if err := s.saveSecret(ctx, settings.KeyNotifyWebhookSecret, webhookPlain, webhook.HasSecret); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := s.saveSecret(ctx, settings.KeyNotifyTelegramToken, in.TelegramToken, telegram.HasBotToken); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *Store) saveSecret(ctx context.Context, key, plain string, hasExisting bool) error {
|
|
plain = strings.TrimSpace(plain)
|
|
if plain == "" || plain == MaskedSecret {
|
|
if !hasExisting {
|
|
return nil
|
|
}
|
|
return nil
|
|
}
|
|
if s.secret == nil {
|
|
return fmt.Errorf("加密模块未初始化")
|
|
}
|
|
enc, err := s.secret.Encrypt(plain)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return s.settings.Set(ctx, key, enc)
|
|
}
|
|
|
|
func validateSaveInput(in SaveInput) error {
|
|
switch in.Type {
|
|
case "":
|
|
return nil
|
|
case NotifyTypeEmail:
|
|
if strings.TrimSpace(in.Email.Host) == "" {
|
|
return fmt.Errorf("请填写 SMTP 主机")
|
|
}
|
|
if len(in.Email.To) == 0 {
|
|
return fmt.Errorf("请填写收件人邮箱")
|
|
}
|
|
password := strings.TrimSpace(in.SMTPPassword)
|
|
if password == "" || password == MaskedSecret {
|
|
if !in.Email.HasPassword {
|
|
return fmt.Errorf("请填写 SMTP 密码")
|
|
}
|
|
}
|
|
case NotifyTypeWebhook:
|
|
switch in.Webhook.Provider {
|
|
case WebhookBark:
|
|
if strings.TrimSpace(in.Webhook.Key) == "" && !in.Webhook.HasSecret {
|
|
return fmt.Errorf("请填写 Bark Device Key")
|
|
}
|
|
case WebhookNtfy:
|
|
if strings.TrimSpace(in.Webhook.Topic) == "" {
|
|
return fmt.Errorf("请填写 ntfy Topic")
|
|
}
|
|
case WebhookGotify:
|
|
if strings.TrimSpace(in.Webhook.Key) == "" && !in.Webhook.HasSecret {
|
|
return fmt.Errorf("请填写 Gotify App Token")
|
|
}
|
|
case WebhookWecom:
|
|
if strings.TrimSpace(in.Webhook.Key) == "" && !in.Webhook.HasSecret {
|
|
return fmt.Errorf("请填写企业微信机器人 Key")
|
|
}
|
|
case WebhookDingtalk:
|
|
if strings.TrimSpace(in.Webhook.Key) == "" && !in.Webhook.HasSecret {
|
|
return fmt.Errorf("请填写钉钉机器人 Access Token")
|
|
}
|
|
case WebhookFeishu:
|
|
if strings.TrimSpace(in.Webhook.Key) == "" && !in.Webhook.HasSecret {
|
|
return fmt.Errorf("请填写飞书机器人 Key")
|
|
}
|
|
case WebhookCustom:
|
|
if strings.TrimSpace(in.Webhook.URL) == "" {
|
|
return fmt.Errorf("请填写 Webhook URL")
|
|
}
|
|
default:
|
|
return fmt.Errorf("不支持的 Webhook 预设")
|
|
}
|
|
case NotifyTypeTelegram:
|
|
token := strings.TrimSpace(in.TelegramToken)
|
|
if token == "" && !in.Telegram.HasBotToken {
|
|
return fmt.Errorf("请填写 Telegram Bot Token")
|
|
}
|
|
if strings.TrimSpace(in.Telegram.ChatID) == "" {
|
|
return fmt.Errorf("请填写 Telegram Chat ID")
|
|
}
|
|
default:
|
|
return fmt.Errorf("不支持的通知类型")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func SanitizeSettings(values map[string]string) {
|
|
delete(values, settings.KeyNotifySMTPPassword)
|
|
delete(values, settings.KeyNotifyWebhookSecret)
|
|
delete(values, settings.KeyNotifyTelegramToken)
|
|
// ZeroSSL API Key 属证书申请凭据:API 永不回明文,只回掩码。前端以「非空」判断是否已配置,
|
|
// 且只在用户重新输入时才提交该字段,因此掩码不会被当成新密钥写回。
|
|
if values[settings.KeyZeroSSLAPIKey] != "" {
|
|
values[settings.KeyZeroSSLAPIKey] = MaskedSecret
|
|
}
|
|
}
|
|
|
|
func ParseSaveInputFromMap(values map[string]string) (SaveInput, bool) {
|
|
if values[settings.KeyNotifyType] == "" &&
|
|
values[settings.KeyNotifyEmailJSON] == "" &&
|
|
values[settings.KeyNotifyWebhookJSON] == "" &&
|
|
values[settings.KeyNotifyTelegramJSON] == "" &&
|
|
values[settings.KeyNotifySMTPPassword] == "" &&
|
|
values[settings.KeyNotifyWebhookSecret] == "" &&
|
|
values[settings.KeyNotifyTelegramToken] == "" {
|
|
return SaveInput{}, false
|
|
}
|
|
|
|
in := SaveInput{
|
|
Type: NotifyType(values[settings.KeyNotifyType]),
|
|
SMTPPassword: values[settings.KeyNotifySMTPPassword],
|
|
WebhookSecret: values[settings.KeyNotifyWebhookSecret],
|
|
TelegramToken: values[settings.KeyNotifyTelegramToken],
|
|
}
|
|
if raw := values[settings.KeyNotifyEmailJSON]; raw != "" {
|
|
_ = json.Unmarshal([]byte(raw), &in.Email)
|
|
}
|
|
if raw := values[settings.KeyNotifyWebhookJSON]; raw != "" {
|
|
_ = json.Unmarshal([]byte(raw), &in.Webhook)
|
|
}
|
|
if raw := values[settings.KeyNotifyTelegramJSON]; raw != "" {
|
|
_ = json.Unmarshal([]byte(raw), &in.Telegram)
|
|
}
|
|
in.OnDDNSIPChange = values[settings.KeyNotifyOnDDNSIPChange] == "1" || strings.EqualFold(values[settings.KeyNotifyOnDDNSIPChange], "true")
|
|
in.OnDDNSFailure = values[settings.KeyNotifyOnDDNSFailure] == "1" || strings.EqualFold(values[settings.KeyNotifyOnDDNSFailure], "true")
|
|
if !in.OnDDNSFailure {
|
|
in.OnDDNSFailure = values[settings.KeyNotifyOnDDNSError] == "1" || strings.EqualFold(values[settings.KeyNotifyOnDDNSError], "true")
|
|
}
|
|
in.OnCertExpiry = values[settings.KeyNotifyOnCertExpiry] == "1" || strings.EqualFold(values[settings.KeyNotifyOnCertExpiry], "true")
|
|
if !in.OnCertExpiry {
|
|
in.OnCertExpiry = values[settings.KeyNotifyOnCertError] == "1" || strings.EqualFold(values[settings.KeyNotifyOnCertError], "true")
|
|
}
|
|
in.OnCertRenewSuccess = values[settings.KeyNotifyOnCertRenewSuccess] == "1" || strings.EqualFold(values[settings.KeyNotifyOnCertRenewSuccess], "true")
|
|
in.OnIPFrequentAccess = values[settings.KeyNotifyOnIPFrequentAccess] == "1" || strings.EqualFold(values[settings.KeyNotifyOnIPFrequentAccess], "true")
|
|
in.OnCertRenewFailure = values[settings.KeyNotifyOnCertRenewFailure] == "1" || strings.EqualFold(values[settings.KeyNotifyOnCertRenewFailure], "true")
|
|
in.OnLoginFailure = values[settings.KeyNotifyOnLoginFailure] == "1" || strings.EqualFold(values[settings.KeyNotifyOnLoginFailure], "true")
|
|
in.OnNginxReloadFailure = values[settings.KeyNotifyOnNginxReloadFailure] == "1" || strings.EqualFold(values[settings.KeyNotifyOnNginxReloadFailure], "true")
|
|
in.IPFrequentThreshold = intSettingFromMap(values, settings.KeyNotifyIPFrequentThreshold, DefaultIPFrequentThreshold)
|
|
in.IPFrequentWindowSec = intSettingFromMap(values, settings.KeyNotifyIPFrequentWindowSec, DefaultIPFrequentWindowSec)
|
|
in.LoginFailureThreshold = intSettingFromMap(values, settings.KeyNotifyLoginFailureThreshold, DefaultLoginFailureThreshold)
|
|
in.LoginFailureWindowSec = intSettingFromMap(values, settings.KeyNotifyLoginFailureWindowSec, DefaultLoginFailureWindowSec)
|
|
in.OnResourceThreshold = values[settings.KeyNotifyOnResourceThreshold] == "1" || strings.EqualFold(values[settings.KeyNotifyOnResourceThreshold], "true")
|
|
in.OnTunnelDown = values[settings.KeyNotifyOnTunnelDown] == "1" || strings.EqualFold(values[settings.KeyNotifyOnTunnelDown], "true")
|
|
in.CPUThreshold = intSettingFromMap(values, settings.KeyNotifyCPUThreshold, DefaultCPUThreshold)
|
|
in.MemoryThreshold = intSettingFromMap(values, settings.KeyNotifyMemoryThreshold, DefaultMemoryThreshold)
|
|
in.DiskThreshold = intSettingFromMap(values, settings.KeyNotifyDiskThreshold, DefaultDiskThreshold)
|
|
in.TunnelFailThreshold = intSettingFromMap(values, settings.KeyNotifyTunnelFailThreshold, DefaultTunnelFailThreshold)
|
|
in.OnFrpsDown = values[settings.KeyNotifyOnFrpsDown] == "1" || strings.EqualFold(values[settings.KeyNotifyOnFrpsDown], "true")
|
|
in.OnAgentUnreachable = values[settings.KeyNotifyOnAgentUnreachable] == "1" || strings.EqualFold(values[settings.KeyNotifyOnAgentUnreachable], "true")
|
|
in.OnRouteDrift = values[settings.KeyNotifyOnRouteDrift] == "1" || strings.EqualFold(values[settings.KeyNotifyOnRouteDrift], "true")
|
|
in.FrpsFailThreshold = intSettingFromMap(values, settings.KeyNotifyFrpsFailThreshold, DefaultFrpsFailThreshold)
|
|
in.AgentFailThreshold = intSettingFromMap(values, settings.KeyNotifyAgentFailThreshold, DefaultAgentFailThreshold)
|
|
in.DriftCooldownHours = intSettingFromMap(values, settings.KeyNotifyDriftCooldownHours, DefaultDriftCooldownHours)
|
|
return in, true
|
|
}
|
|
|
|
// clampThreshold 把未配置或越界的阈值拉回合法范围:0/负数用默认值,超过 100 的按 100 处理
|
|
// (百分比阈值与「连续失败次数」的上限都用这一条)。
|
|
func clampThreshold(value, fallback int) int {
|
|
if value <= 0 {
|
|
return fallback
|
|
}
|
|
if value > 100 {
|
|
return 100
|
|
}
|
|
return value
|
|
}
|
|
|
|
func intSettingFromMap(values map[string]string, key string, fallback int) int {
|
|
raw := strings.TrimSpace(values[key])
|
|
if raw == "" {
|
|
return fallback
|
|
}
|
|
n, err := strconv.Atoi(raw)
|
|
if err != nil || n <= 0 {
|
|
return fallback
|
|
}
|
|
return n
|
|
}
|