// Copyright 2022 E99p1ant. All rights reserved. // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. package form import ( "mime" "mime/multipart" "net/http" "reflect" "github.com/flamego/flamego" "github.com/flamego/template" "github.com/unknwon/com" "github.com/wuhan005/govalid" "github.com/tamakyi/TamaBox/internal/context" ) type ErrorCategory string const ( ErrorCategoryDeserialization ErrorCategory = "deserialization" ErrorCategoryValidation ErrorCategory = "validation" ) type Error struct { Category ErrorCategory Error error } const ( // multipartMemoryLimit 是 multipart 解析时驻留内存的上限,超出部分由 // mime/multipart 落盘到临时文件。 multipartMemoryLimit = 10 << 20 // 10 MiB // MaxRequestBodySize 是单次请求体的**总**大小上限(含落盘部分)。 // // ⚠️ 必须显式限制:ParseMultipartForm 的参数只约束内存缓冲,超出部分会无上限地 // 写入临时文件。匿名可达的提问接口即接受 multipart,若不加总量限制,攻击者可持续 // POST 超大 body 撑满磁盘(http.Server 的 ReadTimeout 在高带宽下不足以形成约束)。 // 略高于内存上限,为 multipart 边界与其他表单字段留出开销。 // 导出供 internal/route 的全局 requestBodyLimitMiddleware 复用同一上限: // 仅靠本 Bind 内的 MaxBytesReader 不够——Contexter 组级 CSRF 校验先于 Bind 执行, // 无 X-CSRF-Token 头时它会整包惰性解析 body,超大 multipart 会在 Bind 限制生效前 // 落盘;全局中间件把上限提前到一切解析之前(见 internal/route/bodylimit.go)。 MaxRequestBodySize = 12 << 20 // 12 MiB ) func Bind(model interface{}) flamego.Handler { // Ensure not pointer. if reflect.TypeOf(model).Kind() == reflect.Ptr { panic("form: pointer can not be accepted as binding model") } return func(c context.Context, data template.Data) { obj := reflect.New(reflect.TypeOf(model)) defer func() { c.Map(obj.Elem().Interface()) }() r := c.Request() // 在任何解析之前封顶请求体总大小。 // 注意:c.Request() 返回 flamego 的包装类型,其 Body 是方法而非字段, // 必须取内嵌的 *http.Request 才能替换 Body。 // MaxBytesReader 在超限时让后续 Read 返回错误,从而使 ParseForm / // ParseMultipartForm 失败并走下方的反序列化错误分支,避免超大 body 落盘或读入内存。 httpReq := r.Request httpReq.Body = http.MaxBytesReader(c.ResponseWriter(), httpReq.Body, MaxRequestBodySize) contentType, _, _ := mime.ParseMediaType(r.Header.Get("Content-Type")) switch contentType { case "application/x-www-form-urlencoded", "": if err := r.ParseForm(); err != nil { // ⚠️ 必须同时 SetError(写响应则在此提前中止):flamego 的 handler 链 // 只要响应未写入就会自动执行下一 handler,仅 Map(Error) 不中断链, // 零值结构体会照常进入业务动作(如把用户资料/屏蔽词静默清空、解除页面密码)。 // SetError 置位 HasError,业务 action 开头若检查 ctx.HasError() 即会早退; // 不检查的 action 也已在 route/user 各 handler 补了 abortOnFormError 守卫。 c.SetError(err) c.Map(Error{Category: ErrorCategoryDeserialization, Error: err}) return } case "multipart/form-data": if err := r.ParseMultipartForm(multipartMemoryLimit); err != nil { c.SetError(err) c.Map(Error{Category: ErrorCategoryDeserialization, Error: err}) return } } // Bind the form data to the given struct. typ := reflect.TypeOf(obj.Interface()) val := reflect.ValueOf(obj.Interface()) if typ.Kind() == reflect.Ptr { typ = typ.Elem() val = val.Elem() } for i := 0; i < typ.NumField(); i++ { field := typ.Field(i) fieldName := typ.Field(i).Tag.Get("form") if fieldName == "" { fieldName = com.ToSnakeCase(field.Name) } if r.MultipartForm != nil { fhType := reflect.TypeOf((*multipart.FileHeader)(nil)) value, ok := r.MultipartForm.Value[fieldName] if ok { if val.Field(i).Kind() == reflect.Slice && val.Field(i).Type().Elem() == fhType { continue } // FIXME: We don't implement the slice type yet. val.Field(i).Set(reflect.ValueOf(value[0])) } else { value, ok := r.MultipartForm.File[fieldName] if ok { numElems := len(value) if val.Field(i).Kind() == reflect.Slice && numElems > 0 && val.Field(i).Type().Elem() == fhType { slice := reflect.MakeSlice(val.Field(i).Type(), numElems, numElems) for i := 0; i < numElems; i++ { slice.Index(i).Set(reflect.ValueOf(value[i])) } val.Field(i).Set(slice) } else if val.Field(i).Type() == fhType { val.Field(i).Set(reflect.ValueOf(value[0])) } } } } else { // 非 multipart 请求(application/x-www-form-urlencoded):仅绑定 string 字段。 // 其余类型(如 []*multipart.FileHeader 图片字段)保持零值, // 否则 reflect.Set 会因类型不匹配 panic(string → []*multipart.FileHeader)。 // 此前提问表单因带 enctype="multipart/form-data" 走上面分支而从未触发; // 独立私聊表单等纯文本提交会命中此路径,必须先做类型判断。 if val.Field(i).Kind() == reflect.String { val.Field(i).Set(reflect.ValueOf(r.Form.Get(fieldName))) } } } errors, ok := govalid.Check(obj.Interface()) if !ok { Assign(obj.Interface(), data) c.SetError(errors[0]) c.Map(Error{Category: ErrorCategoryValidation, Error: errors[0]}) return } } } // Assign assigns form values back to the template data. func Assign(form interface{}, data map[string]interface{}) { typ := reflect.TypeOf(form) val := reflect.ValueOf(form) if typ.Kind() == reflect.Ptr { typ = typ.Elem() val = val.Elem() } for i := 0; i < typ.NumField(); i++ { field := typ.Field(i) fieldName := com.ToSnakeCase(field.Name) data[fieldName] = val.Field(i).Interface() } }