package contract import ( "bufio" "encoding/json" "errors" "fmt" "io" "math" "mime" "net/http" "net/url" "reflect" "strconv" "strings" apierrors "git.yuntuo.hk/k2safe/go-omni/errors" ) // Bind fills a fresh DTO transactionally. A JSON body, if declared, is required, // strictly decoded, and limited to 1 MiB unless maxBytes is supplied. Query // parameters never overwrite path/body/header values. form means URL query only. func (b *Binder) Bind(r *http.Request, params map[string]string, target any, maxBytes ...int64) error { return b.BindWithPolicy(r, params, target, RequestPolicy{}, maxBytes...) } // BindWithPolicy merges route overrides over the App request policy. func (b *Binder) BindWithPolicy(r *http.Request, params map[string]string, target any, policy RequestPolicy, maxBytes ...int64) error { if b == nil || r == nil || b.schemaOnly { return apierrors.Internal(errors.New("contract: nil binder or request")) } if b.HasMultipart() { return apierrors.Internal(errors.New("multipart fields require BindMultipart")) } v := reflect.ValueOf(target) if v.Kind() != reflect.Pointer || v.IsNil() || v.Elem().Type() != b.typ { return apierrors.Internal(errors.New("contract: target type mismatch")) } limit := DefaultMaxBodyBytes if len(maxBytes) > 1 || (len(maxBytes) == 1 && (maxBytes[0] <= 0 || maxBytes[0] == math.MaxInt64)) { return apierrors.Internal(errors.New("contract: invalid body limit")) } if len(maxBytes) == 1 { limit = maxBytes[0] } policy = MergePolicy(RequestPolicyFromContext(r.Context()), policy) if err := policy.Validate(); err != nil { return apierrors.Internal(err) } query := url.Values{} if r.URL != nil { var err error query, err = url.ParseQuery(r.URL.RawQuery) if err != nil { return invalid(errors.New("malformed query")) } } if policy.UnknownQuery == "reject" { allowed := map[string]bool{} for _, f := range b.root.fields { if f.source == "form" { allowed[f.name] = true } } for key := range query { if !allowed[key] { return invalid(fmt.Errorf("unknown query field %s", key)) } } } fresh := reflect.New(b.typ).Elem() body := map[string]any{} bodyAbsent := false bodyMode := policy.Body if bodyMode == "" || bodyMode == "auto" { if len(b.root.schema.Properties) > 0 { bodyMode = "required" } else { bodyMode = "auto" } } if (bodyMode == "none" || bodyMode == "emptyObject") && len(b.root.schema.Properties) > 0 { return apierrors.Internal(errors.New("request body policy conflicts with JSON fields")) } if bodyMode == "none" { if r.Body != nil { var one [1]byte n, err := io.ReadFull(r.Body, one[:]) if n > 0 { return invalid(errors.New("request body is forbidden")) } if err != io.EOF { return invalid(err) } } } if bodyMode == "required" || bodyMode == "optional" || bodyMode == "emptyObject" { var bodyReader io.Reader = r.Body if bodyReader == nil { bodyReader = strings.NewReader("") } if r.ContentLength > limit { return tooLarge() } reader := &io.LimitedReader{R: bodyReader, N: limit + 1} var checked io.Reader = reader if policy.UTF8 == "reject" { checked = &utf8Reader{r: checked} } buffered := bufio.NewReader(checked) _, peekErr := buffered.Peek(1) absent := peekErr == io.EOF bodyAbsent = absent if absent && bodyMode != "optional" { return invalid(errors.New("body required")) } if peekErr != nil && !absent { var maxErr *http.MaxBytesError if errors.As(peekErr, &maxErr) { return tooLarge() } return invalid(peekErr) } if !absent { ct, mediaParams, err := mime.ParseMediaType(r.Header.Get("Content-Type")) if err != nil || ct != "application/json" || (policy.Charset == "utf8" && mediaParams["charset"] != "" && !strings.EqualFold(mediaParams["charset"], "utf-8")) { return apierrors.New(415, "UNSUPPORTED_MEDIA_TYPE", "Content-Type must be application/json with an allowed charset") } dec := json.NewDecoder(buffered) dec.UseNumber() value, err := decodeValue(dec, 0) if reader.N == 0 { return tooLarge() } if err != nil { var maxErr *http.MaxBytesError if errors.As(err, &maxErr) { return tooLarge() } return invalid(err) } _, err = dec.Token() if reader.N == 0 { return tooLarge() } if err != io.EOF { var maxErr *http.MaxBytesError if errors.As(err, &maxErr) { return tooLarge() } return invalid(errors.New("exactly one JSON value required")) } var ok bool body, ok = value.(map[string]any) if !ok { return invalid(errors.New("body must be an object")) } for key := range body { if _, ok = b.root.schema.Properties[key]; !ok { return invalid(fmt.Errorf("unknown body field %s", key)) } } } } for _, f := range b.root.fields { var value any var present bool if f.source == "json" { if bodyAbsent && bodyMode == "optional" { continue } value, present = body[f.name] } else { var raw []string switch f.source { case "path": if s, ok := params[f.name]; ok { raw = []string{s} } case "form": raw = query[f.name] case "header": raw = r.Header.Values(f.name) } present = len(raw) > 0 if present { var err error value, err = fromStrings(f.value, raw) if err != nil { return invalid(fmt.Errorf("%s: %w", f.name, err)) } } } if err := assignField(f, fresh.Field(f.index), value, present); err != nil { return invalid(fmt.Errorf("%s: %w", f.name, err)) } } v.Elem().Set(fresh) return nil } func invalid(err error) error { return apierrors.BadRequest("INVALID_REQUEST", "request parameters are invalid").WithCause(err) } func tooLarge() error { return apierrors.New(413, "BODY_TOO_LARGE", "request body exceeds limit") } func fromStrings(n *node, values []string) (any, error) { n = base(n) if n.scalar != nil { if len(values) != 1 { return nil, errors.New("duplicate scalar parameter") } return scalarText(values[0]), nil } if n.typ.Kind() == reflect.Slice { out := make([]any, 0, len(values)) for _, s := range values { v, err := fromStrings(n.elem, []string{s}) if err != nil { return nil, err } out = append(out, v) } return out, nil } if len(values) != 1 { return nil, errors.New("duplicate scalar parameter") } return parseScalar(values[0], n.typ) } func assignField(f field, dst reflect.Value, value any, present bool) error { if !present { if f.hasDefault { value = f.fallback } else if f.optional { return nil } else { return errors.New("required field missing") } } if err := setValue(f.value, dst, value); err != nil { return err } return checkField(f, dst) } func setValue(n *node, dst reflect.Value, value any) error { if n.scalar != nil { return n.scalar.decode(dst, value) } if n.typ.Kind() == reflect.Interface && n.typ.NumMethod() == 0 { if value == nil { dst.SetZero() } else { dst.Set(reflect.ValueOf(value)) } return nil } if value == nil { if n.typ.Kind() == reflect.Pointer { dst.SetZero() return nil } return errors.New("null requires a pointer type") } if n.typ.Kind() == reflect.Pointer { dst.Set(reflect.New(n.typ.Elem())) return setValue(n.elem, dst.Elem(), value) } switch n.typ.Kind() { case reflect.Struct: object, ok := value.(map[string]any) if !ok { return errors.New("expected object") } for key := range object { if _, ok = n.schema.Properties[key]; !ok { return fmt.Errorf("unknown field %s", key) } } for _, f := range n.fields { item, exists := object[f.name] if err := assignField(f, dst.Field(f.index), item, exists); err != nil { return fmt.Errorf("%s: %w", f.name, err) } } case reflect.Slice: items, ok := value.([]any) if !ok { return errors.New("expected array") } dst.Set(reflect.MakeSlice(n.typ, len(items), len(items))) for i, item := range items { if err := setValue(n.elem, dst.Index(i), item); err != nil { return fmt.Errorf("item %d: %w", i, err) } } case reflect.Map: items, ok := value.(map[string]any) if !ok { return errors.New("expected object") } dst.Set(reflect.MakeMapWithSize(n.typ, len(items))) for key, item := range items { v := reflect.New(n.typ.Elem()).Elem() if err := setValue(n.elem, v, item); err != nil { return fmt.Errorf("map value: %w", err) } dst.SetMapIndex(reflect.ValueOf(key).Convert(n.typ.Key()), v) } case reflect.String: s, ok := value.(string) if !ok { return errors.New("expected string") } dst.SetString(s) case reflect.Bool: v, ok := value.(bool) if !ok { return errors.New("expected boolean") } dst.SetBool(v) default: num, ok := value.(json.Number) if !ok { return errors.New("expected number") } switch n.typ.Kind() { case reflect.Float32, reflect.Float64: v, err := strconv.ParseFloat(string(num), n.typ.Bits()) if err != nil || math.IsInf(v, 0) || math.IsNaN(v) { return errors.New("number out of range") } dst.SetFloat(v) case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: v, err := strconv.ParseUint(string(num), 10, n.typ.Bits()) if err != nil { return errors.New("invalid unsigned integer") } dst.SetUint(v) default: v, err := strconv.ParseInt(string(num), 10, n.typ.Bits()) if err != nil { return errors.New("invalid integer") } dst.SetInt(v) } } return nil } // Token decoding rejects duplicate keys at every level (encoding/json normally // keeps the last value). Depth and body limits bound client-controlled work. func decodeValue(dec *json.Decoder, depth int) (any, error) { if depth > 64 { return nil, errors.New("JSON nesting exceeds 64") } token, err := dec.Token() if err != nil { return nil, err } switch token { case json.Delim('{'): object := map[string]any{} for dec.More() { key, err := dec.Token() if err != nil { return nil, err } name, ok := key.(string) if !ok { return nil, errors.New("invalid object key") } if _, exists := object[name]; exists { return nil, errors.New("duplicate JSON field") } value, err := decodeValue(dec, depth+1) if err != nil { return nil, err } object[name] = value } end, err := dec.Token() if err != nil { return nil, err } if end != json.Delim('}') { return nil, errors.New("unterminated object") } return object, nil case json.Delim('['): items := []any{} for dec.More() { v, err := decodeValue(dec, depth+1) if err != nil { return nil, err } items = append(items, v) } end, err := dec.Token() if err != nil { return nil, err } if end != json.Delim(']') { return nil, errors.New("unterminated array") } return items, nil default: if _, bad := token.(json.Delim); bad { return nil, errors.New("unexpected JSON delimiter") } return token, nil } }