From c8fee93dfff506eda4bdd0d9a40662c08c23dbe8 Mon Sep 17 00:00:00 2001 From: Will McCutchen <will@mccutch.org> Date: Fri, 7 Oct 2016 15:42:40 -0700 Subject: [PATCH] Re-org into httpbin package --- handlers.go => httpbin/handlers.go | 38 ++++---- handlers_test.go => httpbin/handlers_test.go | 46 ++++++---- helpers.go => httpbin/helpers.go | 4 +- httpbin/httpbin.go | 89 +++++++++++++++++++ middleware.go => httpbin/middleware.go | 19 +--- .../templates}/forms-post.html | 0 .../templates}/httpbin.1.html | 0 {templates => httpbin/templates}/index.html | 0 {templates => httpbin/templates}/utf8.html | 0 main.go | 32 ++----- types.go | 38 -------- 11 files changed, 146 insertions(+), 120 deletions(-) rename handlers.go => httpbin/handlers.go (59%) rename handlers_test.go => httpbin/handlers_test.go (94%) rename helpers.go => httpbin/helpers.go (97%) create mode 100644 httpbin/httpbin.go rename middleware.go => httpbin/middleware.go (74%) rename {templates => httpbin/templates}/forms-post.html (100%) rename {templates => httpbin/templates}/httpbin.1.html (100%) rename {templates => httpbin/templates}/index.html (100%) rename {templates => httpbin/templates}/utf8.html (100%) delete mode 100644 types.go diff --git a/handlers.go b/httpbin/handlers.go similarity index 59% rename from handlers.go rename to httpbin/handlers.go index 98e4aa4..b4768aa 100644 --- a/handlers.go +++ b/httpbin/handlers.go @@ -1,16 +1,15 @@ -package main +package httpbin import ( "encoding/json" "fmt" - "html/template" "net/http" "net/url" ) -// Index must be wrapped by the withTemplates middleware before it can be used -func index(w http.ResponseWriter, r *http.Request, t *template.Template) { - t = t.Lookup("index.html") +// Index renders an HTML index page +func (h *HTTPBin) Index(w http.ResponseWriter, r *http.Request) { + t := h.templates.Lookup("index.html") if t == nil { http.Error(w, fmt.Sprintf("error looking up index.html"), http.StatusInternalServerError) return @@ -18,9 +17,9 @@ func index(w http.ResponseWriter, r *http.Request, t *template.Template) { t.Execute(w, nil) } -// FormsPost must be wrapped by withTemplates middleware before it can be used -func formsPost(w http.ResponseWriter, r *http.Request, t *template.Template) { - t = t.Lookup("forms-post.html") +// FormsPost renders an HTML form that submits a request to the /post endpoint +func (h *HTTPBin) FormsPost(w http.ResponseWriter, r *http.Request) { + t := h.templates.Lookup("forms-post.html") if t == nil { http.Error(w, fmt.Sprintf("error looking up index.html"), http.StatusInternalServerError) return @@ -28,9 +27,9 @@ func formsPost(w http.ResponseWriter, r *http.Request, t *template.Template) { t.Execute(w, nil) } -// utf8 must be wrapped by withTemplates middleware before it can be used -func utf8(w http.ResponseWriter, r *http.Request, t *template.Template) { - t = t.Lookup("utf8.html") +// UTF8 renders an HTML encoding stress test +func (h *HTTPBin) UTF8(w http.ResponseWriter, r *http.Request) { + t := h.templates.Lookup("utf8.html") if t == nil { http.Error(w, fmt.Sprintf("error looking up index.html"), http.StatusInternalServerError) return @@ -39,7 +38,8 @@ func utf8(w http.ResponseWriter, r *http.Request, t *template.Template) { t.Execute(w, nil) } -func get(w http.ResponseWriter, r *http.Request) { +// Get handles HTTP GET requests +func (h *HTTPBin) Get(w http.ResponseWriter, r *http.Request) { args, err := url.ParseQuery(r.URL.RawQuery) if err != nil { http.Error(w, fmt.Sprintf("error parsing query params: %s", err), http.StatusBadRequest) @@ -55,7 +55,8 @@ func get(w http.ResponseWriter, r *http.Request) { writeJSON(w, body, http.StatusOK) } -func requestWithBody(w http.ResponseWriter, r *http.Request) { +// RequestWithBody handles POST, PUT, and PATCH requests +func (h *HTTPBin) RequestWithBody(w http.ResponseWriter, r *http.Request) { args, err := url.ParseQuery(r.URL.RawQuery) if err != nil { http.Error(w, fmt.Sprintf("error parsing query params: %s", err), http.StatusBadRequest) @@ -69,7 +70,7 @@ func requestWithBody(w http.ResponseWriter, r *http.Request) { URL: getURL(r), } - err = parseBody(w, r, resp) + err = parseBody(w, r, resp, h.options.MaxMemory) if err != nil { http.Error(w, fmt.Sprintf("error parsing request body: %s", err), http.StatusBadRequest) } @@ -78,21 +79,24 @@ func requestWithBody(w http.ResponseWriter, r *http.Request) { writeJSON(w, body, http.StatusOK) } -func ip(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{ Origin: getOrigin(r), }) writeJSON(w, body, http.StatusOK) } -func userAgent(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{ UserAgent: r.Header.Get("User-Agent"), }) writeJSON(w, body, http.StatusOK) } -func headers(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{ Headers: r.Header, }) diff --git a/handlers_test.go b/httpbin/handlers_test.go similarity index 94% rename from handlers_test.go rename to httpbin/handlers_test.go index 57333dc..7df70e1 100644 --- a/handlers_test.go +++ b/httpbin/handlers_test.go @@ -1,4 +1,4 @@ -package main +package httpbin import ( "bytes" @@ -11,10 +11,18 @@ import ( "testing" ) +const maxMemory = 1024 * 1024 + +var app = NewHTTPBin(&Options{ + MaxMemory: maxMemory, +}) + +var handler = app.Handler() + func TestIndex(t *testing.T) { r, _ := http.NewRequest("GET", "/", nil) w := httptest.NewRecorder() - app().ServeHTTP(w, r) + handler.ServeHTTP(w, r) if !strings.Contains(w.Body.String(), "go-httpbin") { t.Fatalf("expected go-httpbin in index body") @@ -24,7 +32,7 @@ func TestIndex(t *testing.T) { func TestFormsPost(t *testing.T) { r, _ := http.NewRequest("GET", "/forms/post", nil) w := httptest.NewRecorder() - app().ServeHTTP(w, r) + handler.ServeHTTP(w, r) if !strings.Contains(w.Body.String(), `<form method="post" action="/post">`) { t.Fatalf("expected <form> in body") @@ -34,7 +42,7 @@ func TestFormsPost(t *testing.T) { func TestUTF8(t *testing.T) { r, _ := http.NewRequest("GET", "/encoding/utf8", nil) w := httptest.NewRecorder() - app().ServeHTTP(w, r) + handler.ServeHTTP(w, r) if w.Header().Get("Content-Type") != "text/html; charset=utf-8" { t.Fatalf("expected 'text/html; charset=utf-8' content type") @@ -49,7 +57,7 @@ func TestGet__Basic(t *testing.T) { r.Host = "localhost" r.Header.Set("User-Agent", "test") w := httptest.NewRecorder() - app().ServeHTTP(w, r) + handler.ServeHTTP(w, r) if w.Code != 200 { t.Fatalf("expected status code 200, got %d", w.Code) @@ -88,7 +96,7 @@ func TestGet__Basic(t *testing.T) { func TestGet__OnlyAllowsGets(t *testing.T) { r, _ := http.NewRequest("POST", "/get", nil) w := httptest.NewRecorder() - app().ServeHTTP(w, r) + handler.ServeHTTP(w, r) if w.Code != http.StatusMethodNotAllowed { t.Fatalf("expected HTTP 405, got %d", w.Code) @@ -98,7 +106,7 @@ func TestGet__OnlyAllowsGets(t *testing.T) { func TestGet__CORSHeadersWithoutRequestOrigin(t *testing.T) { r, _ := http.NewRequest("GET", "/get", nil) w := httptest.NewRecorder() - app().ServeHTTP(w, r) + handler.ServeHTTP(w, r) if w.Header().Get("Access-Control-Allow-Origin") != "*" { t.Fatalf("expected Access-Control-Allow-Origin=*, got %#v", w.Header().Get("Access-Control-Allow-Origin")) @@ -109,7 +117,7 @@ func TestGet__CORSHeadersWithRequestOrigin(t *testing.T) { r, _ := http.NewRequest("GET", "/get", nil) r.Header.Set("Origin", "origin") w := httptest.NewRecorder() - app().ServeHTTP(w, r) + handler.ServeHTTP(w, r) if w.Header().Get("Access-Control-Allow-Origin") != "origin" { t.Fatalf("expected Access-Control-Allow-Origin=origin, got %#v", w.Header().Get("Access-Control-Allow-Origin")) @@ -119,7 +127,7 @@ func TestGet__CORSHeadersWithRequestOrigin(t *testing.T) { func TestGet__CORSHeadersWithOptionsVerb(t *testing.T) { r, _ := http.NewRequest("OPTIONS", "/get", nil) w := httptest.NewRecorder() - app().ServeHTTP(w, r) + handler.ServeHTTP(w, r) var headerTests = []struct { key string @@ -142,7 +150,7 @@ func TestGet__CORSAllowHeaders(t *testing.T) { r, _ := http.NewRequest("OPTIONS", "/get", nil) r.Header.Set("Access-Control-Request-Headers", "X-Test-Header") w := httptest.NewRecorder() - app().ServeHTTP(w, r) + handler.ServeHTTP(w, r) var headerTests = []struct { key string @@ -171,7 +179,7 @@ func TestGet__XForwardedProto(t *testing.T) { r, _ := http.NewRequest("GET", "/get", nil) r.Header.Set(test.key, test.value) w := httptest.NewRecorder() - app().ServeHTTP(w, r) + handler.ServeHTTP(w, r) var resp *bodyResponse err := json.Unmarshal(w.Body.Bytes(), &resp) @@ -189,7 +197,7 @@ func TestIP(t *testing.T) { r, _ := http.NewRequest("GET", "/ip", nil) r.RemoteAddr = "192.168.0.100" w := httptest.NewRecorder() - app().ServeHTTP(w, r) + handler.ServeHTTP(w, r) var resp *ipResponse err := json.Unmarshal(w.Body.Bytes(), &resp) @@ -206,7 +214,7 @@ func TestUserAgent(t *testing.T) { r, _ := http.NewRequest("GET", "/user-agent", nil) r.Header.Set("User-Agent", "test") w := httptest.NewRecorder() - app().ServeHTTP(w, r) + handler.ServeHTTP(w, r) var resp *userAgentResponse err := json.Unmarshal(w.Body.Bytes(), &resp) @@ -226,7 +234,7 @@ func TestHeaders(t *testing.T) { r.Header.Add("Bar-Header", "bar1") r.Header.Add("Bar-Header", "bar2") w := httptest.NewRecorder() - app().ServeHTTP(w, r) + handler.ServeHTTP(w, r) var resp *headersResponse err := json.Unmarshal(w.Body.Bytes(), &resp) @@ -248,7 +256,7 @@ func TestHeaders(t *testing.T) { func TestPost__EmptyBody(t *testing.T) { r, _ := http.NewRequest("POST", "/post", nil) w := httptest.NewRecorder() - app().ServeHTTP(w, r) + handler.ServeHTTP(w, r) var resp *bodyResponse err := json.Unmarshal(w.Body.Bytes(), &resp) @@ -273,7 +281,7 @@ func TestPost__FormEncodedBody(t *testing.T) { r, _ := http.NewRequest("POST", "/post", strings.NewReader(params.Encode())) r.Header.Set("Content-Type", "application/x-www-form-urlencoded") w := httptest.NewRecorder() - app().ServeHTTP(w, r) + handler.ServeHTTP(w, r) var resp *bodyResponse err := json.Unmarshal(w.Body.Bytes(), &resp) @@ -306,7 +314,7 @@ func TestPost__FormEncodedBodyNoContentType(t *testing.T) { r, _ := http.NewRequest("POST", "/post", strings.NewReader(params.Encode())) w := httptest.NewRecorder() - app().ServeHTTP(w, r) + handler.ServeHTTP(w, r) var resp *bodyResponse err := json.Unmarshal(w.Body.Bytes(), &resp) @@ -343,7 +351,7 @@ func TestPost__JSON(t *testing.T) { r, _ := http.NewRequest("POST", "/post", bytes.NewReader(inputBody)) r.Header.Set("Content-Type", "application/json; charset=utf-8") w := httptest.NewRecorder() - app().ServeHTTP(w, r) + handler.ServeHTTP(w, r) var resp *bodyResponse err := json.Unmarshal(w.Body.Bytes(), &resp) @@ -380,7 +388,7 @@ func TestPost__BodyTooBig(t *testing.T) { r, _ := http.NewRequest("POST", "/post", bytes.NewReader(body)) w := httptest.NewRecorder() - app().ServeHTTP(w, r) + handler.ServeHTTP(w, r) if w.Code != http.StatusBadRequest { t.Fatalf("expected code %d, got %d", http.StatusBadRequest, w.Code) diff --git a/helpers.go b/httpbin/helpers.go similarity index 97% rename from helpers.go rename to httpbin/helpers.go index b8daae9..9cac73d 100644 --- a/helpers.go +++ b/httpbin/helpers.go @@ -1,4 +1,4 @@ -package main +package httpbin import ( "encoding/json" @@ -56,7 +56,7 @@ func writeJSON(w http.ResponseWriter, body []byte, status int) { // parseBody handles parsing a request body into our standard API response, // taking care to only consume the request body once based on the Content-Type // of the request. The given Resp will be updated. -func parseBody(w http.ResponseWriter, r *http.Request, resp *bodyResponse) error { +func parseBody(w http.ResponseWriter, r *http.Request, resp *bodyResponse, maxMemory int64) error { if r.Body == nil { return nil } diff --git a/httpbin/httpbin.go b/httpbin/httpbin.go new file mode 100644 index 0000000..0302a5b --- /dev/null +++ b/httpbin/httpbin.go @@ -0,0 +1,89 @@ +package httpbin + +import ( + "html/template" + "log" + "net/http" + "net/url" +) + +type headersResponse struct { + Headers http.Header `json:"headers"` +} + +type ipResponse struct { + Origin string `json:"origin"` +} + +type userAgentResponse struct { + UserAgent string `json:"user-agent"` +} + +type getResponse 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 +type bodyResponse struct { + Args url.Values `json:"args"` + Headers http.Header `json:"headers"` + Origin string `json:"origin"` + URL string `json:"url"` + + Data []byte `json:"data"` + Files map[string][]string `json:"files"` + Form map[string][]string `json:"form"` + JSON interface{} `json:"json"` +} + +// Options are used to configure HTTPBin +type Options struct { + MaxMemory int64 +} + +// HTTPBin contains the business logic +type HTTPBin struct { + options *Options + templates *template.Template +} + +// Handler returns an http.Handler that exposes all HTTPBin endpoints +func (h *HTTPBin) Handler() http.Handler { + mux := http.NewServeMux() + + mux.HandleFunc("/", methods(h.Index, "GET")) + mux.HandleFunc("/forms/post", methods(h.FormsPost, "GET")) + mux.HandleFunc("/encoding/utf8", methods(h.UTF8, "GET")) + + mux.HandleFunc("/get", methods(h.Get, "GET")) + mux.HandleFunc("/post", methods(h.RequestWithBody, "POST")) + mux.HandleFunc("/put", methods(h.RequestWithBody, "PUT")) + mux.HandleFunc("/patch", methods(h.RequestWithBody, "PATCH")) + mux.HandleFunc("/delete", methods(h.RequestWithBody, "DELETE")) + + mux.HandleFunc("/ip", h.IP) + mux.HandleFunc("/user-agent", h.UserAgent) + mux.HandleFunc("/headers", h.Headers) + + return logger(cors(mux)) +} + +// NewHTTPBin creates a new HTTPBin +func NewHTTPBin(options *Options) *HTTPBin { + if options == nil { + options = &Options{} + } + + t, err := template.ParseGlob("templates/*.html") + if err != nil { + log.Fatalf("error parsing templates: %s", err) + } + + return &HTTPBin{ + options: options, + templates: t, + } +} diff --git a/middleware.go b/httpbin/middleware.go similarity index 74% rename from middleware.go rename to httpbin/middleware.go index e857f19..8769302 100644 --- a/middleware.go +++ b/httpbin/middleware.go @@ -1,8 +1,7 @@ -package main +package httpbin import ( "fmt" - "html/template" "log" "net/http" ) @@ -51,19 +50,3 @@ func methods(h http.HandlerFunc, methods ...string) http.HandlerFunc { h.ServeHTTP(w, r) } } - -// Simplify function signatures below -type templateHandlerFunc func(http.ResponseWriter, *http.Request, *template.Template) - -func withTemplates(path string) func(templateHandlerFunc) http.HandlerFunc { - t, err := template.ParseGlob(path) - if err != nil { - log.Fatalf("error parsing templates: %s", err) - } - - return func(handler templateHandlerFunc) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - handler(w, r, t) - } - } -} diff --git a/templates/forms-post.html b/httpbin/templates/forms-post.html similarity index 100% rename from templates/forms-post.html rename to httpbin/templates/forms-post.html diff --git a/templates/httpbin.1.html b/httpbin/templates/httpbin.1.html similarity index 100% rename from templates/httpbin.1.html rename to httpbin/templates/httpbin.1.html diff --git a/templates/index.html b/httpbin/templates/index.html similarity index 100% rename from templates/index.html rename to httpbin/templates/index.html diff --git a/templates/utf8.html b/httpbin/templates/utf8.html similarity index 100% rename from templates/utf8.html rename to httpbin/templates/utf8.html diff --git a/main.go b/main.go index 1f421f6..e4f8310 100644 --- a/main.go +++ b/main.go @@ -3,35 +3,15 @@ package main import ( "log" "net/http" -) - -// Max size of a request body we'll handle -const maxMemory = 1024*1024*5 + 1 - -func app() http.Handler { - h := http.NewServeMux() - templateWrapper := withTemplates("templates/*.html") - - h.HandleFunc("/", methods(templateWrapper(index), "GET")) - h.HandleFunc("/forms/post", methods(templateWrapper(formsPost), "GET")) - h.HandleFunc("/encoding/utf8", methods(templateWrapper(utf8), "GET")) - h.HandleFunc("/get", methods(get, "GET")) - h.HandleFunc("/post", methods(requestWithBody, "POST")) - h.HandleFunc("/put", methods(requestWithBody, "PUT")) - h.HandleFunc("/patch", methods(requestWithBody, "PATCH")) - h.HandleFunc("/delete", methods(requestWithBody, "DELETE")) - - h.HandleFunc("/ip", ip) - h.HandleFunc("/user-agent", userAgent) - h.HandleFunc("/headers", headers) - - return logger(cors(h)) -} + "github.com/mccutchen/go-httpbin/httpbin" +) func main() { - a := app() + h := httpbin.NewHTTPBin(&httpbin.Options{ + MaxMemory: 1024 * 1024 * 5, + }) log.Printf("listening on 9999") - err := http.ListenAndServe(":9999", a) + err := http.ListenAndServe(":9999", h.Handler()) log.Fatal(err) } diff --git a/types.go b/types.go deleted file mode 100644 index 75a314f..0000000 --- a/types.go +++ /dev/null @@ -1,38 +0,0 @@ -package main - -import ( - "net/http" - "net/url" -) - -type headersResponse struct { - Headers http.Header `json:"headers"` -} - -type ipResponse struct { - Origin string `json:"origin"` -} - -type userAgentResponse struct { - UserAgent string `json:"user-agent"` -} - -type getResponse 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 -type bodyResponse struct { - Args url.Values `json:"args"` - Headers http.Header `json:"headers"` - Origin string `json:"origin"` - URL string `json:"url"` - - Data []byte `json:"data"` - Files map[string][]string `json:"files"` - Form map[string][]string `json:"form"` - JSON interface{} `json:"json"` -} -- GitLab