mirror of
https://github.com/Awuqing/BackupX.git
synced 2026-06-06 10:19:43 +08:00
修复: 后端审查发现的 5 项问题
根据 Spec + Code Quality 双审查修复: 1. BatchCreate 事务保护(node_service.go/node_repository.go) 原循环 Create 在 DB 约束失败时会残留半截数据。改为预先构造所有 Node 再走 repo.BatchCreate 单一事务,任一失败整体回滚。 2. Peek 语义与 Consume 对齐(agent_install_token_repository.go) FindByToken 无条件返回任意记录,导致已消费/已过期的僵尸 token 可通过 compose 端点的 mode 检查但必然 Consume 失败,出现 410 假错。 新增 FindValidByToken,Peek 改用之。 3. MasterURL / AgentToken / AgentVersion 渲染前校验(installscript/renderer.go) 防止 YAML 注入(换行/引号逃逸 compose 配置)、shell 注入($(...))、 非法字符。加 TestRenderScriptRejects* 系列测试覆盖。 4. ipLimiter 无界增长修复(install_handler.go) 新增 gc 方法 + startGC 后台协程,每 window 周期清理过期 IP 条目。 RouterDependencies.Context 控制生命周期;app 传入 ctx,测试 t.Cleanup 取消。 5. CreateInstallToken 的 CreatedByID 从 JWT subject 解析(node_handler.go) 原硬编码 0 导致审计不可追溯。新增 resolveCurrentUserID helper, 借助 UserRepository 把 JWT subject(用户名)→ user.ID;失败退回 0。
This commit is contained in:
@@ -61,7 +61,13 @@ func setupInstallFlowRouter(t *testing.T) (http.Handler, string) {
|
||||
auditLogRepo := repository.NewAuditLogRepository(db)
|
||||
auditSvc := service.NewAuditService(auditLogRepo)
|
||||
|
||||
// 用 cancelable ctx,测试结束时停掉 handler 启动的后台 GC 协程,
|
||||
// 避免 goroutine 持有 map 导致 tempdir 清理失败。
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
t.Cleanup(cancel)
|
||||
|
||||
router := NewRouter(RouterDependencies{
|
||||
Context: ctx,
|
||||
Config: cfg,
|
||||
Version: "test",
|
||||
Logger: log,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"context"
|
||||
stdhttp "net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -21,12 +22,16 @@ type InstallHandler struct {
|
||||
limiter *ipLimiter
|
||||
}
|
||||
|
||||
func NewInstallHandler(tokenService *service.InstallTokenService, auditService *service.AuditService, externalURL string) *InstallHandler {
|
||||
// NewInstallHandler 构造 handler 并启动限流器的后台 GC 协程。
|
||||
// gcCtx 控制 GC 协程生命周期,建议传入 app context。
|
||||
func NewInstallHandler(gcCtx context.Context, tokenService *service.InstallTokenService, auditService *service.AuditService, externalURL string) *InstallHandler {
|
||||
limiter := newIPLimiter(20, time.Minute)
|
||||
limiter.startGC(gcCtx)
|
||||
return &InstallHandler{
|
||||
tokenService: tokenService,
|
||||
auditService: auditService,
|
||||
externalURL: externalURL,
|
||||
limiter: newIPLimiter(20, time.Minute),
|
||||
limiter: limiter,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -177,3 +182,40 @@ func (l *ipLimiter) allow(ip string) bool {
|
||||
l.events[ip] = append(keep, now)
|
||||
return true
|
||||
}
|
||||
|
||||
// gc 清理窗口外所有过期的 IP 条目,防止公网扫描导致 map 无界增长。
|
||||
// 由后台 goroutine 周期性调用。
|
||||
func (l *ipLimiter) gc(now time.Time) {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
cutoff := now.Add(-l.window)
|
||||
for k, v := range l.events {
|
||||
stale := true
|
||||
for _, t := range v {
|
||||
if t.After(cutoff) {
|
||||
stale = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if stale {
|
||||
delete(l.events, k)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// startGC 启动后台清理协程,每 window 周期清扫一次 map。
|
||||
// ctx 取消时协程退出。
|
||||
func (l *ipLimiter) startGC(ctx context.Context) {
|
||||
go func() {
|
||||
ticker := time.NewTicker(l.window)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case t := <-ticker.C:
|
||||
l.gc(t)
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
|
||||
"backupx/server/internal/apperror"
|
||||
"backupx/server/internal/installscript"
|
||||
"backupx/server/internal/repository"
|
||||
"backupx/server/internal/service"
|
||||
"backupx/server/pkg/response"
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -16,23 +17,49 @@ type NodeHandler struct {
|
||||
service *service.NodeService
|
||||
auditService *service.AuditService
|
||||
installTokenSvc *service.InstallTokenService
|
||||
userRepo repository.UserRepository
|
||||
externalURL string
|
||||
}
|
||||
|
||||
// NewNodeHandler 构造 handler。
|
||||
// userRepo 用于把 JWT subject(用户名)解析为 user.ID,填入 install_token.created_by_id 做审计追溯;
|
||||
// 传 nil 时 created_by_id 记为 0(仍可用,不阻断)。
|
||||
func NewNodeHandler(
|
||||
nodeService *service.NodeService,
|
||||
auditService *service.AuditService,
|
||||
installTokenSvc *service.InstallTokenService,
|
||||
userRepo repository.UserRepository,
|
||||
externalURL string,
|
||||
) *NodeHandler {
|
||||
return &NodeHandler{
|
||||
service: nodeService,
|
||||
auditService: auditService,
|
||||
installTokenSvc: installTokenSvc,
|
||||
userRepo: userRepo,
|
||||
externalURL: externalURL,
|
||||
}
|
||||
}
|
||||
|
||||
// resolveCurrentUserID 从 JWT subject 解析出 user.ID,失败返回 0。
|
||||
func (h *NodeHandler) resolveCurrentUserID(c *gin.Context) uint {
|
||||
if h.userRepo == nil {
|
||||
return 0
|
||||
}
|
||||
subjectValue, ok := c.Get(contextUserSubjectKey)
|
||||
if !ok {
|
||||
return 0
|
||||
}
|
||||
subject, err := service.SubjectFromContextValue(subjectValue)
|
||||
if err != nil || subject == "" {
|
||||
return 0
|
||||
}
|
||||
user, err := h.userRepo.FindByUsername(c.Request.Context(), subject)
|
||||
if err != nil || user == nil {
|
||||
return 0
|
||||
}
|
||||
return user.ID
|
||||
}
|
||||
|
||||
func (h *NodeHandler) List(c *gin.Context) {
|
||||
items, err := h.service.List(c.Request.Context())
|
||||
if err != nil {
|
||||
@@ -224,7 +251,7 @@ func (h *NodeHandler) CreateInstallToken(c *gin.Context) {
|
||||
AgentVersion: input.AgentVersion,
|
||||
DownloadSrc: input.DownloadSrc,
|
||||
TTLSeconds: input.TTLSeconds,
|
||||
CreatedByID: 0, // 如需关联 userID,后续可通过 auth 中间件注入
|
||||
CreatedByID: h.resolveCurrentUserID(c),
|
||||
})
|
||||
if err != nil {
|
||||
response.Error(c, err)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
stdhttp "net/http"
|
||||
|
||||
@@ -15,6 +16,9 @@ import (
|
||||
)
|
||||
|
||||
type RouterDependencies struct {
|
||||
// Context 控制 handler 启动的后台协程(如 ipLimiter GC)的生命周期。
|
||||
// app 应传入随进程退出可取消的 ctx;若为 nil 则退化为 context.Background()。
|
||||
Context context.Context
|
||||
Config config.Config
|
||||
Version string
|
||||
Logger *zap.Logger
|
||||
@@ -143,7 +147,7 @@ func NewRouter(deps RouterDependencies) *gin.Engine {
|
||||
database.POST("/discover", databaseHandler.Discover)
|
||||
}
|
||||
|
||||
nodeHandler := NewNodeHandler(deps.NodeService, deps.AuditService, deps.InstallTokenService, deps.MasterExternalURL)
|
||||
nodeHandler := NewNodeHandler(deps.NodeService, deps.AuditService, deps.InstallTokenService, deps.UserRepository, deps.MasterExternalURL)
|
||||
nodes := api.Group("/nodes")
|
||||
nodes.Use(AuthMiddleware(deps.JWTManager))
|
||||
nodes.GET("", nodeHandler.List)
|
||||
@@ -178,7 +182,11 @@ func NewRouter(deps RouterDependencies) *gin.Engine {
|
||||
|
||||
// 公开安装路由(不走 JWT 中间件)
|
||||
if deps.InstallTokenService != nil {
|
||||
installHandler := NewInstallHandler(deps.InstallTokenService, deps.AuditService, deps.MasterExternalURL)
|
||||
gcCtx := deps.Context
|
||||
if gcCtx == nil {
|
||||
gcCtx = context.Background()
|
||||
}
|
||||
installHandler := NewInstallHandler(gcCtx, deps.InstallTokenService, deps.AuditService, deps.MasterExternalURL)
|
||||
engine.GET("/install/:token", installHandler.Script)
|
||||
engine.GET("/install/:token/compose.yml", installHandler.Compose)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user