FileCodeBox Go 重写版 v2.5.6(安全审计修复版)
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 全绿;二进制端到端冒烟通过
This commit is contained in:
@@ -0,0 +1,187 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"filecodebox/internal/audit"
|
||||
"filecodebox/internal/config"
|
||||
"filecodebox/internal/middleware"
|
||||
"filecodebox/internal/response"
|
||||
"filecodebox/internal/settings"
|
||||
"filecodebox/internal/storage"
|
||||
)
|
||||
|
||||
// Deps API 层共享依赖(main.go 装配后注入)。
|
||||
type Deps struct {
|
||||
DB *gorm.DB
|
||||
Cfg *config.Config
|
||||
Mgr *settings.Manager
|
||||
AuditSvc *audit.Service
|
||||
Limiter *middleware.RateLimiter
|
||||
Store *storage.Manager // v3:可热切换引擎管理器(实现 Storage 接口)
|
||||
Version string
|
||||
}
|
||||
|
||||
// jwtSecret 当前 JWT 签名密钥(settings KV 运行时可变)。
|
||||
func (d *Deps) jwtSecret() string { return d.Mgr.SecretProvider()() }
|
||||
|
||||
// Register 注册全部 API 路由与前端静态资源回退。
|
||||
// 业务路由挂根路径(/share /chunk /presign /admin),与审计中间件
|
||||
// DefaultClassifier 的路由模式一致(t1 冻结契约);公共接口保留
|
||||
// /api/v1/health 与 /api/v1/config(对齐 t1 骨架)。
|
||||
func Register(r *gin.Engine, d *Deps) {
|
||||
// —— 公共接口 ——
|
||||
r.GET("/api/v1/health", d.health)
|
||||
r.GET("/api/v1/config", d.publicConfig)
|
||||
// Info3:robotsText 配置键此前无路由承接,补上(内容可在管理端自定义)
|
||||
r.GET("/robots.txt", d.robotsText)
|
||||
|
||||
// —— 初始化向导(未初始化时唯一可用入口,GuardNotInitialized 白名单)——
|
||||
registerSetup(r, d)
|
||||
|
||||
// —— 分享 ——
|
||||
share := r.Group("/share")
|
||||
{
|
||||
share.POST("/text", d.shareText)
|
||||
share.POST("/file", d.shareFile)
|
||||
// metadata:每次访问即计数(RequireRateLimit=进入检查+完成计数)
|
||||
share.GET("/metadata", d.Limiter.RequireRateLimit(middleware.LimitMeta), d.shareMetadata)
|
||||
share.POST("/metadata", d.Limiter.RequireRateLimit(middleware.LimitMeta), d.shareMetadataPost)
|
||||
share.GET("/select", d.shareSelect)
|
||||
share.POST("/select", d.shareSelectPost)
|
||||
share.GET("/download", d.shareDownload)
|
||||
}
|
||||
|
||||
// —— 分片上传 ——
|
||||
chunk := r.Group("/chunk")
|
||||
{
|
||||
chunk.POST("/upload/init", d.chunkInit)
|
||||
// 主路径(参考语义):/chunk/upload/{uploadID}/{index};
|
||||
// 扁平兼容:/chunk/upload + 表单/query 传 upload_id/chunk_index
|
||||
chunk.POST("/upload/:uploadID/:chunkIndex", d.chunkUpload)
|
||||
chunk.POST("/upload", d.chunkUploadFlat)
|
||||
chunk.GET("/upload/status/:uploadID", d.chunkStatus)
|
||||
chunk.POST("/upload/complete/:uploadID", d.chunkComplete)
|
||||
chunk.DELETE("/upload/:uploadID", d.chunkCancel)
|
||||
}
|
||||
|
||||
// —— 预签名直传 ——
|
||||
presign := r.Group("/presign")
|
||||
{
|
||||
presign.POST("/upload/init", d.presignInit)
|
||||
presign.PUT("/upload/proxy/:uploadID", d.presignProxy)
|
||||
presign.POST("/upload/confirm/:uploadID", d.presignConfirm)
|
||||
presign.GET("/upload/status/:uploadID", d.presignStatus)
|
||||
presign.DELETE("/upload/:uploadID", d.presignCancel)
|
||||
}
|
||||
|
||||
// —— 管理端(login 公开,其余需管理员 JWT)——
|
||||
registerAdmin(r, d)
|
||||
|
||||
// —— 前端静态资源 + SPA 回退(须最后注册)——
|
||||
registerWeb(r, d)
|
||||
}
|
||||
|
||||
// health 健康检查(对齐 t1 骨架,保持 /api/v1/health 语义不变)。
|
||||
func (d *Deps) health(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 200,
|
||||
"msg": "ok",
|
||||
"data": gin.H{
|
||||
"status": "ok",
|
||||
"version": d.Version,
|
||||
"storage": d.Cfg.Engine(),
|
||||
"time": nowRFC3339(),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// robotsText 输出管理端可配置的 robots.txt 内容。
|
||||
func (d *Deps) robotsText(c *gin.Context) {
|
||||
c.Data(http.StatusOK, "text/plain; charset=utf-8", []byte(d.Cfg.GetString("robotsText")))
|
||||
}
|
||||
|
||||
// publicConfig 公共配置(前端首页/上传页所需;v2 需求 ①②③④⑩ 扩展):
|
||||
// - 展示字段:站点信息、Logo/favicon、背景图、页脚文案/备案号、通知;
|
||||
// - 策略范围(上传页动态渲染):大小上限、类型白名单、过期方式、保存
|
||||
// 时间/次数上限、上传频率(仅范围,不含内部实现键)。
|
||||
//
|
||||
// 敏感键(admin_token/jwt_secret,settings.SensitiveKeys)与本端点无关:
|
||||
// 下发字段为白名单显式构造,任何敏感键均不会出现在响应中。
|
||||
func (d *Deps) publicConfig(c *gin.Context) {
|
||||
cfg := d.Cfg
|
||||
policy := d.CurrentUploadPolicy()
|
||||
uploadCount := cfg.GetInt("uploadCount")
|
||||
uploadMinute := cfg.GetInt("uploadMinute")
|
||||
// uploadSize 为参考语义的回落上限,单独下发供管理端联动展示
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 200,
|
||||
"msg": "ok",
|
||||
"data": gin.H{
|
||||
"config": gin.H{
|
||||
"name": cfg.SiteName(),
|
||||
"description": cfg.GetString("description"),
|
||||
"explain": cfg.GetString("page_explain"),
|
||||
// 需求 ①:Logo/favicon/背景图
|
||||
"logo_url": cfg.LogoURL(),
|
||||
"favicon_url": cfg.FaviconURL(),
|
||||
"background_url": cfg.BackgroundURL(),
|
||||
// 需求 ②:页脚自定义内容与备案号
|
||||
"footer_text": cfg.FooterText(),
|
||||
"footer_beian": cfg.FooterBeian(),
|
||||
// v3:当前存储引擎名(仅名称,任何引擎参数/凭据不下发)
|
||||
"storage_engine": d.Store.CurrentName(),
|
||||
"site_domain": d.Cfg.SiteDomain(),
|
||||
// 需求 ③:系统通知(开关 + 内容,前台右上角悬浮窗)
|
||||
// L7:读取侧再做一次白名单净化,覆盖历史存量与直改库的数据
|
||||
"notify_enabled": boolToInt(cfg.NotifyEnabled()),
|
||||
"notify_title": cfg.GetString("notify_title"),
|
||||
"notify_content": settings.SanitizeInlineHTML(cfg.GetString("notify_content")),
|
||||
// 策略范围(需求 ④⑩):上传页动态读取并在范围内选择
|
||||
"uploadSize": cfg.UploadSize(),
|
||||
"max_file_size": policy.MaxFileSize,
|
||||
"maxFileSize": policy.MaxFileSize,
|
||||
"allowedFileTypes": policy.AllowedTypes,
|
||||
"expireStyle": policy.ExpireStyles,
|
||||
"max_save_seconds": policy.MaxSaveSeconds,
|
||||
"maxSaveSeconds": policy.MaxSaveSeconds,
|
||||
"max_save_count": policy.MaxSaveCount,
|
||||
"maxSaveCount": policy.MaxSaveCount,
|
||||
"uploadCount": uploadCount,
|
||||
"uploadMinute": uploadMinute,
|
||||
"enableChunk": cfg.EnableChunk(),
|
||||
"openUpload": cfg.OpenUpload(),
|
||||
},
|
||||
"meta": gin.H{
|
||||
"version": d.Version,
|
||||
"features": gin.H{
|
||||
"chunkUpload": cfg.EnableChunk(),
|
||||
"guestUpload": cfg.OpenUpload(),
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// requireShareLogin 分享上传权限(对齐参考 share_required_login):
|
||||
// openUpload 开启时游客可传;关闭时要求管理员 Bearer token(403)。
|
||||
func (d *Deps) requireShareLogin(c *gin.Context) bool {
|
||||
if d.Cfg.OpenUpload() {
|
||||
return true
|
||||
}
|
||||
header := c.GetHeader("Authorization")
|
||||
const prefix = "Bearer "
|
||||
if len(header) <= len(prefix) || header[:len(prefix)] != prefix {
|
||||
response.Fail(c, http.StatusForbidden, "本站未开启游客上传,如需上传请先登录后台")
|
||||
return false
|
||||
}
|
||||
token := header[len(prefix):]
|
||||
if _, err := middleware.VerifyAdminToken(d.jwtSecret(), token); err != nil {
|
||||
response.Fail(c, http.StatusForbidden, "本站未开启游客上传,如需上传请先登录后台")
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
Reference in New Issue
Block a user