332 lines
8.4 KiB
Go
332 lines
8.4 KiB
Go
package contract
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"math"
|
|
"mime"
|
|
"net/http"
|
|
"net/url"
|
|
"reflect"
|
|
"strconv"
|
|
|
|
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 {
|
|
if b == nil || r == nil || b.schemaOnly {
|
|
return apierrors.Internal(errors.New("contract: nil binder or request"))
|
|
}
|
|
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]
|
|
}
|
|
fresh := reflect.New(b.typ).Elem()
|
|
body := map[string]any{}
|
|
if len(b.root.schema.Properties) > 0 {
|
|
ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
|
|
if err != nil || ct != "application/json" {
|
|
return apierrors.New(415, "UNSUPPORTED_MEDIA_TYPE", "Content-Type must be application/json")
|
|
}
|
|
if r.Body == nil {
|
|
return invalid(errors.New("body required"))
|
|
}
|
|
if r.ContentLength > limit {
|
|
return tooLarge()
|
|
}
|
|
reader := &io.LimitedReader{R: r.Body, N: limit + 1}
|
|
dec := json.NewDecoder(reader)
|
|
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))
|
|
}
|
|
}
|
|
}
|
|
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"))
|
|
}
|
|
}
|
|
for _, f := range b.root.fields {
|
|
var value any
|
|
var present bool
|
|
if f.source == "json" {
|
|
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 || 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 || 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
|
|
}
|
|
}
|