diff --git a/httpbin/handlers.go b/httpbin/handlers.go index 25fd90073d872befe8194f6d76dd32b57481e677..2073654f633baafd9ee65202af9fcd18151019dc 100644 --- a/httpbin/handlers.go +++ b/httpbin/handlers.go @@ -284,3 +284,28 @@ func (h *HTTPBin) DeleteCookies(w http.ResponseWriter, r *http.Request) { w.Header().Set("Location", "/cookies") w.WriteHeader(http.StatusFound) } + +// BasicAuth requires basic authentication +func (h *HTTPBin) BasicAuth(w http.ResponseWriter, r *http.Request) { + parts := strings.Split(r.URL.Path, "/") + if len(parts) != 4 { + http.Error(w, "Not Found", http.StatusNotFound) + return + } + expectedUser := parts[2] + expectedPass := parts[3] + + givenUser, givenPass, _ := r.BasicAuth() + + status := http.StatusOK + authorized := givenUser == expectedUser && givenPass == expectedPass + if !authorized { + status = http.StatusUnauthorized + } + + body, _ := json.Marshal(&authResponse{ + Authorized: authorized, + User: givenUser, + }) + writeJSON(w, body, status) +} diff --git a/httpbin/handlers_test.go b/httpbin/handlers_test.go index 97b87cc1706315c9f0580ce0fa54ff7164cafb68..4f87bc1f754b31714c28f8fb421ce51d150d11f7 100644 --- a/httpbin/handlers_test.go +++ b/httpbin/handlers_test.go @@ -908,3 +908,85 @@ func TestDeleteCookies(t *testing.T) { } } } + +func TestBasicAuth(t *testing.T) { + t.Run("ok", func(t *testing.T) { + r, _ := http.NewRequest("GET", "/basic-auth/user/pass", nil) + r.SetBasicAuth("user", "pass") + w := httptest.NewRecorder() + handler.ServeHTTP(w, r) + + assertStatusCode(t, w, http.StatusOK) + assertContentType(t, w, jsonContentType) + + resp := &authResponse{} + json.Unmarshal(w.Body.Bytes(), resp) + + expectedResp := &authResponse{ + Authorized: true, + User: "user", + } + if !reflect.DeepEqual(resp, expectedResp) { + t.Fatalf("expected response %#v, got %#v", expectedResp, resp) + } + }) + + t.Run("error/no auth", func(t *testing.T) { + r, _ := http.NewRequest("GET", "/basic-auth/user/pass", nil) + w := httptest.NewRecorder() + handler.ServeHTTP(w, r) + + assertStatusCode(t, w, http.StatusUnauthorized) + assertContentType(t, w, jsonContentType) + + resp := &authResponse{} + json.Unmarshal(w.Body.Bytes(), resp) + + expectedResp := &authResponse{ + Authorized: false, + User: "", + } + if !reflect.DeepEqual(resp, expectedResp) { + t.Fatalf("expected response %#v, got %#v", expectedResp, resp) + } + }) + + t.Run("error/bad auth", func(t *testing.T) { + r, _ := http.NewRequest("GET", "/basic-auth/user/pass", nil) + r.SetBasicAuth("bad", "auth") + w := httptest.NewRecorder() + handler.ServeHTTP(w, r) + + assertStatusCode(t, w, http.StatusUnauthorized) + assertContentType(t, w, jsonContentType) + + resp := &authResponse{} + json.Unmarshal(w.Body.Bytes(), resp) + + expectedResp := &authResponse{ + Authorized: false, + User: "bad", + } + if !reflect.DeepEqual(resp, expectedResp) { + t.Fatalf("expected response %#v, got %#v", expectedResp, resp) + } + }) + + var errorTests = []struct { + url string + status int + }{ + {"/basic-auth", http.StatusNotFound}, + {"/basic-auth/user", http.StatusNotFound}, + {"/basic-auth/user/pass/extra", http.StatusNotFound}, + } + for _, test := range errorTests { + t.Run("error"+test.url, func(t *testing.T) { + r, _ := http.NewRequest("GET", test.url, nil) + r.SetBasicAuth("foo", "bar") + w := httptest.NewRecorder() + handler.ServeHTTP(w, r) + assertStatusCode(t, w, test.status) + }) + } +} diff --git a/httpbin/httpbin.go b/httpbin/httpbin.go index 7576af150d693c3e748f31be69d2c6b3869a3656..fca0002d01b8789cb22fc4135349f483de3921b8 100644 --- a/httpbin/httpbin.go +++ b/httpbin/httpbin.go @@ -41,6 +41,11 @@ type bodyResponse struct { type cookiesResponse map[string]string +type authResponse struct { + Authorized bool `json:"authorized"` + User string `json:"user"` +} + // Options are used to configure HTTPBin type Options struct { MaxMemory int64 @@ -79,13 +84,16 @@ func (h *HTTPBin) Handler() http.Handler { mux.HandleFunc("/cookies/set", h.SetCookies) mux.HandleFunc("/cookies/delete", h.DeleteCookies) + mux.HandleFunc("/basic-auth/", h.BasicAuth) + // Make sure our ServeMux doesn't "helpfully" redirect these invalid // endpoints by adding a trailing slash. See the ServeMux docs for more // info: https://golang.org/pkg/net/http/#ServeMux - mux.HandleFunc("/status", http.NotFound) + mux.HandleFunc("/absolute-redirect", http.NotFound) + mux.HandleFunc("/basic-auth", http.NotFound) mux.HandleFunc("/redirect", http.NotFound) mux.HandleFunc("/relative-redirect", http.NotFound) - mux.HandleFunc("/absolute-redirect", http.NotFound) + mux.HandleFunc("/status", http.NotFound) return logger(cors(mux)) }