mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-09-29 05:56:38 +08:00
323 lines
14 KiB
Go
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)
|
|
}
|