Skip to content

Commit 0defb3c

Browse files
authored
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.
1 parent 6a245c4 commit 0defb3c

File tree

7 files changed

+126
-27
lines changed

7 files changed

+126
-27
lines changed

Makefile

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ TOOL_BIN_DIR ?= $(shell go env GOPATH)/bin
1616
TOOL_GOLINT := $(TOOL_BIN_DIR)/golint
1717
TOOL_STATICCHECK := $(TOOL_BIN_DIR)/staticcheck
1818

19-
GO_SOURCES = $(wildcard **/*.go)
19+
GO_SOURCES = $(shell find . -name *.go)
2020

2121

2222
# =============================================================================

README.md

+27-18
Original file line numberDiff line numberDiff line change
@@ -6,29 +6,34 @@ A reasonably complete and well-tested golang port of [Kenneth Reitz][kr]'s
66
[![GoDoc](https://pkg.go.dev/badge/github.com/mccutchen/go-httpbin/v2)](https://pkg.go.dev/github.com/mccutchen/go-httpbin/v2)
77
[![Build status](https://github.com/mccutchen/go-httpbin/actions/workflows/test.yaml/badge.svg)](https://github.com/mccutchen/go-httpbin/actions/workflows/test.yaml)
88
[![Coverage](https://codecov.io/gh/mccutchen/go-httpbin/branch/main/graph/badge.svg)](https://codecov.io/gh/mccutchen/go-httpbin)
9+
[![Docker Pulls](https://badgen.net/docker/pulls/mccutchen/go-httpbin?icon=docker&label=pulls)](https://hub.docker.com/r/mccutchen/go-httpbin/)
910

1011

1112
## Usage
1213

13-
Run as a standalone binary, configured by command line flags or environment
14-
variables:
1514

16-
```
17-
$ go-httpbin --help
18-
Usage of go-httpbin:
19-
-host string
20-
Host to listen on (default "0.0.0.0")
21-
-https-cert-file string
22-
HTTPS Server certificate file
23-
-https-key-file string
24-
HTTPS Server private key file
25-
-max-body-size int
26-
Maximum size of request or response, in bytes (default 1048576)
27-
-max-duration duration
28-
Maximum duration a response may take (default 10s)
29-
-port int
30-
Port to listen on (default 8080)
31-
```
15+
### Configuration
16+
17+
go-httpbin can be configured via either command line arguments or environment
18+
variables (or a combination of the two):
19+
20+
| Argument| Env var | Documentation | Default |
21+
| - | - | - | - |
22+
| `-host` | `HOST` | Host to listen on | "0.0.0.0" |
23+
| `-https-cert-file` | `HTTPS_CERT_FILE` | HTTPS Server certificate file | |
24+
| `-https-key-file` | `HTTPS_KEY_FILE` | HTTPS Server private key file | |
25+
| `-max-body-size` | `MAX_BODY_SIZE` | Maximum size of request or response, in bytes | 1048576 |
26+
| `-max-duration` | `MAX_DURATION` | Maximum duration a response may take | 10s |
27+
| `-port` | `PORT` | Port to listen on | 8080 |
28+
| `-use-real-hostname` | `USE_REAL_HOSTNAME` | Expose real hostname as reported by os.Hostname() in the /hostname endpoint | false |
29+
30+
**Note:** Command line arguments take precedence over environment variables.
31+
32+
33+
### Standalone binary
34+
35+
Follow the [Installation](#installation) instructions to install go-httpbin as
36+
a standalone binary. (This currently requires a working Go runtime.)
3237

3338
Examples:
3439

@@ -43,6 +48,8 @@ $ openssl req -new -x509 -sha256 -key server.key -out server.crt -days 3650
4348
$ go-httpbin -host 127.0.0.1 -port 8081 -https-cert-file ./server.crt -https-key-file ./server.key
4449
```
4550

51+
### Docker
52+
4653
Docker images are published to [Docker Hub][docker-hub]:
4754

4855
```bash
@@ -53,6 +60,8 @@ $ docker run -P mccutchen/go-httpbin
5360
$ 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
5461
```
5562

63+
### Unit testing helper library
64+
5665
The `github.com/mccutchen/go-httpbin/httpbin/v2` package can also be used as a
5766
library for testing an application's interactions with an upstream HTTP
5867
service, like so:

cmd/go-httpbin/main.go

+26-8
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,13 @@ const (
2222
)
2323

2424
var (
25-
host string
26-
port int
27-
maxBodySize int64
28-
maxDuration time.Duration
29-
httpsCertFile string
30-
httpsKeyFile string
25+
host string
26+
port int
27+
maxBodySize int64
28+
maxDuration time.Duration
29+
httpsCertFile string
30+
httpsKeyFile string
31+
useRealHostname bool
3132
)
3233

3334
func main() {
@@ -37,6 +38,7 @@ func main() {
3738
flag.StringVar(&httpsKeyFile, "https-key-file", "", "HTTPS Server private key file")
3839
flag.Int64Var(&maxBodySize, "max-body-size", httpbin.DefaultMaxBodySize, "Maximum size of request or response, in bytes")
3940
flag.DurationVar(&maxDuration, "max-duration", httpbin.DefaultMaxDuration, "Maximum duration a response may take")
41+
flag.BoolVar(&useRealHostname, "use-real-hostname", false, "Expose value of os.Hostname() in the /hostname endpoint instead of dummy value")
4042
flag.Parse()
4143

4244
// Command line flags take precedence over environment vars, so we only
@@ -88,6 +90,13 @@ func main() {
8890
}
8991
}
9092

93+
// useRealHostname will be true if either the `-use-real-hostname`
94+
// arg is given on the command line or if the USE_REAL_HOSTNAME env var
95+
// is one of "1" or "true".
96+
if useRealHostnameEnv := os.Getenv("USE_REAL_HOSTNAME"); useRealHostnameEnv == "1" || useRealHostnameEnv == "true" {
97+
useRealHostname = true
98+
}
99+
91100
logger := log.New(os.Stderr, "", 0)
92101

93102
// A hacky log helper function to ensure that shutdown messages are
@@ -101,11 +110,20 @@ func main() {
101110
logger.Printf(logFmt, time.Now().Format(dateFmt), fmt.Sprintf(msg, args...))
102111
}
103112

104-
h := httpbin.New(
113+
opts := []httpbin.OptionFunc{
105114
httpbin.WithMaxBodySize(maxBodySize),
106115
httpbin.WithMaxDuration(maxDuration),
107116
httpbin.WithObserver(httpbin.StdLogObserver(logger)),
108-
)
117+
}
118+
if useRealHostname {
119+
hostname, err := os.Hostname()
120+
if err != nil {
121+
fmt.Fprintf(os.Stderr, "Error: use-real-hostname=true but hostname lookup failed: %s\n", err)
122+
os.Exit(1)
123+
}
124+
opts = append(opts, httpbin.WithHostname(hostname))
125+
}
126+
h := httpbin.New(opts...)
109127

110128
listenAddr := net.JoinHostPort(host, strconv.Itoa(port))
111129

httpbin/handlers.go

+8
Original file line numberDiff line numberDiff line change
@@ -1004,3 +1004,11 @@ func (h *HTTPBin) Bearer(w http.ResponseWriter, r *http.Request) {
10041004
})
10051005
writeJSON(w, body, http.StatusOK)
10061006
}
1007+
1008+
// Hostname - returns the hostname.
1009+
func (h *HTTPBin) Hostname(w http.ResponseWriter, r *http.Request) {
1010+
body, _ := json.Marshal(hostnameResponse{
1011+
Hostname: h.hostname,
1012+
})
1013+
writeJSON(w, body, http.StatusOK)
1014+
}

httpbin/handlers_test.go

+46
Original file line numberDiff line numberDiff line change
@@ -2571,3 +2571,49 @@ func TestNotImplemented(t *testing.T) {
25712571
})
25722572
}
25732573
}
2574+
2575+
func TestHostname(t *testing.T) {
2576+
t.Parallel()
2577+
2578+
loadResponse := func(t *testing.T, bodyBytes []byte) hostnameResponse {
2579+
var resp hostnameResponse
2580+
err := json.Unmarshal(bodyBytes, &resp)
2581+
if err != nil {
2582+
t.Fatalf("failed to unmarshal body %q from JSON: %s", string(bodyBytes), err)
2583+
}
2584+
return resp
2585+
}
2586+
2587+
t.Run("default hostname", func(t *testing.T) {
2588+
t.Parallel()
2589+
2590+
var (
2591+
handler = New().Handler()
2592+
r, _ = http.NewRequest("GET", "/hostname", nil)
2593+
w = httptest.NewRecorder()
2594+
)
2595+
handler.ServeHTTP(w, r)
2596+
assertStatusCode(t, w, http.StatusOK)
2597+
resp := loadResponse(t, w.Body.Bytes())
2598+
if resp.Hostname != DefaultHostname {
2599+
t.Errorf("expected hostname %q, got %q", DefaultHostname, resp.Hostname)
2600+
}
2601+
})
2602+
2603+
t.Run("real hostname", func(t *testing.T) {
2604+
t.Parallel()
2605+
2606+
var (
2607+
realHostname = "real-hostname"
2608+
handler = New(WithHostname(realHostname)).Handler()
2609+
r, _ = http.NewRequest("GET", "/hostname", nil)
2610+
w = httptest.NewRecorder()
2611+
)
2612+
handler.ServeHTTP(w, r)
2613+
assertStatusCode(t, w, http.StatusOK)
2614+
resp := loadResponse(t, w.Body.Bytes())
2615+
if resp.Hostname != realHostname {
2616+
t.Errorf("expected hostname %q, got %q", realHostname, resp.Hostname)
2617+
}
2618+
})
2619+
}

httpbin/httpbin.go

+17
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
const (
1111
DefaultMaxBodySize int64 = 1024 * 1024
1212
DefaultMaxDuration = 10 * time.Second
13+
DefaultHostname = "go-httpbin"
1314
)
1415

1516
const (
@@ -87,6 +88,10 @@ type bearerResponse struct {
8788
Token string `json:"token"`
8889
}
8990

91+
type hostnameResponse struct {
92+
Hostname string `json:"hostname"`
93+
}
94+
9095
// HTTPBin contains the business logic
9196
type HTTPBin struct {
9297
// Max size of an incoming request generated response body, in bytes
@@ -101,6 +106,9 @@ type HTTPBin struct {
101106

102107
// Default parameter values
103108
DefaultParams DefaultParams
109+
110+
// The hostname to expose via /hostname.
111+
hostname string
104112
}
105113

106114
// DefaultParams defines default parameter values
@@ -137,6 +145,7 @@ func (h *HTTPBin) Handler() http.Handler {
137145
mux.HandleFunc("/user-agent", h.UserAgent)
138146
mux.HandleFunc("/headers", h.Headers)
139147
mux.HandleFunc("/response-headers", h.ResponseHeaders)
148+
mux.HandleFunc("/hostname", h.Hostname)
140149

141150
mux.HandleFunc("/status/", h.Status)
142151
mux.HandleFunc("/unstable", h.Unstable)
@@ -222,6 +231,7 @@ func New(opts ...OptionFunc) *HTTPBin {
222231
MaxBodySize: DefaultMaxBodySize,
223232
MaxDuration: DefaultMaxDuration,
224233
DefaultParams: DefaultDefaultParams,
234+
hostname: DefaultHostname,
225235
}
226236
for _, opt := range opts {
227237
opt(h)
@@ -254,6 +264,13 @@ func WithMaxDuration(d time.Duration) OptionFunc {
254264
}
255265
}
256266

267+
// WithHostname sets the hostname to return via the /hostname endpoint.
268+
func WithHostname(s string) OptionFunc {
269+
return func(h *HTTPBin) {
270+
h.hostname = s
271+
}
272+
}
273+
257274
// WithObserver sets the request observer callback
258275
func WithObserver(o Observer) OptionFunc {
259276
return func(h *HTTPBin) {

httpbin/static/index.html

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)