Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: services/httpd: parse correctly Accept header with extra test cases #14485

Merged
merged 1 commit into from
Mar 12, 2020
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
80 changes: 80 additions & 0 deletions services/httpd/accept.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
// This file is an adaptation of https://github.com/markusthoemmes/goautoneg.
// The copyright and license header are reproduced below.
//
// Copyright [yyyy] [name of copyright owner]
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// http://www.apache.org/licenses/LICENSE-2.0

package httpd

import (
"mime"
"sort"
"strconv"
"strings"
)

// accept is a structure to represent a clause in an HTTP Accept Header.
type accept struct {
Type, SubType string
Q float64
Params map[string]string
}

// parseAccept parses the given string as an Accept header as defined in
// https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.1.
// Some rules are only loosely applied and might not be as strict as defined in the RFC.
func parseAccept(headers []string) []accept {
var res []accept
for _, header := range headers {
parts := strings.Split(header, ",")
for _, part := range parts {
mt, params, err := mime.ParseMediaType(part)
if err != nil {
continue
}

accept := accept{
Q: 1.0, // "[...] The default value is q=1"
Params: params,
}

// A media-type is defined as
// "*/*" | ( type "/" "*" ) | ( type "/" subtype )
types := strings.Split(mt, "/")
switch {
// This case is not defined in the spec keep it to mimic the original code.
case len(types) == 1 && types[0] == "*":
accept.Type = "*"
accept.SubType = "*"
case len(types) == 2:
accept.Type = types[0]
accept.SubType = types[1]
default:
continue
}

if qVal, ok := params["q"]; ok {
// A parsing failure will set Q to 0.
accept.Q, _ = strconv.ParseFloat(qVal, 64)
delete(params, "q")
}

res = append(res, accept)
}
}
sort.SliceStable(res, func(i, j int) bool {
return res[i].Q > res[j].Q
})
return res
}
54 changes: 42 additions & 12 deletions services/httpd/response_writer.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,27 +20,57 @@ type ResponseWriter interface {
http.ResponseWriter
}

type formatter interface {
WriteResponse(w io.Writer, resp Response) error
}

type supportedContentType struct {
full string
acceptType string
acceptSubType string
formatter func(pretty bool) formatter
}

var (
csvFormatFactory = func(pretty bool) formatter { return &csvFormatter{statementID: -1} }
msgpackFormatFactory = func(pretty bool) formatter { return &msgpackFormatter{} }
jsonFormatFactory = func(pretty bool) formatter { return &jsonFormatter{Pretty: pretty} }

contentTypes = []supportedContentType{
{full: "application/json", acceptType: "application", acceptSubType: "json", formatter: jsonFormatFactory},
{full: "application/csv", acceptType: "application", acceptSubType: "csv", formatter: csvFormatFactory},
{full: "text/csv", acceptType: "text", acceptSubType: "csv", formatter: csvFormatFactory},
{full: "application/x-msgpack", acceptType: "application", acceptSubType: "x-msgpack", formatter: msgpackFormatFactory},
}
defaultContentType = contentTypes[0]
)

// NewResponseWriter creates a new ResponseWriter based on the Accept header
// in the request that wraps the ResponseWriter.
func NewResponseWriter(w http.ResponseWriter, r *http.Request) ResponseWriter {
pretty := r.URL.Query().Get("pretty") == "true"
rw := &responseWriter{ResponseWriter: w}
switch r.Header.Get("Accept") {
case "application/csv", "text/csv":
w.Header().Add("Content-Type", "text/csv")
rw.formatter = &csvFormatter{statementID: -1}
case "application/x-msgpack":
w.Header().Add("Content-Type", "application/x-msgpack")
rw.formatter = &msgpackFormatter{}
case "application/json":
fallthrough
default:
w.Header().Add("Content-Type", "application/json")
rw.formatter = &jsonFormatter{Pretty: pretty}

acceptHeaders := parseAccept(r.Header["Accept"])
for _, accept := range acceptHeaders {
for _, ct := range contentTypes {
if match(accept, ct) {
w.Header().Add("Content-Type", ct.full)
rw.formatter = ct.formatter(pretty)
return rw
}
}
}
w.Header().Add("Content-Type", defaultContentType.full)
rw.formatter = defaultContentType.formatter(pretty)
return rw
}

func match(ah accept, sct supportedContentType) bool {
return (ah.Type == "*" || ah.Type == sct.acceptType) &&
(ah.SubType == "*" || ah.SubType == sct.acceptSubType)
}

// WriteError is a convenience function for writing an error response to the ResponseWriter.
func WriteError(w ResponseWriter, err error) (int, error) {
return w.WriteResponse(Response{Err: err})
Expand Down
Loading