mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-09-28 05:46:36 +08:00
06d5fedbfc
Keep only plugin entry, httpapi, migrate and updater at the server root. Shared model/repository/adapters live in kernel/; product bounded contexts live in domain/.
285 lines
11 KiB
Go
285 lines
11 KiB
Go
// Copyright 2026 Arctel.net
|
||
// SPDX-License-Identifier: Apache-2.0
|
||
|
||
package option
|
||
|
||
import (
|
||
"encoding/json"
|
||
"fmt"
|
||
"regexp"
|
||
"strconv"
|
||
"strings"
|
||
|
||
"Wavelet/OpenFlare/plugins/server/kernel/model"
|
||
openrestyrender "Wavelet/OpenFlare/share/render/openresty"
|
||
)
|
||
|
||
const (
|
||
maxOriginErrorPageHTMLBytes = 256 << 10 // 256 KiB
|
||
maxSWOfflineDomains = 1000
|
||
)
|
||
|
||
var openRestyOptionValidators = map[string]func(key, value string) error{
|
||
model.ConfigKeyOpenRestyDefaultServerReturnStatus: validateOpenRestyDefaultServerReturnStatus,
|
||
model.ConfigKeyOpenRestyWorkerProcesses: validateOpenRestyWorkerProcesses,
|
||
model.ConfigKeyOpenRestyWorkerConnections: validatePositiveIntegerOption,
|
||
model.ConfigKeyOpenRestyWorkerRlimitNofile: validatePositiveIntegerOption,
|
||
model.ConfigKeyOpenRestyKeepaliveTimeout: validatePositiveIntegerOption,
|
||
model.ConfigKeyOpenRestyKeepaliveRequests: validatePositiveIntegerOption,
|
||
model.ConfigKeyOpenRestyClientHeaderTimeout: validatePositiveIntegerOption,
|
||
model.ConfigKeyOpenRestyClientBodyTimeout: validatePositiveIntegerOption,
|
||
model.ConfigKeyOpenRestySendTimeout: validatePositiveIntegerOption,
|
||
model.ConfigKeyOpenRestyProxyConnectTimeout: validatePositiveIntegerOption,
|
||
model.ConfigKeyOpenRestyProxySendTimeout: validatePositiveIntegerOption,
|
||
model.ConfigKeyOpenRestyProxyReadTimeout: validatePositiveIntegerOption,
|
||
model.ConfigKeyOpenRestyGzipMinLength: validatePositiveIntegerOption,
|
||
model.ConfigKeyOpenRestyGzipCompLevel: validateOpenRestyGzipCompLevel,
|
||
model.ConfigKeyOpenRestyEventsUse: validateOpenRestyEventsUse,
|
||
model.ConfigKeyOpenRestyResolvers: validateOpenRestyResolvers,
|
||
model.ConfigKeyOpenRestyEventsMultiAcceptEnabled: validateBooleanOption,
|
||
model.ConfigKeyOpenRestyWebsocketEnabled: validateBooleanOption,
|
||
model.ConfigKeyOpenRestyHTTP3Enabled: validateBooleanOption,
|
||
model.ConfigKeyOpenRestyProxyRequestBufferingEnabled: validateBooleanOption,
|
||
model.ConfigKeyOpenRestyProxyBufferingEnabled: validateBooleanOption,
|
||
model.ConfigKeyOpenRestyGzipEnabled: validateBooleanOption,
|
||
model.ConfigKeyOpenRestyCacheEnabled: validateBooleanOption,
|
||
model.ConfigKeyOpenRestyCacheLockEnabled: validateBooleanOption,
|
||
model.ConfigKeyOpenRestyProxyBuffers: validateOpenRestyProxyBuffers,
|
||
model.ConfigKeyOpenRestyLargeClientHeaderBuffers: validateOpenRestyProxyBuffers,
|
||
model.ConfigKeyOpenRestyProxyBufferSize: validateOpenRestySizeValue,
|
||
model.ConfigKeyOpenRestyProxyBusyBuffersSize: validateOpenRestySizeValue,
|
||
model.ConfigKeyOpenRestyCacheMaxSize: validateOpenRestySizeValue,
|
||
model.ConfigKeyOpenRestyClientMaxBodySize: validateOpenRestySizeValue,
|
||
model.ConfigKeyOpenRestyCachePath: validateOpenRestyCachePath,
|
||
model.ConfigKeyOpenRestyCacheLevels: validateOpenRestyCacheLevels,
|
||
model.ConfigKeyOpenRestyCacheInactive: validateOpenRestyDurationToken,
|
||
model.ConfigKeyOpenRestyCacheLockTimeout: validateOpenRestyDurationToken,
|
||
model.ConfigKeyOpenRestyCacheKeyTemplate: validateOpenRestyCacheKeyTemplate,
|
||
model.ConfigKeyOpenRestyCacheUseStale: validateOpenRestyCacheUseStale,
|
||
model.ConfigKeyOpenRestyMainConfigTemplate: validateOpenRestyMainConfigTemplate,
|
||
model.ConfigKeyOpenRestyDefaultLimitConnPerServer: validateNonNegativeIntegerOption,
|
||
model.ConfigKeyOpenRestyDefaultLimitConnPerIP: validateNonNegativeIntegerOption,
|
||
model.ConfigKeyOpenRestyDefaultLimitRate: validateOpenRestyDefaultLimitRate,
|
||
model.ConfigKeyOpenRestyDefaultLimitReqPerIP: validateOpenRestyDefaultLimitReqPerIP,
|
||
model.ConfigKeyOriginErrorPageEnabled: validateBooleanOption,
|
||
model.ConfigKeyOriginErrorPageStatusCodes: validateOriginErrorPageStatusCodes,
|
||
model.ConfigKeyOriginErrorPageHTML: validateOriginErrorPageHTML,
|
||
model.ConfigKeyOriginErrorPageGetOnly: validateBooleanOption,
|
||
model.ConfigKeySWOfflineEnabled: validateBooleanOption,
|
||
model.ConfigKeySWOfflineHTML: validateSWOfflineHTML,
|
||
model.ConfigKeySWOfflineDomains: validateSWOfflineDomains,
|
||
}
|
||
|
||
var openRestyDefaultLimitRatePattern = regexp.MustCompile(`^\d+[kKmM]?$`)
|
||
|
||
func validateOpenRestyOption(key, value string) error {
|
||
// HTML 按原始字节长度校验,避免 TrimSpace 影响上限判断
|
||
if key == model.ConfigKeyOriginErrorPageHTML || key == model.ConfigKeySWOfflineHTML {
|
||
return validateOriginErrorPageHTML(key, value)
|
||
}
|
||
trimmed := strings.TrimSpace(value)
|
||
if validator, ok := openRestyOptionValidators[key]; ok {
|
||
return validator(key, trimmed)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func validateOpenRestyDefaultServerReturnStatus(key, trimmed string) error {
|
||
if err := validatePositiveIntegerOption(key, trimmed); err != nil {
|
||
return err
|
||
}
|
||
statusCode, _ := strconv.Atoi(trimmed)
|
||
if statusCode < 100 || statusCode > 999 {
|
||
return fmt.Errorf("%s 必须在 100 到 999 之间", key)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func validateOpenRestyWorkerProcesses(key, trimmed string) error {
|
||
if trimmed == "auto" {
|
||
return nil
|
||
}
|
||
return validatePositiveIntegerOption(key, trimmed)
|
||
}
|
||
|
||
func validateOpenRestyGzipCompLevel(key, trimmed string) error {
|
||
if err := validatePositiveIntegerOption(key, trimmed); err != nil {
|
||
return err
|
||
}
|
||
level, _ := strconv.Atoi(trimmed)
|
||
if level > maxOpenRestyGzipCompLevel {
|
||
return fmt.Errorf("%s 不能大于 %d", key, maxOpenRestyGzipCompLevel)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func validateOpenRestyEventsUse(key, trimmed string) error {
|
||
if trimmed == "" {
|
||
return nil
|
||
}
|
||
switch trimmed {
|
||
case "epoll", "kqueue", "poll", "select", "rtsig", "/dev/poll", "eventport":
|
||
return nil
|
||
default:
|
||
return fmt.Errorf("%s 仅支持 epoll、kqueue、poll、select、rtsig、/dev/poll、eventport 或留空", key)
|
||
}
|
||
}
|
||
|
||
func validateOpenRestyResolvers(key, trimmed string) error {
|
||
if trimmed == "" {
|
||
return nil
|
||
}
|
||
if !regexp.MustCompile(`^[a-zA-Z0-9.:\-\s]+$`).MatchString(trimmed) {
|
||
return fmt.Errorf("%s 包含非法字符,请填入有效的 IP 地址或域名,以空格分隔", key)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func validateOpenRestyProxyBuffers(key, trimmed string) error {
|
||
if openRestyProxyBuffersPattern.MatchString(trimmed) {
|
||
return nil
|
||
}
|
||
return fmt.Errorf("%s 格式必须类似 \"16 16k\"", key)
|
||
}
|
||
|
||
func validateOpenRestySizeValue(key, trimmed string) error {
|
||
if openRestySizePattern.MatchString(trimmed) {
|
||
return nil
|
||
}
|
||
return fmt.Errorf("%s 格式必须为整数或带 k/m/g 单位的大小值", key)
|
||
}
|
||
|
||
func validateOpenRestyCachePath(key, trimmed string) error {
|
||
if strings.ContainsAny(trimmed, "\r\n\t") {
|
||
return fmt.Errorf("%s 不能包含换行或制表符", key)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func validateOpenRestyCacheLevels(key, trimmed string) error {
|
||
if openRestyCacheLevelsPattern.MatchString(trimmed) {
|
||
return nil
|
||
}
|
||
return fmt.Errorf("%s 格式必须类似 \"1:2\" 或 \"1:2:2\"", key)
|
||
}
|
||
|
||
func validateOpenRestyDurationToken(key, trimmed string) error {
|
||
if openRestyDurationTokenPattern.MatchString(trimmed) {
|
||
return nil
|
||
}
|
||
return fmt.Errorf("%s 格式必须为带单位的时长,例如 30m 或 5s", key)
|
||
}
|
||
|
||
func validateOpenRestyCacheKeyTemplate(key, trimmed string) error {
|
||
if trimmed == "" {
|
||
return fmt.Errorf("%s 不能为空", key)
|
||
}
|
||
if strings.ContainsAny(trimmed, "\r\n") {
|
||
return fmt.Errorf("%s 不能包含换行", key)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func validateOpenRestyCacheUseStale(key, trimmed string) error {
|
||
if trimmed == "" {
|
||
return fmt.Errorf("%s 不能为空", key)
|
||
}
|
||
allowedTokens := map[string]struct{}{
|
||
"error": {}, "timeout": {}, "invalid_header": {}, "updating": {},
|
||
"http_500": {}, "http_502": {}, "http_503": {}, "http_504": {},
|
||
"http_403": {}, "http_404": {}, "http_429": {}, "off": {},
|
||
}
|
||
for token := range strings.FieldsSeq(trimmed) {
|
||
if _, ok := allowedTokens[token]; !ok {
|
||
return fmt.Errorf("%s 包含不支持的值 %q", key, token)
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func validateOpenRestyMainConfigTemplate(key, value string) error {
|
||
if strings.TrimSpace(value) == "" {
|
||
return fmt.Errorf("%s 不能为空", key)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func validateOpenRestyDefaultLimitRate(key, trimmed string) error {
|
||
if trimmed == "" || trimmed == "0" {
|
||
return nil
|
||
}
|
||
if !openRestyDefaultLimitRatePattern.MatchString(strings.ToLower(trimmed)) {
|
||
return fmt.Errorf("%s 格式不合法,请使用 512k、1m 或纯数字,空表示关闭", key)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
var openRestyDefaultLimitReqPerIPPattern = regexp.MustCompile(`^\d+r/[sm]$`)
|
||
|
||
func validateOpenRestyDefaultLimitReqPerIP(key, trimmed string) error {
|
||
if trimmed == "" || trimmed == "0" {
|
||
return nil
|
||
}
|
||
if !openRestyDefaultLimitReqPerIPPattern.MatchString(strings.ToLower(trimmed)) {
|
||
return fmt.Errorf("%s 格式不合法,请输入类似 10r/s、100r/m,或留空关闭", key)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func validateOriginErrorPageStatusCodes(key, trimmed string) error {
|
||
if trimmed == "" {
|
||
return fmt.Errorf("%s 不能为空", key)
|
||
}
|
||
var tags []string
|
||
if err := json.Unmarshal([]byte(trimmed), &tags); err != nil {
|
||
return fmt.Errorf("%s 必须为 JSON 字符串数组", key)
|
||
}
|
||
if len(tags) == 0 {
|
||
return fmt.Errorf("%s 至少包含一个状态码标签", key)
|
||
}
|
||
codes, err := openrestyrender.ExpandStatusCodeTags(tags)
|
||
if err != nil {
|
||
return fmt.Errorf("%s: %w", key, err)
|
||
}
|
||
if len(codes) == 0 {
|
||
return fmt.Errorf("%s 展开后不能为空", key)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func validateOriginErrorPageHTML(key, value string) error {
|
||
if len(value) > maxOriginErrorPageHTMLBytes {
|
||
return fmt.Errorf("%s 长度不能超过 %d 字节(256 KiB)", key, maxOriginErrorPageHTMLBytes)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func validateSWOfflineHTML(key, value string) error {
|
||
return validateOriginErrorPageHTML(key, value)
|
||
}
|
||
|
||
func validateSWOfflineDomains(key, value string) error {
|
||
var domains []string
|
||
if err := json.Unmarshal([]byte(value), &domains); err != nil || domains == nil {
|
||
return fmt.Errorf("%s 必须为 JSON 字符串数组", key)
|
||
}
|
||
if len(domains) > maxSWOfflineDomains {
|
||
return fmt.Errorf("%s 最多支持 %d 个域名", key, maxSWOfflineDomains)
|
||
}
|
||
seen := make(map[string]struct{}, len(domains))
|
||
for _, raw := range domains {
|
||
domain := strings.ToLower(strings.TrimSpace(raw))
|
||
if domain == "" {
|
||
return fmt.Errorf("%s 包含空域名", key)
|
||
}
|
||
if raw != domain {
|
||
return fmt.Errorf("%s 域名必须为小写且不含首尾空格:%s", key, raw)
|
||
}
|
||
if _, ok := seen[domain]; ok {
|
||
return fmt.Errorf("%s 包含重复域名 %s", key, domain)
|
||
}
|
||
seen[domain] = struct{}{}
|
||
}
|
||
return nil
|
||
}
|