func (s *SpecValidator) validatePathParamPresence(fromPath, fromOperation []string) *Result { // Each defined operation path parameters must correspond to a named element in the API's path pattern. // (For example, you cannot have a path parameter named id for the following path /pets/{petId} but you must have a path parameter named petId.) res := new(Result) for _, l := range fromPath { var matched bool for _, r := range fromOperation { if l == "{"+r+"}" { matched = true break } } if !matched { res.Errors = append(res.Errors, errors.New(422, "path param %q has no parameter definition", l)) } } for _, p := range fromOperation { var matched bool for _, r := range fromPath { if "{"+p+"}" == r { matched = true break } } if !matched { res.AddErrors(errors.New(422, "path param %q is not present in the path", p)) } } return res }
func (s *SpecValidator) validateParameters() *Result { // each parameter should have a unique `name` and `type` combination // each operation should have only 1 parameter of type body // each api path should be non-verbatim (account for path param names) unique per method res := new(Result) for method, pi := range s.spec.Operations() { knownPaths := make(map[string]string) for path, op := range pi { segments, params := parsePath(path) knowns := make([]string, 0, len(segments)) for _, s := range segments { knowns = append(knowns, s) } var fromPath []string for _, i := range params { fromPath = append(fromPath, knowns[i]) knowns[i] = "!" } knownPath := strings.Join(knowns, "/") if orig, ok := knownPaths[knownPath]; ok { res.AddErrors(errors.New(422, "path %s overlaps with %s", path, orig)) } else { knownPaths[knownPath] = path } ptypes := make(map[string]map[string]struct{}) var firstBodyParam string var paramNames []string for _, pr := range op.Parameters { pnames, ok := ptypes[pr.In] if !ok { pnames = make(map[string]struct{}) ptypes[pr.In] = pnames } _, ok = pnames[pr.Name] if ok { res.AddErrors(errors.New(422, "duplicate parameter name %q for %q in operation %q", pr.Name, pr.In, op.ID)) } pnames[pr.Name] = struct{}{} } for _, pr := range s.spec.ParamsFor(method, path) { if pr.In == "body" { if firstBodyParam != "" { res.AddErrors(errors.New(422, "operation %q has more than 1 body param (accepted: %q, dropped: %q)", op.ID, firstBodyParam, pr.Name)) } firstBodyParam = pr.Name } if pr.In == "path" { paramNames = append(paramNames, pr.Name) } } res.Merge(s.validatePathParamPresence(fromPath, paramNames)) } } return res }
// Respond renders the response after doing some content negotiation func (c *Context) Respond(rw http.ResponseWriter, r *http.Request, produces []string, route *MatchedRoute, data interface{}) { offers := []string{c.api.DefaultProduces()} for _, mt := range produces { if mt != c.api.DefaultProduces() { offers = append(offers, mt) } } format := c.ResponseFormat(r, offers) rw.Header().Set(httpkit.HeaderContentType, format) if err, ok := data.(error); ok { if format == "" { rw.Header().Set(httpkit.HeaderContentType, httpkit.JSONMime) } if route == nil || route.Operation == nil { c.api.ServeErrorFor("")(rw, r, err) return } c.api.ServeErrorFor(route.Operation.ID)(rw, r, err) return } if route == nil || route.Operation == nil { rw.WriteHeader(200) if r.Method == "HEAD" { return } producers := c.api.ProducersFor(offers) prod, ok := producers[format] if !ok { panic(errors.New(http.StatusInternalServerError, "can't find a producer for "+format)) } if err := prod.Produce(rw, data); err != nil { panic(err) // let the recovery middleware deal with this } return } if _, code, ok := route.Operation.SuccessResponse(); ok { rw.WriteHeader(code) if code == 201 || code == 204 || r.Method == "HEAD" { return } producers := route.Producers prod, ok := producers[format] if !ok { panic(errors.New(http.StatusInternalServerError, "can't find a producer for "+format)) } if err := prod.Produce(rw, data); err != nil { panic(err) // let the recovery middleware deal with this } return } c.api.ServeErrorFor(route.Operation.ID)(rw, r, errors.New(http.StatusInternalServerError, "can't produce response")) }
func (s *SpecValidator) validateItems() *Result { // validate parameter, items, schema and response objects for presence of item if type is array res := new(Result) // TODO: implement support for lookups of refs for method, pi := range s.spec.Operations() { for path, op := range pi { for _, param := range s.spec.ParamsFor(method, path) { if param.TypeName() == "array" && param.ItemsTypeName() == "" { res.AddErrors(errors.New(422, "param %q for %q is a collection without an element type", param.Name, op.ID)) continue } if param.In != "body" { if param.Items != nil { items := param.Items for items.TypeName() == "array" { if items.ItemsTypeName() == "" { res.AddErrors(errors.New(422, "param %q for %q is a collection without an element type", param.Name, op.ID)) break } items = items.Items } } } else { if err := s.validateSchemaItems(*param.Schema, fmt.Sprintf("body param %q", param.Name), op.ID); err != nil { res.AddErrors(err) } } } var responses []spec.Response if op.Responses != nil { if op.Responses.Default != nil { responses = append(responses, *op.Responses.Default) } for _, v := range op.Responses.StatusCodeResponses { responses = append(responses, v) } } for _, resp := range responses { for hn, hv := range resp.Headers { if hv.TypeName() == "array" && hv.ItemsTypeName() == "" { res.AddErrors(errors.New(422, "header %q for %q is a collection without an element type", hn, op.ID)) } } if resp.Schema != nil { if err := s.validateSchemaItems(*resp.Schema, "response body", op.ID); err != nil { res.AddErrors(err) } } } } } return res }
// APIKeyAuth creates an authenticator that uses a token for authorization. // This token can be obtained from either a header or a query string func APIKeyAuth(name, in string, authenticate TokenAuthentication) httpkit.Authenticator { inl := strings.ToLower(in) if inl != "query" && inl != "header" { // panic because this is most likely a typo panic(errors.New(500, "api key auth: in value needs to be either \"query\" or \"header\".")) } var getToken func(*http.Request) string switch inl { case "header": getToken = func(r *http.Request) string { return r.Header.Get(name) } case "query": getToken = func(r *http.Request) string { return r.URL.Query().Get(name) } } return httpAuthenticator(func(r *http.Request) (bool, interface{}, error) { token := getToken(r) if token == "" { return false, nil, nil } p, err := authenticate(token) return true, p, err }) }
func (s *SpecValidator) validateRequiredDefinitions() *Result { // Each definition property listed in the required array must be defined in the properties of the model res := new(Result) for d, v := range s.spec.Spec().Definitions { REQUIRED: for _, pn := range v.Required { if _, ok := v.Properties[pn]; ok { continue } for pp := range v.PatternProperties { re := regexp.MustCompile(pp) if re.MatchString(pn) { continue REQUIRED } } if v.AdditionalProperties != nil { if v.AdditionalProperties.Allows { continue } if v.AdditionalProperties.Schema != nil { continue } } res.AddErrors(errors.New(422, "%q is present in required but not defined as property in defintion %q", pn, d)) } } return res }
func (s *schemaSliceValidator) Validate(data interface{}) *Result { result := new(Result) if data == nil { return result } val := reflect.ValueOf(data) size := val.Len() if s.Items != nil && s.Items.Schema != nil { validator := NewSchemaValidator(s.Items.Schema, s.Root, s.Path, s.KnownFormats) for i := 0; i < size; i++ { validator.SetPath(fmt.Sprintf("%s.%d", s.Path, i)) value := val.Index(i) result.Merge(validator.Validate(value.Interface())) } } itemsSize := int64(0) if s.Items != nil && len(s.Items.Schemas) > 0 { itemsSize = int64(len(s.Items.Schemas)) for i := int64(0); i < itemsSize; i++ { validator := NewSchemaValidator(&s.Items.Schemas[i], s.Root, fmt.Sprintf("%s.%d", s.Path, i), s.KnownFormats) result.Merge(validator.Validate(val.Index(int(i)).Interface())) } } if s.AdditionalItems != nil && itemsSize < int64(size) { if s.Items != nil && len(s.Items.Schemas) > 0 && !s.AdditionalItems.Allows { result.AddErrors(errors.New(422, "array doesn't allow for additional items")) } if s.AdditionalItems.Schema != nil { for i := itemsSize; i < (int64(size)-itemsSize)+1; i++ { validator := NewSchemaValidator(s.AdditionalItems.Schema, s.Root, fmt.Sprintf("%s.%d", s.Path, i), s.KnownFormats) result.Merge(validator.Validate(val.Index(int(i)).Interface())) } } } if s.MinItems != nil { if err := validate.MinItems(s.Path, s.In, int64(size), *s.MinItems); err != nil { result.AddErrors(err) } } if s.MaxItems != nil { if err := validate.MaxItems(s.Path, s.In, int64(size), *s.MaxItems); err != nil { result.AddErrors(err) } } if s.UniqueItems { if err := validate.UniqueItems(s.Path, s.In, val.Interface()); err != nil { result.AddErrors(err) } } result.Inc() return result }
// Bind perform the databinding and validation func (o *untypedRequestBinder) Bind(request *http.Request, routeParams RouteParams, consumer httpkit.Consumer, data interface{}) error { val := reflect.Indirect(reflect.ValueOf(data)) isMap := val.Kind() == reflect.Map var result []error for fieldName, param := range o.Parameters { binder := o.paramBinders[fieldName] var target reflect.Value if !isMap { binder.Name = fieldName target = val.FieldByName(fieldName) } if isMap { tpe := binder.Type() if tpe == nil { if param.Schema.Type.Contains("array") { tpe = reflect.TypeOf([]interface{}{}) } else { tpe = reflect.TypeOf(map[string]interface{}{}) } } target = reflect.Indirect(reflect.New(tpe)) } if !target.IsValid() { result = append(result, errors.New(500, "parameter name %q is an unknown field", binder.Name)) continue } if err := binder.Bind(request, routeParams, consumer, target); err != nil { result = append(result, err) continue } if binder.validator != nil { rr := binder.validator.Validate(target.Interface()) if rr != nil && rr.HasErrors() { result = append(result, rr.AsError()) } } if isMap { val.SetMapIndex(reflect.ValueOf(param.Name), target) } } if len(result) > 0 { return errors.CompositeValidationError(result...) } return nil }
func (s *SpecValidator) validateSchemaItems(schema spec.Schema, prefix, opID string) error { if !schema.Type.Contains("array") { return nil } if schema.Items == nil || schema.Items.Len() == 0 { return errors.New(422, "%s for %q is a collection without an element type", prefix, opID) } schemas := schema.Items.Schemas if schema.Items.Schema != nil { schemas = []spec.Schema{*schema.Items.Schema} } for _, sch := range schemas { if err := s.validateSchemaItems(sch, prefix, opID); err != nil { return err } } return nil }
// Validate validates the swagger spec func (s *SpecValidator) Validate(data interface{}) (errs *Result, warnings *Result) { var sd *spec.Document switch v := data.(type) { case *spec.Document: sd = v } if sd == nil { errs = sErr(errors.New(500, "spec validator can only validate spec.Document objects")) return } s.spec = sd errs = new(Result) warnings = new(Result) schv := NewSchemaValidator(s.schema, nil, "", s.KnownFormats) errs.Merge(schv.Validate(sd.Spec())) // error - if errs.HasErrors() { return // no point in continuing } errs.Merge(s.validateReferencesValid()) // error if errs.HasErrors() { return // no point in continuing } errs.Merge(s.validateParameters()) // error - errs.Merge(s.validateItems()) // error - errs.Merge(s.validateRequiredDefinitions()) // error - errs.Merge(s.validateDefaultValueValidAgainstSchema()) // error warnings.Merge(s.validateUniqueSecurityScopes()) // warning warnings.Merge(s.validateUniqueScopesSecurityDefinitions()) // warning warnings.Merge(s.validateReferenced()) // warning return }
func TestOperationExecutor(t *testing.T) { spec, api := petstore.NewAPI(t) api.RegisterOperation("getAllPets", httpkit.OperationHandlerFunc(func(params interface{}) (interface{}, error) { return []interface{}{ map[string]interface{}{"id": 1, "name": "a dog"}, }, nil })) context := NewContext(spec, api, nil) context.router = DefaultRouter(spec, context.api) mw := newOperationExecutor(context) recorder := httptest.NewRecorder() request, _ := http.NewRequest("GET", "/pets", nil) request.Header.Add("Accept", "application/json") request.SetBasicAuth("admin", "admin") mw.ServeHTTP(recorder, request) assert.Equal(t, 200, recorder.Code) assert.Equal(t, `[{"id":1,"name":"a dog"}]`+"\n", recorder.Body.String()) spec, api = petstore.NewAPI(t) api.RegisterOperation("getAllPets", httpkit.OperationHandlerFunc(func(params interface{}) (interface{}, error) { return nil, errors.New(422, "expected") })) context = NewContext(spec, api, nil) context.router = DefaultRouter(spec, context.api) mw = newOperationExecutor(context) recorder = httptest.NewRecorder() request, _ = http.NewRequest("GET", "/pets", nil) request.Header.Add("Accept", "application/json") request.SetBasicAuth("admin", "admin") mw.ServeHTTP(recorder, request) assert.Equal(t, 422, recorder.Code) assert.Equal(t, `{"code":422,"message":"expected"}`, recorder.Body.String()) }
func (s *schemaPropsValidator) Validate(data interface{}) *Result { mainResult := new(Result) if len(s.anyOfValidators) > 0 { var bestFailures *Result succeededOnce := false for _, anyOfSchema := range s.anyOfValidators { result := anyOfSchema.Validate(data) if result.IsValid() { bestFailures = nil succeededOnce = true break } if bestFailures == nil || result.MatchCount > bestFailures.MatchCount { bestFailures = result } } if !succeededOnce { mainResult.AddErrors(errors.New(422, "must validate at least one schema (anyOf)")) } if bestFailures != nil { mainResult.Merge(bestFailures) } } if len(s.oneOfValidators) > 0 { var bestFailures *Result validated := 0 for _, oneOfSchema := range s.oneOfValidators { result := oneOfSchema.Validate(data) if result.IsValid() { validated++ bestFailures = nil continue } if validated == 0 && (bestFailures == nil || result.MatchCount > bestFailures.MatchCount) { bestFailures = result } } if validated != 1 { mainResult.AddErrors(errors.New(422, "must validate one and only one schema (oneOf)")) if bestFailures != nil { mainResult.Merge(bestFailures) } } } if len(s.allOfValidators) > 0 { validated := 0 for _, allOfSchema := range s.allOfValidators { result := allOfSchema.Validate(data) if result.IsValid() { validated++ } mainResult.Merge(result) } if validated != len(s.allOfValidators) { mainResult.AddErrors(errors.New(422, "must validate all the schemas (allOf)")) } } if s.notValidator != nil { result := s.notValidator.Validate(data) if result.IsValid() { mainResult.AddErrors(errors.New(422, "must not validate the schema (not)")) } } if s.Dependencies != nil && len(s.Dependencies) > 0 && reflect.TypeOf(data).Kind() == reflect.Map { val := data.(map[string]interface{}) for key := range val { if dep, ok := s.Dependencies[key]; ok { if dep.Schema != nil { mainResult.Merge(NewSchemaValidator(dep.Schema, s.Root, s.Path+"."+key, s.KnownFormats).Validate(data)) continue } if len(dep.Property) > 0 { for _, depKey := range dep.Property { if _, ok := val[depKey]; !ok { mainResult.AddErrors(errors.New(422, "has a dependency on %s", depKey)) } } } } } } mainResult.Inc() return mainResult }
func (p *untypedParamBinder) Bind(request *http.Request, routeParams RouteParams, consumer httpkit.Consumer, target reflect.Value) error { // fmt.Println("binding", p.name, "as", p.Type()) switch p.parameter.In { case "query": data, custom, err := p.readValue(request.URL.Query(), target) if err != nil { return err } if custom { return nil } return p.bindValue(data, target) case "header": data, custom, err := p.readValue(request.Header, target) if err != nil { return err } if custom { return nil } return p.bindValue(data, target) case "path": data, custom, err := p.readValue(routeParams, target) if err != nil { return err } if custom { return nil } return p.bindValue(data, target) case "formData": var err error var mt string mt, _, e := httpkit.ContentType(request.Header) if e != nil { // because of the interface conversion go thinks the error is not nil // so we first check for nil and then set the err var if it's not nil err = e } if err != nil { return errors.InvalidContentType("", []string{"multipart/form-data", "application/x-www-form-urlencoded"}) } if mt != "multipart/form-data" && mt != "application/x-www-form-urlencoded" { return errors.InvalidContentType(mt, []string{"multipart/form-data", "application/x-www-form-urlencoded"}) } if mt == "multipart/form-data" { if err := request.ParseMultipartForm(defaultMaxMemory); err != nil { return errors.NewParseError(p.Name, p.parameter.In, "", err) } } if err := request.ParseForm(); err != nil { return errors.NewParseError(p.Name, p.parameter.In, "", err) } if p.parameter.Type == "file" { file, header, err := request.FormFile(p.parameter.Name) if err != nil { return errors.NewParseError(p.Name, p.parameter.In, "", err) } target.Set(reflect.ValueOf(httpkit.File{Data: file, Header: header})) return nil } if request.MultipartForm != nil { data, custom, err := p.readValue(url.Values(request.MultipartForm.Value), target) if err != nil { return err } if custom { return nil } return p.bindValue(data, target) } data, custom, err := p.readValue(url.Values(request.PostForm), target) if err != nil { return err } if custom { return nil } return p.bindValue(data, target) case "body": newValue := reflect.New(target.Type()) if err := consumer.Consume(request.Body, newValue.Interface()); err != nil { if err == io.EOF && p.parameter.Default != nil { target.Set(reflect.ValueOf(p.parameter.Default)) return nil } tpe := p.parameter.Type if p.parameter.Format != "" { tpe = p.parameter.Format } return errors.InvalidType(p.Name, p.parameter.In, tpe, nil) } target.Set(reflect.Indirect(newValue)) return nil default: return errors.New(500, fmt.Sprintf("invalid parameter location %q", p.parameter.In)) } }