github.com/icyphox/x@v0.0.355-0.20220311094250-029bd783e8b8/decoderx/http.go (about) 1 package decoderx 2 3 import ( 4 "bytes" 5 "context" 6 "crypto/sha256" 7 "encoding/json" 8 "fmt" 9 "io" 10 "io/ioutil" 11 "net/http" 12 "net/url" 13 "strconv" 14 "strings" 15 16 "github.com/pkg/errors" 17 "github.com/tidwall/gjson" 18 "github.com/tidwall/sjson" 19 20 "github.com/ory/jsonschema/v3" 21 22 "github.com/ory/herodot" 23 24 "github.com/ory/x/httpx" 25 "github.com/ory/x/jsonschemax" 26 "github.com/ory/x/stringslice" 27 ) 28 29 type ( 30 // HTTP decodes json and form-data from HTTP Request Bodies. 31 HTTP struct{} 32 33 httpDecoderOptions struct { 34 keepRequestBody bool 35 allowedContentTypes []string 36 allowedHTTPMethods []string 37 jsonSchemaRef string 38 jsonSchemaCompiler *jsonschema.Compiler 39 jsonSchemaValidate bool 40 maxCircularReferenceDepth uint8 41 handleParseErrors parseErrorStrategy 42 expectJSONFlattened bool 43 queryAndBody bool 44 } 45 46 // HTTPDecoderOption configures the HTTP decoder. 47 HTTPDecoderOption func(*httpDecoderOptions) 48 49 parseErrorStrategy uint8 50 ) 51 52 const ( 53 httpContentTypeMultipartForm = "multipart/form-data" 54 httpContentTypeURLEncodedForm = "application/x-www-form-urlencoded" 55 httpContentTypeJSON = "application/json" 56 ) 57 58 const ( 59 // ParseErrorIgnoreConversionErrors will ignore any errors caused by strconv.Parse* and use the 60 // raw form field value, which is a string, when such a parse error occurs. 61 // 62 // If the JSON Schema defines `{"ratio": {"type": "number"}}` but `ratio=foobar` then field 63 // `ratio` will be handled as a string. If the destination struct is a `json.RawMessage`, then 64 // the output will be `{"ratio": "foobar"}`. 65 ParseErrorIgnoreConversionErrors parseErrorStrategy = iota + 1 66 67 // ParseErrorUseEmptyValueOnConversionErrors will ignore any parse errors caused by strconv.Parse* and use the 68 // default value of the type to be casted, e.g. float64(0), string(""). 69 // 70 // If the JSON Schema defines `{"ratio": {"type": "number"}}` but `ratio=foobar` then field 71 // `ratio` will receive the default value for the primitive type (here `0.0` for `number`). 72 // If the destination struct is a `json.RawMessage`, then the output will be `{"ratio": 0.0}`. 73 ParseErrorUseEmptyValueOnConversionErrors 74 75 // ParseErrorReturnOnConversionErrors will abort and return with an error if strconv.Parse* returns 76 // an error. 77 // 78 // If the JSON Schema defines `{"ratio": {"type": "number"}}` but `ratio=foobar` the parser aborts 79 // and returns an error, here: `strconv.ParseFloat: parsing "foobar"`. 80 ParseErrorReturnOnConversionErrors 81 ) 82 83 var errKeyNotFound = errors.New("key not found") 84 85 // HTTPFormDecoder configures the HTTP decoder to only accept form-data 86 // (application/x-www-form-urlencoded, multipart/form-data) 87 func HTTPFormDecoder() HTTPDecoderOption { 88 return func(o *httpDecoderOptions) { 89 o.allowedContentTypes = []string{httpContentTypeMultipartForm, httpContentTypeURLEncodedForm} 90 } 91 } 92 93 // HTTPJSONDecoder configures the HTTP decoder to only accept form-data 94 // (application/json). 95 func HTTPJSONDecoder() HTTPDecoderOption { 96 return func(o *httpDecoderOptions) { 97 o.allowedContentTypes = []string{httpContentTypeJSON} 98 } 99 } 100 101 // HTTPKeepRequestBody configures the HTTP decoder to allow other 102 // HTTP request body readers to read the body as well by keeping 103 // the data in memory. 104 func HTTPKeepRequestBody(keep bool) HTTPDecoderOption { 105 return func(o *httpDecoderOptions) { 106 o.keepRequestBody = keep 107 } 108 } 109 110 // HTTPDecoderSetValidatePayloads sets if payloads should be validated or not. 111 func HTTPDecoderSetValidatePayloads(validate bool) HTTPDecoderOption { 112 return func(o *httpDecoderOptions) { 113 o.jsonSchemaValidate = validate 114 o.keepRequestBody = true 115 } 116 } 117 118 // HTTPDecoderJSONFollowsFormFormat if set tells the decoder that JSON follows the same conventions 119 // as the form decoder, meaning `{"foo.bar": "..."}` is translated to `{"foo": {"bar": "..."}}`. 120 func HTTPDecoderJSONFollowsFormFormat() HTTPDecoderOption { 121 return func(o *httpDecoderOptions) { 122 o.expectJSONFlattened = true 123 o.keepRequestBody = true 124 } 125 } 126 127 // HTTPDecoderAllowedMethods sets the allowed HTTP methods. Defaults are POST, PUT, PATCH. 128 func HTTPDecoderAllowedMethods(method ...string) HTTPDecoderOption { 129 return func(o *httpDecoderOptions) { 130 o.allowedHTTPMethods = method 131 } 132 } 133 134 // HTTPDecoderUseQueryAndBody will check both the HTTP body and the HTTP query params when decoding. 135 // Only relevant for non-GET operations. 136 func HTTPDecoderUseQueryAndBody() HTTPDecoderOption { 137 return func(o *httpDecoderOptions) { 138 o.queryAndBody = true 139 } 140 } 141 142 // HTTPDecoderSetIgnoreParseErrorsStrategy sets a strategy for dealing with strconv.Parse* errors: 143 // 144 // - decoderx.ParseErrorIgnoreConversionErrors will ignore any parse errors caused by strconv.Parse* and use the 145 // raw form field value, which is a string, when such a parse error occurs. (default) 146 // - decoderx.ParseErrorUseEmptyValueOnConversionErrors will ignore any parse errors caused by strconv.Parse* and use the 147 // default value of the type to be casted, e.g. float64(0), string(""). 148 // - decoderx.ParseErrorReturnOnConversionErrors will abort and return with an error if strconv.Parse* returns 149 // an error. 150 func HTTPDecoderSetIgnoreParseErrorsStrategy(strategy parseErrorStrategy) HTTPDecoderOption { 151 return func(o *httpDecoderOptions) { 152 o.handleParseErrors = strategy 153 } 154 } 155 156 // HTTPDecoderSetMaxCircularReferenceDepth sets the maximum recursive reference resolution depth. 157 func HTTPDecoderSetMaxCircularReferenceDepth(depth uint8) HTTPDecoderOption { 158 return func(o *httpDecoderOptions) { 159 o.maxCircularReferenceDepth = depth 160 } 161 } 162 163 // HTTPJSONSchemaCompiler sets a JSON schema to be used for validation and type assertion of 164 // incoming requests. 165 func HTTPJSONSchemaCompiler(ref string, compiler *jsonschema.Compiler) HTTPDecoderOption { 166 return func(o *httpDecoderOptions) { 167 if compiler == nil { 168 compiler = jsonschema.NewCompiler() 169 } 170 compiler.ExtractAnnotations = true 171 o.jsonSchemaCompiler = compiler 172 o.jsonSchemaRef = ref 173 o.jsonSchemaValidate = true 174 } 175 } 176 177 // HTTPRawJSONSchemaCompiler uses a JSON Schema Compiler with the provided JSON Schema in raw byte form. 178 func HTTPRawJSONSchemaCompiler(raw []byte) (HTTPDecoderOption, error) { 179 compiler := jsonschema.NewCompiler() 180 id := fmt.Sprintf("%x.json", sha256.Sum256(raw)) 181 if err := compiler.AddResource(id, bytes.NewReader(raw)); err != nil { 182 return nil, err 183 } 184 compiler.ExtractAnnotations = true 185 186 return func(o *httpDecoderOptions) { 187 o.jsonSchemaCompiler = compiler 188 o.jsonSchemaRef = id 189 o.jsonSchemaValidate = true 190 }, nil 191 } 192 193 // MustHTTPRawJSONSchemaCompiler uses HTTPRawJSONSchemaCompiler and panics on error. 194 func MustHTTPRawJSONSchemaCompiler(raw []byte) HTTPDecoderOption { 195 f, err := HTTPRawJSONSchemaCompiler(raw) 196 if err != nil { 197 panic(err) 198 } 199 return f 200 } 201 202 func newHTTPDecoderOptions(fs []HTTPDecoderOption) *httpDecoderOptions { 203 o := &httpDecoderOptions{ 204 allowedContentTypes: []string{ 205 httpContentTypeMultipartForm, httpContentTypeURLEncodedForm, httpContentTypeJSON, 206 }, 207 allowedHTTPMethods: []string{"POST", "PUT", "PATCH"}, 208 maxCircularReferenceDepth: 5, 209 handleParseErrors: ParseErrorIgnoreConversionErrors, 210 } 211 212 for _, f := range fs { 213 f(o) 214 } 215 216 return o 217 } 218 219 // NewHTTP creates a new HTTP decoder. 220 func NewHTTP() *HTTP { 221 return new(HTTP) 222 } 223 224 func (t *HTTP) validateRequest(r *http.Request, c *httpDecoderOptions) error { 225 method := strings.ToUpper(r.Method) 226 227 if !stringslice.Has(c.allowedHTTPMethods, method) { 228 return errors.WithStack(herodot.ErrBadRequest.WithReasonf(`Unable to decode body because HTTP Request Method was "%s" but only %v are supported.`, method, c.allowedHTTPMethods)) 229 } 230 231 if method != "GET" { 232 if r.ContentLength == 0 && method != "GET" { 233 return errors.WithStack(herodot.ErrBadRequest.WithReasonf(`Unable to decode HTTP Request Body because its HTTP Header "Content-Length" is zero.`)) 234 } 235 236 if !httpx.HasContentType(r, c.allowedContentTypes...) { 237 return errors.WithStack(herodot.ErrBadRequest.WithReasonf(`HTTP %s Request used unknown HTTP Header "Content-Type: %s", only %v are supported.`, method, r.Header.Get("Content-Type"), c.allowedContentTypes)) 238 } 239 } 240 241 return nil 242 } 243 244 func (t *HTTP) validatePayload(ctx context.Context, raw json.RawMessage, c *httpDecoderOptions) error { 245 if !c.jsonSchemaValidate { 246 return nil 247 } 248 249 if c.jsonSchemaCompiler == nil { 250 return errors.WithStack(herodot.ErrInternalServerError.WithReasonf("JSON Schema Validation is required but no compiler was provided.")) 251 } 252 253 schema, err := c.jsonSchemaCompiler.Compile(ctx, c.jsonSchemaRef) 254 if err != nil { 255 return errors.WithStack(herodot.ErrInternalServerError.WithReasonf("Unable to load JSON Schema from location: %s", c.jsonSchemaRef).WithDebug(err.Error())) 256 } 257 258 if err := schema.Validate(bytes.NewBuffer(raw)); err != nil { 259 if _, ok := err.(*jsonschema.ValidationError); ok { 260 return errors.WithStack(err) 261 } 262 return errors.WithStack(herodot.ErrInternalServerError.WithReasonf("Unable to process JSON Schema and input: %s", err).WithDebug(err.Error())) 263 } 264 265 return nil 266 } 267 268 // Decode takes a HTTP Request Body and decodes it into destination. 269 func (t *HTTP) Decode(r *http.Request, destination interface{}, opts ...HTTPDecoderOption) error { 270 c := newHTTPDecoderOptions(opts) 271 if err := t.validateRequest(r, c); err != nil { 272 return err 273 } 274 275 if r.Method == "GET" { 276 return t.decodeForm(r, destination, c) 277 } else if httpx.HasContentType(r, httpContentTypeJSON) { 278 if c.expectJSONFlattened { 279 return t.decodeJSONForm(r, destination, c) 280 } 281 return t.decodeJSON(r, destination, c, false) 282 } else if httpx.HasContentType(r, httpContentTypeMultipartForm, httpContentTypeURLEncodedForm) { 283 return t.decodeForm(r, destination, c) 284 } 285 286 return errors.WithStack(herodot.ErrInternalServerError.WithReasonf("Unable to determine decoder for content type: %s", r.Header.Get("Content-Type"))) 287 } 288 289 func (t *HTTP) requestBody(r *http.Request, o *httpDecoderOptions) (reader io.ReadCloser, err error) { 290 if strings.ToUpper(r.Method) == "GET" { 291 return ioutil.NopCloser(bytes.NewBufferString(r.URL.Query().Encode())), nil 292 } 293 294 if !o.keepRequestBody { 295 return r.Body, nil 296 } 297 298 bodyBytes, err := ioutil.ReadAll(r.Body) 299 if err != nil { 300 return nil, errors.Wrapf(err, "unable to read body") 301 } 302 303 _ = r.Body.Close() // must close 304 r.Body = ioutil.NopCloser(bytes.NewBuffer(bodyBytes)) 305 306 return ioutil.NopCloser(bytes.NewBuffer(bodyBytes)), nil 307 } 308 309 func (t *HTTP) decodeJSONForm(r *http.Request, destination interface{}, o *httpDecoderOptions) error { 310 if o.jsonSchemaCompiler == nil { 311 return errors.WithStack(herodot.ErrInternalServerError.WithReasonf("Unable to decode HTTP Form Body because no validation schema was provided. This is a code bug.")) 312 } 313 314 paths, err := jsonschemax.ListPathsWithRecursion(r.Context(), o.jsonSchemaRef, o.jsonSchemaCompiler, o.maxCircularReferenceDepth) 315 if err != nil { 316 return errors.WithStack(herodot.ErrInternalServerError.WithTrace(err).WithReasonf("Unable to prepare JSON Schema for HTTP Post Body Form parsing: %s", err).WithDebugf("%+v", err)) 317 } 318 319 reader, err := t.requestBody(r, o) 320 if err != nil { 321 return err 322 } 323 324 var interim json.RawMessage 325 if err := json.NewDecoder(reader).Decode(&interim); err != nil { 326 return err 327 } 328 329 parsed := gjson.ParseBytes(interim) 330 if !parsed.IsObject() { 331 return errors.WithStack(herodot.ErrBadRequest.WithReasonf("Expected JSON sent in request body to be an object but got: %s", parsed.Type.String())) 332 } 333 334 values := url.Values{} 335 var notJSONForm bool 336 parsed.ForEach(func(k, v gjson.Result) bool { 337 if v.IsArray() || v.IsObject() { 338 notJSONForm = true 339 return false 340 } 341 values.Set(k.String(), v.String()) 342 return true 343 }) 344 345 if notJSONForm { 346 return t.decodeJSON(r, destination, o, true) 347 } 348 349 if o.queryAndBody { 350 _ = r.ParseForm() 351 for k := range r.Form { 352 values.Set(k, r.Form.Get(k)) 353 } 354 } 355 356 raw, err := t.decodeURLValues(values, paths, o) 357 if err != nil { 358 return err 359 } 360 361 if err := json.Unmarshal(raw, destination); err != nil { 362 return errors.WithStack(err) 363 } 364 365 return t.validatePayload(r.Context(), raw, o) 366 } 367 368 func (t *HTTP) decodeForm(r *http.Request, destination interface{}, o *httpDecoderOptions) error { 369 if o.jsonSchemaCompiler == nil { 370 return errors.WithStack(herodot.ErrInternalServerError.WithReasonf("Unable to decode HTTP Form Body because no validation schema was provided. This is a code bug.")) 371 } 372 373 reader, err := t.requestBody(r, o) 374 if err != nil { 375 return err 376 } 377 378 defer func() { 379 r.Body = reader 380 }() 381 382 if err := r.ParseForm(); err != nil { 383 return errors.WithStack(herodot.ErrBadRequest.WithReasonf("Unable to decode HTTP %s form body: %s", strings.ToUpper(r.Method), err).WithDebug(err.Error())) 384 } 385 386 paths, err := jsonschemax.ListPathsWithRecursion(r.Context(), o.jsonSchemaRef, o.jsonSchemaCompiler, o.maxCircularReferenceDepth) 387 if err != nil { 388 return errors.WithStack(herodot.ErrInternalServerError.WithTrace(err).WithReasonf("Unable to prepare JSON Schema for HTTP Post Body Form parsing: %s", err).WithDebugf("%+v", err)) 389 } 390 391 values := r.PostForm 392 if r.Method == "GET" || o.queryAndBody { 393 values = r.Form 394 } 395 396 raw, err := t.decodeURLValues(values, paths, o) 397 if err != nil && !errors.Is(err, errKeyNotFound) { 398 return err 399 } 400 401 if err := json.NewDecoder(bytes.NewReader(raw)).Decode(destination); err != nil { 402 return errors.WithStack(herodot.ErrBadRequest.WithReasonf("Unable to decode JSON payload: %s", err)) 403 } 404 405 return t.validatePayload(r.Context(), raw, o) 406 } 407 408 func (t *HTTP) decodeURLValues(values url.Values, paths []jsonschemax.Path, o *httpDecoderOptions) (json.RawMessage, error) { 409 raw := json.RawMessage(`{}`) 410 for key := range values { 411 for _, path := range paths { 412 if key == path.Name { 413 var err error 414 switch path.Type.(type) { 415 case []string: 416 raw, err = sjson.SetBytes(raw, path.Name, values[key]) 417 case []float64: 418 for k, v := range values[key] { 419 if f, err := strconv.ParseFloat(v, 64); err != nil { 420 switch o.handleParseErrors { 421 case ParseErrorIgnoreConversionErrors: 422 raw, err = sjson.SetBytes(raw, path.Name+"."+strconv.Itoa(k), v) 423 case ParseErrorUseEmptyValueOnConversionErrors: 424 raw, err = sjson.SetBytes(raw, path.Name+"."+strconv.Itoa(k), f) 425 case ParseErrorReturnOnConversionErrors: 426 return nil, errors.WithStack(herodot.ErrBadRequest.WithReasonf("Expected value to be a number."). 427 WithDetail("parse_error", err.Error()). 428 WithDetail("name", key). 429 WithDetailf("index", "%d", k). 430 WithDetail("value", v)) 431 } 432 } else { 433 raw, err = sjson.SetBytes(raw, path.Name+"."+strconv.Itoa(k), f) 434 } 435 } 436 case []bool: 437 for k, v := range values[key] { 438 if f, err := strconv.ParseBool(v); err != nil { 439 switch o.handleParseErrors { 440 case ParseErrorIgnoreConversionErrors: 441 raw, err = sjson.SetBytes(raw, path.Name+"."+strconv.Itoa(k), v) 442 case ParseErrorUseEmptyValueOnConversionErrors: 443 raw, err = sjson.SetBytes(raw, path.Name+"."+strconv.Itoa(k), f) 444 case ParseErrorReturnOnConversionErrors: 445 return nil, errors.WithStack(herodot.ErrBadRequest.WithReasonf("Expected value to be a boolean."). 446 WithDetail("parse_error", err.Error()). 447 WithDetail("name", key). 448 WithDetailf("index", "%d", k). 449 WithDetail("value", v)) 450 } 451 } else { 452 raw, err = sjson.SetBytes(raw, path.Name+"."+strconv.Itoa(k), f) 453 } 454 } 455 case []interface{}: 456 raw, err = sjson.SetBytes(raw, path.Name, values[key]) 457 case bool: 458 v := values[key][len(values[key])-1] 459 if len(v) == 0 { 460 if !path.Required { 461 continue 462 } 463 v = "false" 464 } 465 466 if f, err := strconv.ParseBool(v); err != nil { 467 switch o.handleParseErrors { 468 case ParseErrorIgnoreConversionErrors: 469 raw, err = sjson.SetBytes(raw, path.Name, v) 470 case ParseErrorUseEmptyValueOnConversionErrors: 471 raw, err = sjson.SetBytes(raw, path.Name, f) 472 case ParseErrorReturnOnConversionErrors: 473 return nil, errors.WithStack(herodot.ErrBadRequest.WithReasonf("Expected value to be a boolean."). 474 WithDetail("parse_error", err.Error()). 475 WithDetail("name", key). 476 WithDetail("value", values.Get(key))) 477 } 478 } else { 479 raw, err = sjson.SetBytes(raw, path.Name, f) 480 } 481 case float64: 482 v := values.Get(key) 483 if len(v) == 0 { 484 if !path.Required { 485 continue 486 } 487 v = "0.0" 488 } 489 490 if f, err := strconv.ParseFloat(v, 64); err != nil { 491 switch o.handleParseErrors { 492 case ParseErrorIgnoreConversionErrors: 493 raw, err = sjson.SetBytes(raw, path.Name, v) 494 case ParseErrorUseEmptyValueOnConversionErrors: 495 raw, err = sjson.SetBytes(raw, path.Name, f) 496 case ParseErrorReturnOnConversionErrors: 497 return nil, errors.WithStack(herodot.ErrBadRequest.WithReasonf("Expected value to be a number."). 498 WithDetail("parse_error", err.Error()). 499 WithDetail("name", key). 500 WithDetail("value", values.Get(key))) 501 } 502 } else { 503 raw, err = sjson.SetBytes(raw, path.Name, f) 504 } 505 case string: 506 v := values.Get(key) 507 if len(v) == 0 { 508 continue 509 } 510 511 raw, err = sjson.SetBytes(raw, path.Name, v) 512 case map[string]interface{}: 513 v := values.Get(key) 514 if len(v) == 0 && !path.Required { 515 continue 516 } 517 518 raw, err = sjson.SetBytes(raw, path.Name, v) 519 case []map[string]interface{}: 520 raw, err = sjson.SetBytes(raw, path.Name, values[key]) 521 } 522 523 if err != nil { 524 return nil, errors.WithStack(herodot.ErrBadRequest.WithReasonf("Unable to type assert values from HTTP Post Body: %s", err)) 525 } 526 break 527 } 528 } 529 } 530 531 for _, path := range paths { 532 if path.TypeHint != jsonschemax.JSON { 533 continue 534 } 535 536 if !gjson.GetBytes(raw, path.Name).Exists() { 537 var err error 538 raw, err = sjson.SetRawBytes(raw, path.Name, []byte(`{}`)) 539 if err != nil { 540 return nil, errors.WithStack(err) 541 } 542 } 543 } 544 545 return raw, nil 546 } 547 548 func (t *HTTP) decodeJSON(r *http.Request, destination interface{}, o *httpDecoderOptions, isRetry bool) error { 549 reader, err := t.requestBody(r, o) 550 if err != nil { 551 return err 552 } 553 554 raw, err := ioutil.ReadAll(reader) 555 if err != nil { 556 return errors.WithStack(herodot.ErrBadRequest.WithReasonf("Unable to read HTTP POST body: %s", err)) 557 } 558 559 dc := json.NewDecoder(bytes.NewReader(raw)) 560 if !isRetry { 561 dc.DisallowUnknownFields() 562 } 563 if err := json.NewDecoder(bytes.NewReader(raw)).Decode(destination); err != nil { 564 return errors.WithStack(herodot.ErrBadRequest.WithReasonf("Unable to decode JSON payload: %s", err)) 565 } 566 567 if err := t.validatePayload(r.Context(), raw, o); err != nil { 568 if o.expectJSONFlattened && strings.Contains(err.Error(), "json: unknown field") && !isRetry { 569 return t.decodeJSONForm(r, destination, o) 570 } 571 return err 572 } 573 574 return nil 575 }