diff --git a/httpbin/handlers.go b/httpbin/handlers.go index 4d8c7cfcb6fa2e7d47b1e958a9fc551370f25495..aef9ee4a89b90d1a7db206b182c491ce1745cba6 100644 --- a/httpbin/handlers.go +++ b/httpbin/handlers.go @@ -54,14 +54,14 @@ func (h *HTTPBin) Get(w http.ResponseWriter, r *http.Request) { Origin: getClientIP(r), URL: getURL(r).String(), } - body, _ := json.Marshal(resp) + body, _ := jsonMarshalNoEscape(resp) writeJSON(w, body, http.StatusOK) } // Anything returns anything that is passed to request. func (h *HTTPBin) Anything(w http.ResponseWriter, r *http.Request) { switch r.Method { - case "GET", "HEAD": + case "HEAD": h.Get(w, r) default: h.RequestWithBody(w, r) @@ -83,7 +83,7 @@ func (h *HTTPBin) RequestWithBody(w http.ResponseWriter, r *http.Request) { return } - body, _ := json.Marshal(resp) + body, _ := jsonMarshalNoEscape(resp) writeJSON(w, body, http.StatusOK) } @@ -94,7 +94,7 @@ func (h *HTTPBin) Gzip(w http.ResponseWriter, r *http.Request) { Origin: getClientIP(r), Gzipped: true, } - body, _ := json.Marshal(resp) + body, _ := jsonMarshalNoEscape(resp) buf := &bytes.Buffer{} gzw := gzip.NewWriter(buf) @@ -114,7 +114,7 @@ func (h *HTTPBin) Deflate(w http.ResponseWriter, r *http.Request) { Origin: getClientIP(r), Deflated: true, } - body, _ := json.Marshal(resp) + body, _ := jsonMarshalNoEscape(resp) buf := &bytes.Buffer{} w2 := zlib.NewWriter(buf) @@ -129,7 +129,7 @@ func (h *HTTPBin) Deflate(w http.ResponseWriter, r *http.Request) { // IP echoes the IP address of the incoming request func (h *HTTPBin) IP(w http.ResponseWriter, r *http.Request) { - body, _ := json.Marshal(&ipResponse{ + body, _ := jsonMarshalNoEscape(&ipResponse{ Origin: getClientIP(r), }) writeJSON(w, body, http.StatusOK) @@ -137,7 +137,7 @@ func (h *HTTPBin) IP(w http.ResponseWriter, r *http.Request) { // UserAgent echoes the incoming User-Agent header func (h *HTTPBin) UserAgent(w http.ResponseWriter, r *http.Request) { - body, _ := json.Marshal(&userAgentResponse{ + body, _ := jsonMarshalNoEscape(&userAgentResponse{ UserAgent: r.Header.Get("User-Agent"), }) writeJSON(w, body, http.StatusOK) @@ -145,7 +145,7 @@ func (h *HTTPBin) UserAgent(w http.ResponseWriter, r *http.Request) { // Headers echoes the incoming request headers func (h *HTTPBin) Headers(w http.ResponseWriter, r *http.Request) { - body, _ := json.Marshal(&headersResponse{ + body, _ := jsonMarshalNoEscape(&headersResponse{ Headers: getRequestHeaders(r), }) writeJSON(w, body, http.StatusOK) @@ -175,7 +175,7 @@ func (h *HTTPBin) Status(w http.ResponseWriter, r *http.Request) { "Location": "/redirect/1", }, } - notAcceptableBody, _ := json.Marshal(map[string]interface{}{ + notAcceptableBody, _ := jsonMarshalNoEscape(map[string]interface{}{ "message": "Client did not request a supported media type", "accept": acceptedMediaTypes, }) @@ -302,7 +302,7 @@ func (h *HTTPBin) ResponseHeaders(w http.ResponseWriter, r *http.Request) { w.Header().Add(k, v) } } - body, _ := json.Marshal(args) + body, _ := jsonMarshalNoEscape(args) if contentType := w.Header().Get("Content-Type"); contentType == "" { w.Header().Set("Content-Type", jsonContentType) } @@ -400,7 +400,7 @@ func (h *HTTPBin) Cookies(w http.ResponseWriter, r *http.Request) { for _, c := range r.Cookies() { resp[c.Name] = c.Value } - body, _ := json.Marshal(resp) + body, _ := jsonMarshalNoEscape(resp) writeJSON(w, body, http.StatusOK) } @@ -455,7 +455,7 @@ func (h *HTTPBin) BasicAuth(w http.ResponseWriter, r *http.Request) { w.Header().Set("WWW-Authenticate", `Basic realm="Fake Realm"`) } - body, _ := json.Marshal(&authResponse{ + body, _ := jsonMarshalNoEscape(&authResponse{ Authorized: authorized, User: givenUser, }) @@ -481,7 +481,7 @@ func (h *HTTPBin) HiddenBasicAuth(w http.ResponseWriter, r *http.Request) { return } - body, _ := json.Marshal(&authResponse{ + body, _ := jsonMarshalNoEscape(&authResponse{ Authorized: authorized, User: givenUser, }) @@ -517,9 +517,8 @@ func (h *HTTPBin) Stream(w http.ResponseWriter, r *http.Request) { f := w.(http.Flusher) for i := 0; i < n; i++ { resp.ID = i - line, _ := json.Marshal(resp) + line, _ := jsonMarshalNoEscape(resp) w.Write(line) - w.Write([]byte("\n")) f.Flush() } } @@ -728,7 +727,7 @@ func (h *HTTPBin) ETag(w http.ResponseWriter, r *http.Request) { Origin: getClientIP(r), URL: getURL(r).String(), } - body, _ := json.Marshal(resp) + body, _ := jsonMarshalNoEscape(resp) // Let http.ServeContent deal with If-None-Match and If-Match headers: // https://golang.org/pkg/net/http/#ServeContent @@ -954,7 +953,7 @@ func (h *HTTPBin) DigestAuth(w http.ResponseWriter, r *http.Request) { return } - resp, _ := json.Marshal(&authResponse{ + resp, _ := jsonMarshalNoEscape(&authResponse{ Authorized: true, User: user, }) @@ -963,7 +962,7 @@ func (h *HTTPBin) DigestAuth(w http.ResponseWriter, r *http.Request) { // UUID - responds with a generated UUID func (h *HTTPBin) UUID(w http.ResponseWriter, r *http.Request) { - resp, _ := json.Marshal(&uuidResponse{ + resp, _ := jsonMarshalNoEscape(&uuidResponse{ UUID: uuidv4(), }) writeJSON(w, resp, http.StatusOK) @@ -1007,7 +1006,7 @@ func (h *HTTPBin) Bearer(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusUnauthorized) return } - body, _ := json.Marshal(&bearerResponse{ + body, _ := jsonMarshalNoEscape(&bearerResponse{ Authenticated: true, Token: tokenFields[1], }) @@ -1016,8 +1015,18 @@ func (h *HTTPBin) Bearer(w http.ResponseWriter, r *http.Request) { // Hostname - returns the hostname. func (h *HTTPBin) Hostname(w http.ResponseWriter, r *http.Request) { - body, _ := json.Marshal(hostnameResponse{ + body, _ := jsonMarshalNoEscape(hostnameResponse{ Hostname: h.hostname, }) writeJSON(w, body, http.StatusOK) } + +// json.Marshal escapes HTML in strings while httpbin does not, so +// we need to set up the encoder manually to reproduce that behavior. +func jsonMarshalNoEscape(value interface{}) ([]byte, error) { + buffer := &bytes.Buffer{} + encoder := json.NewEncoder(buffer) + encoder.SetEscapeHTML(false) + err := encoder.Encode(value) + return buffer.Bytes(), err +} diff --git a/httpbin/handlers_test.go b/httpbin/handlers_test.go index 89503c9146be286d7a41229af57725e2176d8996..366bccdc1a3123caba911258c1fa6834e0305ea7 100644 --- a/httpbin/handlers_test.go +++ b/httpbin/handlers_test.go @@ -480,6 +480,7 @@ func TestAnything(t *testing.T) { t.Parallel() var ( verbsWithReqBodies = []string{ + "GET", "DELETE", "PATCH", "POST", @@ -494,10 +495,6 @@ func TestAnything(t *testing.T) { for _, verb := range verbsWithReqBodies { testRequestWithBody(t, verb, path) } - // also test GET requests for each path - t.Run("GET "+path, func(t *testing.T) { - testRequestWithoutBody(t, path, nil, nil, http.StatusOK) - }) } } @@ -527,6 +524,7 @@ func testRequestWithBody(t *testing.T, verb, path string) { testRequestWithBodyInvalidFormEncodedBody, testRequestWithBodyInvalidJSON, testRequestWithBodyInvalidMultiPartBody, + testRequestWithBodyHTML, testRequestWithBodyJSON, testRequestWithBodyMultiPartBody, testRequestWithBodyQueryParams, @@ -623,6 +621,26 @@ func testRequestWithBodyFormEncodedBody(t *testing.T, verb, path string) { } } +func testRequestWithBodyHTML(t *testing.T, verb, path string) { + data := "<html><body><h1>hello world</h1></body></html>" + + r, _ := http.NewRequest(verb, path, strings.NewReader(data)) + r.Header.Set("Content-Type", htmlContentType) + w := httptest.NewRecorder() + handler.ServeHTTP(w, r) + + assertStatusCode(t, w, http.StatusOK) + assertContentType(t, w, jsonContentType) + + // We do not use json.Unmarshal here which would unescape any escaped characters. + // For httpbin compatibility, we need to verify the data is returned as-is without + // escaping. + respBody := w.Body.String() + if !strings.Contains(respBody, data) { + t.Fatalf("response data mismatch, %#v != %#v", respBody, data) + } +} + func testRequestWithBodyFormEncodedBodyNoContentType(t *testing.T, verb, path string) { params := url.Values{} params.Set("foo", "foo") diff --git a/httpbin/helpers.go b/httpbin/helpers.go index ef48f60ff7391ffdf02fdde0ee51976f5b506873..48c18c38b3020f7aaf20fb5cd6dbe65be6214dce 100644 --- a/httpbin/helpers.go +++ b/httpbin/helpers.go @@ -123,12 +123,13 @@ func parseBody(w http.ResponseWriter, r *http.Request, resp *bodyResponse) error ct := r.Header.Get("Content-Type") switch { case strings.HasPrefix(ct, "application/x-www-form-urlencoded"): - // r.ParseForm() does not populate r.PostForm for DELETE requests, but + // r.ParseForm() does not populate r.PostForm for DELETE or GET requests, but // we need it to for compatibility with the httpbin implementation, so // we trick it with this ugly hack. - if r.Method == http.MethodDelete { + if r.Method == http.MethodDelete || r.Method == http.MethodGet { + originalMethod := r.Method r.Method = http.MethodPost - defer func() { r.Method = http.MethodDelete }() + defer func() { r.Method = originalMethod }() } if err := r.ParseForm(); err != nil { return err