Skip to content
Snippets Groups Projects
Unverified Commit 0defb3ce authored by Will McCutchen's avatar Will McCutchen Committed by GitHub
Browse files

Add /hostname endpoint (#81)

This adds a new /hostname endpoint as originally proposed in #66. In
this implementation, it exposes a dummy hostname by default, and only
exposes the real hostname (via `os.Hostname()`) if the
`-use-real-hostname` flag is given on the command line.
parent 6a245c41
No related branches found
No related tags found
No related merge requests found
...@@ -16,7 +16,7 @@ TOOL_BIN_DIR ?= $(shell go env GOPATH)/bin ...@@ -16,7 +16,7 @@ TOOL_BIN_DIR ?= $(shell go env GOPATH)/bin
TOOL_GOLINT := $(TOOL_BIN_DIR)/golint TOOL_GOLINT := $(TOOL_BIN_DIR)/golint
TOOL_STATICCHECK := $(TOOL_BIN_DIR)/staticcheck TOOL_STATICCHECK := $(TOOL_BIN_DIR)/staticcheck
GO_SOURCES = $(wildcard **/*.go) GO_SOURCES = $(shell find . -name *.go)
# ============================================================================= # =============================================================================
......
...@@ -6,29 +6,34 @@ A reasonably complete and well-tested golang port of [Kenneth Reitz][kr]'s ...@@ -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) [![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) [![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) [![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 ## Usage
Run as a standalone binary, configured by command line flags or environment
variables:
``` ### Configuration
$ go-httpbin --help
Usage of go-httpbin: go-httpbin can be configured via either command line arguments or environment
-host string variables (or a combination of the two):
Host to listen on (default "0.0.0.0")
-https-cert-file string | Argument| Env var | Documentation | Default |
HTTPS Server certificate file | - | - | - | - |
-https-key-file string | `-host` | `HOST` | Host to listen on | "0.0.0.0" |
HTTPS Server private key file | `-https-cert-file` | `HTTPS_CERT_FILE` | HTTPS Server certificate file | |
-max-body-size int | `-https-key-file` | `HTTPS_KEY_FILE` | HTTPS Server private key file | |
Maximum size of request or response, in bytes (default 1048576) | `-max-body-size` | `MAX_BODY_SIZE` | Maximum size of request or response, in bytes | 1048576 |
-max-duration duration | `-max-duration` | `MAX_DURATION` | Maximum duration a response may take | 10s |
Maximum duration a response may take (default 10s) | `-port` | `PORT` | Port to listen on | 8080 |
-port int | `-use-real-hostname` | `USE_REAL_HOSTNAME` | Expose real hostname as reported by os.Hostname() in the /hostname endpoint | false |
Port to listen on (default 8080)
``` **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: Examples:
...@@ -43,6 +48,8 @@ $ openssl req -new -x509 -sha256 -key server.key -out server.crt -days 3650 ...@@ -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 $ 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]: Docker images are published to [Docker Hub][docker-hub]:
```bash ```bash
...@@ -53,6 +60,8 @@ $ docker run -P mccutchen/go-httpbin ...@@ -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 $ 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 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 library for testing an application's interactions with an upstream HTTP
service, like so: service, like so:
......
...@@ -28,6 +28,7 @@ var ( ...@@ -28,6 +28,7 @@ var (
maxDuration time.Duration maxDuration time.Duration
httpsCertFile string httpsCertFile string
httpsKeyFile string httpsKeyFile string
useRealHostname bool
) )
func main() { func main() {
...@@ -37,6 +38,7 @@ func main() { ...@@ -37,6 +38,7 @@ func main() {
flag.StringVar(&httpsKeyFile, "https-key-file", "", "HTTPS Server private key file") 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.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.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() flag.Parse()
// Command line flags take precedence over environment vars, so we only // Command line flags take precedence over environment vars, so we only
...@@ -88,6 +90,13 @@ func main() { ...@@ -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) logger := log.New(os.Stderr, "", 0)
// A hacky log helper function to ensure that shutdown messages are // A hacky log helper function to ensure that shutdown messages are
...@@ -101,11 +110,20 @@ func main() { ...@@ -101,11 +110,20 @@ func main() {
logger.Printf(logFmt, time.Now().Format(dateFmt), fmt.Sprintf(msg, args...)) logger.Printf(logFmt, time.Now().Format(dateFmt), fmt.Sprintf(msg, args...))
} }
h := httpbin.New( opts := []httpbin.OptionFunc{
httpbin.WithMaxBodySize(maxBodySize), httpbin.WithMaxBodySize(maxBodySize),
httpbin.WithMaxDuration(maxDuration), httpbin.WithMaxDuration(maxDuration),
httpbin.WithObserver(httpbin.StdLogObserver(logger)), 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)) listenAddr := net.JoinHostPort(host, strconv.Itoa(port))
......
...@@ -1004,3 +1004,11 @@ func (h *HTTPBin) Bearer(w http.ResponseWriter, r *http.Request) { ...@@ -1004,3 +1004,11 @@ func (h *HTTPBin) Bearer(w http.ResponseWriter, r *http.Request) {
}) })
writeJSON(w, body, http.StatusOK) 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)
}
...@@ -2571,3 +2571,49 @@ func TestNotImplemented(t *testing.T) { ...@@ -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)
}
})
}
...@@ -10,6 +10,7 @@ import ( ...@@ -10,6 +10,7 @@ import (
const ( const (
DefaultMaxBodySize int64 = 1024 * 1024 DefaultMaxBodySize int64 = 1024 * 1024
DefaultMaxDuration = 10 * time.Second DefaultMaxDuration = 10 * time.Second
DefaultHostname = "go-httpbin"
) )
const ( const (
...@@ -87,6 +88,10 @@ type bearerResponse struct { ...@@ -87,6 +88,10 @@ type bearerResponse struct {
Token string `json:"token"` Token string `json:"token"`
} }
type hostnameResponse struct {
Hostname string `json:"hostname"`
}
// HTTPBin contains the business logic // HTTPBin contains the business logic
type HTTPBin struct { type HTTPBin struct {
// Max size of an incoming request generated response body, in bytes // Max size of an incoming request generated response body, in bytes
...@@ -101,6 +106,9 @@ type HTTPBin struct { ...@@ -101,6 +106,9 @@ type HTTPBin struct {
// Default parameter values // Default parameter values
DefaultParams DefaultParams DefaultParams DefaultParams
// The hostname to expose via /hostname.
hostname string
} }
// DefaultParams defines default parameter values // DefaultParams defines default parameter values
...@@ -137,6 +145,7 @@ func (h *HTTPBin) Handler() http.Handler { ...@@ -137,6 +145,7 @@ func (h *HTTPBin) Handler() http.Handler {
mux.HandleFunc("/user-agent", h.UserAgent) mux.HandleFunc("/user-agent", h.UserAgent)
mux.HandleFunc("/headers", h.Headers) mux.HandleFunc("/headers", h.Headers)
mux.HandleFunc("/response-headers", h.ResponseHeaders) mux.HandleFunc("/response-headers", h.ResponseHeaders)
mux.HandleFunc("/hostname", h.Hostname)
mux.HandleFunc("/status/", h.Status) mux.HandleFunc("/status/", h.Status)
mux.HandleFunc("/unstable", h.Unstable) mux.HandleFunc("/unstable", h.Unstable)
...@@ -222,6 +231,7 @@ func New(opts ...OptionFunc) *HTTPBin { ...@@ -222,6 +231,7 @@ func New(opts ...OptionFunc) *HTTPBin {
MaxBodySize: DefaultMaxBodySize, MaxBodySize: DefaultMaxBodySize,
MaxDuration: DefaultMaxDuration, MaxDuration: DefaultMaxDuration,
DefaultParams: DefaultDefaultParams, DefaultParams: DefaultDefaultParams,
hostname: DefaultHostname,
} }
for _, opt := range opts { for _, opt := range opts {
opt(h) opt(h)
...@@ -254,6 +264,13 @@ func WithMaxDuration(d time.Duration) OptionFunc { ...@@ -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 // WithObserver sets the request observer callback
func WithObserver(o Observer) OptionFunc { func WithObserver(o Observer) OptionFunc {
return func(h *HTTPBin) { return func(h *HTTPBin) {
......
...@@ -88,6 +88,7 @@ ...@@ -88,6 +88,7 @@
<li><a href="/headers"><code>/headers</code></a> Returns request header dict.</li> <li><a href="/headers"><code>/headers</code></a> Returns request header dict.</li>
<li><a href="/hidden-basic-auth/user/passwd"><code>/hidden-basic-auth/:user/:passwd</code></a> 404'd BasicAuth.</li> <li><a href="/hidden-basic-auth/user/passwd"><code>/hidden-basic-auth/:user/:passwd</code></a> 404'd BasicAuth.</li>
<li><a href="/html"><code>/html</code></a> Renders an HTML Page.</li> <li><a href="/html"><code>/html</code></a> Renders an HTML Page.</li>
<li><a href="/hostname"><code>/hostname</code></a> Returns the name of the host serving the request.</li>
<li><a href="/image"><code>/image</code></a> Returns page containing an image based on sent Accept header.</li> <li><a href="/image"><code>/image</code></a> Returns page containing an image based on sent Accept header.</li>
<li><a href="/image/jpeg"><code>/image/jpeg</code></a> Returns a JPEG image.</li> <li><a href="/image/jpeg"><code>/image/jpeg</code></a> Returns a JPEG image.</li>
<li><a href="/image/png"><code>/image/png</code></a> Returns a PNG image.</li> <li><a href="/image/png"><code>/image/png</code></a> Returns a PNG image.</li>
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment