mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-09-29 14:06:36 +08:00
895dec208f
OpenResty running as openflare can no longer write pid or client/proxy temp paths under the OpenResty install prefix. Templates and apply-time rendering now use __OPENFLARE_PID_PATH__ and __OPENFLARE_NGINX_CACHE_DIR__ under data_dir/var/run and data_dir/var/cache/nginx, with legacy pid path patched at apply. Consolidate runtimeuser path helpers into the main package file so IDEs resolve references across build tags.
330 lines
14 KiB
Go
330 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.
|
|
user openflare;
|
|
worker_processes {{OpenRestyWorkerProcesses}};
|
|
worker_rlimit_nofile {{OpenRestyWorkerRlimitNofile}};
|
|
pid __OPENFLARE_PID_PATH__;
|
|
error_log {{OpenRestyErrorLogPath}} warn;
|
|
|
|
events {
|
|
worker_connections {{OpenRestyWorkerConnections}};
|
|
{{OpenRestyEventsUseDirective}}{{OpenRestyEventsMultiAcceptDirective}}}
|
|
|
|
http {
|
|
include mime.types;
|
|
default_type application/octet-stream;
|
|
server_tokens off;
|
|
client_body_temp_path __OPENFLARE_NGINX_CACHE_DIR__/client_temp;
|
|
proxy_temp_path __OPENFLARE_NGINX_CACHE_DIR__/proxy_temp;
|
|
fastcgi_temp_path __OPENFLARE_NGINX_CACHE_DIR__/fastcgi_temp;
|
|
uwsgi_temp_path __OPENFLARE_NGINX_CACHE_DIR__/uwsgi_temp;
|
|
scgi_temp_path __OPENFLARE_NGINX_CACHE_DIR__/scgi_temp;
|
|
{{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)
|
|
}
|