diff --git a/Gopkg.lock b/Gopkg.lock index 3139f7b9ecd..1b1911ed572 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -61,11 +61,13 @@ version = "v4.1.0" [[projects]] - digest = "1:1bc05108527c7941ba26861f769912a4c587d6a46a12dc1fa78ded6e87cecfbb" + digest = "1:812da80f354652604fa5217ac23846cb97535dfba788378b17d4231a486b7d6d" name = "github.com/getkin/kin-openapi" packages = [ "jsoninfo", "openapi3", + "openapi3filter", + "pathpattern", ] pruneopts = "NUT" revision = "90a7df014b4e7699507bf82aa5601503a9ba85da" @@ -1153,6 +1155,7 @@ input-imports = [ "github.com/emicklei/go-restful", "github.com/getkin/kin-openapi/openapi3", + "github.com/getkin/kin-openapi/openapi3filter", "github.com/go-logr/logr", "github.com/go-openapi/spec", "github.com/golang/protobuf/proto", diff --git a/vendor/github.com/getkin/kin-openapi/openapi3filter/authentication_input.go b/vendor/github.com/getkin/kin-openapi/openapi3filter/authentication_input.go new file mode 100644 index 00000000000..bae7c43d3e6 --- /dev/null +++ b/vendor/github.com/getkin/kin-openapi/openapi3filter/authentication_input.go @@ -0,0 +1,34 @@ +package openapi3filter + +import ( + "fmt" + "strings" + + "github.com/getkin/kin-openapi/openapi3" +) + +type AuthenticationInput struct { + RequestValidationInput *RequestValidationInput + SecuritySchemeName string + SecurityScheme *openapi3.SecurityScheme + Scopes []string +} + +func (input *AuthenticationInput) NewError(err error) error { + if err == nil { + scopes := input.Scopes + if len(scopes) == 0 { + err = fmt.Errorf("Security requirement '%s' failed", + input.SecuritySchemeName) + } else { + err = fmt.Errorf("Security requirement '%s' (scopes: '%s') failed", + input.SecuritySchemeName, + strings.Join(input.Scopes, "', '")) + } + } + return &RequestError{ + Input: input.RequestValidationInput, + Reason: "Authorization failed", + Err: err, + } +} diff --git a/vendor/github.com/getkin/kin-openapi/openapi3filter/errors.go b/vendor/github.com/getkin/kin-openapi/openapi3filter/errors.go new file mode 100644 index 00000000000..9b46ebda619 --- /dev/null +++ b/vendor/github.com/getkin/kin-openapi/openapi3filter/errors.go @@ -0,0 +1,86 @@ +package openapi3filter + +import ( + "errors" + "fmt" + "net/http" + + "github.com/getkin/kin-openapi/openapi3" +) + +var ( + errRouteMissingSwagger = errors.New("Route is missing OpenAPI specification") + errRouteMissingOperation = errors.New("Route is missing OpenAPI operation") + ErrAuthenticationServiceMissing = errors.New("Request validator doesn't have an authentication service defined") +) + +type RouteError struct { + Route Route + Reason string +} + +func (err *RouteError) Error() string { + return err.Reason +} + +type RequestError struct { + Input *RequestValidationInput + Parameter *openapi3.Parameter + RequestBody *openapi3.RequestBody + Status int + Reason string + Err error +} + +func (err *RequestError) HTTPStatus() int { + status := err.Status + if status == 0 { + status = http.StatusBadRequest + } + return status +} + +func (err *RequestError) Error() string { + reason := err.Reason + if e := err.Err; e != nil { + if len(reason) == 0 { + reason = e.Error() + } else { + reason += ": " + e.Error() + } + } + if v := err.Parameter; v != nil { + return fmt.Sprintf("Parameter '%s' in %s has an error: %s", v.Name, v.In, reason) + } else if v := err.RequestBody; v != nil { + return fmt.Sprintf("Request body has an error: %s", reason) + } else { + return reason + } +} + +type ResponseError struct { + Input *ResponseValidationInput + Reason string + Err error +} + +func (err *ResponseError) Error() string { + reason := err.Reason + if e := err.Err; e != nil { + if len(reason) == 0 { + reason = e.Error() + } else { + reason += ": " + e.Error() + } + } + return reason +} + +type SecurityRequirementsError struct { + SecurityRequirements openapi3.SecurityRequirements + Errors []error +} + +func (err *SecurityRequirementsError) Error() string { + return "Security requirements failed" +} diff --git a/vendor/github.com/getkin/kin-openapi/openapi3filter/internal.go b/vendor/github.com/getkin/kin-openapi/openapi3filter/internal.go new file mode 100644 index 00000000000..facaf1de57f --- /dev/null +++ b/vendor/github.com/getkin/kin-openapi/openapi3filter/internal.go @@ -0,0 +1,13 @@ +package openapi3filter + +import ( + "strings" +) + +func parseMediaType(contentType string) string { + i := strings.IndexByte(contentType, ';') + if i < 0 { + return contentType + } + return contentType[:i] +} diff --git a/vendor/github.com/getkin/kin-openapi/openapi3filter/options.go b/vendor/github.com/getkin/kin-openapi/openapi3filter/options.go new file mode 100644 index 00000000000..510b7775602 --- /dev/null +++ b/vendor/github.com/getkin/kin-openapi/openapi3filter/options.go @@ -0,0 +1,14 @@ +package openapi3filter + +import ( + "context" +) + +var DefaultOptions = &Options{} + +type Options struct { + ExcludeRequestBody bool + ExcludeResponseBody bool + IncludeResponseStatus bool + AuthenticationFunc func(c context.Context, input *AuthenticationInput) error +} diff --git a/vendor/github.com/getkin/kin-openapi/openapi3filter/req_resp_decoder.go b/vendor/github.com/getkin/kin-openapi/openapi3filter/req_resp_decoder.go new file mode 100644 index 00000000000..2a5f48996cb --- /dev/null +++ b/vendor/github.com/getkin/kin-openapi/openapi3filter/req_resp_decoder.go @@ -0,0 +1,906 @@ +package openapi3filter + +import ( + "encoding/json" + "fmt" + "io" + "io/ioutil" + "mime" + "mime/multipart" + "net/http" + "net/url" + "regexp" + "strconv" + "strings" + + "github.com/getkin/kin-openapi/openapi3" +) + +// ParseErrorKind describes a kind of ParseError. +// The type simplifies comparison of errors. +type ParseErrorKind int + +const ( + // KindOther describes an untyped parsing error. + KindOther ParseErrorKind = iota + // KindUnsupportedFormat describes an error that happens when a value has an unsupported format. + KindUnsupportedFormat + // KindInvalidFormat describes an error that happens when a value does not conform a format + // that is required by a serialization method. + KindInvalidFormat +) + +// ParseError describes errors which happens while parse operation's parameters, requestBody, or response. +type ParseError struct { + Kind ParseErrorKind + Value interface{} + Reason string + Cause error + + path []interface{} +} + +func (e *ParseError) Error() string { + var msg []string + if p := e.Path(); len(p) > 0 { + var arr []string + for _, v := range p { + arr = append(arr, fmt.Sprintf("%v", v)) + } + msg = append(msg, fmt.Sprintf("path %v", strings.Join(arr, "."))) + } + msg = append(msg, e.innerError()) + return strings.Join(msg, ": ") +} + +func (e *ParseError) innerError() string { + var msg []string + if e.Value != nil { + msg = append(msg, fmt.Sprintf("value %v", e.Value)) + } + if e.Reason != "" { + msg = append(msg, e.Reason) + } + if e.Cause != nil { + if v, ok := e.Cause.(*ParseError); ok { + msg = append(msg, v.innerError()) + } else { + msg = append(msg, e.Cause.Error()) + } + } + return strings.Join(msg, ": ") +} + +// RootCause returns a root cause of ParseError. +func (e *ParseError) RootCause() error { + if v, ok := e.Cause.(*ParseError); ok { + return v.RootCause() + } + return e.Cause +} + +// Path returns a path to the root cause. +func (e *ParseError) Path() []interface{} { + var path []interface{} + if v, ok := e.Cause.(*ParseError); ok { + p := v.Path() + if len(p) > 0 { + path = append(path, p...) + } + } + if len(e.path) > 0 { + path = append(path, e.path...) + } + return path +} + +func invalidSerializationMethodErr(sm *openapi3.SerializationMethod) error { + return fmt.Errorf("invalid serialization method: style=%q, explode=%v", sm.Style, sm.Explode) +} + +// Decodes a parameter defined via the content property as an object. It uses +// the user specified decoder, or our build-in decoder for application/json +func decodeContentParameter(param *openapi3.Parameter, input *RequestValidationInput) ( + value interface{}, schema *openapi3.Schema, err error) { + + paramValues := make([]string, 1) + var found bool + switch param.In { + case openapi3.ParameterInPath: + paramValues[0], found = input.PathParams[param.Name] + case openapi3.ParameterInQuery: + paramValues, found = input.GetQueryParams()[param.Name] + case openapi3.ParameterInHeader: + paramValues[0] = input.Request.Header.Get(http.CanonicalHeaderKey(param.Name)) + found = paramValues[0] != "" + case openapi3.ParameterInCookie: + var cookie *http.Cookie + cookie, err = input.Request.Cookie(param.Name) + if err == http.ErrNoCookie { + found = false + } else if err != nil { + return + } else { + paramValues[0] = cookie.Value + found = true + } + default: + err = fmt.Errorf("unsupported parameter's 'in': %s", param.In) + return + } + + if !found { + if param.Required { + err = fmt.Errorf("parameter '%s' is required, but missing", param.Name) + } + return + } + + decoder := input.ParamDecoder + if decoder == nil { + decoder = defaultContentParameterDecoder + } + + value, schema, err = decoder(param, paramValues) + return +} + +func defaultContentParameterDecoder(param *openapi3.Parameter, values []string) ( + outValue interface{}, outSchema *openapi3.Schema, err error) { + // Only query parameters can have multiple values. + if len(values) > 1 && param.In != openapi3.ParameterInQuery { + err = fmt.Errorf("%s parameter '%s' can't have multiple values", param.In, param.Name) + return + } + + content := param.Content + if content == nil { + err = fmt.Errorf("parameter '%s' expected to have content", param.Name) + return + } + + // We only know how to decode a parameter if it has one content, application/json + if len(content) != 1 { + err = fmt.Errorf("multiple content types for parameter '%s'", + param.Name) + return + } + + mt := content.Get("application/json") + if mt == nil { + err = fmt.Errorf("parameter '%s' has no json content schema", param.Name) + return + } + outSchema = mt.Schema.Value + + if len(values) == 1 { + err = json.Unmarshal([]byte(values[0]), &outValue) + if err != nil { + err = fmt.Errorf("error unmarshaling parameter '%s' as json", param.Name) + return + } + } else { + outArray := make([]interface{}, len(values)) + for i, v := range values { + err = json.Unmarshal([]byte(v), &outArray[i]) + if err != nil { + err = fmt.Errorf("error unmarshaling parameter '%s' as json", param.Name) + return + } + } + outValue = outArray + } + return +} + +type valueDecoder interface { + DecodePrimitive(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) (interface{}, error) + DecodeArray(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) ([]interface{}, error) + DecodeObject(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) (map[string]interface{}, error) +} + +// decodeStyledParameter returns a value of an operation's parameter from HTTP request for +// parameters defined using the style format. +// The function returns ParseError when HTTP request contains an invalid value of a parameter. +func decodeStyledParameter(param *openapi3.Parameter, input *RequestValidationInput) (interface{}, error) { + sm, err := param.SerializationMethod() + if err != nil { + return nil, err + } + + var dec valueDecoder + switch param.In { + case openapi3.ParameterInPath: + dec = &pathParamDecoder{pathParams: input.PathParams} + case openapi3.ParameterInQuery: + dec = &urlValuesDecoder{values: input.GetQueryParams()} + case openapi3.ParameterInHeader: + dec = &headerParamDecoder{header: input.Request.Header} + case openapi3.ParameterInCookie: + dec = &cookieParamDecoder{req: input.Request} + default: + return nil, fmt.Errorf("unsupported parameter's 'in': %s", param.In) + } + + return decodeValue(dec, param.Name, sm, param.Schema) +} + +func decodeValue(dec valueDecoder, param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) (interface{}, error) { + var decodeFn func(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) (interface{}, error) + switch schema.Value.Type { + case "array": + decodeFn = func(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) (interface{}, error) { + return dec.DecodeArray(param, sm, schema) + } + case "object": + decodeFn = func(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) (interface{}, error) { + return dec.DecodeObject(param, sm, schema) + } + default: + decodeFn = dec.DecodePrimitive + } + + return decodeFn(param, sm, schema) +} + +// pathParamDecoder decodes values of path parameters. +type pathParamDecoder struct { + pathParams map[string]string +} + +func (d *pathParamDecoder) DecodePrimitive(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) (interface{}, error) { + var prefix string + switch sm.Style { + case "simple": + // A prefix is empty for style "simple". + case "label": + prefix = "." + case "matrix": + prefix = ";" + param + "=" + default: + return nil, invalidSerializationMethodErr(sm) + } + + if d.pathParams == nil { + // HTTP request does not contains a value of the target path parameter. + return nil, nil + } + raw, ok := d.pathParams[paramKey(param, sm)] + if !ok || raw == "" { + // HTTP request does not contains a value of the target path parameter. + return nil, nil + } + src, err := cutPrefix(raw, prefix) + if err != nil { + return nil, err + } + return parsePrimitive(src, schema) +} + +func (d *pathParamDecoder) DecodeArray(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) ([]interface{}, error) { + var prefix, delim string + switch { + case sm.Style == "simple": + delim = "," + case sm.Style == "label" && sm.Explode == false: + prefix = "." + delim = "," + case sm.Style == "label" && sm.Explode == true: + prefix = "." + delim = "." + case sm.Style == "matrix" && sm.Explode == false: + prefix = ";" + param + "=" + delim = "," + case sm.Style == "matrix" && sm.Explode == true: + prefix = ";" + param + "=" + delim = ";" + param + "=" + default: + return nil, invalidSerializationMethodErr(sm) + } + + if d.pathParams == nil { + // HTTP request does not contains a value of the target path parameter. + return nil, nil + } + raw, ok := d.pathParams[paramKey(param, sm)] + if !ok || raw == "" { + // HTTP request does not contains a value of the target path parameter. + return nil, nil + } + src, err := cutPrefix(raw, prefix) + if err != nil { + return nil, err + } + return parseArray(strings.Split(src, delim), schema) +} + +func (d *pathParamDecoder) DecodeObject(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) (map[string]interface{}, error) { + var prefix, propsDelim, valueDelim string + switch { + case sm.Style == "simple" && sm.Explode == false: + propsDelim = "," + valueDelim = "," + case sm.Style == "simple" && sm.Explode == true: + propsDelim = "," + valueDelim = "=" + case sm.Style == "label" && sm.Explode == false: + prefix = "." + propsDelim = "," + valueDelim = "," + case sm.Style == "label" && sm.Explode == true: + prefix = "." + propsDelim = "." + valueDelim = "=" + case sm.Style == "matrix" && sm.Explode == false: + prefix = ";" + param + "=" + propsDelim = "," + valueDelim = "," + case sm.Style == "matrix" && sm.Explode == true: + prefix = ";" + propsDelim = ";" + valueDelim = "=" + default: + return nil, invalidSerializationMethodErr(sm) + } + + if d.pathParams == nil { + // HTTP request does not contains a value of the target path parameter. + return nil, nil + } + raw, ok := d.pathParams[paramKey(param, sm)] + if !ok || raw == "" { + // HTTP request does not contains a value of the target path parameter. + return nil, nil + } + src, err := cutPrefix(raw, prefix) + if err != nil { + return nil, err + } + props, err := propsFromString(src, propsDelim, valueDelim) + if err != nil { + return nil, err + } + return makeObject(props, schema) +} + +// paramKey returns a key to get a raw value of a path parameter. +func paramKey(param string, sm *openapi3.SerializationMethod) string { + switch sm.Style { + case "label": + return "." + param + case "matrix": + return ";" + param + default: + return param + } +} + +// cutPrefix validates that a raw value of a path parameter has the specified prefix, +// and returns a raw value without the prefix. +func cutPrefix(raw, prefix string) (string, error) { + if prefix == "" { + return raw, nil + } + if len(raw) < len(prefix) || raw[:len(prefix)] != prefix { + return "", &ParseError{ + Kind: KindInvalidFormat, + Value: raw, + Reason: fmt.Sprintf("a value must be prefixed with %q", prefix), + } + } + return raw[len(prefix):], nil +} + +// urlValuesDecoder decodes values of query parameters. +type urlValuesDecoder struct { + values url.Values +} + +func (d *urlValuesDecoder) DecodePrimitive(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) (interface{}, error) { + if sm.Style != "form" { + return nil, invalidSerializationMethodErr(sm) + } + + values := d.values[param] + if len(values) == 0 { + // HTTP request does not contain a value of the target query parameter. + return nil, nil + } + return parsePrimitive(values[0], schema) +} + +func (d *urlValuesDecoder) DecodeArray(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) ([]interface{}, error) { + if sm.Style == "deepObject" { + return nil, invalidSerializationMethodErr(sm) + } + + values := d.values[param] + if len(values) == 0 { + // HTTP request does not contain a value of the target query parameter. + return nil, nil + } + if !sm.Explode { + var delim string + switch sm.Style { + case "form": + delim = "," + case "spaceDelimited": + delim = " " + case "pipeDelimited": + delim = "|" + } + values = strings.Split(values[0], delim) + } + return parseArray(values, schema) +} + +func (d *urlValuesDecoder) DecodeObject(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) (map[string]interface{}, error) { + var propsFn func(url.Values) (map[string]string, error) + switch sm.Style { + case "form": + propsFn = func(params url.Values) (map[string]string, error) { + if len(params) == 0 { + // HTTP request does not contain query parameters. + return nil, nil + } + if sm.Explode { + props := make(map[string]string) + for key, values := range params { + props[key] = values[0] + } + return props, nil + } + values := params[param] + if len(values) == 0 { + // HTTP request does not contain a value of the target query parameter. + return nil, nil + } + return propsFromString(values[0], ",", ",") + } + case "deepObject": + propsFn = func(params url.Values) (map[string]string, error) { + props := make(map[string]string) + for key, values := range params { + groups := regexp.MustCompile(fmt.Sprintf("%s\\[(.+?)\\]", param)).FindAllStringSubmatch(key, -1) + if len(groups) == 0 { + // A query parameter's name does not match the required format, so skip it. + continue + } + props[groups[0][1]] = values[0] + } + if len(props) == 0 { + // HTTP request does not contain query parameters encoded by rules of style "deepObject". + return nil, nil + } + return props, nil + } + default: + return nil, invalidSerializationMethodErr(sm) + } + + props, err := propsFn(d.values) + if err != nil { + return nil, err + } + if props == nil { + return nil, nil + } + return makeObject(props, schema) +} + +// headerParamDecoder decodes values of header parameters. +type headerParamDecoder struct { + header http.Header +} + +func (d *headerParamDecoder) DecodePrimitive(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) (interface{}, error) { + if sm.Style != "simple" { + return nil, invalidSerializationMethodErr(sm) + } + + raw := d.header.Get(http.CanonicalHeaderKey(param)) + return parsePrimitive(raw, schema) +} + +func (d *headerParamDecoder) DecodeArray(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) ([]interface{}, error) { + if sm.Style != "simple" { + return nil, invalidSerializationMethodErr(sm) + } + + raw := d.header.Get(http.CanonicalHeaderKey(param)) + if raw == "" { + // HTTP request does not contains a corresponding header + return nil, nil + } + return parseArray(strings.Split(raw, ","), schema) +} + +func (d *headerParamDecoder) DecodeObject(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) (map[string]interface{}, error) { + if sm.Style != "simple" { + return nil, invalidSerializationMethodErr(sm) + } + valueDelim := "," + if sm.Explode { + valueDelim = "=" + } + + raw := d.header.Get(http.CanonicalHeaderKey(param)) + if raw == "" { + // HTTP request does not contain a corresponding header. + return nil, nil + } + props, err := propsFromString(raw, ",", valueDelim) + if err != nil { + return nil, err + } + return makeObject(props, schema) +} + +// cookieParamDecoder decodes values of cookie parameters. +type cookieParamDecoder struct { + req *http.Request +} + +func (d *cookieParamDecoder) DecodePrimitive(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) (interface{}, error) { + if sm.Style != "form" { + return nil, invalidSerializationMethodErr(sm) + } + + cookie, err := d.req.Cookie(param) + if err == http.ErrNoCookie { + // HTTP request does not contain a corresponding cookie. + return nil, nil + } + if err != nil { + return nil, fmt.Errorf("decode param %q: %s", param, err) + } + return parsePrimitive(cookie.Value, schema) +} + +func (d *cookieParamDecoder) DecodeArray(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) ([]interface{}, error) { + if sm.Style != "form" || sm.Explode { + return nil, invalidSerializationMethodErr(sm) + } + + cookie, err := d.req.Cookie(param) + if err == http.ErrNoCookie { + // HTTP request does not contain a corresponding cookie. + return nil, nil + } + if err != nil { + return nil, fmt.Errorf("decode param %q: %s", param, err) + } + return parseArray(strings.Split(cookie.Value, ","), schema) +} + +func (d *cookieParamDecoder) DecodeObject(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) (map[string]interface{}, error) { + if sm.Style != "form" || sm.Explode { + return nil, invalidSerializationMethodErr(sm) + } + + cookie, err := d.req.Cookie(param) + if err == http.ErrNoCookie { + // HTTP request does not contain a corresponding cookie. + return nil, nil + } + if err != nil { + return nil, fmt.Errorf("decode param %q: %s", param, err) + } + props, err := propsFromString(cookie.Value, ",", ",") + if err != nil { + return nil, err + } + return makeObject(props, schema) +} + +// propsFromString returns a properties map that is created by splitting a source string by propDelim and valueDelim. +// The source string must have a valid format: pairs separated by . +// The function returns an error when the source string has an invalid format. +func propsFromString(src, propDelim, valueDelim string) (map[string]string, error) { + props := make(map[string]string) + pairs := strings.Split(src, propDelim) + + // When propDelim and valueDelim is equal the source string follow the next rule: + // every even item of pairs is a properies's name, and the subsequent odd item is a property's value. + if propDelim == valueDelim { + // Taking into account the rule above, a valid source string must be splitted by propDelim + // to an array with an even number of items. + if len(pairs)%2 != 0 { + return nil, &ParseError{ + Kind: KindInvalidFormat, + Value: src, + Reason: fmt.Sprintf("a value must be a list of object's properties in format \"name%svalue\" separated by %s", valueDelim, propDelim), + } + } + for i := 0; i < len(pairs)/2; i++ { + props[pairs[i*2]] = pairs[i*2+1] + } + return props, nil + } + + // When propDelim and valueDelim is not equal the source string follow the next rule: + // every item of pairs is a string that follows format . + for _, pair := range pairs { + prop := strings.Split(pair, valueDelim) + if len(prop) != 2 { + return nil, &ParseError{ + Kind: KindInvalidFormat, + Value: src, + Reason: fmt.Sprintf("a value must be a list of object's properties in format \"name%svalue\" separated by %s", valueDelim, propDelim), + } + } + props[prop[0]] = prop[1] + } + return props, nil +} + +// makeObject returns an object that contains properties from props. +// A value of every property is parsed as a primitive value. +// The function returns an error when an error happened while parse object's properties. +func makeObject(props map[string]string, schema *openapi3.SchemaRef) (map[string]interface{}, error) { + obj := make(map[string]interface{}) + for propName, propSchema := range schema.Value.Properties { + value, err := parsePrimitive(props[propName], propSchema) + if err != nil { + if v, ok := err.(*ParseError); ok { + return nil, &ParseError{path: []interface{}{propName}, Cause: v} + } + return nil, fmt.Errorf("property %q: %s", propName, err) + } + obj[propName] = value + } + return obj, nil +} + +// parseArray returns an array that contains items from a raw array. +// Every item is parsed as a primitive value. +// The function returns an error when an error happened while parse array's items. +func parseArray(raw []string, schemaRef *openapi3.SchemaRef) ([]interface{}, error) { + var value []interface{} + for i, v := range raw { + item, err := parsePrimitive(v, schemaRef.Value.Items) + if err != nil { + if v, ok := err.(*ParseError); ok { + return nil, &ParseError{path: []interface{}{i}, Cause: v} + } + return nil, fmt.Errorf("item %d: %s", i, err) + } + value = append(value, item) + } + return value, nil +} + +// parsePrimitive returns a value that is created by parsing a source string to a primitive type +// that is specified by a JSON schema. The function returns nil when the source string is empty. +// The function panics when a JSON schema has a non primitive type. +func parsePrimitive(raw string, schema *openapi3.SchemaRef) (interface{}, error) { + if raw == "" { + return nil, nil + } + switch schema.Value.Type { + case "integer": + v, err := strconv.ParseFloat(raw, 64) + if err != nil { + return nil, &ParseError{Kind: KindInvalidFormat, Value: raw, Reason: "an invalid interger", Cause: err} + } + return v, nil + case "number": + v, err := strconv.ParseFloat(raw, 64) + if err != nil { + return nil, &ParseError{Kind: KindInvalidFormat, Value: raw, Reason: "an invalid number", Cause: err} + } + return v, nil + case "boolean": + v, err := strconv.ParseBool(raw) + if err != nil { + return nil, &ParseError{Kind: KindInvalidFormat, Value: raw, Reason: "an invalid number", Cause: err} + } + return v, nil + case "string": + return raw, nil + default: + panic(fmt.Sprintf("schema has non primitive type %q", schema.Value.Type)) + } +} + +// EncodingFn is a function that returns an encoding of a request body's part. +type EncodingFn func(partName string) *openapi3.Encoding + +// BodyDecoder is an interface to decode a body of a request or response. +// An implementation must return a value that is a primitive, []interface{}, or map[string]interface{}. +type BodyDecoder func(io.Reader, http.Header, *openapi3.SchemaRef, EncodingFn) (interface{}, error) + +// bodyDecoders contains decoders for supported content types of a body. +// By default, there is content type "application/json" is supported only. +var bodyDecoders = make(map[string]BodyDecoder) + +// RegisterBodyDecoder registers a request body's decoder for a content type. +// +// If a decoder for the specified content type already exists, the function replaces +// it with the specified decoder. +func RegisterBodyDecoder(contentType string, decoder BodyDecoder) { + if contentType == "" { + panic("contentType is empty") + } + if decoder == nil { + panic("decoder is not defined") + } + bodyDecoders[contentType] = decoder +} + +// UnregisterBodyDecoder dissociates a body decoder from a content type. +// +// Decoding this content type will result in an error. +func UnregisterBodyDecoder(contentType string) { + if contentType == "" { + panic("contentType is empty") + } + delete(bodyDecoders, contentType) +} + +// decodeBody returns a decoded body. +// The function returns ParseError when a body is invalid. +func decodeBody(body io.Reader, header http.Header, schema *openapi3.SchemaRef, encFn EncodingFn) (interface{}, error) { + contentType := header.Get(http.CanonicalHeaderKey("Content-Type")) + mediaType := parseMediaType(contentType) + decoder, ok := bodyDecoders[mediaType] + if !ok { + return nil, &ParseError{ + Kind: KindUnsupportedFormat, + Reason: fmt.Sprintf("unsupported content type %q", mediaType), + } + } + value, err := decoder(body, header, schema, encFn) + if err != nil { + return nil, err + } + return value, nil +} + +func init() { + RegisterBodyDecoder("text/plain", plainBodyDecoder) + RegisterBodyDecoder("application/json", jsonBodyDecoder) + RegisterBodyDecoder("application/x-www-form-urlencoded", urlencodedBodyDecoder) + RegisterBodyDecoder("multipart/form-data", multipartBodyDecoder) + RegisterBodyDecoder("application/octet-stream", FileBodyDecoder) +} + +func plainBodyDecoder(body io.Reader, header http.Header, schema *openapi3.SchemaRef, encFn EncodingFn) (interface{}, error) { + data, err := ioutil.ReadAll(body) + if err != nil { + return nil, &ParseError{Kind: KindInvalidFormat, Cause: err} + } + return string(data), nil +} + +func jsonBodyDecoder(body io.Reader, header http.Header, schema *openapi3.SchemaRef, encFn EncodingFn) (interface{}, error) { + var value interface{} + if err := json.NewDecoder(body).Decode(&value); err != nil { + return nil, &ParseError{Kind: KindInvalidFormat, Cause: err} + } + return value, nil +} + +func urlencodedBodyDecoder(body io.Reader, header http.Header, schema *openapi3.SchemaRef, encFn EncodingFn) (interface{}, error) { + // Validate JSON schema of request body. + // By the OpenAPI 3 specification request body's schema must have type "object". + // Properties of the schema describes individual parts of request body. + if schema.Value.Type != "object" { + return nil, fmt.Errorf("unsupported JSON schema of request body") + } + for propName, propSchema := range schema.Value.Properties { + switch propSchema.Value.Type { + case "object": + return nil, fmt.Errorf("unsupported JSON schema of request body's property %q", propName) + case "array": + items := propSchema.Value.Items.Value + if items.Type != "string" && items.Type != "integer" && items.Type != "number" && items.Type != "boolean" { + return nil, fmt.Errorf("unsupported JSON schema of request body's property %q", propName) + } + } + } + + // Parse form. + b, err := ioutil.ReadAll(body) + if err != nil { + return nil, err + } + values, err := url.ParseQuery(string(b)) + if err != nil { + return nil, err + } + + // Make an object value from form values. + obj := make(map[string]interface{}) + dec := &urlValuesDecoder{values: values} + for name, prop := range schema.Value.Properties { + var ( + value interface{} + enc *openapi3.Encoding + ) + if encFn != nil { + enc = encFn(name) + } + sm := enc.SerializationMethod() + + if value, err = decodeValue(dec, name, sm, prop); err != nil { + return nil, err + } + obj[name] = value + } + + return obj, nil +} + +func multipartBodyDecoder(body io.Reader, header http.Header, schema *openapi3.SchemaRef, encFn EncodingFn) (interface{}, error) { + if schema.Value.Type != "object" { + return nil, fmt.Errorf("unsupported JSON schema of request body") + } + + // Parse form. + values := make(map[string][]interface{}) + contentType := header.Get(http.CanonicalHeaderKey("Content-Type")) + _, params, err := mime.ParseMediaType(contentType) + if err != nil { + return nil, err + } + mr := multipart.NewReader(body, params["boundary"]) + for { + var part *multipart.Part + if part, err = mr.NextPart(); err == io.EOF { + break + } + if err != nil { + return nil, err + } + + var ( + name = part.FormName() + enc *openapi3.Encoding + ) + if encFn != nil { + enc = encFn(name) + } + subEncFn := func(string) *openapi3.Encoding { return enc } + // If the property's schema has type "array" it is means that the form contains a few parts with the same name. + // Every such part has a type that is defined by an items schema in the property's schema. + valueSchema := schema.Value.Properties[name] + if valueSchema.Value.Type == "array" { + valueSchema = valueSchema.Value.Items + } + + var value interface{} + if value, err = decodeBody(part, http.Header(part.Header), valueSchema, subEncFn); err != nil { + if v, ok := err.(*ParseError); ok { + return nil, &ParseError{path: []interface{}{name}, Cause: v} + } + return nil, fmt.Errorf("part %s: %s", name, err) + } + values[name] = append(values[name], value) + } + + // Make an object value from form values. + obj := make(map[string]interface{}) + for name, prop := range schema.Value.Properties { + vv := values[name] + if len(vv) == 0 { + continue + } + if prop.Value.Type == "array" { + obj[name] = vv + } else { + obj[name] = vv[0] + } + } + + return obj, nil +} + +// FileBodyDecoder is a body decoder that decodes a file body to a string. +func FileBodyDecoder(body io.Reader, header http.Header, schema *openapi3.SchemaRef, encFn EncodingFn) (interface{}, error) { + data, err := ioutil.ReadAll(body) + if err != nil { + return nil, err + } + return string(data), nil +} diff --git a/vendor/github.com/getkin/kin-openapi/openapi3filter/router.go b/vendor/github.com/getkin/kin-openapi/openapi3filter/router.go new file mode 100644 index 00000000000..fefe313dd76 --- /dev/null +++ b/vendor/github.com/getkin/kin-openapi/openapi3filter/router.go @@ -0,0 +1,214 @@ +package openapi3filter + +import ( + "context" + "errors" + "fmt" + "net/http" + "net/url" + "strings" + + "github.com/getkin/kin-openapi/openapi3" + "github.com/getkin/kin-openapi/pathpattern" +) + +type Route struct { + Swagger *openapi3.Swagger + Server *openapi3.Server + Path string + PathItem *openapi3.PathItem + Method string + Operation *openapi3.Operation + + // For developers who want use the router for handling too + Handler http.Handler +} + +// Routers maps a HTTP request to a Router. +type Routers []*Router + +func (routers Routers) FindRoute(method string, url *url.URL) (*Router, *Route, map[string]string, error) { + for _, router := range routers { + // Skip routers that have DO NOT have servers + if len(router.swagger.Servers) == 0 { + continue + } + route, pathParams, err := router.FindRoute(method, url) + if err == nil { + return router, route, pathParams, nil + } + } + for _, router := range routers { + // Skip routers that DO have servers + if len(router.swagger.Servers) > 0 { + continue + } + route, pathParams, err := router.FindRoute(method, url) + if err == nil { + return router, route, pathParams, nil + } + } + return nil, nil, nil, &RouteError{ + Reason: "None of the routers matches", + } +} + +// Router maps a HTTP request to an OpenAPI operation. +type Router struct { + swagger *openapi3.Swagger + pathNode *pathpattern.Node +} + +// NewRouter creates a new router. +// +// If the given Swagger has servers, router will use them. +// All operations of the Swagger will be added to the router. +func NewRouter() *Router { + return &Router{} +} + +// WithSwaggerFromFile loads the Swagger file and adds it using WithSwagger. +// Panics on any error. +func (router *Router) WithSwaggerFromFile(path string) *Router { + if err := router.AddSwaggerFromFile(path); err != nil { + panic(err) + } + return router +} + +// WithSwagger adds all operations in the OpenAPI specification. +// Panics on any error. +func (router *Router) WithSwagger(swagger *openapi3.Swagger) *Router { + if err := router.AddSwagger(swagger); err != nil { + panic(err) + } + return router +} + +// AddSwaggerFromFile loads the Swagger file and adds it using AddSwagger. +func (router *Router) AddSwaggerFromFile(path string) error { + swagger, err := openapi3.NewSwaggerLoader().LoadSwaggerFromFile(path) + if err != nil { + return err + } + return router.AddSwagger(swagger) +} + +// AddSwagger adds all operations in the OpenAPI specification. +func (router *Router) AddSwagger(swagger *openapi3.Swagger) error { + if err := swagger.Validate(context.TODO()); err != nil { + return fmt.Errorf("Validating Swagger failed: %v", err) + } + router.swagger = swagger + root := router.node() + for path, pathItem := range swagger.Paths { + for method, operation := range pathItem.Operations() { + method = strings.ToUpper(method) + if err := root.Add(method+" "+path, &Route{ + Swagger: swagger, + Path: path, + PathItem: pathItem, + Method: method, + Operation: operation, + }, nil); err != nil { + return err + } + } + } + return nil +} + +// AddRoute adds a route in the router. +func (router *Router) AddRoute(route *Route) error { + method := route.Method + if method == "" { + return errors.New("Route is missing method") + } + method = strings.ToUpper(method) + path := route.Path + if path == "" { + return errors.New("Route is missing path") + } + return router.node().Add(method+" "+path, router, nil) +} + +func (router *Router) node() *pathpattern.Node { + root := router.pathNode + if root == nil { + root = &pathpattern.Node{} + router.pathNode = root + } + return root +} + +func (router *Router) FindRoute(method string, url *url.URL) (*Route, map[string]string, error) { + swagger := router.swagger + + // Get server + servers := swagger.Servers + var server *openapi3.Server + var remainingPath string + var pathParams map[string]string + if len(servers) == 0 { + remainingPath = url.Path + } else { + var paramValues []string + server, paramValues, remainingPath = servers.MatchURL(url) + if server == nil { + return nil, nil, &RouteError{ + Route: Route{ + Swagger: swagger, + }, + Reason: "Does not match any server", + } + } + pathParams = make(map[string]string, 8) + paramNames, _ := server.ParameterNames() + for i, value := range paramValues { + name := paramNames[i] + pathParams[name] = value + } + } + + // Get PathItem + root := router.node() + var route *Route + node, paramValues := root.Match(method + " " + remainingPath) + if node != nil { + route, _ = node.Value.(*Route) + } + if route == nil { + return nil, nil, &RouteError{ + Route: Route{ + Swagger: swagger, + Server: server, + }, + Reason: "Path was not found", + } + } + + // Get operation + pathItem := route.PathItem + operation := pathItem.GetOperation(method) + if operation == nil { + return nil, nil, &RouteError{ + Route: Route{ + Swagger: swagger, + Server: server, + }, + Reason: "Path doesn't support the HTTP method", + } + } + if pathParams == nil { + pathParams = make(map[string]string, len(paramValues)) + } + paramKeys := node.VariableNames + for i, value := range paramValues { + key := paramKeys[i] + if strings.HasSuffix(key, "*") { + key = key[:len(key)-1] + } + pathParams[key] = value + } + return route, pathParams, nil +} diff --git a/vendor/github.com/getkin/kin-openapi/openapi3filter/validate_request.go b/vendor/github.com/getkin/kin-openapi/openapi3filter/validate_request.go new file mode 100644 index 00000000000..1a3a9badf52 --- /dev/null +++ b/vendor/github.com/getkin/kin-openapi/openapi3filter/validate_request.go @@ -0,0 +1,294 @@ +package openapi3filter + +import ( + "bytes" + "context" + "errors" + "fmt" + "io/ioutil" + "net/http" + "sort" + + "github.com/getkin/kin-openapi/openapi3" +) + +// ErrInvalidRequired is an error that happens when a required value of a parameter or request's body is not defined. +var ErrInvalidRequired = fmt.Errorf("must have a value") + +func ValidateRequest(c context.Context, input *RequestValidationInput) error { + options := input.Options + if options == nil { + options = DefaultOptions + } + route := input.Route + if route == nil { + return errors.New("invalid route") + } + operation := route.Operation + if operation == nil { + return errRouteMissingOperation + } + operationParameters := operation.Parameters + pathItemParameters := route.PathItem.Parameters + + // For each parameter of the PathItem + for _, parameterRef := range pathItemParameters { + parameter := parameterRef.Value + if operationParameters != nil { + if override := operationParameters.GetByInAndName(parameter.In, parameter.Name); override != nil { + continue + } + if err := ValidateParameter(c, input, parameter); err != nil { + return err + } + } + } + + // For each parameter of the Operation + for _, parameter := range operationParameters { + if err := ValidateParameter(c, input, parameter.Value); err != nil { + return err + } + } + + // RequestBody + requestBody := operation.RequestBody + if requestBody != nil && !options.ExcludeRequestBody { + if err := ValidateRequestBody(c, input, requestBody.Value); err != nil { + return err + } + } + + // Security + security := operation.Security + if security != nil { + if err := ValidateSecurityRequirements(c, input, *security); err != nil { + return err + } + } + return nil +} + +// ValidateParameter validates a parameter's value by JSON schema. +// The function returns RequestError with a ParseError cause when unable to parse a value. +// The function returns RequestError with ErrInvalidRequired cause when a value of a required parameter is not defined. +// The function returns RequestError with a openapi3.SchemaError cause when a value is invalid by JSON schema. +func ValidateParameter(c context.Context, input *RequestValidationInput, parameter *openapi3.Parameter) error { + if parameter.Schema == nil && parameter.Content == nil { + // We have no schema for the parameter. Assume that everything passes + // a schema-less check, but this could also be an error. The Swagger + // validation allows this to happen. + return nil + } + + var value interface{} + var err error + var schema *openapi3.Schema + + // Validation will ensure that we either have content or schema. + if parameter.Content != nil { + value, schema, err = decodeContentParameter(parameter, input) + if err != nil { + return &RequestError{Input: input, Parameter: parameter, Err: err} + } + } else { + value, err = decodeStyledParameter(parameter, input) + if err != nil { + return &RequestError{Input: input, Parameter: parameter, Err: err} + } + schema = parameter.Schema.Value + } + // Validate a parameter's value. + if value == nil { + if parameter.Required { + return &RequestError{Input: input, Parameter: parameter, Reason: "must have a value", Err: ErrInvalidRequired} + } + return nil + } + if schema == nil { + // A parameter's schema is not defined so skip validation of a parameter's value. + return nil + } + if err = schema.VisitJSON(value); err != nil { + return &RequestError{Input: input, Parameter: parameter, Err: err} + } + return nil +} + +// ValidateRequestBody validates data of a request's body. +// +// The function returns RequestError with ErrInvalidRequired cause when a value is required but not defined. +// The function returns RequestError with a openapi3.SchemaError cause when a value is invalid by JSON schema. +func ValidateRequestBody(c context.Context, input *RequestValidationInput, requestBody *openapi3.RequestBody) error { + var ( + req = input.Request + data []byte + ) + + if req.Body != http.NoBody { + defer req.Body.Close() + var err error + if data, err = ioutil.ReadAll(req.Body); err != nil { + return &RequestError{ + Input: input, + RequestBody: requestBody, + Reason: "reading failed", + Err: err, + } + } + // Put the data back into the input + req.Body = ioutil.NopCloser(bytes.NewReader(data)) + } + + if len(data) == 0 { + if requestBody.Required { + return &RequestError{Input: input, RequestBody: requestBody, Err: ErrInvalidRequired} + } + return nil + } + + content := requestBody.Content + if len(content) == 0 { + // A request's body does not have declared content, so skip validation. + return nil + } + + inputMIME := req.Header.Get("Content-Type") + contentType := requestBody.Content.Get(inputMIME) + if contentType == nil { + return &RequestError{ + Input: input, + RequestBody: requestBody, + Reason: fmt.Sprintf("header 'Content-Type' has unexpected value: %q", inputMIME), + } + } + + if contentType.Schema == nil { + // A JSON schema that describes the received data is not declared, so skip validation. + return nil + } + + encFn := func(name string) *openapi3.Encoding { return contentType.Encoding[name] } + value, err := decodeBody(bytes.NewReader(data), req.Header, contentType.Schema, encFn) + if err != nil { + return &RequestError{ + Input: input, + RequestBody: requestBody, + Reason: "failed to decode request body", + Err: err, + } + } + + // Validate JSON with the schema + if err := contentType.Schema.Value.VisitJSON(value); err != nil { + return &RequestError{ + Input: input, + RequestBody: requestBody, + Reason: "doesn't match the schema", + Err: err, + } + } + return nil +} + +// ValidateSecurityRequirements validates a multiple OpenAPI 3 security requirements. +// Returns nil if one of them inputed. +// Otherwise returns an error describing the security failures. +func ValidateSecurityRequirements(c context.Context, input *RequestValidationInput, srs openapi3.SecurityRequirements) error { + // Alternative requirements + if len(srs) == 0 { + return nil + } + + doneChan := make(chan bool, len(srs)) + errs := make([]error, len(srs)) + + // For each alternative + for i, securityRequirement := range srs { + // Capture index from iteration variable + currentIndex := i + currentSecurityRequirement := securityRequirement + go func() { + defer func() { + v := recover() + if v != nil { + if err, ok := v.(error); ok { + errs[currentIndex] = err + } else { + errs[currentIndex] = errors.New("Panicked") + } + doneChan <- false + } + }() + if err := validateSecurityRequirement(c, input, currentSecurityRequirement); err == nil { + doneChan <- true + } else { + errs[currentIndex] = err + doneChan <- false + } + }() + } + + // Wait for all + for i := 0; i < len(srs); i++ { + ok := <-doneChan + if ok { + close(doneChan) + return nil + } + } + return &SecurityRequirementsError{ + SecurityRequirements: srs, + Errors: errs, + } +} + +// validateSecurityRequirement validates a single OpenAPI 3 security requirement +func validateSecurityRequirement(c context.Context, input *RequestValidationInput, securityRequirement openapi3.SecurityRequirement) error { + swagger := input.Route.Swagger + if swagger == nil { + return errRouteMissingSwagger + } + securitySchemes := swagger.Components.SecuritySchemes + + // Ensure deterministic order + names := make([]string, 0, len(securityRequirement)) + for name := range securityRequirement { + names = append(names, name) + } + sort.Strings(names) + + // Get authentication function + options := input.Options + if options == nil { + options = DefaultOptions + } + f := options.AuthenticationFunc + if f == nil { + return ErrAuthenticationServiceMissing + } + + if len(names) > 0 { + name := names[0] + var securityScheme *openapi3.SecurityScheme + if securitySchemes != nil { + if ref := securitySchemes[name]; ref != nil { + securityScheme = ref.Value + } + } + if securityScheme == nil { + return &RequestError{ + Input: input, + Err: fmt.Errorf("Security scheme '%s' is not declared", name), + } + } + scopes := securityRequirement[name] + return f(c, &AuthenticationInput{ + RequestValidationInput: input, + SecuritySchemeName: name, + SecurityScheme: securityScheme, + Scopes: scopes, + }) + } + return nil +} diff --git a/vendor/github.com/getkin/kin-openapi/openapi3filter/validate_request_input.go b/vendor/github.com/getkin/kin-openapi/openapi3filter/validate_request_input.go new file mode 100644 index 00000000000..150e2700bc1 --- /dev/null +++ b/vendor/github.com/getkin/kin-openapi/openapi3filter/validate_request_input.go @@ -0,0 +1,37 @@ +package openapi3filter + +import ( + "net/http" + "net/url" + + "github.com/getkin/kin-openapi/openapi3" +) + +// This function takes a parameter definition from the swagger spec, and +// the value which we received for it. It is expected to return the +// value unmarshaled into an interface which can be traversed for +// validation, it should also return the schema to be used for validating the +// object, since there can be more than one in the content spec. +// +// If a query parameter appears multiple times, values[] will have more +// than one value, but for all other parameter types it should have just +// one. +type ContentParameterDecoder func(param *openapi3.Parameter, values []string) (interface{}, *openapi3.Schema, error) + +type RequestValidationInput struct { + Request *http.Request + PathParams map[string]string + QueryParams url.Values + Route *Route + Options *Options + ParamDecoder ContentParameterDecoder +} + +func (input *RequestValidationInput) GetQueryParams() url.Values { + q := input.QueryParams + if q == nil { + q = input.Request.URL.Query() + input.QueryParams = q + } + return q +} diff --git a/vendor/github.com/getkin/kin-openapi/openapi3filter/validate_response.go b/vendor/github.com/getkin/kin-openapi/openapi3filter/validate_response.go new file mode 100644 index 00000000000..ce376ce9166 --- /dev/null +++ b/vendor/github.com/getkin/kin-openapi/openapi3filter/validate_response.go @@ -0,0 +1,134 @@ +// Package openapi3filter validates that requests and inputs request an OpenAPI 3 specification file. +package openapi3filter + +import ( + "bytes" + "context" + "fmt" + "io/ioutil" + "net/http" + + "github.com/getkin/kin-openapi/openapi3" +) + +func ValidateResponse(c context.Context, input *ResponseValidationInput) error { + req := input.RequestValidationInput.Request + switch req.Method { + case "HEAD": + return nil + } + status := input.Status + if status < 100 { + return &ResponseError{ + Input: input, + Reason: "illegal status code", + Err: fmt.Errorf("Status %d", status), + } + } + + // These status codes will never be validated. + // TODO: The list is probably missing some. + switch status { + case http.StatusNotModified, + http.StatusPermanentRedirect, + http.StatusTemporaryRedirect, + http.StatusMovedPermanently: + return nil + } + route := input.RequestValidationInput.Route + options := input.Options + if options == nil { + options = DefaultOptions + } + + // Find input for the current status + responses := route.Operation.Responses + if len(responses) == 0 { + return nil + } + responseRef := responses.Get(status) // Response + if responseRef == nil { + responseRef = responses.Default() // Default input + } + if responseRef == nil { + // By default, status that is not documented is allowed. + if !options.IncludeResponseStatus { + return nil + } + + return &ResponseError{Input: input, Reason: "status is not supported"} + } + response := responseRef.Value + if response == nil { + return &ResponseError{Input: input, Reason: "response has not been resolved"} + } + + if options.ExcludeResponseBody { + // A user turned off validation of a response's body. + return nil + } + + content := response.Content + if len(content) == 0 || options.ExcludeResponseBody { + // An operation does not contains a validation schema for responses with this status code. + return nil + } + + inputMIME := input.Header.Get("Content-Type") + contentType := content.Get(inputMIME) + if contentType == nil { + return &ResponseError{ + Input: input, + Reason: fmt.Sprintf("input header 'Content-Type' has unexpected value: %q", inputMIME), + } + } + + if contentType.Schema == nil { + // An operation does not contains a validation schema for responses with this status code. + return nil + } + + // Read response's body. + body := input.Body + + // Response would contain partial or empty input body + // after we begin reading. + // Ensure that this doesn't happen. + input.Body = nil + + // Ensure we close the reader + defer body.Close() + + // Read all + data, err := ioutil.ReadAll(body) + if err != nil { + return &ResponseError{ + Input: input, + Reason: "failed to read response body", + Err: err, + } + } + + // Put the data back into the response. + input.SetBodyBytes(data) + + encFn := func(name string) *openapi3.Encoding { return contentType.Encoding[name] } + value, err := decodeBody(bytes.NewBuffer(data), input.Header, contentType.Schema, encFn) + if err != nil { + return &ResponseError{ + Input: input, + Reason: "failed to decode response body", + Err: err, + } + } + + // Validate data with the schema. + if err := contentType.Schema.Value.VisitJSON(value); err != nil { + return &ResponseError{ + Input: input, + Reason: "response body doesn't match the schema", + Err: err, + } + } + return nil +} diff --git a/vendor/github.com/getkin/kin-openapi/openapi3filter/validate_response_input.go b/vendor/github.com/getkin/kin-openapi/openapi3filter/validate_response_input.go new file mode 100644 index 00000000000..edf38730a5e --- /dev/null +++ b/vendor/github.com/getkin/kin-openapi/openapi3filter/validate_response_input.go @@ -0,0 +1,42 @@ +package openapi3filter + +import ( + "bytes" + "io" + "io/ioutil" + "net/http" +) + +type ResponseValidationInput struct { + RequestValidationInput *RequestValidationInput + Status int + Header http.Header + Body io.ReadCloser + Options *Options +} + +func (input *ResponseValidationInput) SetBodyBytes(value []byte) *ResponseValidationInput { + input.Body = ioutil.NopCloser(bytes.NewReader(value)) + return input +} + +var JSONPrefixes = []string{ + ")]}',\n", +} + +// TrimJSONPrefix trims one of the possible prefixes +func TrimJSONPrefix(data []byte) []byte { +search: + for _, prefix := range JSONPrefixes { + if len(data) < len(prefix) { + continue + } + for i, b := range data[:len(prefix)] { + if b != prefix[i] { + continue search + } + } + return data[len(prefix):] + } + return data +} diff --git a/vendor/github.com/getkin/kin-openapi/pathpattern/node.go b/vendor/github.com/getkin/kin-openapi/pathpattern/node.go new file mode 100644 index 00000000000..883d63af055 --- /dev/null +++ b/vendor/github.com/getkin/kin-openapi/pathpattern/node.go @@ -0,0 +1,333 @@ +// Package pathpattern implements path matching. +// +// Examples of supported patterns: +// * "/" +// * "/abc"" +// * "/abc/{variable}" (matches until next '/' or end-of-string) +// * "/abc/{variable*}" (matches everything, including "/abc" if "/abc" has noot) +// * "/abc/{ variable | prefix_(.*}_suffix }" (matches regular expressions) +package pathpattern + +import ( + "bytes" + "fmt" + "regexp" + "sort" + "strings" +) + +var DefaultOptions = &Options{ + SupportWildcard: true, +} + +type Options struct { + SupportWildcard bool + SupportRegExp bool +} + +// PathFromHost converts a host pattern to a path pattern. +// +// Examples: +// * PathFromHost("some-subdomain.domain.com", false) -> "com/./domain/./some-subdomain" +// * PathFromHost("some-subdomain.domain.com", true) -> "com/./domain/./subdomain/-/some" +func PathFromHost(host string, specialDashes bool) string { + buf := make([]byte, 0, len(host)) + end := len(host) + + // Go from end to start + for start := end - 1; start >= 0; start-- { + switch host[start] { + case '.': + buf = append(buf, host[start+1:end]...) + buf = append(buf, '/', '.', '/') + end = start + case '-': + if specialDashes { + buf = append(buf, host[start+1:end]...) + buf = append(buf, '/', '-', '/') + end = start + } + } + } + buf = append(buf, host[:end]...) + return string(buf) +} + +type Node struct { + VariableNames []string + Value interface{} + Suffixes SuffixList +} + +func (currentNode *Node) String() string { + buf := bytes.NewBuffer(make([]byte, 0, 255)) + currentNode.toBuffer(buf, "") + return buf.String() +} + +func (currentNode *Node) toBuffer(buf *bytes.Buffer, linePrefix string) { + if value := currentNode.Value; value != nil { + buf.WriteString(linePrefix) + buf.WriteString("VALUE: ") + fmt.Fprint(buf, value) + buf.WriteString("\n") + } + suffixes := currentNode.Suffixes + if len(suffixes) > 0 { + newLinePrefix := linePrefix + " " + for _, suffix := range suffixes { + buf.WriteString(linePrefix) + buf.WriteString("PATTERN: ") + buf.WriteString(suffix.String()) + buf.WriteString("\n") + suffix.Node.toBuffer(buf, newLinePrefix) + } + } +} + +type SuffixKind int + +// Note that order is important! +const ( + // SuffixKindConstant matches a constant string + SuffixKindConstant = SuffixKind(iota) + + // SuffixKindRegExp matches a regular expression + SuffixKindRegExp + + // SuffixKindVariable matches everything until '/' + SuffixKindVariable + + // SuffixKindEverything matches everything (until end-of-string) + SuffixKindEverything +) + +// Suffix describes condition that +type Suffix struct { + Kind SuffixKind + Pattern string + + // compiled regular expression + regExp *regexp.Regexp + + // Next node + Node *Node +} + +func EqualSuffix(a, b Suffix) bool { + return a.Kind == b.Kind && a.Pattern == b.Pattern +} + +func (suffix Suffix) String() string { + switch suffix.Kind { + case SuffixKindConstant: + return suffix.Pattern + case SuffixKindVariable: + return "{_}" + case SuffixKindEverything: + return "{_*}" + default: + return "{_|" + suffix.Pattern + "}" + } +} + +type SuffixList []Suffix + +func (list SuffixList) Less(i, j int) bool { + a, b := list[i], list[j] + ak, bk := a.Kind, b.Kind + if ak < bk { + return true + } else if bk < ak { + return false + } + return a.Pattern > b.Pattern +} + +func (list SuffixList) Len() int { + return len(list) +} + +func (list SuffixList) Swap(i, j int) { + a, b := list[i], list[j] + list[i], list[j] = b, a +} + +func (currentNode *Node) MustAdd(path string, value interface{}, options *Options) { + node, err := currentNode.CreateNode(path, options) + if err != nil { + panic(err) + } + node.Value = value +} + +func (currentNode *Node) Add(path string, value interface{}, options *Options) error { + node, err := currentNode.CreateNode(path, options) + if err != nil { + return err + } + node.Value = value + return nil +} + +func (currentNode *Node) CreateNode(path string, options *Options) (*Node, error) { + if options == nil { + options = DefaultOptions + } + for strings.HasSuffix(path, "/") { + path = path[:len(path)-1] + } + remaining := path + var variableNames []string +loop: + for { + //remaining = strings.TrimPrefix(remaining, "/") + if len(remaining) == 0 { + // This node is the right one + // Check whether another route already leads to this node + currentNode.VariableNames = variableNames + return currentNode, nil + } + + suffix := Suffix{} + var i int + if strings.HasPrefix(remaining, "/") { + remaining = remaining[1:] + suffix.Kind = SuffixKindConstant + suffix.Pattern = "/" + } else { + i = strings.IndexAny(remaining, "/{") + if i < 0 { + i = len(remaining) + } + if i > 0 { + // Constant string pattern + suffix.Kind = SuffixKindConstant + suffix.Pattern = remaining[:i] + remaining = remaining[i:] + } else if remaining[0] == '{' { + // This is probably a variable + suffix.Kind = SuffixKindVariable + + // Find variable name + i := strings.IndexByte(remaining, '}') + if i < 0 { + return nil, fmt.Errorf("Missing '}' in: %s", path) + } + variableName := strings.TrimSpace(remaining[1:i]) + remaining = remaining[i+1:] + + if options.SupportRegExp { + // See if it has regular expression + i = strings.IndexByte(variableName, '|') + if i >= 0 { + suffix.Kind = SuffixKindRegExp + suffix.Pattern = strings.TrimSpace(variableName[i+1:]) + variableName = strings.TrimSpace(variableName[:i]) + } + } + if suffix.Kind == SuffixKindVariable && options.SupportWildcard { + if strings.HasSuffix(variableName, "*") { + suffix.Kind = SuffixKindEverything + } + } + variableNames = append(variableNames, variableName) + } + } + + // Find existing matcher + for _, existing := range currentNode.Suffixes { + if EqualSuffix(existing, suffix) { + currentNode = existing.Node + continue loop + } + } + + // Compile regular expression + if suffix.Kind == SuffixKindRegExp { + regExp, err := regexp.Compile(suffix.Pattern) + if err != nil { + return nil, fmt.Errorf("Invalid regular expression in: %s", path) + } + suffix.regExp = regExp + } + + // Create new node + newNode := &Node{} + suffix.Node = newNode + currentNode.Suffixes = append(currentNode.Suffixes, suffix) + sort.Sort(currentNode.Suffixes) + currentNode = newNode + continue loop + } +} + +func (currentNode *Node) Match(path string) (*Node, []string) { + for strings.HasSuffix(path, "/") { + path = path[:len(path)-1] + } + variableValues := make([]string, 0, 8) + return currentNode.matchRemaining(path, false, variableValues) +} + +func (currentNode *Node) matchRemaining(remaining string, hasExtraSlash bool, paramValues []string) (*Node, []string) { + // Remove "/" from the beginning + // if len(remaining) > 0 && remaining[0] == '/' { + // remaining = remaining[1:] + // } + + // Check if this node matches + if len(remaining) == 0 && currentNode.Value != nil { + return currentNode, paramValues + } + + // See if any suffix matches + for _, suffix := range currentNode.Suffixes { + var resultNode *Node + var resultValues []string + switch suffix.Kind { + case SuffixKindConstant: + pattern := suffix.Pattern + if strings.HasPrefix(remaining, pattern) { + newRemaining := remaining[len(pattern):] + resultNode, resultValues = suffix.Node.matchRemaining(newRemaining, hasExtraSlash, paramValues) + } else if len(remaining) == 0 && pattern == "/" { + resultNode, resultValues = suffix.Node.matchRemaining(remaining, hasExtraSlash, paramValues) + } + case SuffixKindVariable: + i := strings.IndexByte(remaining, '/') + if i < 0 { + i = len(remaining) + } + newParamValues := append(paramValues, remaining[:i]) + newRemaining := remaining[i:] + resultNode, resultValues = suffix.Node.matchRemaining(newRemaining, hasExtraSlash, newParamValues) + case SuffixKindEverything: + newParamValues := append(paramValues, remaining) + resultNode, resultValues = suffix.Node, newParamValues + case SuffixKindRegExp: + i := strings.IndexByte(remaining, '/') + if i < 0 { + i = len(remaining) + } + paramValue := remaining[:i] + regExp := suffix.regExp + if regExp.MatchString(paramValue) { + matches := regExp.FindStringSubmatch(paramValue) + if len(matches) > 1 { + paramValue = matches[1] + } + newParamValues := append(paramValues, paramValue) + newRemaining := remaining[i:] + resultNode, resultValues = suffix.Node.matchRemaining(newRemaining, hasExtraSlash, newParamValues) + } + } + if resultNode != nil && resultNode.Value != nil { + // This suffix matched + return resultNode, resultValues + } + } + + // No suffix matched + return nil, nil +}