Go 1.27.1 (Gin+GORM) + Vue 3 文件快传服务: - 安全审计全部修复(docs/security-audit-2026-09-05.md): bcrypt 密码哈希与自动升级、presign 直传服务端大小/内容校验、 全局请求体上限、依赖升级(govulncheck 0 命中)、janitor 后台清理、 管理端审计动作落库、/admin CORS 收紧、通知内容白名单净化、 会话默认 7 天、限流缓存故障降级、robots.txt 端点等 - 前端:取件链接复制修复(不再重复拼接提取码)、markdown 净化器加固 - Redis 支持库号(FCB_REDIS_DB / redis://…/db URL) - 文档:docs/api/* 与 openapi.yaml 同步最新行为(robots.txt、 提码 5 位起、chunk 32MiB 上限、admin 审计动作等) 验证:gofmt/go vet/go test 全绿;二进制端到端冒烟通过
418 lines
14 KiB
Go
418 lines
14 KiB
Go
// Package config 提供全局配置:默认值对齐参考实现 core/settings.py,
|
||
// 支持 FCB_* 环境变量覆盖默认值,再由数据库 settings KV 做运行时覆盖。
|
||
package config
|
||
|
||
import (
|
||
"fmt"
|
||
"os"
|
||
"strconv"
|
||
"strings"
|
||
)
|
||
|
||
// 会话有效期边界(与参考实现保持一致:天级、可配 1~365 天)。
|
||
const (
|
||
// AdminSessionExpireDefault 默认 7 天(L8:由 30 天缩短,降低 localStorage
|
||
// token 泄露后的暴露窗口;管理员可在 1~365 天内自行调整)
|
||
AdminSessionExpireDefault = 7 * 24 * 60 * 60
|
||
AdminSessionExpireMin = 24 * 60 * 60 // 最小 1 天
|
||
AdminSessionExpireMax = 365 * 24 * 60 * 60 // 最大 365 天
|
||
|
||
// DefaultSQLitePath SQLite 模式默认数据库文件路径(相对运行目录,自动创建 data/)。
|
||
DefaultSQLitePath = "./data/filecodebox.db"
|
||
)
|
||
|
||
// 数据库驱动常量(需求 ⑧:SQLite 默认、Postgres 可选)。
|
||
const (
|
||
DBDriverSQLite = "sqlite"
|
||
DBDriverPostgres = "postgres"
|
||
)
|
||
|
||
// DefaultLogoURL / DefaultFaviconURL 默认 Logo 与 favicon(需求 ⑤):
|
||
// v2 起默认改用前端打包的本地资源(web/src/assets/brand/logo.svg + favicon.png,
|
||
// 经 Vite 产出 /assets/logo-*.svg 与 /assets/favicon-*.png)。此处留空,
|
||
// GET /api/v1/config 下发空值时前端 displayLogoUrl/displayFaviconUrl 回落到本地打包资源;
|
||
// 管理端仍可设置任意 URL 全站替换。
|
||
const DefaultLogoURL = ""
|
||
|
||
// DefaultFaviconURL favicon/备用 Logo 默认空串(语义见 DefaultLogoURL 注释)。
|
||
const DefaultFaviconURL = ""
|
||
|
||
// Config 运行时配置。Env 为 FCB_* 环境变量解析结果(进程级),
|
||
// KV 为数据库 settings 键值覆盖(可被管理端动态修改)。
|
||
type Config struct {
|
||
Env *EnvConfig
|
||
KV map[string]any
|
||
}
|
||
|
||
// EnvConfig 进程级环境变量配置,仅能通过环境变量修改。
|
||
type EnvConfig struct {
|
||
DBDriver string // FCB_DB_DRIVER,sqlite|postgres,默认 sqlite(需求 ⑧)
|
||
DBDSN string // FCB_DB_DSN;postgres 必需;sqlite 为空时用 DefaultSQLitePath
|
||
RedisAddr string // FCB_REDIS_ADDR,可选;为空时缓存降级为内存实现
|
||
RedisDB int // FCB_REDIS_DB,Redis 逻辑库号 0-15,默认 0(URL 形式地址以 URL 内库号优先)
|
||
Listen string // FCB_LISTEN,监听地址,默认 :8466
|
||
StorageEngine string // FCB_STORAGE_ENGINE,local|s3|webdav,默认 local
|
||
TrustedProxies []string // FCB_TRUSTED_PROXIES,逗号分隔的可信代理 CIDR
|
||
}
|
||
|
||
// defaults 返回与参考实现 core/settings.py DEFAULT_CONFIG 对齐的默认配置。
|
||
func defaults() map[string]any {
|
||
return map[string]any{
|
||
// 存储引擎与路径
|
||
"file_storage": "local",
|
||
"storage_path": "",
|
||
"storageLimit": 0,
|
||
// v3:存储引擎运行时可配(热切换);空=沿用 Env.StorageEngine 启动值
|
||
"storage_engine": "",
|
||
"site_domain": "",
|
||
// 站点信息
|
||
"name": "文件快传",
|
||
"site_name": "文件快传", // 新增:管理端可自定义
|
||
"description": "开箱即用的文件快传系统",
|
||
"notify_title": "系统通知",
|
||
"notify_content": "欢迎使用文件快传,拖拽或粘贴即可分享文本与文件。",
|
||
"page_explain": "请勿上传或分享违法内容。根据《中华人民共和国网络安全法》、《中华人民共和国刑法》、《中华人民共和国治安管理处罚法》等相关规定。 传播或存储违法、违规内容,会受到相关处罚,严重者将承担刑事责任。本站坚决配合相关部门,确保网络内容的安全,和谐,打造绿色网络环境。",
|
||
"keywords": "文件快传, 文件分享, 匿名口令分享文本, 文件",
|
||
// 需求 ⑤:默认 Logo 与 favicon(空 = 前端使用打包的本地资源)
|
||
"logo_url": DefaultLogoURL,
|
||
"favicon_url": DefaultFaviconURL,
|
||
// 需求 ①:背景图(v2 新增 background_url;background 为参考实现既有键,保留兼容)
|
||
"background": "",
|
||
"background_url": "",
|
||
// 需求 ②:页脚自定义内容与备案号
|
||
"footer_text": "",
|
||
"footer_beian": "",
|
||
// 需求 ③:系统通知(notify_enabled 新增开关,title/content 沿用参考语义)
|
||
"notify_enabled": 1,
|
||
// 需求 ④:保存策略(次数上限新增;时间上限沿用 max_save_seconds)
|
||
"max_save_count": 0,
|
||
// 需求 ⑩:存储策略-单文件上限(0=回落 uploadSize,避免与参考键冲突)
|
||
"max_file_size": 0,
|
||
// 本地存储
|
||
"local_storage_path": "",
|
||
// S3 引擎
|
||
"s3_access_key_id": "",
|
||
"s3_secret_access_key": "",
|
||
"s3_bucket_name": "",
|
||
"s3_endpoint_url": "",
|
||
"s3_region_name": "auto",
|
||
"s3_signature_version": "s3v4",
|
||
"s3_hostname": "",
|
||
"s3_addressing_style": "auto",
|
||
"s3_proxy": 0,
|
||
"aws_session_token": "",
|
||
// WebDAV 引擎
|
||
"webdav_url": "",
|
||
"webdav_username": "",
|
||
"webdav_password": "",
|
||
"webdav_root_path": "filebox_storage",
|
||
"webdav_proxy": 0,
|
||
// 安全
|
||
"admin_token": "", // 管理员密码哈希;为空表示未初始化
|
||
"jwt_secret": "",
|
||
"adminSessionExpire": AdminSessionExpireDefault,
|
||
// 上传与分享策略
|
||
"openUpload": 1,
|
||
"uploadSize": 1024 * 1024 * 10,
|
||
"allowed_file_types": []string{"*"},
|
||
"expireStyle": []string{"day", "hour", "minute", "forever", "count"},
|
||
"code_generate_type": "secret",
|
||
"uploadMinute": 1,
|
||
"uploadCount": 10,
|
||
"errorMinute": 1,
|
||
"errorCount": 10,
|
||
"loginCount": 5,
|
||
"loginMinute": 15,
|
||
"max_save_seconds": 0,
|
||
"enableChunk": 0,
|
||
// 界面
|
||
"opacity": 0.9,
|
||
"showAdminAddr": 0,
|
||
"robotsText": "User-agent: *\nDisallow: /",
|
||
"serverWorkers": 1,
|
||
"serverHost": "0.0.0.0",
|
||
"serverPort": 8466,
|
||
}
|
||
}
|
||
|
||
// loadEnv 解析 FCB_* 环境变量;返回 nil 表示未设置任何必需项。
|
||
func loadEnv() (*EnvConfig, error) {
|
||
env := &EnvConfig{
|
||
DBDriver: strings.ToLower(strings.TrimSpace(os.Getenv("FCB_DB_DRIVER"))),
|
||
DBDSN: strings.TrimSpace(os.Getenv("FCB_DB_DSN")),
|
||
RedisAddr: strings.TrimSpace(os.Getenv("FCB_REDIS_ADDR")),
|
||
Listen: strings.TrimSpace(os.Getenv("FCB_LISTEN")),
|
||
StorageEngine: strings.TrimSpace(os.Getenv("FCB_STORAGE_ENGINE")),
|
||
}
|
||
// Redis 库号(FCB_REDIS_DB,0-15;非法值忽略用默认 0)
|
||
if v := strings.TrimSpace(os.Getenv("FCB_REDIS_DB")); v != "" {
|
||
if n, err := strconv.Atoi(v); err == nil && n >= 0 && n <= 15 {
|
||
env.RedisDB = n
|
||
}
|
||
}
|
||
if env.Listen == "" {
|
||
env.Listen = ":8466"
|
||
}
|
||
if env.StorageEngine == "" {
|
||
env.StorageEngine = "local"
|
||
}
|
||
switch env.StorageEngine {
|
||
case "local", "s3", "webdav":
|
||
default:
|
||
return nil, fmt.Errorf("FCB_STORAGE_ENGINE 无效值 %q,仅支持 local|s3|webdav", env.StorageEngine)
|
||
}
|
||
if raw := strings.TrimSpace(os.Getenv("FCB_TRUSTED_PROXIES")); raw != "" {
|
||
for _, item := range strings.Split(raw, ",") {
|
||
if item = strings.TrimSpace(item); item != "" {
|
||
env.TrustedProxies = append(env.TrustedProxies, item)
|
||
}
|
||
}
|
||
}
|
||
return env, nil
|
||
}
|
||
|
||
// New 从环境变量构造配置;KV 覆盖先为空。
|
||
// 需求 ⑧:FCB_DB_DRIVER 默认 sqlite(零依赖);postgres 必须提供 FCB_DB_DSN。
|
||
func New() (*Config, error) {
|
||
env, err := loadEnv()
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
switch env.DBDriver {
|
||
case "", DBDriverSQLite:
|
||
env.DBDriver = DBDriverSQLite
|
||
// sqlite 模式 DSN 可为空:数据库层回退到 DefaultSQLitePath
|
||
case DBDriverPostgres:
|
||
if env.DBDSN == "" {
|
||
return nil, fmt.Errorf("FCB_DB_DRIVER=postgres 时必须提供 FCB_DB_DSN(Postgres 连接串)")
|
||
}
|
||
default:
|
||
return nil, fmt.Errorf("FCB_DB_DRIVER 无效值 %q,仅支持 sqlite|postgres", env.DBDriver)
|
||
}
|
||
return &Config{Env: env, KV: map[string]any{}}, nil
|
||
}
|
||
|
||
// ApplyKV 用数据库 settings KV 覆盖运行时配置(内部键以 _ 开头的不允许覆盖)。
|
||
func (c *Config) ApplyKV(kv map[string]any) {
|
||
for k, v := range kv {
|
||
if strings.HasPrefix(k, "_") {
|
||
continue
|
||
}
|
||
c.KV[k] = v
|
||
}
|
||
}
|
||
|
||
// Get 按 键读取:KV 覆盖 > 默认值;找不到返回零值与 false。
|
||
func (c *Config) Get(key string) (any, bool) {
|
||
if v, ok := c.KV[key]; ok {
|
||
return v, true
|
||
}
|
||
v, ok := defaults()[key]
|
||
return v, ok
|
||
}
|
||
|
||
// GetString 取字符串配置。
|
||
func (c *Config) GetString(key string) string {
|
||
v, ok := c.Get(key)
|
||
if !ok || v == nil {
|
||
return ""
|
||
}
|
||
if s, ok := v.(string); ok {
|
||
return s
|
||
}
|
||
return fmt.Sprintf("%v", v)
|
||
}
|
||
|
||
// GetInt 取整型配置,兼容 JSON 数字(float64)与字符串。
|
||
func (c *Config) GetInt(key string) int {
|
||
n, _ := c.getInt64(key)
|
||
return int(n)
|
||
}
|
||
|
||
// GetInt64 取长整型配置。
|
||
func (c *Config) GetInt64(key string) int64 {
|
||
n, _ := c.getInt64(key)
|
||
return n
|
||
}
|
||
|
||
func (c *Config) getInt64(key string) (int64, bool) {
|
||
v, ok := c.Get(key)
|
||
if !ok || v == nil {
|
||
return 0, false
|
||
}
|
||
switch n := v.(type) {
|
||
case int:
|
||
return int64(n), true
|
||
case int64:
|
||
return n, true
|
||
case float64:
|
||
return int64(n), true
|
||
case string:
|
||
if n, err := strconv.ParseInt(strings.TrimSpace(n), 10, 64); err == nil {
|
||
return n, true
|
||
}
|
||
}
|
||
return 0, false
|
||
}
|
||
|
||
// GetBool 取布尔配置,兼容 1/0、"true"/"false"/"on"/"yes"。
|
||
func (c *Config) GetBool(key string) bool {
|
||
v, ok := c.Get(key)
|
||
if !ok || v == nil {
|
||
return false
|
||
}
|
||
switch b := v.(type) {
|
||
case bool:
|
||
return b
|
||
case int:
|
||
return b != 0
|
||
case float64:
|
||
return b != 0
|
||
case string:
|
||
switch strings.ToLower(strings.TrimSpace(b)) {
|
||
case "1", "true", "on", "yes":
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
|
||
// GetStringSlice 取字符串切片配置。
|
||
// SiteDomain 站点对外域名(v3.1):空=分享链接用当前访问地址。
|
||
func (c *Config) SiteDomain() string {
|
||
return strings.TrimRight(strings.TrimSpace(c.GetString("site_domain")), "/")
|
||
}
|
||
|
||
func (c *Config) GetStringSlice(key string) []string {
|
||
v, ok := c.Get(key)
|
||
if !ok || v == nil {
|
||
return nil
|
||
}
|
||
switch s := v.(type) {
|
||
case []string:
|
||
return s
|
||
case []any:
|
||
out := make([]string, 0, len(s))
|
||
for _, item := range s {
|
||
if item == nil {
|
||
continue
|
||
}
|
||
out = append(out, fmt.Sprintf("%v", item))
|
||
}
|
||
return out
|
||
case string:
|
||
var out []string
|
||
for _, item := range strings.Split(s, ",") {
|
||
if item = strings.TrimSpace(item); item != "" {
|
||
out = append(out, item)
|
||
}
|
||
}
|
||
return out
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// —— 常用字段的便捷访问(与参考 settings.xxx 对齐)——
|
||
|
||
// SiteName 站点名称。
|
||
func (c *Config) SiteName() string {
|
||
if v := c.GetString("site_name"); v != "" {
|
||
return v
|
||
}
|
||
return c.GetString("name")
|
||
}
|
||
|
||
// LogoURL 页面 Logo。
|
||
func (c *Config) LogoURL() string { return c.GetString("logo_url") }
|
||
|
||
// FaviconURL favicon 地址。
|
||
func (c *Config) FaviconURL() string { return c.GetString("favicon_url") }
|
||
|
||
// OpenUpload 是否允许游客上传。
|
||
func (c *Config) OpenUpload() bool { return c.GetBool("openUpload") }
|
||
|
||
// UploadSize 单文件大小上限(字节)。
|
||
func (c *Config) UploadSize() int64 { return c.GetInt64("uploadSize") }
|
||
|
||
// AllowedFileTypes 允许的文件类型列表("*" 表示不限制)。
|
||
func (c *Config) AllowedFileTypes() []string { return c.GetStringSlice("allowed_file_types") }
|
||
|
||
// ExpireStyle 允许的过期方式。
|
||
func (c *Config) ExpireStyle() []string { return c.GetStringSlice("expireStyle") }
|
||
|
||
// EnableChunk 是否启用分片上传。
|
||
func (c *Config) EnableChunk() bool { return c.GetBool("enableChunk") }
|
||
|
||
// MaxSaveSeconds 最长保存秒数,0 表示不限制。
|
||
func (c *Config) MaxSaveSeconds() int64 { return c.GetInt64("max_save_seconds") }
|
||
|
||
// MaxSaveCount 单次分享最大可取次数上限(需求 ④),0 表示不限制。
|
||
func (c *Config) MaxSaveCount() int { return c.GetInt("max_save_count") }
|
||
|
||
// MaxFileSize 存储策略-单文件上限(需求 ⑩);0 表示回落 uploadSize。
|
||
func (c *Config) MaxFileSize() int64 {
|
||
if n := c.GetInt64("max_file_size"); n > 0 {
|
||
return n
|
||
}
|
||
return c.UploadSize()
|
||
}
|
||
|
||
// FooterText 页脚自定义内容(需求 ②)。
|
||
func (c *Config) FooterText() string { return c.GetString("footer_text") }
|
||
|
||
// FooterBeian 备案号(需求 ②)。
|
||
func (c *Config) FooterBeian() string { return c.GetString("footer_beian") }
|
||
|
||
// BackgroundURL 背景图地址(需求 ①);空表示使用主题默认。
|
||
func (c *Config) BackgroundURL() string {
|
||
if v := c.GetString("background_url"); v != "" {
|
||
return v
|
||
}
|
||
return c.GetString("background")
|
||
}
|
||
|
||
// NotifyEnabled 系统通知开关(需求 ③):默认开启。
|
||
func (c *Config) NotifyEnabled() bool {
|
||
if v, ok := c.Get("notify_enabled"); ok && v != nil {
|
||
return c.GetBool("notify_enabled")
|
||
}
|
||
return true
|
||
}
|
||
|
||
// SQLitePath 数据库文件路径:sqlite 模式下 DSN 为空时回退默认路径(需求 ⑧)。
|
||
func (c *Config) SQLitePath() string {
|
||
if c.Env.DBDriver != DBDriverSQLite {
|
||
return ""
|
||
}
|
||
if c.Env.DBDSN != "" {
|
||
return c.Env.DBDSN
|
||
}
|
||
return DefaultSQLitePath
|
||
}
|
||
|
||
// AdminSessionExpireSeconds 管理员会话有效期(秒),
|
||
// 参考 apps/admin/dependencies.py 的 get_admin_session_expire_seconds。
|
||
func (c *Config) AdminSessionExpireSeconds() int {
|
||
n := c.GetInt("adminSessionExpire")
|
||
if n < AdminSessionExpireMin || n > AdminSessionExpireMax || n%AdminSessionExpireMin != 0 {
|
||
return AdminSessionExpireDefault
|
||
}
|
||
return n
|
||
}
|
||
|
||
// Engine 当前存储引擎。
|
||
// Engine 返回当前存储引擎名:KV storage_engine 优先(v3 运行时可改),
|
||
// 空(未设置/历史数据)回落启动值 Env.StorageEngine(env 校验过的 local|s3|webdav)。
|
||
// 枚举校验内联(避免 config→storage 反向依赖)。
|
||
func (c *Config) Engine() string {
|
||
if v, ok := c.Get(KeyStorageEngine); ok {
|
||
if s, isStr := v.(string); isStr {
|
||
switch s {
|
||
case "local", "s3", "webdav":
|
||
return s
|
||
}
|
||
}
|
||
}
|
||
return c.Env.StorageEngine
|
||
}
|