0904
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
// Copyright 2022 E99p1ant. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package conf
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"gopkg.in/ini.v1"
|
||||
)
|
||||
|
||||
// File is the configuration object.
|
||||
var File *ini.File
|
||||
|
||||
func Init() error {
|
||||
configFile := os.Getenv("NEKOBOX_CONFIG_PATH")
|
||||
if configFile == "" {
|
||||
configFile = "conf/app.ini"
|
||||
}
|
||||
|
||||
var err error
|
||||
File, err = ini.LoadSources(ini.LoadOptions{
|
||||
IgnoreInlineComment: true,
|
||||
}, configFile)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "parse %q", configFile)
|
||||
}
|
||||
|
||||
if err := File.Section("app").MapTo(&App); err != nil {
|
||||
return errors.Wrap(err, "map 'server'")
|
||||
}
|
||||
|
||||
if App.ExternalURL == "" {
|
||||
return errors.New("app.external_url is required")
|
||||
}
|
||||
App.ExternalURL = strings.TrimRight(App.ExternalURL, "/")
|
||||
|
||||
if err := File.Section("security").MapTo(&Security); err != nil {
|
||||
return errors.Wrap(err, "map 'security'")
|
||||
}
|
||||
|
||||
if err := File.Section("server").MapTo(&Server); err != nil {
|
||||
return errors.Wrap(err, "map 'server'")
|
||||
}
|
||||
|
||||
// XSRFKey 为空会导致 CSRF token 以空密钥生成、可被预测,
|
||||
// 使删题/置顶/拉黑/改密等所有 POST 操作的 CSRF 防护形同虚设。启动时强制校验。
|
||||
if Server.XSRFKey == "" {
|
||||
return errors.New("server.xsrf_key is required (used to generate CSRF tokens, must not be empty)")
|
||||
}
|
||||
|
||||
// Salt 为空会让密码哈希以空盐计算(HmacSha1(password, "")),
|
||||
// 相同密码产生相同哈希且可被离线字典/彩虹表攻击。启动时强制校验。
|
||||
if Server.Salt == "" {
|
||||
return errors.New("server.salt is required (used to hash user passwords, must not be empty)")
|
||||
}
|
||||
|
||||
if err := File.Section("database").MapTo(&Database); err != nil {
|
||||
return errors.Wrap(err, "map 'database'")
|
||||
}
|
||||
|
||||
if err := File.Section("redis").MapTo(&Redis); err != nil {
|
||||
return errors.Wrap(err, "map 'redis'")
|
||||
}
|
||||
|
||||
if err := File.Section("recaptcha").MapTo(&Recaptcha); err != nil {
|
||||
return errors.Wrap(err, "map 'recaptcha'")
|
||||
}
|
||||
|
||||
if err := File.Section("pixel").MapTo(&Pixel); err != nil {
|
||||
return errors.Wrap(err, "map 'pixel'")
|
||||
}
|
||||
|
||||
if err := File.Section("upload").MapTo(&Upload); err != nil {
|
||||
return errors.Wrap(err, "map 'upload'")
|
||||
}
|
||||
|
||||
if err := File.Section("limits").MapTo(&Limits); err != nil {
|
||||
return errors.Wrap(err, "map 'limits'")
|
||||
}
|
||||
// ini 键留空会被 int 解析为 0;<1 的值一律回退内置默认,防止上限被静默置零。
|
||||
sanitizeLimits(&Limits)
|
||||
|
||||
if err := File.Section("mail").MapTo(&Mail); err != nil {
|
||||
return errors.Wrap(err, "map 'mail'")
|
||||
}
|
||||
|
||||
serviceSections := File.Section("service").ChildSections()
|
||||
for _, serviceSection := range serviceSections {
|
||||
serviceSection := serviceSection
|
||||
var backend struct {
|
||||
Prefix string `ini:"prefix"`
|
||||
ForwardURL string `ini:"forward_url"`
|
||||
}
|
||||
if err := serviceSection.MapTo(&backend); err != nil {
|
||||
return errors.Wrapf(err, "map 'service.%s'", serviceSection.Name())
|
||||
}
|
||||
Service.Backends = append(Service.Backends, backend)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package conf
|
||||
|
||||
import "fmt"
|
||||
|
||||
func MySQLDsn() string {
|
||||
return fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local",
|
||||
Database.User,
|
||||
Database.Password,
|
||||
Database.Host,
|
||||
Database.Port,
|
||||
Database.Name,
|
||||
)
|
||||
}
|
||||
|
||||
func PostgresDsn() string {
|
||||
dsn := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=disable TimeZone=Asia/Shanghai",
|
||||
Database.Host,
|
||||
Database.Port,
|
||||
Database.User,
|
||||
Database.Password,
|
||||
Database.Name,
|
||||
)
|
||||
if Database.Schema != "" {
|
||||
dsn += fmt.Sprintf(" search_path=%s", Database.Schema)
|
||||
}
|
||||
return dsn
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
// Copyright 2026 tamakyi. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package conf
|
||||
|
||||
// 各项上限的内置默认值(与旧版硬编码一致)。既是未配置 [limits] 段时的兜底,
|
||||
// 也是 conf.Init 后归一化(sanitizeLimits)的回退基准。
|
||||
const (
|
||||
defaultMaxTags = 5
|
||||
defaultMaxTagLen = 5
|
||||
defaultMaxBlockWords = 10
|
||||
defaultMaxBlockWordLen = 10
|
||||
defaultCustomAnswerLen = 200
|
||||
defaultVoteOptionLen = 300
|
||||
defaultQuestionContentLen = 1000
|
||||
defaultAnswerContentLen = 2000
|
||||
defaultFollowUpContentLen = 2000
|
||||
defaultChatMessageLen = 2000
|
||||
defaultChatMaxMessages = 200
|
||||
defaultDailyMessageLimitMax = 999
|
||||
)
|
||||
|
||||
// LimitsConfig 全站内容 / 数量上限([limits] 配置段)的字段集合。
|
||||
//
|
||||
// 过去这些上限以常量形式散落在 internal/db(MaxTags / MaxCustomTextLen 等)与
|
||||
// 路由层硬编码(私聊单条 2000 字、会话条数 clamp 5~200 等)中,调整需改代码重编译,
|
||||
// 且前后端、i18n 文案各自携带一份数值容易漂移。统一收口到 [limits] 段后:
|
||||
// - 后端校验、模板注入、前端提示全部引用 conf.Limits 的同一份值;
|
||||
// - 调整仅需修改 conf/app.ini 并重启进程,无需改代码;
|
||||
// - 长度类上限统一按字符(rune / Unicode 码点)计数。
|
||||
type LimitsConfig struct {
|
||||
// ── 标签与屏蔽词(账号设置页)──
|
||||
|
||||
// 标签个数上限
|
||||
MaxTags int `ini:"max_tags"`
|
||||
// 单个标签最大字符数
|
||||
MaxTagLen int `ini:"max_tag_len"`
|
||||
// 屏蔽词个数上限
|
||||
MaxBlockWords int `ini:"max_block_words"`
|
||||
// 单个屏蔽词最大字符数
|
||||
MaxBlockWordLen int `ini:"max_block_word_len"`
|
||||
|
||||
// ── 投票 ──
|
||||
|
||||
// 参与者填写的「自定义补充选项」文本最大字符数。
|
||||
// 覆盖两类入口:单选/多选/排序的全局补充选项,以及表单收集字段的
|
||||
// 「其他/自定义选项」(field_{id}_custom)。
|
||||
CustomAnswerLen int `ini:"custom_answer_len"`
|
||||
// 站长创建投票时单个「选项」文本的最大字符数。
|
||||
// 覆盖单选/多选/排序的预设选项,以及表单收集字段(select/radio/checkbox/rank)
|
||||
// 的预置选项。与 CustomAnswerLen 是两条独立规则(站长侧预设刻意放宽),可各自调参。
|
||||
VoteOptionLen int `ini:"vote_option_len"`
|
||||
|
||||
// ── 提问 / 回答 / 追问 ──
|
||||
|
||||
// 提问正文最大字符数(访客向提问箱提交的问题内容)。
|
||||
// 旧实现依赖 govalid 的 maxlen 标签(编译期写死 2000),无法随配置调整,
|
||||
// 且与前端 maxlength=1000 不一致,现已移到路由层按本值做 rune 校验。
|
||||
QuestionContentLen int `ini:"question_content_len"`
|
||||
// 回答正文最大字符数:箱主对提问的正式回答,以及箱主对追问的回复(共用)。
|
||||
AnswerContentLen int `ini:"answer_content_len"`
|
||||
// 追问正文最大字符数(提问者在问题被回答后继续追问的内容)。
|
||||
FollowUpContentLen int `ini:"followup_content_len"`
|
||||
|
||||
// ── 私聊 ──
|
||||
|
||||
// 单条消息(发送 / 回复共用)最大字符数
|
||||
ChatMessageLen int `ini:"chat_message_len"`
|
||||
// 每个私聊会话的消息条数上限(系统硬顶)。用户只能在 1..该值 之间设置自己的
|
||||
// chat_max_messages;新注册用户的默认值即该硬顶(而非固定 20)。
|
||||
ChatMaxMessages int `ini:"chat_max_messages"`
|
||||
// 单用户每日私信条数上限(用户可设范围 0..该值;0 表示不限制每日条数)。
|
||||
// 该值只是「用户可设置的上限」,不是站点强制的每日配额。
|
||||
DailyMessageLimitMax int `ini:"daily_message_limit_max"`
|
||||
}
|
||||
|
||||
// Limits 保存运行时的全部上限配置。conf.Init 中会先 MapTo [limits] 段覆盖显式键,
|
||||
// 再调用 sanitizeLimits 把 <1 的值回退到内置默认,因此:
|
||||
// - 缺失 [limits] 段或个别键 → 使用内置默认;
|
||||
// - ini 键留空(int 解析为 0)或误填 0/负数 → 自动回退默认,不会把上限静默置零。
|
||||
var Limits = LimitsConfig{
|
||||
MaxTags: defaultMaxTags,
|
||||
MaxTagLen: defaultMaxTagLen,
|
||||
MaxBlockWords: defaultMaxBlockWords,
|
||||
MaxBlockWordLen: defaultMaxBlockWordLen,
|
||||
CustomAnswerLen: defaultCustomAnswerLen,
|
||||
VoteOptionLen: defaultVoteOptionLen,
|
||||
QuestionContentLen: defaultQuestionContentLen,
|
||||
AnswerContentLen: defaultAnswerContentLen,
|
||||
FollowUpContentLen: defaultFollowUpContentLen,
|
||||
ChatMessageLen: defaultChatMessageLen,
|
||||
ChatMaxMessages: defaultChatMaxMessages,
|
||||
DailyMessageLimitMax: defaultDailyMessageLimitMax,
|
||||
}
|
||||
|
||||
// sanitizeLimits 归一化 [limits] 配置:所有上限最小为 1(0 / 负数说明未配置或手滑,
|
||||
// 一律回退内置默认,避免「留空=0」把标签全拒、消息全超长等隐性灾难)。
|
||||
// 注意 DailyMessageLimitMax 同样要求 >=1:0 会让用户侧永远只能设为 0(=不限制),
|
||||
// 语义上等于关闭每日配额功能,需显式使用该字段其余语义时请另行设计。
|
||||
func sanitizeLimits(l *LimitsConfig) {
|
||||
clamp1 := func(p *int, def int) {
|
||||
if *p < 1 {
|
||||
*p = def
|
||||
}
|
||||
}
|
||||
clamp1(&l.MaxTags, defaultMaxTags)
|
||||
clamp1(&l.MaxTagLen, defaultMaxTagLen)
|
||||
clamp1(&l.MaxBlockWords, defaultMaxBlockWords)
|
||||
clamp1(&l.MaxBlockWordLen, defaultMaxBlockWordLen)
|
||||
clamp1(&l.CustomAnswerLen, defaultCustomAnswerLen)
|
||||
clamp1(&l.VoteOptionLen, defaultVoteOptionLen)
|
||||
clamp1(&l.QuestionContentLen, defaultQuestionContentLen)
|
||||
clamp1(&l.AnswerContentLen, defaultAnswerContentLen)
|
||||
clamp1(&l.FollowUpContentLen, defaultFollowUpContentLen)
|
||||
clamp1(&l.ChatMessageLen, defaultChatMessageLen)
|
||||
clamp1(&l.ChatMaxMessages, defaultChatMaxMessages)
|
||||
clamp1(&l.DailyMessageLimitMax, defaultDailyMessageLimitMax)
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
// Copyright 2026 tamakyi. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package conf
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"gopkg.in/ini.v1"
|
||||
)
|
||||
|
||||
// TestLimitsDefaults 保证内置默认值与旧版硬编码一致:未配置 [limits] 段(或 conf.Init
|
||||
// 尚未调用)时,程序按这些默认值运行。
|
||||
func TestLimitsDefaults(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
got int
|
||||
want int
|
||||
}{
|
||||
{"MaxTags", Limits.MaxTags, 5},
|
||||
{"MaxTagLen", Limits.MaxTagLen, 5},
|
||||
{"MaxBlockWords", Limits.MaxBlockWords, 10},
|
||||
{"MaxBlockWordLen", Limits.MaxBlockWordLen, 10},
|
||||
{"CustomAnswerLen", Limits.CustomAnswerLen, 200},
|
||||
{"VoteOptionLen", Limits.VoteOptionLen, 300},
|
||||
{"QuestionContentLen", Limits.QuestionContentLen, 1000},
|
||||
{"AnswerContentLen", Limits.AnswerContentLen, 2000},
|
||||
{"FollowUpContentLen", Limits.FollowUpContentLen, 2000},
|
||||
{"ChatMessageLen", Limits.ChatMessageLen, 2000},
|
||||
{"ChatMaxMessages", Limits.ChatMaxMessages, 200},
|
||||
{"DailyMessageLimitMax", Limits.DailyMessageLimitMax, 999},
|
||||
}
|
||||
for _, c := range cases {
|
||||
if c.got != c.want {
|
||||
t.Errorf("%s = %d, want %d", c.name, c.got, c.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestLimitsMapTo 模拟 conf.Init 的映射行为:显式键被覆盖,缺失键保留默认。
|
||||
// 启动逻辑是 File.Section("limits").MapTo(&Limits),与这里同一路径。
|
||||
func TestLimitsMapTo(t *testing.T) {
|
||||
src := `
|
||||
[limits]
|
||||
max_tags = 7
|
||||
chat_message_len = 500
|
||||
answer_content_len = 300
|
||||
daily_message_limit_max = 0
|
||||
`
|
||||
f, err := ini.LoadSources(ini.LoadOptions{IgnoreInlineComment: true}, []byte(src))
|
||||
if err != nil {
|
||||
t.Fatalf("parse ini: %v", err)
|
||||
}
|
||||
l := Limits // 拷贝默认值,等价于启动前状态
|
||||
if err := f.Section("limits").MapTo(&l); err != nil {
|
||||
t.Fatalf("map [limits]: %v", err)
|
||||
}
|
||||
if l.MaxTags != 7 {
|
||||
t.Errorf("MaxTags = %d, want 7 (ini 覆盖失败)", l.MaxTags)
|
||||
}
|
||||
if l.ChatMessageLen != 500 {
|
||||
t.Errorf("ChatMessageLen = %d, want 500", l.ChatMessageLen)
|
||||
}
|
||||
if l.AnswerContentLen != 300 {
|
||||
t.Errorf("AnswerContentLen = %d, want 300(ini 覆盖失败)", l.AnswerContentLen)
|
||||
}
|
||||
if l.DailyMessageLimitMax != 0 {
|
||||
t.Errorf("DailyMessageLimitMax = %d, want 0(配置为 0 应生效)", l.DailyMessageLimitMax)
|
||||
}
|
||||
// 未显式出现的键保留内置默认
|
||||
if l.MaxTagLen != 5 {
|
||||
t.Errorf("MaxTagLen = %d, want 5(缺失键应保留默认)", l.MaxTagLen)
|
||||
}
|
||||
if l.VoteOptionLen != 300 {
|
||||
t.Errorf("VoteOptionLen = %d, want 300", l.VoteOptionLen)
|
||||
}
|
||||
if l.QuestionContentLen != 1000 || l.FollowUpContentLen != 2000 {
|
||||
t.Errorf("QuestionContentLen/FollowUpContentLen = %d/%d, want 1000/2000(缺失键应保留默认)",
|
||||
l.QuestionContentLen, l.FollowUpContentLen)
|
||||
}
|
||||
}
|
||||
|
||||
// TestLimitsEmptyValueSanitized 验证「键留空」的防护:ini 会把空 int 解析为 0,
|
||||
// sanitizeLimits 随后将其回退到内置默认 —— 防止留空把上限静默置零(标签全拒等)。
|
||||
func TestLimitsEmptyValueSanitized(t *testing.T) {
|
||||
src := `
|
||||
[limits]
|
||||
max_tags =
|
||||
chat_message_len = 0
|
||||
followup_content_len = -1
|
||||
daily_message_limit_max = -3
|
||||
`
|
||||
f, err := ini.LoadSources(ini.LoadOptions{IgnoreInlineComment: true}, []byte(src))
|
||||
if err != nil {
|
||||
t.Fatalf("parse ini: %v", err)
|
||||
}
|
||||
l := Limits
|
||||
if err := f.Section("limits").MapTo(&l); err != nil {
|
||||
t.Fatalf("map [limits]: %v", err)
|
||||
}
|
||||
// 前提确认:空串对 int 是「保留原值」而非 0(gopkg.in/ini 行为);
|
||||
// 显式 0 / 负数则照常映射。真正的 0 陷阱来自显式填 0 / 负数,由 sanitize 兜底。
|
||||
if l.MaxTags != defaultMaxTags {
|
||||
t.Fatalf("precondition broken: 空值 max_tags 应保留默认, got %d", l.MaxTags)
|
||||
}
|
||||
if l.ChatMessageLen != 0 || l.DailyMessageLimitMax != -3 {
|
||||
t.Fatalf("precondition broken: got %d/%d, want 0/-3",
|
||||
l.ChatMessageLen, l.DailyMessageLimitMax)
|
||||
}
|
||||
sanitizeLimits(&l)
|
||||
if l.MaxTags != defaultMaxTags {
|
||||
t.Errorf("MaxTags = %d, want 默认 %d", l.MaxTags, defaultMaxTags)
|
||||
}
|
||||
if l.ChatMessageLen != defaultChatMessageLen {
|
||||
t.Errorf("ChatMessageLen = %d, want 默认 %d(0 应回退默认)", l.ChatMessageLen, defaultChatMessageLen)
|
||||
}
|
||||
if l.FollowUpContentLen != defaultFollowUpContentLen {
|
||||
t.Errorf("FollowUpContentLen = %d, want 默认 %d(负数应回退默认)", l.FollowUpContentLen, defaultFollowUpContentLen)
|
||||
}
|
||||
if l.DailyMessageLimitMax != defaultDailyMessageLimitMax {
|
||||
t.Errorf("DailyMessageLimitMax = %d, want 默认 %d(负数应回退默认)", l.DailyMessageLimitMax, defaultDailyMessageLimitMax)
|
||||
}
|
||||
if l.ChatMaxMessages != defaultChatMaxMessages {
|
||||
t.Errorf("ChatMaxMessages = %d, want 默认 %d(缺失键不受影响)", l.ChatMaxMessages, defaultChatMaxMessages)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
// Copyright 2022 E99p1ant. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package conf
|
||||
|
||||
// Build time and commit information.
|
||||
//
|
||||
// ⚠️ WARNING: should only be set by "-ldflags".
|
||||
var (
|
||||
BuildTime string
|
||||
BuildCommit = "dev"
|
||||
)
|
||||
|
||||
var (
|
||||
App struct {
|
||||
Production bool `ini:"production"`
|
||||
ExternalURL string `ini:"external_url"`
|
||||
ICP string `ini:"icp"`
|
||||
DefaultLang string `ini:"default_lang"` // 站点默认语言(zh-CN/zh-TW/en/ja),留空回退简体中文
|
||||
UptraceDSN string `ini:"uptrace_dsn"`
|
||||
QiniuAccessKey string `ini:"qiniu_access_key"`
|
||||
QiniuAccessSecret string `ini:"qiniu_access_secret"`
|
||||
AliyunAccessKey string `ini:"aliyun_access_key"`
|
||||
AliyunAccessKeySecret string `ini:"aliyun_access_key_secret"`
|
||||
SentryDSN string `ini:"sentry_dsn"`
|
||||
MaintenanceMode bool `ini:"maintenance_mode"`
|
||||
ShareBackground string `ini:"share_background"`
|
||||
FontPath string `ini:"font_path"`
|
||||
}
|
||||
|
||||
Security struct {
|
||||
EnableTextCensor bool `ini:"enable_text_censor"`
|
||||
IPBlacklist string `ini:"ip_blacklist"`
|
||||
IPBlacklistRedirectURL string `ini:"ip_blacklist_redirect_url"`
|
||||
// TrustProxy 为 true 时信任 CDN 权威客户端头 CF-Connecting-IP / EO-Client-IP
|
||||
// (以及受信代理设置的 X-Real-IP / X-Forwarded-For);否则一律使用 RemoteAddr,
|
||||
// 防止客户端伪造 header 绕过速率限制与 IP 黑名单。
|
||||
TrustProxy bool `ini:"trust_proxy"`
|
||||
// TrustedProxies 逗号分隔的 IP / CIDR 列表,仅限“你自己的”反向代理/负载均衡基础设施
|
||||
// (例如本机 nginx 的 127.0.0.1、私有网络 LB)。仅当 TCP 对端落在该清单内时才会解析
|
||||
// X-Forwarded-For / X-Real-IP。留空则这两个头永不被信任(CDN 权威头 CF-Connecting-IP /
|
||||
// EO-Client-IP 仍会被采用,前提是源站已被对应 CDN 锁定)。
|
||||
// 目的:避免维护 Cloudflare / EdgeOne 等上游 CDN 的庞大 CIDR 清单——源站应配合
|
||||
// Cloudflare Tunnel / Authenticated Origin Pull / 防火墙仅放行 CDN 回源网段锁定,
|
||||
// 使 CF-Connecting-IP / EO-Client-IP 成为权威客户端 IP。
|
||||
TrustedProxies string `ini:"trusted_proxies"`
|
||||
// 自动封禁(fail2ban 风格):检测批量扫描/探测特征后临时封禁来源 IP。
|
||||
EnableAutoBan bool `ini:"enable_auto_ban"`
|
||||
// BanTrackNotFound 是否把「未注册路径的 404」计入封禁统计。默认 false:正常业务
|
||||
// (域名/题目不存在等均走 302 业务跳转,不产生计数)一律放行,仅命中后台扫描特征
|
||||
// 路径(KindSensitive)才参与统计封禁。避免共享出口(CDN 边缘 / CGNAT NAT444)下
|
||||
// 正常用户被 404 探测流量连坐误伤。如需恢复对纯 404 字典扫描的封禁再置 true。
|
||||
BanTrackNotFound bool `ini:"ban_track_notfound"`
|
||||
BanThresholdSensitive int `ini:"ban_threshold_sensitive"` // 窗口内敏感路径命中次数
|
||||
BanThreshold404 int `ini:"ban_threshold_404"` // 窗口内 404 次数
|
||||
BanWindowSeconds int `ini:"ban_window_seconds"` // 统计窗口(秒)
|
||||
BanDurationMinutes int `ini:"ban_duration_minutes"` // 封禁时长(分钟)
|
||||
SecurityAlertEmail string `ini:"security_alert_email"` // 触发封禁时告警收件人(留空不发送)
|
||||
}
|
||||
|
||||
Server struct {
|
||||
Port int `ini:"port"`
|
||||
Listen string `ini:"listen"` // 绑定监听地址(含端口)。留空则默认 ":port"(监听所有网卡)。
|
||||
Salt string `ini:"salt"`
|
||||
XSRFKey string `ini:"xsrf_key"`
|
||||
}
|
||||
|
||||
Database struct {
|
||||
DSN string
|
||||
|
||||
Type string `ini:"type"`
|
||||
User string `ini:"user"`
|
||||
Password string `ini:"password"`
|
||||
Host string `ini:"host"`
|
||||
Port uint `ini:"port"`
|
||||
Name string `ini:"name"`
|
||||
Schema string `ini:"schema"`
|
||||
DedupeUniqueFields bool `ini:"dedupe_unique_fields"`
|
||||
}
|
||||
|
||||
Redis struct {
|
||||
Addr string `ini:"addr"`
|
||||
Password string `ini:"password"`
|
||||
}
|
||||
|
||||
Recaptcha struct {
|
||||
CaptchaType string `ini:"captcha_type"`
|
||||
Domain string `ini:"domain"`
|
||||
SiteKey string `ini:"site_key"`
|
||||
ServerKey string `ini:"server_key"`
|
||||
TurnstileStyle bool `ini:"turnstile_style"`
|
||||
}
|
||||
|
||||
Pixel struct {
|
||||
Host string `ini:"host"`
|
||||
}
|
||||
|
||||
Upload struct {
|
||||
DefaultAvatarURL string `ini:"default_avatar"`
|
||||
DefaultBackground string `ini:"default_background"`
|
||||
DefaultBackgroundImageURL string `ini:"default_qrbackgroundimage"`
|
||||
AliyunEndpoint string `ini:"aliyun_endpoint"`
|
||||
AliyunAccessID string `ini:"aliyun_access_id"`
|
||||
AliyunAccessSecret string `ini:"aliyun_access_secret"`
|
||||
AliyunBucket string `ini:"aliyun_bucket"`
|
||||
AliyunBucketCDNHost string `ini:"aliyun_bucket_cdn_host"`
|
||||
ImageEndpoint string `ini:"image_endpoint"`
|
||||
ImageAccessID string `ini:"image_access_id"`
|
||||
ImageAccessSecret string `ini:"image_access_secret"`
|
||||
ImageBucket string `ini:"image_bucket"`
|
||||
ImageBucketCDNHost string `ini:"image_bucket_cdn_host"`
|
||||
}
|
||||
|
||||
Mail struct {
|
||||
Account string `ini:"account"`
|
||||
Password string `ini:"password"`
|
||||
Port int `ini:"port"`
|
||||
SMTP string `ini:"smtp"`
|
||||
// SkipTLSVerify 跳过 SMTP TLS 证书校验(仅自签名/内网 SMTP 需要,默认 false 校验证书)
|
||||
SkipTLSVerify bool `ini:"skip_tls_verify"`
|
||||
}
|
||||
|
||||
Service struct {
|
||||
Backends []struct {
|
||||
Prefix string `ini:"prefix"`
|
||||
ForwardURL string `ini:"forward_url"`
|
||||
}
|
||||
// TLSInsecureSkipVerify 跳过反向代理到后端服务的 TLS 证书校验
|
||||
// (仅内网后端使用自签名证书时需要,默认 false 校验证书)
|
||||
TLSInsecureSkipVerify bool `ini:"tls_insecure_skip_verify"`
|
||||
}
|
||||
)
|
||||
Reference in New Issue
Block a user