Files
OpenFlare/backend/plugins/domain/auth/cap_runtime_settings.go
T

198 lines
5.5 KiB
Go

// Copyright 2026 Arctel.net
// SPDX-License-Identifier: Apache-2.0
package auth
import (
"context"
"errors"
"strconv"
"sync/atomic"
"time"
"golang.org/x/sync/singleflight"
)
const (
defaultCapChallengeCount = 1
defaultCapChallengeSize = 32
defaultCapChallengeDifficulty = 4
defaultCapChallengeTTL = 10 * time.Minute
defaultCapTokenTTL = 20 * time.Minute
)
// CapRuntimeSettings is the parsed CAPTCHA runtime configuration loaded from system_configs.
type CapRuntimeSettings struct {
LoginEnabled bool
ChallengeCount int
ChallengeSize int
ChallengeDifficulty int
ChallengeTTL time.Duration
TokenTTL time.Duration
}
// CAP 动态配置键常量
const (
ConfigKeyCapLoginEnabled = "cap_login_enabled"
ConfigKeyCapChallengeCount = "cap_challenge_count"
ConfigKeyCapChallengeSize = "cap_challenge_size"
ConfigKeyCapChallengeDifficulty = "cap_challenge_difficulty"
ConfigKeyCapChallengeTTL = "cap_challenge_ttl"
// ConfigKeyCapTokenTTL 验证码 Token 过期时间键
// #nosec G101
ConfigKeyCapTokenTTL = "cap_token_ttl"
)
var capRuntimeConfigKeys = []string{
ConfigKeyCapLoginEnabled,
ConfigKeyCapChallengeCount,
ConfigKeyCapChallengeSize,
ConfigKeyCapChallengeDifficulty,
ConfigKeyCapChallengeTTL,
ConfigKeyCapTokenTTL,
}
var capRuntimeConfigKeySet = func() map[string]struct{} {
set := make(map[string]struct{}, len(capRuntimeConfigKeys))
for _, key := range capRuntimeConfigKeys {
set[key] = struct{}{}
}
return set
}()
type capRuntimeSettingsStore struct {
snapshot atomic.Pointer[CapRuntimeSettings]
loadGroup singleflight.Group
}
var capSettingsStore = &capRuntimeSettingsStore{}
// IsCapRuntimeConfigKey reports whether a system config key affects CAPTCHA runtime settings.
func IsCapRuntimeConfigKey(key string) bool {
_, ok := capRuntimeConfigKeySet[key]
return ok
}
// CurrentCapSettings returns the cached CAPTCHA runtime settings snapshot.
func CurrentCapSettings(ctx context.Context) (CapRuntimeSettings, error) {
return capSettingsStore.current(ctx)
}
// CapProtectionEnabled reports whether CAPTCHA verification is required for protected routes.
func CapProtectionEnabled(ctx context.Context) bool {
settings, err := CurrentCapSettings(ctx)
if err != nil {
return false
}
return settings.LoginEnabled
}
// InvalidateCapRuntimeSettings drops the in-process CAPTCHA settings snapshot.
func InvalidateCapRuntimeSettings() {
capSettingsStore.snapshot.Store(nil)
}
// ResetCapRuntimeSettingsForTest clears the CAPTCHA runtime snapshot.
func ResetCapRuntimeSettingsForTest() {
InvalidateCapRuntimeSettings()
}
// InstallCapTestRuntimeSettings installs a fixed snapshot for unit tests.
func InstallCapTestRuntimeSettings(settings CapRuntimeSettings) func() {
snapshot := settings
capSettingsStore.snapshot.Store(&snapshot)
return InvalidateCapRuntimeSettings
}
func (s *capRuntimeSettingsStore) current(ctx context.Context) (CapRuntimeSettings, error) {
if snapshot := s.snapshot.Load(); snapshot != nil {
return *snapshot, nil
}
loaded, err, _ := s.loadGroup.Do("cap-runtime-settings", func() (any, error) {
if snapshot := s.snapshot.Load(); snapshot != nil {
return *snapshot, nil
}
settings, loadErr := loadCapRuntimeSettings(ctx)
if loadErr != nil {
return CapRuntimeSettings{}, loadErr
}
s.snapshot.Store(&settings)
return settings, nil
})
if err != nil {
return CapRuntimeSettings{}, err
}
settings, ok := loaded.(CapRuntimeSettings)
if !ok {
return CapRuntimeSettings{}, errors.New("cap runtime settings loader returned unexpected type")
}
return settings, nil
}
func loadCapRuntimeSettings(ctx context.Context) (CapRuntimeSettings, error) {
var records []capConfigRecord
db := getDB(ctx)
if db == nil {
return parseCapRuntimeSettings(nil), nil
}
if err := db.Table("w_system_configs").Where("key IN ?", capRuntimeConfigKeys).Find(&records).Error; err != nil {
return CapRuntimeSettings{}, err
}
configs := make(map[string]string, len(records))
for _, r := range records {
configs[r.Key] = r.Value
}
return parseCapRuntimeSettings(configs), nil
}
func parseCapRuntimeSettings(configs map[string]string) CapRuntimeSettings {
settings := CapRuntimeSettings{
ChallengeCount: defaultCapChallengeCount,
ChallengeSize: defaultCapChallengeSize,
ChallengeDifficulty: defaultCapChallengeDifficulty,
ChallengeTTL: defaultCapChallengeTTL,
TokenTTL: defaultCapTokenTTL,
}
if len(configs) == 0 {
return settings
}
if val, ok := configs[ConfigKeyCapLoginEnabled]; ok {
if enabled, err := strconv.ParseBool(val); err == nil {
settings.LoginEnabled = enabled
}
}
if val, ok := configs[ConfigKeyCapChallengeCount]; ok {
if count, err := strconv.Atoi(val); err == nil && count > 0 {
settings.ChallengeCount = count
}
}
if val, ok := configs[ConfigKeyCapChallengeSize]; ok {
if size, err := strconv.Atoi(val); err == nil && size > 0 {
settings.ChallengeSize = size
}
}
if val, ok := configs[ConfigKeyCapChallengeDifficulty]; ok {
if diff, err := strconv.Atoi(val); err == nil && diff > 0 {
settings.ChallengeDifficulty = diff
}
}
if val, ok := configs[ConfigKeyCapChallengeTTL]; ok {
if ttlSeconds, err := strconv.Atoi(val); err == nil && ttlSeconds > 0 {
settings.ChallengeTTL = time.Duration(ttlSeconds) * time.Second
}
}
if val, ok := configs[ConfigKeyCapTokenTTL]; ok {
if ttlSeconds, err := strconv.Atoi(val); err == nil && ttlSeconds > 0 {
settings.TokenTTL = time.Duration(ttlSeconds) * time.Second
}
}
return settings
}