diff --git a/README.md b/README.md new file mode 100644 index 0000000000000000000000000000000000000000..895b7205826d4353e00061e7489f82f5bcc7ba1c --- /dev/null +++ b/README.md @@ -0,0 +1,17 @@ +# go-httpbin + +A WIP golang port of https://httpbin.org/. + +## Testing + +``` +go test +go test -cover +go test -coverprofile=cover.out && go tool cover -html=cover.out +``` + +## Running + +``` +go build && ./go-httpbin +``` diff --git a/main.go b/main.go new file mode 100644 index 0000000000000000000000000000000000000000..288e276df4fbd40fd5a2e78446e186711622921c --- /dev/null +++ b/main.go @@ -0,0 +1,96 @@ +package main + +import ( + "encoding/json" + "log" + "net/http" + "net/url" +) + +// Resp is the standard JSON response from httpbin +type Resp struct { + Args url.Values `json:"args"` + Headers http.Header `json:"headers"` + Origin string `json:"origin"` + URL string `json:"url"` + + Data string `json:"data,omitempty"` + Files map[string][]string `json:"files,omitempty"` + Form map[string][]string `json:"form,omitempty"` + JSON map[string][]string `json:"json,omitempty"` +} + +func getOrigin(r *http.Request) string { + origin := r.Header.Get("X-Forwarded-For") + if origin == "" { + origin = r.RemoteAddr + } + return origin +} + +func getURL(r *http.Request) string { + scheme := r.Header.Get("X-Forwarded-Proto") + if scheme == "" { + scheme = r.Header.Get("X-Forwarded-Protocol") + } + if scheme == "" && r.Header.Get("X-Forwarded-Ssl") == "on" { + scheme = "https" + } + if scheme == "" { + scheme = "http" + } + + host := r.URL.Host + if host == "" { + host = r.Host + } + + u := &url.URL{ + Scheme: scheme, + Opaque: r.URL.Opaque, + User: r.URL.User, + Host: host, + Path: r.URL.Path, + RawPath: r.URL.RawPath, + ForceQuery: r.URL.ForceQuery, + RawQuery: r.URL.RawQuery, + Fragment: r.URL.Fragment, + } + return u.String() +} + +func writeResponse(w http.ResponseWriter, r *http.Request, resp *Resp) { + resp.Origin = getOrigin(r) + resp.URL = getURL(r) + + body, err := json.Marshal(resp) + if err != nil { + log.Printf("error marshalling %v as JSON: %s", resp, err) + } + w.Write(body) +} + +func logger(h http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + h.ServeHTTP(w, r) + log.Printf("%s %s %s", r.RemoteAddr, r.Method, r.URL) + }) +} + +func get(w http.ResponseWriter, r *http.Request) { + r.ParseForm() + resp := &Resp{ + Args: r.Form, + Headers: r.Header, + } + writeResponse(w, r, resp) +} + +func main() { + h := http.NewServeMux() + h.HandleFunc("/get", get) + + log.Printf("listening on 9999") + err := http.ListenAndServe(":9999", logger(h)) + log.Fatal(err) +} diff --git a/main_test.go b/main_test.go new file mode 100644 index 0000000000000000000000000000000000000000..2068b3d8a4c78ab395e7a1b59d999604e084fba3 --- /dev/null +++ b/main_test.go @@ -0,0 +1,49 @@ +package main + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "testing" +) + +func TestGet(t *testing.T) { + r, _ := http.NewRequest("GET", "/get", nil) + r.Host = "localhost" + r.Header.Set("User-Agent", "test") + w := httptest.NewRecorder() + get(w, r) + + if w.Code != 200 { + t.Fatalf("expected status code 200, got %d", w.Code) + } + + var resp *Resp + err := json.Unmarshal(w.Body.Bytes(), &resp) + if err != nil { + t.Fatalf("failed to unmarshal body %s from JSON: %s", w.Body, err) + } + + if resp.Args.Encode() != "" { + t.Fatalf("expected empty args, got %s", resp.Args.Encode()) + } + if resp.Origin != "" { + t.Fatalf("expected empty origin, got %#v", resp.Origin) + } + if resp.URL != "http://localhost/get" { + t.Fatalf("unexpected url: %#v", resp.URL) + } + + var headerTests = []struct { + key string + expected 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)) + } + } +}