- 新增 middleware/bodylimit.go:BodyLimit 中间件,非 multipart 统一 4 MiB(覆盖文章正文上限);multipart 按平台上传策略派生(启用类型 限制与全局默认取最大 +1 MiB 开销,下限不低于编译期默认)。 Content-Length 已知且超限读体前直接 413,其余经 MaxBytesReader 截断 - handlers/api.go:bindJSON 识别 *http.MaxBytesError → 413 + request_too_large(i18n 中英新增) - main.go:中间件顺序调整为 SetUserContext → BodyLimit → CSRF—— 必须先于 CSRF(其解析 multipart 会读取整个请求体),SetUserContext 提前使 413 文案可按请求语言翻译;测试环境链同步(security_test.go) - middleware/auth.go:apiAuthError 更名 apiError(BodyLimit 复用) - 新增 handlers/bodylimit_test.go:超限 JSON 两种形态(已知长度/ chunked)413、正常体放行至认证层、3MB multipart 拒绝且附件表零写入、 GET 不受影响 - SECURITY_TODO.md:#26 勾选完成并记录验证;API 化复审新增 #26–#32 待办清单(P1 注册/评论限流、P2 favicon 魔数校验/最后管理员竞态/ 置顶权限、P3 零碎项)与执行顺序 go build / vet / test -race ./... 全绿
78 lines
2.0 KiB
Go
78 lines
2.0 KiB
Go
package handlers
|
||
|
||
import (
|
||
"errors"
|
||
"fmt"
|
||
"net/http"
|
||
|
||
"github.com/gin-gonic/gin"
|
||
)
|
||
|
||
// APIOK 返回统一成功响应。
|
||
// redirect 非空时附带相对跳转地址(原 302 目标,可含 ?saved=1 等 query);
|
||
// data 额外字段会合并进响应。
|
||
func APIOK(c *gin.Context, redirect string, data gin.H) {
|
||
resp := gin.H{"ok": true}
|
||
if redirect != "" {
|
||
resp["redirect"] = redirect
|
||
}
|
||
for k, v := range data {
|
||
resp[k] = v
|
||
}
|
||
c.JSON(http.StatusOK, resp)
|
||
}
|
||
|
||
// APIError 返回统一错误响应:{ok:false, code, error}。
|
||
// trKey 是 i18n 键,直接作为 code 返回;error 为该键按请求语言的翻译文案。
|
||
// trKey 为空时回退到通用键 "api_error"。
|
||
func APIError(c *gin.Context, status int, trKey string) {
|
||
tr := getTr(c)
|
||
code := trKey
|
||
if code == "" {
|
||
code = "api_error"
|
||
}
|
||
msg := tr[code]
|
||
if msg == "" {
|
||
msg = tr["api_error"]
|
||
}
|
||
c.JSON(status, gin.H{
|
||
"ok": false,
|
||
"code": code,
|
||
"error": msg,
|
||
})
|
||
}
|
||
|
||
// APIErrorf 同 APIError,但 i18n 文案可按 fmt.Sprintf 格式化
|
||
// (针对含 %d/%s 占位符的键,如 comments_too_long)。
|
||
func APIErrorf(c *gin.Context, status int, trKey string, args ...interface{}) {
|
||
tr := getTr(c)
|
||
msg := tr[trKey]
|
||
if msg == "" {
|
||
msg = tr["api_error"]
|
||
} else if len(args) > 0 {
|
||
msg = fmt.Sprintf(msg, args...)
|
||
}
|
||
c.JSON(status, gin.H{
|
||
"ok": false,
|
||
"code": trKey,
|
||
"error": msg,
|
||
})
|
||
}
|
||
|
||
// bindJSON 将 JSON 请求体绑定到 v。
|
||
// 绑定失败时返回 400 + api_invalid_request,并返回 false;
|
||
// 请求体超出 BodyLimit 中间件设置的上限时返回 413 + request_too_large
|
||
// (SECURITY_TODO #26)。使用前必须保证请求是 JSON。
|
||
func bindJSON(c *gin.Context, v interface{}) bool {
|
||
if err := c.ShouldBindJSON(v); err != nil {
|
||
var maxErr *http.MaxBytesError
|
||
if errors.As(err, &maxErr) {
|
||
APIError(c, http.StatusRequestEntityTooLarge, "request_too_large")
|
||
return false
|
||
}
|
||
APIError(c, http.StatusBadRequest, "api_invalid_request")
|
||
return false
|
||
}
|
||
return true
|
||
}
|