Files
go-omni/contract/bind.go

414 lines
11 KiB
Go

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
}
}