Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 44 additions & 10 deletions middleware/validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import (

"github.com/go-openapi/errors"
"github.com/go-openapi/runtime"
"github.com/go-openapi/swag/stringutils"
)

type validation struct {
Expand All @@ -22,27 +21,62 @@ type validation struct {
}

// ContentType validates the content type of a request.
//
// An allowed entry may carry MIME type parameters (e.g. "text/plain;charset=utf-8").
// In that case every parameter the client sends must be present on the allowed entry
// with the same value; the allowed entry may carry additional parameters the client
// omits. An allowed entry without parameters accepts any client parameters.
// "*/*" and "type/*" wildcards are matched on the bare type only.
func validateContentType(allowed []string, actual string) error {
if len(allowed) == 0 {
return nil
}
mt, _, err := mime.ParseMediaType(actual)
actualType, actualParams, err := mime.ParseMediaType(actual)
if err != nil {
return errors.InvalidContentType(actual, allowed)
}
if stringutils.ContainsStringsCI(allowed, mt) {
return nil
}
if stringutils.ContainsStringsCI(allowed, "*/*") {
return nil
typeWildcard := ""
if slash := strings.IndexByte(actualType, '/'); slash > 0 {
typeWildcard = actualType[:slash] + "/*"
}
parts := strings.Split(actual, "/")
if len(parts) == 2 && stringutils.ContainsStringsCI(allowed, parts[0]+"/*") {
return nil
for _, a := range allowed {
if strings.EqualFold(a, "*/*") {
return nil
}
if typeWildcard != "" && strings.EqualFold(a, typeWildcard) {
return nil
}
if mediaTypeMatches(a, actualType, actualParams) {
return nil
}
}
return errors.InvalidContentType(actual, allowed)
}

// mediaTypeMatches reports whether the actual client media type satisfies the
// server-side allowed media type, with parameter-aware comparison.
func mediaTypeMatches(allowed, actualType string, actualParams map[string]string) bool {
allowedType, allowedParams, err := mime.ParseMediaType(allowed)
if err != nil {
// Fall back to a case-insensitive bare match if the configured value
// can't be parsed as a media type.
return strings.EqualFold(allowed, actualType)
}
if !strings.EqualFold(allowedType, actualType) {
return false
}
if len(allowedParams) == 0 {
return true
}
for k, v := range actualParams {
sv, ok := allowedParams[k]
if !ok || !strings.EqualFold(sv, v) {
return false
}
}
return true
}

func validateRequest(ctx *Context, request *http.Request, route *MatchedRoute) *validation {
validate := &validation{
context: ctx,
Expand Down
21 changes: 21 additions & 0 deletions middleware/validation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,11 @@ func TestResponseFormatValidation(t *testing.T) {
}

func TestValidateContentType(t *testing.T) {
const (
textPlain = "text/plain"
textPlainUTF8 = "text/plain;charset=utf-8"
textPlainParamSrv = "text/plain; charset=utf-8"
)
data := []struct {
hdr string
allowed []string
Expand All @@ -152,6 +157,22 @@ func TestValidateContentType(t *testing.T) {
{"application/json;char*", []string{"application/json"}, errors.InvalidContentType("application/json;char*", []string{"application/json"})},
{"application/octet-stream", []string{"image/jpeg", "application/*"}, nil},
{"image/png", []string{"*/*", "application/json"}, nil},
// regression for https://github.com/go-openapi/runtime/issues/136:
// allowed entries with MIME parameters should not block matching clients.
// (1) client sends bare type, server allows type with params -> accept
{textPlain, []string{textPlainParamSrv}, nil},
// (2) client sends a different param than server -> reject
{"text/plain;blah=true", []string{textPlainParamSrv},
errors.InvalidContentType("text/plain;blah=true", []string{textPlainParamSrv})},
// (3) client sends params, server allows bare type -> accept
{textPlainUTF8, []string{textPlain}, nil},
// (4) exact param match -> accept
{textPlainUTF8, []string{textPlainUTF8}, nil},
// param value compare is case-insensitive (charset is case-insensitive)
{"text/plain;charset=UTF-8", []string{textPlainUTF8}, nil},
// (5) conflicting param values -> reject
{textPlainUTF8, []string{"text/plain;charset=ascii"},
errors.InvalidContentType(textPlainUTF8, []string{"text/plain;charset=ascii"})},
}

for _, v := range data {
Expand Down
Loading