Files
OpenFlare/internal/model/openflare_option.go
T

323 lines
14 KiB
Go

// Copyright 2026 Arctel.net
// SPDX-License-Identifier: Apache-2.0
package model
import (
"context"
"strconv"
"strings"
"sync"
"time"
"github.com/Rain-kl/Wavelet/internal/db"
"gorm.io/gorm"
)
// OpenFlareOption stores a hot-reloadable OpenFlare system option.
type OpenFlareOption struct {
Key string `json:"key" gorm:"column:key;primaryKey;size:128;not null"`
Value string `json:"value" gorm:"type:text;not null"`
}
// TableName returns the OpenFlare options table name.
func (OpenFlareOption) TableName() string {
return "of_options"
}
// OptionMap holds the in-memory option snapshot for fast reads and hot reload.
var (
OptionMap map[string]string
OptionMapRWMutex sync.RWMutex
// StartTime records process start time (seconds) for legacy /api/status.
StartTime = time.Now().Unix()
// Hot-reload mirrors for frequently read options (legacy OpenFlare keys).
SystemName = "OpenFlare"
ServerAddress = ""
Footer = ""
HomePageLink = ""
PasswordLoginEnabled = true
CapLoginEnabled = true
PasswordRegisterEnabled = false
EmailVerificationEnabled = false
GitHubOAuthEnabled = false
WeChatAuthEnabled = false
GitHubClientID = ""
WeChatAccountQRCodeImageURL = ""
SMTPPort = 587
AgentDiscoveryToken = ""
AgentHeartbeatInterval = 10000
AgentWebsocketUpgradeEnabled = true
NodeOfflineThreshold = 2 * time.Minute
AgentUpdateRepo = "Rain-kl/OpenFlare"
GeoIPProvider = "ipinfo"
DatabaseAutoCleanupEnabled = false
DatabaseAutoCleanupRetentionDays = 30
UptimeKumaEnabled = false
UptimeKumaURL = ""
UptimeKumaUsername = ""
UptimeKumaPassword = ""
UptimeKumaMonitorScope = "all"
UptimeKumaSelectedSites = ""
UptimeKumaSyncInterval = 5
UptimeKumaInterval = 60
UptimeKumaRetry = 0
UptimeKumaRetryInterval = 60
UptimeKumaTimeout = 48
OpenRestyDefaultServerReturnStatus = 421
OpenRestyWorkerProcesses = "auto"
OpenRestyWorkerConnections = 4096
OpenRestyWorkerRlimitNofile = 65535
OpenRestyEventsUse = "epoll"
OpenRestyEventsMultiAcceptEnabled = true
OpenRestyKeepaliveTimeout = 20
OpenRestyKeepaliveRequests = 1000
OpenRestyClientHeaderTimeout = 15
OpenRestyClientBodyTimeout = 15
OpenRestyClientMaxBodySize = "64m"
OpenRestyLargeClientHeaderBuffers = "4 16k"
OpenRestySendTimeout = 30
OpenRestyResolvers = ""
OpenRestyProxyConnectTimeout = 3
OpenRestyProxySendTimeout = 60
OpenRestyProxyReadTimeout = 60
OpenRestyWebsocketEnabled = true
OpenRestyHTTP3Enabled = true
OpenRestyProxyRequestBufferingEnabled = false
OpenRestyProxyBufferingEnabled = true
OpenRestyProxyBuffers = "16 16k"
OpenRestyProxyBufferSize = "8k"
OpenRestyProxyBusyBuffersSize = "64k"
OpenRestyGzipEnabled = true
OpenRestyGzipMinLength = 1024
OpenRestyGzipCompLevel = 5
OpenRestyCacheEnabled = false
OpenRestyCachePath = ""
OpenRestyCacheLevels = "1:2"
OpenRestyCacheInactive = "30m"
OpenRestyCacheMaxSize = "1g"
OpenRestyCacheKeyTemplate = "$scheme$host$request_uri"
OpenRestyCacheLockEnabled = true
OpenRestyCacheLockTimeout = "5s"
OpenRestyCacheUseStale = "error timeout updating http_500 http_502 http_503 http_504"
OpenRestyMainConfigTemplate = defaultOpenRestyMainConfigTemplate
)
const defaultOpenRestyMainConfigTemplate = `# This file is generated by OpenFlare. Do not edit manually.
worker_processes {{OpenRestyWorkerProcesses}};
worker_rlimit_nofile {{OpenRestyWorkerRlimitNofile}};
pid logs/nginx.pid;
error_log {{OpenRestyErrorLogPath}} warn;
events {
worker_connections {{OpenRestyWorkerConnections}};
{{OpenRestyEventsUseDirective}}{{OpenRestyEventsMultiAcceptDirective}}}
http {
include mime.types;
default_type application/octet-stream;
{{OpenRestyConnectionUpgradeMap}}{{OpenRestyDefaultServerBlock}} log_format openflare_json escape=json '{"ts":"$time_iso8601","host":"$host","path":"$request_uri","remote_addr":"$remote_addr","status":$status,"request_time":$request_time,"bytes_sent":$body_bytes_sent,"request_length":$request_length}';
access_log {{OpenRestyAccessLogPath}} openflare_json;
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout {{OpenRestyKeepaliveTimeout}};
keepalive_requests {{OpenRestyKeepaliveRequests}};
client_header_timeout {{OpenRestyClientHeaderTimeout}};
client_body_timeout {{OpenRestyClientBodyTimeout}};
client_max_body_size {{OpenRestyClientMaxBodySize}};
large_client_header_buffers {{OpenRestyLargeClientHeaderBuffers}};
send_timeout {{OpenRestySendTimeout}};
proxy_connect_timeout {{OpenRestyProxyConnectTimeout}};
proxy_send_timeout {{OpenRestyProxySendTimeout}};
proxy_read_timeout {{OpenRestyProxyReadTimeout}};
proxy_request_buffering {{OpenRestyProxyRequestBuffering}};
proxy_buffering {{OpenRestyProxyBuffering}};
proxy_buffers {{OpenRestyProxyBuffers}};
proxy_buffer_size {{OpenRestyProxyBufferSize}};
proxy_busy_buffers_size {{OpenRestyProxyBusyBuffersSize}};
gzip {{OpenRestyGzip}};
gzip_min_length {{OpenRestyGzipMinLength}};
gzip_comp_level {{OpenRestyGzipCompLevel}};
{{OpenRestyResolverDirective}}{{OpenRestyCacheBlock}} include {{OpenRestyRouteConfigInclude}};
}
`
// DefaultOpenFlareOptions returns built-in defaults keyed by legacy OpenFlare option names.
func DefaultOpenFlareOptions() map[string]string {
return map[string]string{
"PasswordLoginEnabled": strconv.FormatBool(PasswordLoginEnabled),
"CapLoginEnabled": strconv.FormatBool(CapLoginEnabled),
"PasswordRegisterEnabled": strconv.FormatBool(PasswordRegisterEnabled),
"EmailVerificationEnabled": strconv.FormatBool(EmailVerificationEnabled),
"GitHubOAuthEnabled": strconv.FormatBool(GitHubOAuthEnabled),
"WeChatAuthEnabled": strconv.FormatBool(WeChatAuthEnabled),
"SMTPServer": "",
"SMTPPort": strconv.Itoa(SMTPPort),
"SMTPAccount": "",
"SMTPToken": "",
"Notice": "",
"About": "",
"Footer": Footer,
"HomePageLink": HomePageLink,
"SystemName": SystemName,
"ServerAddress": "",
"GitHubClientId": "",
"GitHubClientSecret": "",
"WeChatServerAddress": "",
"WeChatServerToken": "",
"WeChatAccountQRCodeImageURL": "",
"AgentDiscoveryToken": "",
"AgentHeartbeatInterval": strconv.Itoa(AgentHeartbeatInterval),
"AgentWebsocketUpgradeEnabled": strconv.FormatBool(AgentWebsocketUpgradeEnabled),
"NodeOfflineThreshold": strconv.Itoa(int(NodeOfflineThreshold.Milliseconds())),
"AgentUpdateRepo": AgentUpdateRepo,
"GeoIPProvider": GeoIPProvider,
"DatabaseAutoCleanupEnabled": strconv.FormatBool(DatabaseAutoCleanupEnabled),
"UptimeKumaEnabled": strconv.FormatBool(UptimeKumaEnabled),
"UptimeKumaUrl": UptimeKumaURL,
"UptimeKumaUsername": UptimeKumaUsername,
optionKeyUptimeKumaPassword: UptimeKumaPassword,
"UptimeKumaMonitorScope": UptimeKumaMonitorScope,
"UptimeKumaSelectedSites": UptimeKumaSelectedSites,
"UptimeKumaSyncInterval": strconv.Itoa(UptimeKumaSyncInterval),
"UptimeKumaInterval": strconv.Itoa(UptimeKumaInterval),
"UptimeKumaRetry": strconv.Itoa(UptimeKumaRetry),
"UptimeKumaRetryInterval": strconv.Itoa(UptimeKumaRetryInterval),
"UptimeKumaTimeout": strconv.Itoa(UptimeKumaTimeout),
"DatabaseAutoCleanupRetentionDays": strconv.Itoa(DatabaseAutoCleanupRetentionDays),
"OpenRestyDefaultServerReturnStatus": strconv.Itoa(OpenRestyDefaultServerReturnStatus),
"OpenRestyWorkerProcesses": OpenRestyWorkerProcesses,
"OpenRestyWorkerConnections": strconv.Itoa(OpenRestyWorkerConnections),
"OpenRestyWorkerRlimitNofile": strconv.Itoa(OpenRestyWorkerRlimitNofile),
"OpenRestyEventsUse": OpenRestyEventsUse,
"OpenRestyEventsMultiAcceptEnabled": strconv.FormatBool(OpenRestyEventsMultiAcceptEnabled),
"OpenRestyKeepaliveTimeout": strconv.Itoa(OpenRestyKeepaliveTimeout),
"OpenRestyKeepaliveRequests": strconv.Itoa(OpenRestyKeepaliveRequests),
"OpenRestyClientHeaderTimeout": strconv.Itoa(OpenRestyClientHeaderTimeout),
"OpenRestyClientBodyTimeout": strconv.Itoa(OpenRestyClientBodyTimeout),
"OpenRestyClientMaxBodySize": OpenRestyClientMaxBodySize,
"OpenRestyLargeClientHeaderBuffers": OpenRestyLargeClientHeaderBuffers,
"OpenRestySendTimeout": strconv.Itoa(OpenRestySendTimeout),
"OpenRestyProxyConnectTimeout": strconv.Itoa(OpenRestyProxyConnectTimeout),
"OpenRestyProxySendTimeout": strconv.Itoa(OpenRestyProxySendTimeout),
"OpenRestyProxyReadTimeout": strconv.Itoa(OpenRestyProxyReadTimeout),
"OpenRestyWebsocketEnabled": strconv.FormatBool(OpenRestyWebsocketEnabled),
"OpenRestyHTTP3Enabled": strconv.FormatBool(OpenRestyHTTP3Enabled),
"OpenRestyProxyRequestBufferingEnabled": strconv.FormatBool(OpenRestyProxyRequestBufferingEnabled),
"OpenRestyProxyBufferingEnabled": strconv.FormatBool(OpenRestyProxyBufferingEnabled),
"OpenRestyProxyBuffers": OpenRestyProxyBuffers,
"OpenRestyProxyBufferSize": OpenRestyProxyBufferSize,
"OpenRestyProxyBusyBuffersSize": OpenRestyProxyBusyBuffersSize,
"OpenRestyGzipEnabled": strconv.FormatBool(OpenRestyGzipEnabled),
"OpenRestyGzipMinLength": strconv.Itoa(OpenRestyGzipMinLength),
"OpenRestyGzipCompLevel": strconv.Itoa(OpenRestyGzipCompLevel),
"OpenRestyCacheEnabled": strconv.FormatBool(OpenRestyCacheEnabled),
"OpenRestyCachePath": OpenRestyCachePath,
"OpenRestyCacheLevels": OpenRestyCacheLevels,
"OpenRestyCacheInactive": OpenRestyCacheInactive,
"OpenRestyCacheMaxSize": OpenRestyCacheMaxSize,
"OpenRestyCacheKeyTemplate": OpenRestyCacheKeyTemplate,
"OpenRestyCacheLockEnabled": strconv.FormatBool(OpenRestyCacheLockEnabled),
"OpenRestyCacheLockTimeout": OpenRestyCacheLockTimeout,
"OpenRestyCacheUseStale": OpenRestyCacheUseStale,
"OpenRestyMainConfigTemplate": OpenRestyMainConfigTemplate,
}
}
// InitOptionMap seeds defaults and overlays persisted options from of_options.
func InitOptionMap(ctx context.Context) error {
OptionMapRWMutex.Lock()
OptionMap = DefaultOpenFlareOptions()
OptionMapRWMutex.Unlock()
options, err := ListOpenFlareOptions(ctx)
if err != nil {
return err
}
for _, option := range options {
applyOptionMap(option.Key, option.Value)
}
return nil
}
// ListOpenFlareOptions returns all persisted options.
func ListOpenFlareOptions(ctx context.Context) ([]OpenFlareOption, error) {
var options []OpenFlareOption
if err := db.DB(ctx).Find(&options).Error; err != nil {
return nil, err
}
return options, nil
}
// UpdateOpenFlareOption updates a single option in DB and memory.
func UpdateOpenFlareOption(ctx context.Context, key, value string) error {
return UpdateOpenFlareOptions(ctx, []OpenFlareOption{{Key: key, Value: value}})
}
// UpdateOpenFlareOptions batch-updates options in a transaction and refreshes OptionMap.
func UpdateOpenFlareOptions(ctx context.Context, options []OpenFlareOption) error {
if len(options) == 0 {
return nil
}
if err := db.DB(ctx).Transaction(func(tx *gorm.DB) error {
for _, item := range options {
if item.Key == optionKeyUptimeKumaPassword && strings.TrimSpace(item.Value) == "" {
continue
}
option := OpenFlareOption{Key: item.Key}
if err := tx.FirstOrCreate(&option, OpenFlareOption{Key: item.Key}).Error; err != nil {
return err
}
option.Value = item.Value
if err := tx.Save(&option).Error; err != nil {
return err
}
}
return nil
}); err != nil {
return err
}
for _, item := range options {
if item.Key == optionKeyUptimeKumaPassword && strings.TrimSpace(item.Value) == "" {
continue
}
applyOptionMap(item.Key, item.Value)
}
return nil
}
// OptionValue returns a snapshot value from OptionMap.
func OptionValue(key string) string {
OptionMapRWMutex.RLock()
defer OptionMapRWMutex.RUnlock()
if OptionMap == nil {
return ""
}
return OptionMap[key]
}
// ResetOptionMapForTest clears in-memory option state for unit tests.
func ResetOptionMapForTest() {
OptionMapRWMutex.Lock()
OptionMap = nil
OptionMapRWMutex.Unlock()
}
func applyOptionMap(key, value string) {
OptionMapRWMutex.Lock()
defer OptionMapRWMutex.Unlock()
if OptionMap == nil {
OptionMap = make(map[string]string)
}
OptionMap[key] = value
applyEnabledOptionMirror(key, value)
applyStringOptionMirror(key, value)
applyIntegerOptionMirror(key, value)
applySpecialOptionMirror(key, value)
}