diff --git a/Makefile b/Makefile index e5e0d135..b577d443 100644 --- a/Makefile +++ b/Makefile @@ -16,7 +16,7 @@ TOOL_BIN_DIR ?= $(shell go env GOPATH)/bin TOOL_GOLINT := $(TOOL_BIN_DIR)/golint TOOL_STATICCHECK := $(TOOL_BIN_DIR)/staticcheck -GO_SOURCES = $(wildcard **/*.go) +GO_SOURCES = $(shell find . -name *.go) # ============================================================================= diff --git a/README.md b/README.md index c9542097..0b058630 100644 --- a/README.md +++ b/README.md @@ -6,29 +6,34 @@ A reasonably complete and well-tested golang port of [Kenneth Reitz][kr]'s [![GoDoc](https://pkg.go.dev/badge/github.com/mccutchen/go-httpbin/v2)](https://pkg.go.dev/github.com/mccutchen/go-httpbin/v2) [![Build status](https://github.com/mccutchen/go-httpbin/actions/workflows/test.yaml/badge.svg)](https://github.com/mccutchen/go-httpbin/actions/workflows/test.yaml) [![Coverage](https://codecov.io/gh/mccutchen/go-httpbin/branch/main/graph/badge.svg)](https://codecov.io/gh/mccutchen/go-httpbin) +[![Docker Pulls](https://badgen.net/docker/pulls/mccutchen/go-httpbin?icon=docker&label=pulls)](https://hub.docker.com/r/mccutchen/go-httpbin/) ## Usage -Run as a standalone binary, configured by command line flags or environment -variables: -``` -$ go-httpbin --help -Usage of go-httpbin: - -host string - Host to listen on (default "0.0.0.0") - -https-cert-file string - HTTPS Server certificate file - -https-key-file string - HTTPS Server private key file - -max-body-size int - Maximum size of request or response, in bytes (default 1048576) - -max-duration duration - Maximum duration a response may take (default 10s) - -port int - Port to listen on (default 8080) -``` +### Configuration + +go-httpbin can be configured via either command line arguments or environment +variables (or a combination of the two): + +| Argument| Env var | Documentation | Default | +| - | - | - | - | +| `-host` | `HOST` | Host to listen on | "0.0.0.0" | +| `-https-cert-file` | `HTTPS_CERT_FILE` | HTTPS Server certificate file | | +| `-https-key-file` | `HTTPS_KEY_FILE` | HTTPS Server private key file | | +| `-max-body-size` | `MAX_BODY_SIZE` | Maximum size of request or response, in bytes | 1048576 | +| `-max-duration` | `MAX_DURATION` | Maximum duration a response may take | 10s | +| `-port` | `PORT` | Port to listen on | 8080 | +| `-use-real-hostname` | `USE_REAL_HOSTNAME` | Expose real hostname as reported by os.Hostname() in the /hostname endpoint | false | + +**Note:** Command line arguments take precedence over environment variables. + + +### Standalone binary + +Follow the [Installation](#installation) instructions to install go-httpbin as +a standalone binary. (This currently requires a working Go runtime.) Examples: @@ -43,6 +48,8 @@ $ openssl req -new -x509 -sha256 -key server.key -out server.crt -days 3650 $ go-httpbin -host 127.0.0.1 -port 8081 -https-cert-file ./server.crt -https-key-file ./server.key ``` +### Docker + Docker images are published to [Docker Hub][docker-hub]: ```bash @@ -53,6 +60,8 @@ $ docker run -P mccutchen/go-httpbin $ docker run -e HTTPS_CERT_FILE='/tmp/server.crt' -e HTTPS_KEY_FILE='/tmp/server.key' -p 8080:8080 -v /tmp:/tmp mccutchen/go-httpbin ``` +### Unit testing helper library + The `github.com/mccutchen/go-httpbin/httpbin/v2` package can also be used as a library for testing an application's interactions with an upstream HTTP service, like so: diff --git a/cmd/go-httpbin/main.go b/cmd/go-httpbin/main.go index 0743a117..b9196f4e 100644 --- a/cmd/go-httpbin/main.go +++ b/cmd/go-httpbin/main.go @@ -22,12 +22,13 @@ const ( ) var ( - host string - port int - maxBodySize int64 - maxDuration time.Duration - httpsCertFile string - httpsKeyFile string + host string + port int + maxBodySize int64 + maxDuration time.Duration + httpsCertFile string + httpsKeyFile string + useRealHostname bool ) func main() { @@ -37,6 +38,7 @@ func main() { flag.StringVar(&httpsKeyFile, "https-key-file", "", "HTTPS Server private key file") flag.Int64Var(&maxBodySize, "max-body-size", httpbin.DefaultMaxBodySize, "Maximum size of request or response, in bytes") flag.DurationVar(&maxDuration, "max-duration", httpbin.DefaultMaxDuration, "Maximum duration a response may take") + flag.BoolVar(&useRealHostname, "use-real-hostname", false, "Expose value of os.Hostname() in the /hostname endpoint instead of dummy value") flag.Parse() // Command line flags take precedence over environment vars, so we only @@ -88,6 +90,13 @@ func main() { } } + // useRealHostname will be true if either the `-use-real-hostname` + // arg is given on the command line or if the USE_REAL_HOSTNAME env var + // is one of "1" or "true". + if useRealHostnameEnv := os.Getenv("USE_REAL_HOSTNAME"); useRealHostnameEnv == "1" || useRealHostnameEnv == "true" { + useRealHostname = true + } + logger := log.New(os.Stderr, "", 0) // A hacky log helper function to ensure that shutdown messages are @@ -101,11 +110,20 @@ func main() { logger.Printf(logFmt, time.Now().Format(dateFmt), fmt.Sprintf(msg, args...)) } - h := httpbin.New( + opts := []httpbin.OptionFunc{ httpbin.WithMaxBodySize(maxBodySize), httpbin.WithMaxDuration(maxDuration), httpbin.WithObserver(httpbin.StdLogObserver(logger)), - ) + } + if useRealHostname { + hostname, err := os.Hostname() + if err != nil { + fmt.Fprintf(os.Stderr, "Error: use-real-hostname=true but hostname lookup failed: %s\n", err) + os.Exit(1) + } + opts = append(opts, httpbin.WithHostname(hostname)) + } + h := httpbin.New(opts...) listenAddr := net.JoinHostPort(host, strconv.Itoa(port)) diff --git a/httpbin/handlers.go b/httpbin/handlers.go index 3ff4ccfb..ebb2c331 100644 --- a/httpbin/handlers.go +++ b/httpbin/handlers.go @@ -1004,3 +1004,11 @@ func (h *HTTPBin) Bearer(w http.ResponseWriter, r *http.Request) { }) writeJSON(w, body, http.StatusOK) } + +// Hostname - returns the hostname. +func (h *HTTPBin) Hostname(w http.ResponseWriter, r *http.Request) { + body, _ := json.Marshal(hostnameResponse{ + Hostname: h.hostname, + }) + writeJSON(w, body, http.StatusOK) +} diff --git a/httpbin/handlers_test.go b/httpbin/handlers_test.go index 8c4a31c8..b27d76f0 100644 --- a/httpbin/handlers_test.go +++ b/httpbin/handlers_test.go @@ -2571,3 +2571,49 @@ func TestNotImplemented(t *testing.T) { }) } } + +func TestHostname(t *testing.T) { + t.Parallel() + + loadResponse := func(t *testing.T, bodyBytes []byte) hostnameResponse { + var resp hostnameResponse + err := json.Unmarshal(bodyBytes, &resp) + if err != nil { + t.Fatalf("failed to unmarshal body %q from JSON: %s", string(bodyBytes), err) + } + return resp + } + + t.Run("default hostname", func(t *testing.T) { + t.Parallel() + + var ( + handler = New().Handler() + r, _ = http.NewRequest("GET", "/hostname", nil) + w = httptest.NewRecorder() + ) + handler.ServeHTTP(w, r) + assertStatusCode(t, w, http.StatusOK) + resp := loadResponse(t, w.Body.Bytes()) + if resp.Hostname != DefaultHostname { + t.Errorf("expected hostname %q, got %q", DefaultHostname, resp.Hostname) + } + }) + + t.Run("real hostname", func(t *testing.T) { + t.Parallel() + + var ( + realHostname = "real-hostname" + handler = New(WithHostname(realHostname)).Handler() + r, _ = http.NewRequest("GET", "/hostname", nil) + w = httptest.NewRecorder() + ) + handler.ServeHTTP(w, r) + assertStatusCode(t, w, http.StatusOK) + resp := loadResponse(t, w.Body.Bytes()) + if resp.Hostname != realHostname { + t.Errorf("expected hostname %q, got %q", realHostname, resp.Hostname) + } + }) +} diff --git a/httpbin/httpbin.go b/httpbin/httpbin.go index d7336ad9..7f5fa9e0 100644 --- a/httpbin/httpbin.go +++ b/httpbin/httpbin.go @@ -10,6 +10,7 @@ import ( const ( DefaultMaxBodySize int64 = 1024 * 1024 DefaultMaxDuration = 10 * time.Second + DefaultHostname = "go-httpbin" ) const ( @@ -87,6 +88,10 @@ type bearerResponse struct { Token string `json:"token"` } +type hostnameResponse struct { + Hostname string `json:"hostname"` +} + // HTTPBin contains the business logic type HTTPBin struct { // Max size of an incoming request generated response body, in bytes @@ -101,6 +106,9 @@ type HTTPBin struct { // Default parameter values DefaultParams DefaultParams + + // The hostname to expose via /hostname. + hostname string } // DefaultParams defines default parameter values @@ -137,6 +145,7 @@ func (h *HTTPBin) Handler() http.Handler { mux.HandleFunc("/user-agent", h.UserAgent) mux.HandleFunc("/headers", h.Headers) mux.HandleFunc("/response-headers", h.ResponseHeaders) + mux.HandleFunc("/hostname", h.Hostname) mux.HandleFunc("/status/", h.Status) mux.HandleFunc("/unstable", h.Unstable) @@ -222,6 +231,7 @@ func New(opts ...OptionFunc) *HTTPBin { MaxBodySize: DefaultMaxBodySize, MaxDuration: DefaultMaxDuration, DefaultParams: DefaultDefaultParams, + hostname: DefaultHostname, } for _, opt := range opts { opt(h) @@ -254,6 +264,13 @@ func WithMaxDuration(d time.Duration) OptionFunc { } } +// WithHostname sets the hostname to return via the /hostname endpoint. +func WithHostname(s string) OptionFunc { + return func(h *HTTPBin) { + h.hostname = s + } +} + // WithObserver sets the request observer callback func WithObserver(o Observer) OptionFunc { return func(h *HTTPBin) { diff --git a/httpbin/static/index.html b/httpbin/static/index.html index e9ed07b7..a4fa3c00 100644 --- a/httpbin/static/index.html +++ b/httpbin/static/index.html @@ -88,6 +88,7 @@

ENDPOINTS

  • /headers Returns request header dict.
  • /hidden-basic-auth/:user/:passwd 404'd BasicAuth.
  • /html Renders an HTML Page.
  • +
  • /hostname Returns the name of the host serving the request.
  • /image Returns page containing an image based on sent Accept header.
  • /image/jpeg Returns a JPEG image.
  • /image/png Returns a PNG image.