Files
sundynix-agentix/sundynix-gateway/internal/handler/safego.go
T
Blizzard ac38d5e663 fix(prod): 后端生产级 A 类硬伤全清(7 项:授权/崩溃点/全表扫/限流/鉴权/限额)
部署前生产级审计(可靠性/数据层/安全三路)后,清掉 7 处代码级硬伤:

A1 后台定时器 goroutine 无 panic recover → 单个 DB panic 崩整个 gateway。加 safeGo/
   safeCall,包住订阅/掉单补偿/微信推送/探针 goroutine,单轮 tick 再兜一层。
A2 提示词控制面(建/激活/停用,热广播全服务)只 RequireAuth → 任意登录用户改全局提示词。
   三写端点+列表挂 RequireAdmin。
A3 HITL 审批端点无角色门 → viewer 可放行烧钱执行。加 RequireTenantRole(member)。
A4 审计/护栏列表 limit 无校验,limit=-1 让 gorm 取消 LIMIT 全表扫。加 clampLimit/
   clampOffset,AdminTasks/AdminSpaces 补上界。
A5 限流 Redis 一挂就完全放行(fail-open)。加进程内固定窗口兜底(fail-safe) + 登录/注册
   按 IP 专用严限流(10/min)。
A6 公开 by-id 端点(stream/exec/report导出/kb导入流)无鉴权无租户过滤。加
   AuthFromHeaderOrQuery(从 ?token= 取 JWT) + task/report 按 owner 归属校验;桌面端
   5 处 EventSource/下载 URL 经 tokenQuery 附 JWT。
A7 文件上传无大小上限(整文件进内存 OOM 面) → 50MB 闸(KB_MAX_UPLOAD_BYTES)+ LimitReader;
   http.Server 加 ReadHeaderTimeout/ReadTimeout/MaxHeaderBytes(不设 WriteTimeout 保 SSE)。

带单测:clampLimit/safeCall/procLimiter/AuthFromHeaderOrQuery/TaskOwner。
build+vet+全量 test 绿;desktop tsc 绿。B(迁移工具/实时探针/出网韧性/登录锁定/leader选举)
与 C(TLS/PG HA/K8s/备份自动化/可观测)分期后做,参照 production_readiness.md。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 15:04:47 +08:00

41 lines
1.6 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.
package handler
import (
"log"
"net/http"
"runtime/debug"
"github.com/gin-gonic/gin"
)
// requireTaskOwner 校验请求者(AuthFromHeaderOrQuery 已注入的 uid)是该 task 的提交者。
// 用于公开 by-id 端点(SSE 流 / 报告导出):这些资源始终由本人的客户端访问(用户看/导出
// 自己提交的运行),故按 owner 判权即安全。非本人 → 403,返回 false。
func (h *Handler) requireTaskOwner(c *gin.Context, taskID string) bool {
uid := userID(c)
if uid == "" || h.db.TaskOwner(c.Request.Context(), taskID) != uid {
c.JSON(http.StatusForbidden, gin.H{"error": "无权访问该资源"})
return false
}
return true
}
// safeGo 起一个带 panic 兜底的后台 goroutinepanic 只记日志(含 name + stack)不外抛。
// 为什么必须有:Go 里未 recover 的 panic 会崩掉**整个进程**,而 gin.Recovery() 只保护
// 请求 goroutine、不覆盖 handler 派生的后台 goroutine(定时器/推送/探针)。一个后台任务的
// 意外 panic 不该拖垮整个 gateway、连带所有在途 HTTP。
func safeGo(name string, fn func()) {
go safeCall(name, fn)
}
// safeCall 同步执行 fn 并兜底 panic。用于定时器**单轮**内部:单轮 panic 不该终止整个 ticker
// 兜住后下一轮照常继续(若把 recover 只放在 safeGo 外层,单轮 panic 会让整个循环 goroutine 结束)。
func safeCall(name string, fn func()) {
defer func() {
if r := recover(); r != nil {
log.Printf("[panic] 后台任务 %q panic 已兜底: %v\n%s", name, r, debug.Stack())
}
}()
fn()
}