// Copyright 2026 tamakyi. All rights reserved. // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. package route import ( "net/http" "github.com/flamego/flamego" "github.com/tamakyi/TamaBox/internal/form" ) // requestBodyLimitMiddleware 在任何业务代码读取请求体之前,全局封顶单次请求体总大小, // 上限与 form.Bind 共用 form.MaxRequestBodySize(12 MiB),单一事实来源。 // // 为什么需要全局层(而不是只依赖 form.Bind 内部限制): // 1. 走 form.Bind 的路由中,Bind 内 MaxBytesReader 的执行顺序晚于 Contexter 组级 // CSRF 校验 —— flamego/csrf 的 Validate 在请求缺少 X-CSRF-Token 头时通过 // FormValue("_csrf") 惰性解析整包 body(multipart 超过 32 MiB 内存阈值后无上限 // 落盘临时文件),校验失败写出的 400 又发生在路由级限流之前。攻击者无需有效 // CSRF token、也不受限流约束,即可持续 POST 超大 multipart 打满磁盘。 // 2. chat /send、vote /submit 等不走 form.Bind 的入口此前完全没有请求体上限。 // // 挂载于 internal/route/route.go 的 f.Use 链(autoban 之后、所有 Group 之前), // 因此任何后续解析(CSRF FormValue、form.Bind、业务 FormValue)都发生在受限 reader 上。 // 合法最大单请求约为 8 MiB 单图(storage.MaxUploadBytes)+ 表单开销,12 MiB 不会误伤。 func requestBodyLimitMiddleware() flamego.Handler { return func(ctx flamego.Context) { req := ctx.Request() // 无 body 的方法不处理(避免对静态资源等无谓开销)。 switch req.Method { case http.MethodPost, http.MethodPut, http.MethodPatch: default: return } // Content-Length 明确超限时直接 413 快速失败,不进入解析流程。 // (chunked 或未知长度请求的 ContentLength 为 0/-1,走下方 MaxBytesReader 惰性封顶。) if req.ContentLength > form.MaxRequestBodySize { http.Error(ctx.ResponseWriter(), http.StatusText(http.StatusRequestEntityTooLarge), http.StatusRequestEntityTooLarge) return } // 其余情况:用 MaxBytesReader 包一层。MaxBytesReader 在读取超过上限时令后续 // Read 报错,使 ParseForm / ParseMultipartForm 失败 —— 超大 body 不会落盘或读入内存。 // 注意:c.Request() 返回 flamego 的包装类型,其 Body 是方法而非字段, // 必须取内嵌的 *http.Request 才能替换 Body(与 internal/form/form.go 同款写法)。 httpReq := req.Request httpReq.Body = http.MaxBytesReader(ctx.ResponseWriter(), httpReq.Body, form.MaxRequestBodySize) } }