Files
tamabox/internal/conf/conf.go
T
2026-09-10 18:04:19 +08:00

148 lines
5.1 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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 := validateTextCensorSource(Security.EnableTextCensor, Security.TextCensorSource,
App.QiniuAccessKey, App.QiniuAccessSecret, App.AliyunAccessKey, App.AliyunAccessKeySecret); err != nil {
return err
}
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
}
// validateTextCensorSource 校验文本审核通道([security] text_censor_source)与凭据的搭配。
//
// 单独抽成纯函数(只依赖入参、不读全局)是为了能直接被单测覆盖,也让 Init 保持轻薄。
//
// 规则:
// - 审核整体关闭(enabled=false)→ 不校验,此时不需要任何凭据。
// - 留空 / auto → 通过。auto 是历史默认(七牛优先、阿里云兜底),两者都没配凭据时
// 维持既有行为(运行期 fail-closed),不阻断启动 —— 否则 app.sample.ini 原样拷贝
// (enable_text_censor = true 且凭据留空)将无法启动,破坏开箱流程。
// - 显式选 qiniu / aliyun → 必须配齐该通道的凭据,否则启动即失败。
// 理由:缺凭据时运行期审核必然失败,而审核失败是 fail-closed(拒绝提交),
// 会让发布/私信整站不可用,且现象隐蔽(只在日志可见)——不如启动就报错。
// - 其它取值 → 直接报错,不做静默回退:拼写错误若被悄悄当成 auto,
// 运维会以为「已经只用七牛了」,实际仍在走双通道。
func validateTextCensorSource(enabled bool, source, qiniuKey, qiniuSecret, aliyunKey, aliyunSecret string) error {
if !enabled {
return nil
}
switch strings.ToLower(strings.TrimSpace(source)) {
case "", CensorSourceAuto:
return nil
case CensorSourceQiniu:
if qiniuKey == "" || qiniuSecret == "" {
return errors.New("security.text_censor_source = qiniu requires app.qiniu_access_key and app.qiniu_access_secret")
}
return nil
case CensorSourceAliyun:
if aliyunKey == "" || aliyunSecret == "" {
return errors.New("security.text_censor_source = aliyun requires app.aliyun_access_key and app.aliyun_access_key_secret")
}
return nil
default:
return errors.Errorf("security.text_censor_source must be one of qiniu / aliyun / auto, got %q", source)
}
}