diff --git a/httpbin/digest/digest.go b/httpbin/digest/digest.go index f679382277a7c7db1eec2ad918c87dca3ca0b6c1..7497b0f0998714a8f71167eb630bebe6c6b54ffd 100644 --- a/httpbin/digest/digest.go +++ b/httpbin/digest/digest.go @@ -92,15 +92,15 @@ type authorization struct { // parseAuthorizationHeader parses an Authorization header into an // Authorization struct, given a an authorization header like: // -// Authorization: Digest username="Mufasa", -// realm="testrealm@host.com", -// nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093", -// uri="/dir/index.html", -// qop=auth, -// nc=00000001, -// cnonce="0a4f113b", -// response="6629fae49393a05397450978507c4ef1", -// opaque="5ccc069c403ebaf9f0171e9517f40e41" +// Authorization: Digest username="Mufasa", +// realm="testrealm@host.com", +// nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093", +// uri="/dir/index.html", +// qop=auth, +// nc=00000001, +// cnonce="0a4f113b", +// response="6629fae49393a05397450978507c4ef1", +// opaque="5ccc069c403ebaf9f0171e9517f40e41" // // If the given value does not contain a Digest authorization header, or is in // some other way malformed, nil is returned. @@ -174,7 +174,7 @@ func hash(data []byte, algorithm digestAlgorithm) string { // makeHA1 returns the HA1 hash, where // -// HA1 = H(A1) = H(username:realm:password) +// HA1 = H(A1) = H(username:realm:password) // // and H is one of MD5 or SHA256. func makeHA1(realm, username, password string, algorithm digestAlgorithm) string { @@ -184,7 +184,7 @@ func makeHA1(realm, username, password string, algorithm digestAlgorithm) string // makeHA2 returns the HA2 hash, where // -// HA2 = H(A2) = H(method:digestURI) +// HA2 = H(A2) = H(method:digestURI) // // and H is one of MD5 or SHA256. func makeHA2(auth *authorization, method, uri string) string { @@ -195,11 +195,11 @@ func makeHA2(auth *authorization, method, uri string) string { // Response calculates the correct digest auth response. If the qop directive's // value is "auth" or "auth-int" , then compute the response as // -// RESPONSE = H(HA1:nonce:nonceCount:clientNonce:qop:HA2) +// RESPONSE = H(HA1:nonce:nonceCount:clientNonce:qop:HA2) // // and if the qop directive is unspecified, then compute the response as // -// RESPONSE = H(HA1:nonce:HA2) +// RESPONSE = H(HA1:nonce:HA2) // // where H is one of MD5 or SHA256. func response(auth *authorization, password, method, uri string) string { diff --git a/httpbin/handlers.go b/httpbin/handlers.go index fd5f7c0f0c951d812f2aaf6125289216eac4aa0b..4d8c7cfcb6fa2e7d47b1e958a9fc551370f25495 100644 --- a/httpbin/handlers.go +++ b/httpbin/handlers.go @@ -48,7 +48,7 @@ func (h *HTTPBin) UTF8(w http.ResponseWriter, r *http.Request) { // Get handles HTTP GET requests func (h *HTTPBin) Get(w http.ResponseWriter, r *http.Request) { - resp := &getResponse{ + resp := &noBodyResponse{ Args: r.URL.Query(), Headers: getRequestHeaders(r), Origin: getClientIP(r), @@ -58,6 +58,16 @@ func (h *HTTPBin) Get(w http.ResponseWriter, r *http.Request) { 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": + h.Get(w, r) + default: + h.RequestWithBody(w, r) + } +} + // RequestWithBody handles POST, PUT, and PATCH requests func (h *HTTPBin) RequestWithBody(w http.ResponseWriter, r *http.Request) { resp := &bodyResponse{ @@ -712,7 +722,7 @@ func (h *HTTPBin) ETag(w http.ResponseWriter, r *http.Request) { // TODO: This mostly duplicates the work of Get() above, should this be // pulled into a little helper? - resp := &getResponse{ + resp := &noBodyResponse{ Args: r.URL.Query(), Headers: getRequestHeaders(r), Origin: getClientIP(r), diff --git a/httpbin/handlers_test.go b/httpbin/handlers_test.go index 90c75fa839f044f7a306e0a922d723c7d7bc9aba..89503c9146be286d7a41229af57725e2176d8996 100644 --- a/httpbin/handlers_test.go +++ b/httpbin/handlers_test.go @@ -18,6 +18,7 @@ import ( "net/url" "reflect" "regexp" + "runtime" "strconv" "strings" "testing" @@ -46,28 +47,33 @@ var app = New( var handler = app.Handler() func assertStatusCode(t *testing.T, w *httptest.ResponseRecorder, code int) { + t.Helper() if w.Code != code { t.Fatalf("expected status code %d, got %d", code, w.Code) } } func assertHeader(t *testing.T, w *httptest.ResponseRecorder, key, val string) { + t.Helper() if w.Header().Get(key) != val { t.Fatalf("expected header %s=%#v, got %#v", key, val, w.Header().Get(key)) } } func assertContentType(t *testing.T, w *httptest.ResponseRecorder, contentType string) { + t.Helper() assertHeader(t, w, "Content-Type", contentType) } func assertBodyContains(t *testing.T, w *httptest.ResponseRecorder, needle string) { + t.Helper() if !strings.Contains(w.Body.String(), needle) { t.Fatalf("expected string %q in body %q", needle, w.Body.String()) } } func assertBodyEquals(t *testing.T, w *httptest.ResponseRecorder, want string) { + t.Helper() have := w.Body.String() if want != have { t.Fatalf("expected body = %v, got %v", want, have) @@ -124,39 +130,10 @@ func TestUTF8(t *testing.T) { func TestGet(t *testing.T) { t.Parallel() - makeGetRequest := func(params *url.Values, headers *http.Header, expectedStatus int) (*getResponse, *httptest.ResponseRecorder) { - urlStr := "/get" - if params != nil { - urlStr = fmt.Sprintf("%s?%s", urlStr, params.Encode()) - } - r, _ := http.NewRequest("GET", urlStr, nil) - r.Host = "localhost" - r.Header.Set("User-Agent", "test") - if headers != nil { - for k, vs := range *headers { - for _, v := range vs { - r.Header.Set(k, v) - } - } - } - w := httptest.NewRecorder() - handler.ServeHTTP(w, r) - - assertStatusCode(t, w, expectedStatus) - - var resp *getResponse - if expectedStatus == http.StatusOK { - err := json.Unmarshal(w.Body.Bytes(), &resp) - if err != nil { - t.Fatalf("failed to unmarshal body %s from JSON: %s", w.Body, err) - } - } - return resp, w - } t.Run("basic", func(t *testing.T) { t.Parallel() - resp, _ := makeGetRequest(nil, nil, http.StatusOK) + resp, _ := testRequestWithoutBody(t, "/get", nil, nil, http.StatusOK) if resp.Args.Encode() != "" { t.Fatalf("expected empty args, got %s", resp.Args.Encode()) @@ -168,16 +145,13 @@ func TestGet(t *testing.T) { t.Fatalf("unexpected url: %#v", resp.URL) } - headerTests := []struct { - key string - expected string - }{ - {"Content-Type", ""}, - {"User-Agent", "test"}, + wantHeaders := map[string]string{ + "Content-Type": "", + "User-Agent": "test", } - for _, test := range headerTests { - if resp.Headers.Get(test.key) != test.expected { - t.Fatalf("expected %s = %#v, got %#v", test.key, test.expected, resp.Headers.Get(test.key)) + for key, val := range wantHeaders { + if resp.Headers.Get(key) != val { + t.Fatalf("expected %s = %#v, got %#v", key, val, resp.Headers.Get(key)) } } }) @@ -189,7 +163,7 @@ func TestGet(t *testing.T) { params.Add("bar", "bar1") params.Add("bar", "bar2") - resp, _ := makeGetRequest(params, nil, http.StatusOK) + resp, _ := testRequestWithoutBody(t, "/get", params, nil, http.StatusOK) if resp.Args.Encode() != params.Encode() { t.Fatalf("args mismatch: %s != %s", resp.Args.Encode(), params.Encode()) } @@ -219,7 +193,7 @@ func TestGet(t *testing.T) { t.Parallel() headers := &http.Header{} headers.Set(test.key, test.value) - resp, _ := makeGetRequest(nil, headers, http.StatusOK) + resp, _ := testRequestWithoutBody(t, "/get", nil, headers, http.StatusOK) if !strings.HasPrefix(resp.URL, "https://") { t.Fatalf("%s=%s should result in https URL", test.key, test.value) } @@ -227,7 +201,38 @@ func TestGet(t *testing.T) { } } -func TestHEAD(t *testing.T) { +func testRequestWithoutBody(t *testing.T, path string, params *url.Values, headers *http.Header, expectedStatus int) (*noBodyResponse, *httptest.ResponseRecorder) { + t.Helper() + + urlStr := path + if params != nil { + urlStr = fmt.Sprintf("%s?%s", urlStr, params.Encode()) + } + r, _ := http.NewRequest("GET", urlStr, nil) + r.Host = "localhost" + r.Header.Set("User-Agent", "test") + if headers != nil { + for k, vs := range *headers { + for _, v := range vs { + r.Header.Set(k, v) + } + } + } + w := httptest.NewRecorder() + handler.ServeHTTP(w, r) + + assertStatusCode(t, w, expectedStatus) + + var resp *noBodyResponse + if expectedStatus == http.StatusOK { + if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil { + t.Fatalf("failed to unmarshal body %s from JSON: %s", w.Body, err) + } + } + return resp, w +} + +func TestHead(t *testing.T) { t.Parallel() testCases := []struct { verb string @@ -267,7 +272,7 @@ func TestHEAD(t *testing.T) { t.Fatalf("error converting Content-Lengh %v to integer: %s", contentLengthStr, err) } if contentLength <= 0 { - t.Fatalf("Content-Lengh %v should be greater than 0", contentLengthStr) + t.Fatalf("Content-Length %v should be greater than 0", contentLengthStr) } }) } @@ -451,8 +456,93 @@ func TestHeaders(t *testing.T) { } } -func TestPost__EmptyBody(t *testing.T) { +func TestPost(t *testing.T) { t.Parallel() + testRequestWithBody(t, "POST", "/post") +} + +func TestPut(t *testing.T) { + t.Parallel() + testRequestWithBody(t, "PUT", "/put") +} + +func TestDelete(t *testing.T) { + t.Parallel() + testRequestWithBody(t, "DELETE", "/delete") +} + +func TestPatch(t *testing.T) { + t.Parallel() + testRequestWithBody(t, "PATCH", "/patch") +} + +func TestAnything(t *testing.T) { + t.Parallel() + var ( + verbsWithReqBodies = []string{ + "DELETE", + "PATCH", + "POST", + "PUT", + } + paths = []string{ + "/anything", + "/anything/else", + } + ) + for _, path := range paths { + 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) + }) + } +} + +// getFuncName uses runtime type reflection to get the name of the given +// function. +// +// Cribbed from https://stackoverflow.com/a/70535822/151221 +func getFuncName(f interface{}) string { + parts := strings.Split((runtime.FuncForPC(reflect.ValueOf(f).Pointer()).Name()), ".") + return parts[len(parts)-1] +} + +// getTestName expects a function named like testRequestWithBody__BodyTooBig +// and returns only the trailing BodyTooBig part. +func getTestName(prefix string, f interface{}) string { + name := strings.TrimPrefix(getFuncName(f), "testRequestWithBody") + return fmt.Sprintf("%s/%s", prefix, name) +} + +func testRequestWithBody(t *testing.T, verb, path string) { + type testFunc func(t *testing.T, verb, path string) + testFuncs := []testFunc{ + testRequestWithBodyBodyTooBig, + testRequestWithBodyEmptyBody, + testRequestWithBodyFormEncodedBody, + testRequestWithBodyFormEncodedBodyNoContentType, + testRequestWithBodyInvalidFormEncodedBody, + testRequestWithBodyInvalidJSON, + testRequestWithBodyInvalidMultiPartBody, + testRequestWithBodyJSON, + testRequestWithBodyMultiPartBody, + testRequestWithBodyQueryParams, + testRequestWithBodyQueryParamsAndBody, + } + for _, testFunc := range testFuncs { + testFunc := testFunc + + t.Run(getTestName(verb, testFunc), func(t *testing.T) { + t.Parallel() + testFunc(t, verb, path) + }) + } +} + +func testRequestWithBodyEmptyBody(t *testing.T, verb string, path string) { tests := []struct { contentType string }{ @@ -465,7 +555,7 @@ func TestPost__EmptyBody(t *testing.T) { test := test t.Run("content type/"+test.contentType, func(t *testing.T) { t.Parallel() - r, _ := http.NewRequest("POST", "/post", nil) + r, _ := http.NewRequest(verb, path, nil) r.Header.Set("Content-Type", test.contentType) w := httptest.NewRecorder() handler.ServeHTTP(w, r) @@ -496,14 +586,13 @@ func TestPost__EmptyBody(t *testing.T) { } } -func TestPost__FormEncodedBody(t *testing.T) { - t.Parallel() +func testRequestWithBodyFormEncodedBody(t *testing.T, verb, path string) { params := url.Values{} params.Set("foo", "foo") params.Add("bar", "bar1") params.Add("bar", "bar2") - r, _ := http.NewRequest("POST", "/post", strings.NewReader(params.Encode())) + r, _ := http.NewRequest(verb, path, strings.NewReader(params.Encode())) r.Header.Set("Content-Type", "application/x-www-form-urlencoded") w := httptest.NewRecorder() handler.ServeHTTP(w, r) @@ -534,14 +623,13 @@ func TestPost__FormEncodedBody(t *testing.T) { } } -func TestPost__FormEncodedBodyNoContentType(t *testing.T) { - t.Parallel() +func testRequestWithBodyFormEncodedBodyNoContentType(t *testing.T, verb, path string) { params := url.Values{} params.Set("foo", "foo") params.Add("bar", "bar1") params.Add("bar", "bar2") - r, _ := http.NewRequest("POST", "/post", strings.NewReader(params.Encode())) + r, _ := http.NewRequest(verb, path, strings.NewReader(params.Encode())) w := httptest.NewRecorder() handler.ServeHTTP(w, r) @@ -565,8 +653,7 @@ func TestPost__FormEncodedBodyNoContentType(t *testing.T) { } } -func TestPost__MultiPartBody(t *testing.T) { - t.Parallel() +func testRequestWithBodyMultiPartBody(t *testing.T, verb, path string) { params := map[string][]string{ "foo": {"foo"}, "bar": {"bar1", "bar2"}, @@ -589,7 +676,7 @@ func TestPost__MultiPartBody(t *testing.T) { } mw.Close() - r, _ := http.NewRequest("POST", "/post", bytes.NewReader(body.Bytes())) + r, _ := http.NewRequest(verb, path, bytes.NewReader(body.Bytes())) r.Header.Set("Content-Type", mw.FormDataContentType()) w := httptest.NewRecorder() handler.ServeHTTP(w, r) @@ -620,26 +707,23 @@ func TestPost__MultiPartBody(t *testing.T) { } } -func TestPost__InvalidFormEncodedBody(t *testing.T) { - t.Parallel() - r, _ := http.NewRequest("POST", "/post", strings.NewReader("%ZZ")) +func testRequestWithBodyInvalidFormEncodedBody(t *testing.T, verb, path string) { + r, _ := http.NewRequest(verb, path, strings.NewReader("%ZZ")) r.Header.Set("Content-Type", "application/x-www-form-urlencoded") w := httptest.NewRecorder() handler.ServeHTTP(w, r) assertStatusCode(t, w, http.StatusBadRequest) } -func TestPost__InvalidMultiPartBody(t *testing.T) { - t.Parallel() - r, _ := http.NewRequest("POST", "/post", strings.NewReader("%ZZ")) +func testRequestWithBodyInvalidMultiPartBody(t *testing.T, verb, path string) { + r, _ := http.NewRequest(verb, path, strings.NewReader("%ZZ")) r.Header.Set("Content-Type", "multipart/form-data; etc") w := httptest.NewRecorder() handler.ServeHTTP(w, r) assertStatusCode(t, w, http.StatusBadRequest) } -func TestPost__JSON(t *testing.T) { - t.Parallel() +func testRequestWithBodyJSON(t *testing.T, verb, path string) { type testInput struct { Foo string Bar int @@ -654,7 +738,7 @@ func TestPost__JSON(t *testing.T) { } inputBody, _ := json.Marshal(input) - r, _ := http.NewRequest("POST", "/post", bytes.NewReader(inputBody)) + r, _ := http.NewRequest(verb, path, bytes.NewReader(inputBody)) r.Header.Set("Content-Type", "application/json; charset=utf-8") w := httptest.NewRecorder() handler.ServeHTTP(w, r) @@ -692,8 +776,7 @@ func TestPost__JSON(t *testing.T) { } } -func TestPost__InvalidJSON(t *testing.T) { - t.Parallel() +func testRequestWithBodyInvalidJSON(t *testing.T, verb, path string) { r, _ := http.NewRequest("POST", "/post", bytes.NewReader([]byte("foo"))) r.Header.Set("Content-Type", "application/json; charset=utf-8") w := httptest.NewRecorder() @@ -701,8 +784,7 @@ func TestPost__InvalidJSON(t *testing.T) { assertStatusCode(t, w, http.StatusBadRequest) } -func TestPost__BodyTooBig(t *testing.T) { - t.Parallel() +func testRequestWithBodyBodyTooBig(t *testing.T, verb, path string) { body := make([]byte, maxBodySize+1) r, _ := http.NewRequest("POST", "/post", bytes.NewReader(body)) @@ -712,8 +794,7 @@ func TestPost__BodyTooBig(t *testing.T) { assertStatusCode(t, w, http.StatusBadRequest) } -func TestPost__QueryParams(t *testing.T) { - t.Parallel() +func testRequestWithBodyQueryParams(t *testing.T, verb, path string) { params := url.Values{} params.Set("foo", "foo") params.Add("bar", "bar1") @@ -741,8 +822,7 @@ func TestPost__QueryParams(t *testing.T) { } } -func TestPost__QueryParamsAndBody(t *testing.T) { - t.Parallel() +func testRequestWithBodyQueryParamsAndBody(t *testing.T, verb, path string) { args := url.Values{} args.Set("query1", "foo") args.Add("query2", "bar1") @@ -2103,7 +2183,7 @@ func TestCache(t *testing.T) { t.Fatalf("expected ETag header %v, got %v", sha1hash(lastModified), etag) } - var resp *getResponse + var resp *noBodyResponse err := json.Unmarshal(w.Body.Bytes(), &resp) if err != nil { t.Fatalf("failed to unmarshal body %s from JSON: %s", w.Body, err) diff --git a/httpbin/helpers.go b/httpbin/helpers.go index 4b15e08db78ca89b5b5a91d6aba23258176ba870..ef48f60ff7391ffdf02fdde0ee51976f5b506873 100644 --- a/httpbin/helpers.go +++ b/httpbin/helpers.go @@ -123,6 +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 + // we need it to for compatibility with the httpbin implementation, so + // we trick it with this ugly hack. + if r.Method == http.MethodDelete { + r.Method = http.MethodPost + defer func() { r.Method = http.MethodDelete }() + } if err := r.ParseForm(); err != nil { return err } diff --git a/httpbin/httpbin.go b/httpbin/httpbin.go index 921f56510b9500d2353a57414ffee2bc50e7e521..81493a55e2864c0ef50494e78a97b377f86c1d03 100644 --- a/httpbin/httpbin.go +++ b/httpbin/httpbin.go @@ -30,14 +30,17 @@ type userAgentResponse struct { UserAgent string `json:"user-agent"` } -type getResponse struct { +// A generic response for any incoming request that should not contain a body +// (GET, HEAD, OPTIONS, etc). +type noBodyResponse struct { Args url.Values `json:"args"` Headers http.Header `json:"headers"` Origin string `json:"origin"` URL string `json:"url"` } -// A generic response for any incoming request that might contain a body +// A generic response for any incoming request that might contain a body (POST, +// PUT, PATCH, etc). type bodyResponse struct { Args url.Values `json:"args"` Headers http.Header `json:"headers"` @@ -141,6 +144,9 @@ func (h *HTTPBin) Handler() http.Handler { mux.HandleFunc("/post", methods(h.RequestWithBody, "POST")) mux.HandleFunc("/put", methods(h.RequestWithBody, "PUT")) + mux.HandleFunc("/anything", h.Anything) + mux.HandleFunc("/anything/", h.Anything) + mux.HandleFunc("/ip", h.IP) mux.HandleFunc("/user-agent", h.UserAgent) mux.HandleFunc("/headers", h.Headers) @@ -155,9 +161,6 @@ func (h *HTTPBin) Handler() http.Handler { mux.HandleFunc("/absolute-redirect/", h.AbsoluteRedirect) mux.HandleFunc("/redirect-to", h.RedirectTo) - mux.HandleFunc("/anything/", h.RequestWithBody) - mux.HandleFunc("/anything", h.RequestWithBody) - mux.HandleFunc("/cookies", h.Cookies) mux.HandleFunc("/cookies/set", h.SetCookies) mux.HandleFunc("/cookies/delete", h.DeleteCookies)