From add87e1051dc924d73ac9286c1fb9d41882c6001 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20=C5=81oskot?= Date: Sat, 21 Sep 2024 10:36:21 +0200 Subject: [PATCH 01/13] feat: add /env endpoint to allow exposing operator-controlled info from the server MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ref #114 Signed-off-by: Mateusz Łoskot --- httpbin/handlers.go | 17 +++++++++++++++++ httpbin/handlers_test.go | 10 ++++++++++ httpbin/httpbin.go | 1 + httpbin/responses.go | 4 ++++ 4 files changed, 32 insertions(+) diff --git a/httpbin/handlers.go b/httpbin/handlers.go index 16503f6..fdd6ab9 100644 --- a/httpbin/handlers.go +++ b/httpbin/handlers.go @@ -11,6 +11,7 @@ import ( "net/http" "net/http/httputil" "net/url" + "os" "strconv" "strings" "time" @@ -35,6 +36,22 @@ func (h *HTTPBin) Index(w http.ResponseWriter, r *http.Request) { writeHTML(w, h.indexHTML, http.StatusOK) } +// Env - returns environment variables with HTTPBIN_ prefix, if any pre-configured by operator +func (h *HTTPBin) Env(w http.ResponseWriter, _ *http.Request) { + variables := make(map[string]string) + for _, e := range os.Environ() { + v := strings.SplitN(e, "=", 2) + if !strings.HasPrefix(v[0], "HTTPBIN_") { + continue + } + variables[v[0]] = v[1] + } + + writeJSON(http.StatusOK, w, &environmentResponse{ + Environment: variables, + }) +} + // FormsPost renders an HTML form that submits a request to the /post endpoint func (h *HTTPBin) FormsPost(w http.ResponseWriter, _ *http.Request) { writeHTML(w, h.formsPostHTML, http.StatusOK) diff --git a/httpbin/handlers_test.go b/httpbin/handlers_test.go index 048c684..782b02a 100644 --- a/httpbin/handlers_test.go +++ b/httpbin/handlers_test.go @@ -120,6 +120,16 @@ func TestIndex(t *testing.T) { } } +func TestEnv(t *testing.T) { + t.Run("default environment", func(t *testing.T) { + t.Parallel() + req := newTestRequest(t, "GET", "/env") + resp := must.DoReq(t, client, req) + result := mustParseResponse[environmentResponse](t, resp) + assert.Equal(t, len(result.Environment), 0, "environment variables unexpeced") + }) +} + func TestFormsPost(t *testing.T) { t.Parallel() diff --git a/httpbin/httpbin.go b/httpbin/httpbin.go index 82ae6e6..3dfc99f 100644 --- a/httpbin/httpbin.go +++ b/httpbin/httpbin.go @@ -159,6 +159,7 @@ func (h *HTTPBin) Handler() http.Handler { mux.HandleFunc("/digest-auth/{qop}/{user}/{password}/{algorithm}", h.DigestAuth) mux.HandleFunc("/drip", h.Drip) mux.HandleFunc("/dump/request", h.DumpRequest) + mux.HandleFunc("/env", h.Env) mux.HandleFunc("/etag/{etag}", h.ETag) mux.HandleFunc("/gzip", h.Gzip) mux.HandleFunc("/headers", h.Headers) diff --git a/httpbin/responses.go b/httpbin/responses.go index 6b1e82b..ff7ba82 100644 --- a/httpbin/responses.go +++ b/httpbin/responses.go @@ -13,6 +13,10 @@ const ( textContentType = "text/plain; charset=utf-8" ) +type environmentResponse struct { + Environment map[string]string `json:"env"` +} + type headersResponse struct { Headers http.Header `json:"headers"` } From 69ee243d1c44219d6efd470df0e0a0bcb6aba564 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20=C5=81oskot?= Date: Sat, 21 Sep 2024 19:15:45 +0200 Subject: [PATCH 02/13] Add /env to index page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Mateusz Łoskot --- httpbin/static/index.html.tmpl | 1 + 1 file changed, 1 insertion(+) diff --git a/httpbin/static/index.html.tmpl b/httpbin/static/index.html.tmpl index d70030a..7ae3743 100644 --- a/httpbin/static/index.html.tmpl +++ b/httpbin/static/index.html.tmpl @@ -82,6 +82,7 @@
  • {{.Prefix}}/drip?numbytes=n&duration=s&delay=s&code=code Drips data over the given duration after an optional initial delay, simulating a slow HTTP server.
  • {{.Prefix}}/dump/request Returns the given request in its HTTP/1.x wire approximate representation.
  • {{.Prefix}}/encoding/utf8 Returns page containing UTF-8 data.
  • +
  • {{.Prefix}}/env Returns all environment variables named with HTTPBIN_ prefix.
  • {{.Prefix}}/etag/:etag Assumes the resource has the given etag and responds to If-None-Match header with a 200 or 304 and If-Match with a 200 or 412 as appropriate.
  • {{.Prefix}}/forms/post HTML form that submits to {{.Prefix}}/post
  • {{.Prefix}}/get Returns GET data.
  • From 9ca4fa95c314671a5c28079eec0bff0ebb1ddaff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20=C5=81oskot?= Date: Sat, 21 Sep 2024 20:16:19 +0200 Subject: [PATCH 03/13] Abbreviate [Ee]nvironment into [Ee]nv to match /env endpoint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Mateusz Łoskot --- httpbin/handlers.go | 4 ++-- httpbin/handlers_test.go | 4 ++-- httpbin/responses.go | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/httpbin/handlers.go b/httpbin/handlers.go index fdd6ab9..8342f9d 100644 --- a/httpbin/handlers.go +++ b/httpbin/handlers.go @@ -47,8 +47,8 @@ func (h *HTTPBin) Env(w http.ResponseWriter, _ *http.Request) { variables[v[0]] = v[1] } - writeJSON(http.StatusOK, w, &environmentResponse{ - Environment: variables, + writeJSON(http.StatusOK, w, &envResponse{ + Env: variables, }) } diff --git a/httpbin/handlers_test.go b/httpbin/handlers_test.go index 782b02a..27b1a5a 100644 --- a/httpbin/handlers_test.go +++ b/httpbin/handlers_test.go @@ -125,8 +125,8 @@ func TestEnv(t *testing.T) { t.Parallel() req := newTestRequest(t, "GET", "/env") resp := must.DoReq(t, client, req) - result := mustParseResponse[environmentResponse](t, resp) - assert.Equal(t, len(result.Environment), 0, "environment variables unexpeced") + result := mustParseResponse[envResponse](t, resp) + assert.Equal(t, len(result.Env), 0, "environment variables unexpected") }) } diff --git a/httpbin/responses.go b/httpbin/responses.go index ff7ba82..d05d525 100644 --- a/httpbin/responses.go +++ b/httpbin/responses.go @@ -13,8 +13,8 @@ const ( textContentType = "text/plain; charset=utf-8" ) -type environmentResponse struct { - Environment map[string]string `json:"env"` +type envResponse struct { + Env map[string]string `json:"env"` } type headersResponse struct { From ed202f73ddbaad81fefbc3aef53eba88e4488014 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20=C5=81oskot?= Date: Sat, 21 Sep 2024 20:52:46 +0200 Subject: [PATCH 04/13] Discover HTTPBIN_ env vars at process startup, not per request MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Mateusz Łoskot --- httpbin/cmd/cmd.go | 10 ++++++++++ httpbin/handlers.go | 12 +----------- httpbin/httpbin.go | 4 ++++ httpbin/options.go | 8 ++++++++ 4 files changed, 23 insertions(+), 11 deletions(-) diff --git a/httpbin/cmd/cmd.go b/httpbin/cmd/cmd.go index 08a9846..9fa3a10 100644 --- a/httpbin/cmd/cmd.go +++ b/httpbin/cmd/cmd.go @@ -74,10 +74,20 @@ func mainImpl(args []string, getEnv func(string) string, getHostname func() (str logger = slog.New(handler) } + env := make(map[string]string) + for _, e := range os.Environ() { + v := strings.SplitN(e, "=", 2) + if !strings.HasPrefix(v[0], "HTTPBIN_") { + continue + } + env[v[0]] = v[1] + } + opts := []httpbin.OptionFunc{ httpbin.WithMaxBodySize(cfg.MaxBodySize), httpbin.WithMaxDuration(cfg.MaxDuration), httpbin.WithObserver(httpbin.StdLogObserver(logger)), + httpbin.WithEnv(env), httpbin.WithExcludeHeaders(cfg.ExcludeHeaders), } if cfg.Prefix != "" { diff --git a/httpbin/handlers.go b/httpbin/handlers.go index 8342f9d..fbd3d46 100644 --- a/httpbin/handlers.go +++ b/httpbin/handlers.go @@ -11,7 +11,6 @@ import ( "net/http" "net/http/httputil" "net/url" - "os" "strconv" "strings" "time" @@ -38,17 +37,8 @@ func (h *HTTPBin) Index(w http.ResponseWriter, r *http.Request) { // Env - returns environment variables with HTTPBIN_ prefix, if any pre-configured by operator func (h *HTTPBin) Env(w http.ResponseWriter, _ *http.Request) { - variables := make(map[string]string) - for _, e := range os.Environ() { - v := strings.SplitN(e, "=", 2) - if !strings.HasPrefix(v[0], "HTTPBIN_") { - continue - } - variables[v[0]] = v[1] - } - writeJSON(http.StatusOK, w, &envResponse{ - Env: variables, + Env: h.env, }) } diff --git a/httpbin/httpbin.go b/httpbin/httpbin.go index 3dfc99f..7c60924 100644 --- a/httpbin/httpbin.go +++ b/httpbin/httpbin.go @@ -57,6 +57,10 @@ type HTTPBin struct { // Set of hosts to which the /redirect-to endpoint will allow redirects AllowedRedirectDomains map[string]struct{} + // The operator-controlled environment variables filtered from + // the process environment, based on named HTTPBIN_ prefix. + env map[string]string + // Pre-computed error message for the /redirect-to endpoint, based on // -allowed-redirect-domains/ALLOWED_REDIRECT_DOMAINS forbiddenRedirectError string diff --git a/httpbin/options.go b/httpbin/options.go index e697a1c..15f6090 100644 --- a/httpbin/options.go +++ b/httpbin/options.go @@ -46,6 +46,14 @@ func WithObserver(o Observer) OptionFunc { } } +// WithEnv sets the HTTPBIN_-prefixed environment variables reported +// by the /env endpoint. +func WithEnv(env map[string]string) OptionFunc { + return func(h *HTTPBin) { + h.env = env + } +} + // WithExcludeHeaders sets the headers to exclude in outgoing responses, to // prevent possible information leakage. func WithExcludeHeaders(excludeHeaders string) OptionFunc { From 056d0cf18caebb2c590c3f8aab75eeefbd51efde Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20=C5=81oskot?= Date: Sun, 22 Sep 2024 11:22:21 +0200 Subject: [PATCH 05/13] Prefer giving things human-friendly names MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Mateusz Łoskot --- httpbin/cmd/cmd.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/httpbin/cmd/cmd.go b/httpbin/cmd/cmd.go index 9fa3a10..8219001 100644 --- a/httpbin/cmd/cmd.go +++ b/httpbin/cmd/cmd.go @@ -74,20 +74,20 @@ func mainImpl(args []string, getEnv func(string) string, getHostname func() (str logger = slog.New(handler) } - env := make(map[string]string) - for _, e := range os.Environ() { - v := strings.SplitN(e, "=", 2) - if !strings.HasPrefix(v[0], "HTTPBIN_") { + httpbinEnv := make(map[string]string) + for _, envVar := range os.Environ() { + name, value, _ := strings.Cut(envVar, "=") + if !strings.HasPrefix(name, "HTTPBIN_") { continue } - env[v[0]] = v[1] + httpbinEnv[name] = value } opts := []httpbin.OptionFunc{ httpbin.WithMaxBodySize(cfg.MaxBodySize), httpbin.WithMaxDuration(cfg.MaxDuration), httpbin.WithObserver(httpbin.StdLogObserver(logger)), - httpbin.WithEnv(env), + httpbin.WithEnv(httpbinEnv), httpbin.WithExcludeHeaders(cfg.ExcludeHeaders), } if cfg.Prefix != "" { From f9e35451c9ba73802a9893f3a695b388ec92e42c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20=C5=81oskot?= Date: Sun, 22 Sep 2024 11:39:35 +0200 Subject: [PATCH 06/13] Prefer HTTPBIN_ENV_ as more specific env var prefix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This will help reinforce that only the special env vars are reported by /env endpoint. This is especially important in case of deploying the service to Kubernetes which by default sets lots of service name-prefixed variables which may be unintentionally listed by /env if the service is named httpbin which in fact is a common default. Signed-off-by: Mateusz Łoskot --- httpbin/cmd/cmd.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/httpbin/cmd/cmd.go b/httpbin/cmd/cmd.go index 8219001..27bc99b 100644 --- a/httpbin/cmd/cmd.go +++ b/httpbin/cmd/cmd.go @@ -77,7 +77,7 @@ func mainImpl(args []string, getEnv func(string) string, getHostname func() (str httpbinEnv := make(map[string]string) for _, envVar := range os.Environ() { name, value, _ := strings.Cut(envVar, "=") - if !strings.HasPrefix(name, "HTTPBIN_") { + if !strings.HasPrefix(name, "HTTPBIN_ENV_") { continue } httpbinEnv[name] = value From 88e6b2ec4fdc1d946ab03098801b35c72c92f381 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20=C5=81oskot?= Date: Sun, 22 Sep 2024 19:52:47 +0200 Subject: [PATCH 07/13] Allow to inject custom environment for better testability MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Mateusz Łoskot --- httpbin/cmd/cmd.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/httpbin/cmd/cmd.go b/httpbin/cmd/cmd.go index 27bc99b..0f75d87 100644 --- a/httpbin/cmd/cmd.go +++ b/httpbin/cmd/cmd.go @@ -35,13 +35,13 @@ const ( // Main is the main entrypoint for the go-httpbin binary. See loadConfig() for // command line argument parsing. func Main() int { - return mainImpl(os.Args[1:], os.Getenv, os.Hostname, os.Stderr) + return mainImpl(os.Args[1:], os.Getenv, os.Environ, os.Hostname, os.Stderr) } // mainImpl is the real implementation of Main(), extracted for better // testability. -func mainImpl(args []string, getEnv func(string) string, getHostname func() (string, error), out io.Writer) int { - cfg, err := loadConfig(args, getEnv, getHostname) +func mainImpl(args []string, getEnvVal func(string) string, getEnviron func() []string, getHostname func() (string, error), out io.Writer) int { + cfg, err := loadConfig(args, getEnvVal, getHostname) if err != nil { if cfgErr, ok := err.(ConfigError); ok { // for -h/-help, just print usage and exit without error @@ -75,7 +75,7 @@ func mainImpl(args []string, getEnv func(string) string, getHostname func() (str } httpbinEnv := make(map[string]string) - for _, envVar := range os.Environ() { + for _, envVar := range getEnviron() { name, value, _ := strings.Cut(envVar, "=") if !strings.HasPrefix(name, "HTTPBIN_ENV_") { continue From 78cb0f03c87f67c94854b65fc6c805942242c2a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20=C5=81oskot?= Date: Sun, 22 Sep 2024 20:04:19 +0200 Subject: [PATCH 08/13] test: Catch up mainImpl cases with new env injection parameter --- httpbin/cmd/cmd_test.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/httpbin/cmd/cmd_test.go b/httpbin/cmd/cmd_test.go index a061987..ba749fc 100644 --- a/httpbin/cmd/cmd_test.go +++ b/httpbin/cmd/cmd_test.go @@ -540,8 +540,9 @@ func TestMainImpl(t *testing.T) { testCases := map[string]struct { args []string - env map[string]string + optionsEnv map[string]string getHostname func() (string, error) + env []string wantCode int wantOut string wantOutFn func(t *testing.T, out string) @@ -606,7 +607,7 @@ func TestMainImpl(t *testing.T) { } buf := &bytes.Buffer{} - gotCode := mainImpl(tc.args, func(key string) string { return tc.env[key] }, tc.getHostname, buf) + gotCode := mainImpl(tc.args, func(key string) string { return tc.optionsEnv[key] }, func() []string { return tc.env }, tc.getHostname, buf) out := buf.String() if gotCode != tc.wantCode { From bd2accee3cf00576cd958206c3bbc30b0134795b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20=C5=81oskot?= Date: Sun, 22 Sep 2024 20:05:21 +0200 Subject: [PATCH 09/13] refactor: Rename config env to optionsEnv in test cases for purpose clarity --- httpbin/cmd/cmd_test.go | 69 +++++++++++++++++++++-------------------- 1 file changed, 35 insertions(+), 34 deletions(-) diff --git a/httpbin/cmd/cmd_test.go b/httpbin/cmd/cmd_test.go index ba749fc..c4c10e6 100644 --- a/httpbin/cmd/cmd_test.go +++ b/httpbin/cmd/cmd_test.go @@ -53,8 +53,9 @@ func TestLoadConfig(t *testing.T) { testCases := map[string]struct { args []string - env map[string]string + optionsEnv map[string]string getHostname func() (string, error) + env []string wantCfg *config wantErr error wantOut string @@ -83,8 +84,8 @@ func TestLoadConfig(t *testing.T) { wantErr: errors.New("invalid value \"foo\" for flag -max-body-size: parse error"), }, "invalid MAX_BODY_SIZE": { - env: map[string]string{"MAX_BODY_SIZE": "foo"}, - wantErr: errors.New("invalid value \"foo\" for env var MAX_BODY_SIZE: parse error"), + optionsEnv: map[string]string{"MAX_BODY_SIZE": "foo"}, + wantErr: errors.New("invalid value \"foo\" for env var MAX_BODY_SIZE: parse error"), }, "ok -max-body-size": { args: []string{"-max-body-size", "99"}, @@ -97,7 +98,7 @@ func TestLoadConfig(t *testing.T) { }, }, "ok MAX_BODY_SIZE": { - env: map[string]string{"MAX_BODY_SIZE": "9999"}, + optionsEnv: map[string]string{"MAX_BODY_SIZE": "9999"}, wantCfg: &config{ ListenHost: "0.0.0.0", ListenPort: 8080, @@ -107,8 +108,8 @@ func TestLoadConfig(t *testing.T) { }, }, "ok max body size CLI takes precedence over env": { - args: []string{"-max-body-size", "1234"}, - env: map[string]string{"MAX_BODY_SIZE": "5678"}, + args: []string{"-max-body-size", "1234"}, + optionsEnv: map[string]string{"MAX_BODY_SIZE": "5678"}, wantCfg: &config{ ListenHost: "0.0.0.0", ListenPort: 8080, @@ -124,8 +125,8 @@ func TestLoadConfig(t *testing.T) { wantErr: errors.New("invalid value \"foo\" for flag -max-duration: parse error"), }, "invalid MAX_DURATION": { - env: map[string]string{"MAX_DURATION": "foo"}, - wantErr: errors.New("invalid value \"foo\" for env var MAX_DURATION: parse error"), + optionsEnv: map[string]string{"MAX_DURATION": "foo"}, + wantErr: errors.New("invalid value \"foo\" for env var MAX_DURATION: parse error"), }, "ok -max-duration": { args: []string{"-max-duration", "99s"}, @@ -138,7 +139,7 @@ func TestLoadConfig(t *testing.T) { }, }, "ok MAX_DURATION": { - env: map[string]string{"MAX_DURATION": "9999s"}, + optionsEnv: map[string]string{"MAX_DURATION": "9999s"}, wantCfg: &config{ ListenHost: "0.0.0.0", ListenPort: 8080, @@ -148,8 +149,8 @@ func TestLoadConfig(t *testing.T) { }, }, "ok max duration size CLI takes precedence over env": { - args: []string{"-max-duration", "1234s"}, - env: map[string]string{"MAX_DURATION": "5678s"}, + args: []string{"-max-duration", "1234s"}, + optionsEnv: map[string]string{"MAX_DURATION": "5678s"}, wantCfg: &config{ ListenHost: "0.0.0.0", ListenPort: 8080, @@ -171,7 +172,7 @@ func TestLoadConfig(t *testing.T) { }, }, "ok HOST": { - env: map[string]string{"HOST": "192.0.0.2"}, + optionsEnv: map[string]string{"HOST": "192.0.0.2"}, wantCfg: &config{ ListenHost: "192.0.0.2", ListenPort: 8080, @@ -181,8 +182,8 @@ func TestLoadConfig(t *testing.T) { }, }, "ok host cli takes precedence over end": { - args: []string{"-host", "99.99.99.99"}, - env: map[string]string{"HOST": "11.11.11.11"}, + args: []string{"-host", "99.99.99.99"}, + optionsEnv: map[string]string{"HOST": "11.11.11.11"}, wantCfg: &config{ ListenHost: "99.99.99.99", ListenPort: 8080, @@ -198,8 +199,8 @@ func TestLoadConfig(t *testing.T) { wantErr: errors.New("invalid value \"foo\" for flag -port: parse error"), }, "invalid PORT": { - env: map[string]string{"PORT": "foo"}, - wantErr: errors.New("invalid value \"foo\" for env var PORT: parse error"), + optionsEnv: map[string]string{"PORT": "foo"}, + wantErr: errors.New("invalid value \"foo\" for env var PORT: parse error"), }, "ok -port": { args: []string{"-port", "99"}, @@ -212,7 +213,7 @@ func TestLoadConfig(t *testing.T) { }, }, "ok PORT": { - env: map[string]string{"PORT": "9999"}, + optionsEnv: map[string]string{"PORT": "9999"}, wantCfg: &config{ ListenHost: defaultListenHost, ListenPort: 9999, @@ -222,8 +223,8 @@ func TestLoadConfig(t *testing.T) { }, }, "ok port CLI takes precedence over env": { - args: []string{"-port", "1234"}, - env: map[string]string{"PORT": "5678"}, + args: []string{"-port", "1234"}, + optionsEnv: map[string]string{"PORT": "5678"}, wantCfg: &config{ ListenHost: defaultListenHost, ListenPort: 1234, @@ -243,8 +244,8 @@ func TestLoadConfig(t *testing.T) { wantErr: errors.New("Prefix \"/invalidprefix2/\" must not end with a slash"), }, "ok -prefix takes precedence over env": { - args: []string{"-prefix", "/prefix1"}, - env: map[string]string{"PREFIX": "/prefix2"}, + args: []string{"-prefix", "/prefix1"}, + optionsEnv: map[string]string{"PREFIX": "/prefix2"}, wantCfg: &config{ ListenHost: defaultListenHost, ListenPort: defaultListenPort, @@ -255,7 +256,7 @@ func TestLoadConfig(t *testing.T) { }, }, "ok PREFIX": { - env: map[string]string{"PREFIX": "/prefix2"}, + optionsEnv: map[string]string{"PREFIX": "/prefix2"}, wantCfg: &config{ ListenHost: defaultListenHost, ListenPort: defaultListenPort, @@ -291,7 +292,7 @@ func TestLoadConfig(t *testing.T) { }, }, "ok https env": { - env: map[string]string{ + optionsEnv: map[string]string{ "HTTPS_CERT_FILE": "/tmp/test.crt", "HTTPS_KEY_FILE": "/tmp/test.key", }, @@ -310,7 +311,7 @@ func TestLoadConfig(t *testing.T) { "-https-cert-file", "/tmp/cli.crt", "-https-key-file", "/tmp/cli.key", }, - env: map[string]string{ + optionsEnv: map[string]string{ "HTTPS_CERT_FILE": "/tmp/env.crt", "HTTPS_KEY_FILE": "/tmp/env.key", }, @@ -372,7 +373,7 @@ func TestLoadConfig(t *testing.T) { }, }, "ok USE_REAL_HOSTNAME=1": { - env: map[string]string{"USE_REAL_HOSTNAME": "1"}, + optionsEnv: map[string]string{"USE_REAL_HOSTNAME": "1"}, wantCfg: &config{ ListenHost: "0.0.0.0", ListenPort: 8080, @@ -383,7 +384,7 @@ func TestLoadConfig(t *testing.T) { }, }, "ok USE_REAL_HOSTNAME=true": { - env: map[string]string{"USE_REAL_HOSTNAME": "true"}, + optionsEnv: map[string]string{"USE_REAL_HOSTNAME": "true"}, wantCfg: &config{ ListenHost: "0.0.0.0", ListenPort: 8080, @@ -395,7 +396,7 @@ func TestLoadConfig(t *testing.T) { }, // case sensitive "ok USE_REAL_HOSTNAME=TRUE": { - env: map[string]string{"USE_REAL_HOSTNAME": "TRUE"}, + optionsEnv: map[string]string{"USE_REAL_HOSTNAME": "TRUE"}, wantCfg: &config{ ListenHost: "0.0.0.0", ListenPort: 8080, @@ -405,7 +406,7 @@ func TestLoadConfig(t *testing.T) { }, }, "ok USE_REAL_HOSTNAME=false": { - env: map[string]string{"USE_REAL_HOSTNAME": "false"}, + optionsEnv: map[string]string{"USE_REAL_HOSTNAME": "false"}, wantCfg: &config{ ListenHost: "0.0.0.0", ListenPort: 8080, @@ -415,7 +416,7 @@ func TestLoadConfig(t *testing.T) { }, }, "err real hostname error": { - env: map[string]string{"USE_REAL_HOSTNAME": "true"}, + optionsEnv: map[string]string{"USE_REAL_HOSTNAME": "true"}, getHostname: func() (string, error) { return "", errors.New("hostname error") }, wantErr: errors.New("could not look up real hostname: hostname error"), }, @@ -433,7 +434,7 @@ func TestLoadConfig(t *testing.T) { }, }, "ok ALLOWED_REDIRECT_DOMAINS": { - env: map[string]string{"ALLOWED_REDIRECT_DOMAINS": "foo,bar"}, + optionsEnv: map[string]string{"ALLOWED_REDIRECT_DOMAINS": "foo,bar"}, wantCfg: &config{ ListenHost: "0.0.0.0", ListenPort: 8080, @@ -444,8 +445,8 @@ func TestLoadConfig(t *testing.T) { }, }, "ok allowed redirect domains CLI takes precedence over env": { - args: []string{"-allowed-redirect-domains", "foo.cli,bar.cli"}, - env: map[string]string{"ALLOWED_REDIRECT_DOMAINS": "foo.env,bar.env"}, + args: []string{"-allowed-redirect-domains", "foo.cli,bar.cli"}, + optionsEnv: map[string]string{"ALLOWED_REDIRECT_DOMAINS": "foo.env,bar.env"}, wantCfg: &config{ ListenHost: "0.0.0.0", ListenPort: 8080, @@ -496,7 +497,7 @@ func TestLoadConfig(t *testing.T) { }, }, "ok use json log format using LOG_FORMAT env": { - env: map[string]string{"LOG_FORMAT": "json"}, + optionsEnv: map[string]string{"LOG_FORMAT": "json"}, wantCfg: &config{ ListenHost: "0.0.0.0", ListenPort: 8080, @@ -515,7 +516,7 @@ func TestLoadConfig(t *testing.T) { if tc.getHostname == nil { tc.getHostname = getHostnameDefault } - cfg, err := loadConfig(tc.args, func(key string) string { return tc.env[key] }, tc.getHostname) + cfg, err := loadConfig(tc.args, func(key string) string { return tc.optionsEnv[key] }, tc.getHostname) switch { case tc.wantErr != nil && err != nil: From a7aead5ede94c8e78bf0248c179a9a11e09cbffb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20=C5=81oskot?= Date: Mon, 23 Sep 2024 21:35:06 +0200 Subject: [PATCH 10/13] Update httpbin/static/index.html.tmpl Co-authored-by: Will McCutchen --- httpbin/static/index.html.tmpl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/httpbin/static/index.html.tmpl b/httpbin/static/index.html.tmpl index 7ae3743..0cf8b77 100644 --- a/httpbin/static/index.html.tmpl +++ b/httpbin/static/index.html.tmpl @@ -82,7 +82,7 @@
  • {{.Prefix}}/drip?numbytes=n&duration=s&delay=s&code=code Drips data over the given duration after an optional initial delay, simulating a slow HTTP server.
  • {{.Prefix}}/dump/request Returns the given request in its HTTP/1.x wire approximate representation.
  • {{.Prefix}}/encoding/utf8 Returns page containing UTF-8 data.
  • -
  • {{.Prefix}}/env Returns all environment variables named with HTTPBIN_ prefix.
  • +
  • {{.Prefix}}/env Returns all environment variables named with HTTPBIN_ENV_ prefix.
  • {{.Prefix}}/etag/:etag Assumes the resource has the given etag and responds to If-None-Match header with a 200 or 304 and If-Match with a 200 or 412 as appropriate.
  • {{.Prefix}}/forms/post HTML form that submits to {{.Prefix}}/post
  • {{.Prefix}}/get Returns GET data.
  • From 615fe9ff702af3e2c2272676e314cfc9bac55161 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20=C5=81oskot?= Date: Mon, 23 Sep 2024 21:21:32 +0200 Subject: [PATCH 11/13] Revert "refactor: Rename config env to optionsEnv in test cases for purpose clarity" This reverts commit bd2accee3cf00576cd958206c3bbc30b0134795b as too much noise --- httpbin/cmd/cmd_test.go | 69 ++++++++++++++++++++--------------------- 1 file changed, 34 insertions(+), 35 deletions(-) diff --git a/httpbin/cmd/cmd_test.go b/httpbin/cmd/cmd_test.go index c4c10e6..ba749fc 100644 --- a/httpbin/cmd/cmd_test.go +++ b/httpbin/cmd/cmd_test.go @@ -53,9 +53,8 @@ func TestLoadConfig(t *testing.T) { testCases := map[string]struct { args []string - optionsEnv map[string]string + env map[string]string getHostname func() (string, error) - env []string wantCfg *config wantErr error wantOut string @@ -84,8 +83,8 @@ func TestLoadConfig(t *testing.T) { wantErr: errors.New("invalid value \"foo\" for flag -max-body-size: parse error"), }, "invalid MAX_BODY_SIZE": { - optionsEnv: map[string]string{"MAX_BODY_SIZE": "foo"}, - wantErr: errors.New("invalid value \"foo\" for env var MAX_BODY_SIZE: parse error"), + env: map[string]string{"MAX_BODY_SIZE": "foo"}, + wantErr: errors.New("invalid value \"foo\" for env var MAX_BODY_SIZE: parse error"), }, "ok -max-body-size": { args: []string{"-max-body-size", "99"}, @@ -98,7 +97,7 @@ func TestLoadConfig(t *testing.T) { }, }, "ok MAX_BODY_SIZE": { - optionsEnv: map[string]string{"MAX_BODY_SIZE": "9999"}, + env: map[string]string{"MAX_BODY_SIZE": "9999"}, wantCfg: &config{ ListenHost: "0.0.0.0", ListenPort: 8080, @@ -108,8 +107,8 @@ func TestLoadConfig(t *testing.T) { }, }, "ok max body size CLI takes precedence over env": { - args: []string{"-max-body-size", "1234"}, - optionsEnv: map[string]string{"MAX_BODY_SIZE": "5678"}, + args: []string{"-max-body-size", "1234"}, + env: map[string]string{"MAX_BODY_SIZE": "5678"}, wantCfg: &config{ ListenHost: "0.0.0.0", ListenPort: 8080, @@ -125,8 +124,8 @@ func TestLoadConfig(t *testing.T) { wantErr: errors.New("invalid value \"foo\" for flag -max-duration: parse error"), }, "invalid MAX_DURATION": { - optionsEnv: map[string]string{"MAX_DURATION": "foo"}, - wantErr: errors.New("invalid value \"foo\" for env var MAX_DURATION: parse error"), + env: map[string]string{"MAX_DURATION": "foo"}, + wantErr: errors.New("invalid value \"foo\" for env var MAX_DURATION: parse error"), }, "ok -max-duration": { args: []string{"-max-duration", "99s"}, @@ -139,7 +138,7 @@ func TestLoadConfig(t *testing.T) { }, }, "ok MAX_DURATION": { - optionsEnv: map[string]string{"MAX_DURATION": "9999s"}, + env: map[string]string{"MAX_DURATION": "9999s"}, wantCfg: &config{ ListenHost: "0.0.0.0", ListenPort: 8080, @@ -149,8 +148,8 @@ func TestLoadConfig(t *testing.T) { }, }, "ok max duration size CLI takes precedence over env": { - args: []string{"-max-duration", "1234s"}, - optionsEnv: map[string]string{"MAX_DURATION": "5678s"}, + args: []string{"-max-duration", "1234s"}, + env: map[string]string{"MAX_DURATION": "5678s"}, wantCfg: &config{ ListenHost: "0.0.0.0", ListenPort: 8080, @@ -172,7 +171,7 @@ func TestLoadConfig(t *testing.T) { }, }, "ok HOST": { - optionsEnv: map[string]string{"HOST": "192.0.0.2"}, + env: map[string]string{"HOST": "192.0.0.2"}, wantCfg: &config{ ListenHost: "192.0.0.2", ListenPort: 8080, @@ -182,8 +181,8 @@ func TestLoadConfig(t *testing.T) { }, }, "ok host cli takes precedence over end": { - args: []string{"-host", "99.99.99.99"}, - optionsEnv: map[string]string{"HOST": "11.11.11.11"}, + args: []string{"-host", "99.99.99.99"}, + env: map[string]string{"HOST": "11.11.11.11"}, wantCfg: &config{ ListenHost: "99.99.99.99", ListenPort: 8080, @@ -199,8 +198,8 @@ func TestLoadConfig(t *testing.T) { wantErr: errors.New("invalid value \"foo\" for flag -port: parse error"), }, "invalid PORT": { - optionsEnv: map[string]string{"PORT": "foo"}, - wantErr: errors.New("invalid value \"foo\" for env var PORT: parse error"), + env: map[string]string{"PORT": "foo"}, + wantErr: errors.New("invalid value \"foo\" for env var PORT: parse error"), }, "ok -port": { args: []string{"-port", "99"}, @@ -213,7 +212,7 @@ func TestLoadConfig(t *testing.T) { }, }, "ok PORT": { - optionsEnv: map[string]string{"PORT": "9999"}, + env: map[string]string{"PORT": "9999"}, wantCfg: &config{ ListenHost: defaultListenHost, ListenPort: 9999, @@ -223,8 +222,8 @@ func TestLoadConfig(t *testing.T) { }, }, "ok port CLI takes precedence over env": { - args: []string{"-port", "1234"}, - optionsEnv: map[string]string{"PORT": "5678"}, + args: []string{"-port", "1234"}, + env: map[string]string{"PORT": "5678"}, wantCfg: &config{ ListenHost: defaultListenHost, ListenPort: 1234, @@ -244,8 +243,8 @@ func TestLoadConfig(t *testing.T) { wantErr: errors.New("Prefix \"/invalidprefix2/\" must not end with a slash"), }, "ok -prefix takes precedence over env": { - args: []string{"-prefix", "/prefix1"}, - optionsEnv: map[string]string{"PREFIX": "/prefix2"}, + args: []string{"-prefix", "/prefix1"}, + env: map[string]string{"PREFIX": "/prefix2"}, wantCfg: &config{ ListenHost: defaultListenHost, ListenPort: defaultListenPort, @@ -256,7 +255,7 @@ func TestLoadConfig(t *testing.T) { }, }, "ok PREFIX": { - optionsEnv: map[string]string{"PREFIX": "/prefix2"}, + env: map[string]string{"PREFIX": "/prefix2"}, wantCfg: &config{ ListenHost: defaultListenHost, ListenPort: defaultListenPort, @@ -292,7 +291,7 @@ func TestLoadConfig(t *testing.T) { }, }, "ok https env": { - optionsEnv: map[string]string{ + env: map[string]string{ "HTTPS_CERT_FILE": "/tmp/test.crt", "HTTPS_KEY_FILE": "/tmp/test.key", }, @@ -311,7 +310,7 @@ func TestLoadConfig(t *testing.T) { "-https-cert-file", "/tmp/cli.crt", "-https-key-file", "/tmp/cli.key", }, - optionsEnv: map[string]string{ + env: map[string]string{ "HTTPS_CERT_FILE": "/tmp/env.crt", "HTTPS_KEY_FILE": "/tmp/env.key", }, @@ -373,7 +372,7 @@ func TestLoadConfig(t *testing.T) { }, }, "ok USE_REAL_HOSTNAME=1": { - optionsEnv: map[string]string{"USE_REAL_HOSTNAME": "1"}, + env: map[string]string{"USE_REAL_HOSTNAME": "1"}, wantCfg: &config{ ListenHost: "0.0.0.0", ListenPort: 8080, @@ -384,7 +383,7 @@ func TestLoadConfig(t *testing.T) { }, }, "ok USE_REAL_HOSTNAME=true": { - optionsEnv: map[string]string{"USE_REAL_HOSTNAME": "true"}, + env: map[string]string{"USE_REAL_HOSTNAME": "true"}, wantCfg: &config{ ListenHost: "0.0.0.0", ListenPort: 8080, @@ -396,7 +395,7 @@ func TestLoadConfig(t *testing.T) { }, // case sensitive "ok USE_REAL_HOSTNAME=TRUE": { - optionsEnv: map[string]string{"USE_REAL_HOSTNAME": "TRUE"}, + env: map[string]string{"USE_REAL_HOSTNAME": "TRUE"}, wantCfg: &config{ ListenHost: "0.0.0.0", ListenPort: 8080, @@ -406,7 +405,7 @@ func TestLoadConfig(t *testing.T) { }, }, "ok USE_REAL_HOSTNAME=false": { - optionsEnv: map[string]string{"USE_REAL_HOSTNAME": "false"}, + env: map[string]string{"USE_REAL_HOSTNAME": "false"}, wantCfg: &config{ ListenHost: "0.0.0.0", ListenPort: 8080, @@ -416,7 +415,7 @@ func TestLoadConfig(t *testing.T) { }, }, "err real hostname error": { - optionsEnv: map[string]string{"USE_REAL_HOSTNAME": "true"}, + env: map[string]string{"USE_REAL_HOSTNAME": "true"}, getHostname: func() (string, error) { return "", errors.New("hostname error") }, wantErr: errors.New("could not look up real hostname: hostname error"), }, @@ -434,7 +433,7 @@ func TestLoadConfig(t *testing.T) { }, }, "ok ALLOWED_REDIRECT_DOMAINS": { - optionsEnv: map[string]string{"ALLOWED_REDIRECT_DOMAINS": "foo,bar"}, + env: map[string]string{"ALLOWED_REDIRECT_DOMAINS": "foo,bar"}, wantCfg: &config{ ListenHost: "0.0.0.0", ListenPort: 8080, @@ -445,8 +444,8 @@ func TestLoadConfig(t *testing.T) { }, }, "ok allowed redirect domains CLI takes precedence over env": { - args: []string{"-allowed-redirect-domains", "foo.cli,bar.cli"}, - optionsEnv: map[string]string{"ALLOWED_REDIRECT_DOMAINS": "foo.env,bar.env"}, + args: []string{"-allowed-redirect-domains", "foo.cli,bar.cli"}, + env: map[string]string{"ALLOWED_REDIRECT_DOMAINS": "foo.env,bar.env"}, wantCfg: &config{ ListenHost: "0.0.0.0", ListenPort: 8080, @@ -497,7 +496,7 @@ func TestLoadConfig(t *testing.T) { }, }, "ok use json log format using LOG_FORMAT env": { - optionsEnv: map[string]string{"LOG_FORMAT": "json"}, + env: map[string]string{"LOG_FORMAT": "json"}, wantCfg: &config{ ListenHost: "0.0.0.0", ListenPort: 8080, @@ -516,7 +515,7 @@ func TestLoadConfig(t *testing.T) { if tc.getHostname == nil { tc.getHostname = getHostnameDefault } - cfg, err := loadConfig(tc.args, func(key string) string { return tc.optionsEnv[key] }, tc.getHostname) + cfg, err := loadConfig(tc.args, func(key string) string { return tc.env[key] }, tc.getHostname) switch { case tc.wantErr != nil && err != nil: From bc6468e79603bcf7a86861f48732447f7708d218 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20=C5=81oskot?= Date: Mon, 23 Sep 2024 21:37:21 +0200 Subject: [PATCH 12/13] Reuse existing env in TestMainImpl MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Mateusz Łoskot --- httpbin/cmd/cmd_test.go | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/httpbin/cmd/cmd_test.go b/httpbin/cmd/cmd_test.go index ba749fc..7a76e39 100644 --- a/httpbin/cmd/cmd_test.go +++ b/httpbin/cmd/cmd_test.go @@ -4,6 +4,7 @@ import ( "bytes" "errors" "flag" + "fmt" "os" "reflect" "testing" @@ -540,9 +541,8 @@ func TestMainImpl(t *testing.T) { testCases := map[string]struct { args []string - optionsEnv map[string]string + env map[string]string getHostname func() (string, error) - env []string wantCode int wantOut string wantOutFn func(t *testing.T, out string) @@ -607,7 +607,7 @@ func TestMainImpl(t *testing.T) { } buf := &bytes.Buffer{} - gotCode := mainImpl(tc.args, func(key string) string { return tc.optionsEnv[key] }, func() []string { return tc.env }, tc.getHostname, buf) + gotCode := mainImpl(tc.args, func(key string) string { return tc.env[key] }, func() []string { return environSlice(tc.env) }, tc.getHostname, buf) out := buf.String() if gotCode != tc.wantCode { @@ -626,3 +626,11 @@ func TestMainImpl(t *testing.T) { }) } } + +func environSlice(env map[string]string) []string { + envStrings := make([]string, 0, len(env)) + for name, value := range env { + envStrings = append(envStrings, fmt.Sprintf("%s=%s", name, value)) + } + return envStrings +} From 9493ea4d1a7cb43d34e6bf3475498f78867f4df5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20=C5=81oskot?= Date: Fri, 27 Sep 2024 21:53:39 +0200 Subject: [PATCH 13/13] refactor: Move HTTPBIN_ENV_ from HTTPBin to config MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add test cases for config with HTTPBIN_ENV_ variables. Signed-off-by: Mateusz Łoskot --- httpbin/cmd/cmd.go | 75 ++++++++++++++++++++++------------------- httpbin/cmd/cmd_test.go | 45 ++++++++++++++++++++++++- 2 files changed, 84 insertions(+), 36 deletions(-) diff --git a/httpbin/cmd/cmd.go b/httpbin/cmd/cmd.go index 0f75d87..cdda27b 100644 --- a/httpbin/cmd/cmd.go +++ b/httpbin/cmd/cmd.go @@ -25,6 +25,7 @@ const ( defaultListenHost = "0.0.0.0" defaultListenPort = 8080 defaultLogFormat = "text" + defaultEnvPrefix = "HTTPBIN_ENV_" // Reasonable defaults for our http server srvReadTimeout = 5 * time.Second @@ -41,7 +42,7 @@ func Main() int { // mainImpl is the real implementation of Main(), extracted for better // testability. func mainImpl(args []string, getEnvVal func(string) string, getEnviron func() []string, getHostname func() (string, error), out io.Writer) int { - cfg, err := loadConfig(args, getEnvVal, getHostname) + cfg, err := loadConfig(args, getEnvVal, getEnviron, getHostname) if err != nil { if cfgErr, ok := err.(ConfigError); ok { // for -h/-help, just print usage and exit without error @@ -74,20 +75,11 @@ func mainImpl(args []string, getEnvVal func(string) string, getEnviron func() [] logger = slog.New(handler) } - httpbinEnv := make(map[string]string) - for _, envVar := range getEnviron() { - name, value, _ := strings.Cut(envVar, "=") - if !strings.HasPrefix(name, "HTTPBIN_ENV_") { - continue - } - httpbinEnv[name] = value - } - opts := []httpbin.OptionFunc{ + httpbin.WithEnv(cfg.Env), httpbin.WithMaxBodySize(cfg.MaxBodySize), httpbin.WithMaxDuration(cfg.MaxDuration), httpbin.WithObserver(httpbin.StdLogObserver(logger)), - httpbin.WithEnv(httpbinEnv), httpbin.WithExcludeHeaders(cfg.ExcludeHeaders), } if cfg.Prefix != "" { @@ -120,6 +112,7 @@ func mainImpl(args []string, getEnvVal func(string) string, getEnviron func() [] // config holds the configuration needed to initialize and run go-httpbin as a // standalone server. type config struct { + Env map[string]string AllowedRedirectDomains []string ListenHost string ExcludeHeaders string @@ -154,7 +147,7 @@ func (e ConfigError) Error() string { // loadConfig parses command line arguments and env vars into a fully resolved // Config struct. Command line arguments take precedence over env vars. -func loadConfig(args []string, getEnv func(string) string, getHostname func() (string, error)) (*config, error) { +func loadConfig(args []string, getEnvVal func(string) string, getEnviron func() []string, getHostname func() (string, error)) (*config, error) { cfg := &config{} fs := flag.NewFlagSet("go-httpbin", flag.ContinueOnError) @@ -202,24 +195,24 @@ func loadConfig(args []string, getEnv func(string) string, getHostname func() (s // Command line flags take precedence over environment vars, so we only // check for environment vars if we have default values for our command // line flags. - if cfg.MaxBodySize == httpbin.DefaultMaxBodySize && getEnv("MAX_BODY_SIZE") != "" { - cfg.MaxBodySize, err = strconv.ParseInt(getEnv("MAX_BODY_SIZE"), 10, 64) + if cfg.MaxBodySize == httpbin.DefaultMaxBodySize && getEnvVal("MAX_BODY_SIZE") != "" { + cfg.MaxBodySize, err = strconv.ParseInt(getEnvVal("MAX_BODY_SIZE"), 10, 64) if err != nil { - return nil, configErr("invalid value %#v for env var MAX_BODY_SIZE: parse error", getEnv("MAX_BODY_SIZE")) + return nil, configErr("invalid value %#v for env var MAX_BODY_SIZE: parse error", getEnvVal("MAX_BODY_SIZE")) } } - if cfg.MaxDuration == httpbin.DefaultMaxDuration && getEnv("MAX_DURATION") != "" { - cfg.MaxDuration, err = time.ParseDuration(getEnv("MAX_DURATION")) + if cfg.MaxDuration == httpbin.DefaultMaxDuration && getEnvVal("MAX_DURATION") != "" { + cfg.MaxDuration, err = time.ParseDuration(getEnvVal("MAX_DURATION")) if err != nil { - return nil, configErr("invalid value %#v for env var MAX_DURATION: parse error", getEnv("MAX_DURATION")) + return nil, configErr("invalid value %#v for env var MAX_DURATION: parse error", getEnvVal("MAX_DURATION")) } } - if cfg.ListenHost == defaultListenHost && getEnv("HOST") != "" { - cfg.ListenHost = getEnv("HOST") + if cfg.ListenHost == defaultListenHost && getEnvVal("HOST") != "" { + cfg.ListenHost = getEnvVal("HOST") } if cfg.Prefix == "" { - if prefix := getEnv("PREFIX"); prefix != "" { + if prefix := getEnvVal("PREFIX"); prefix != "" { cfg.Prefix = prefix } } @@ -231,29 +224,29 @@ func loadConfig(args []string, getEnv func(string) string, getHostname func() (s return nil, configErr("Prefix %#v must not end with a slash", cfg.Prefix) } } - if cfg.ExcludeHeaders == "" && getEnv("EXCLUDE_HEADERS") != "" { - cfg.ExcludeHeaders = getEnv("EXCLUDE_HEADERS") + if cfg.ExcludeHeaders == "" && getEnvVal("EXCLUDE_HEADERS") != "" { + cfg.ExcludeHeaders = getEnvVal("EXCLUDE_HEADERS") } - if cfg.ListenPort == defaultListenPort && getEnv("PORT") != "" { - cfg.ListenPort, err = strconv.Atoi(getEnv("PORT")) + if cfg.ListenPort == defaultListenPort && getEnvVal("PORT") != "" { + cfg.ListenPort, err = strconv.Atoi(getEnvVal("PORT")) if err != nil { - return nil, configErr("invalid value %#v for env var PORT: parse error", getEnv("PORT")) + return nil, configErr("invalid value %#v for env var PORT: parse error", getEnvVal("PORT")) } } - if cfg.TLSCertFile == "" && getEnv("HTTPS_CERT_FILE") != "" { - cfg.TLSCertFile = getEnv("HTTPS_CERT_FILE") + if cfg.TLSCertFile == "" && getEnvVal("HTTPS_CERT_FILE") != "" { + cfg.TLSCertFile = getEnvVal("HTTPS_CERT_FILE") } - if cfg.TLSKeyFile == "" && getEnv("HTTPS_KEY_FILE") != "" { - cfg.TLSKeyFile = getEnv("HTTPS_KEY_FILE") + if cfg.TLSKeyFile == "" && getEnvVal("HTTPS_KEY_FILE") != "" { + cfg.TLSKeyFile = getEnvVal("HTTPS_KEY_FILE") } if cfg.TLSCertFile != "" || cfg.TLSKeyFile != "" { if cfg.TLSCertFile == "" || cfg.TLSKeyFile == "" { return nil, configErr("https cert and key must both be provided") } } - if cfg.LogFormat == defaultLogFormat && getEnv("LOG_FORMAT") != "" { - cfg.LogFormat = getEnv("LOG_FORMAT") + if cfg.LogFormat == defaultLogFormat && getEnvVal("LOG_FORMAT") != "" { + cfg.LogFormat = getEnvVal("LOG_FORMAT") } if cfg.LogFormat != "text" && cfg.LogFormat != "json" { return nil, configErr(`invalid log format %q, must be "text" or "json"`, cfg.LogFormat) @@ -262,7 +255,7 @@ func loadConfig(args []string, getEnv func(string) string, getHostname func() (s // 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 := getEnv("USE_REAL_HOSTNAME"); useRealHostnameEnv == "1" || useRealHostnameEnv == "true" { + if useRealHostnameEnv := getEnvVal("USE_REAL_HOSTNAME"); useRealHostnameEnv == "1" || useRealHostnameEnv == "true" { cfg.rawUseRealHostname = true } if cfg.rawUseRealHostname { @@ -273,8 +266,8 @@ func loadConfig(args []string, getEnv func(string) string, getHostname func() (s } // split comma-separated list of domains into a slice, if given - if cfg.rawAllowedRedirectDomains == "" && getEnv("ALLOWED_REDIRECT_DOMAINS") != "" { - cfg.rawAllowedRedirectDomains = getEnv("ALLOWED_REDIRECT_DOMAINS") + if cfg.rawAllowedRedirectDomains == "" && getEnvVal("ALLOWED_REDIRECT_DOMAINS") != "" { + cfg.rawAllowedRedirectDomains = getEnvVal("ALLOWED_REDIRECT_DOMAINS") } for _, domain := range strings.Split(cfg.rawAllowedRedirectDomains, ",") { if strings.TrimSpace(domain) != "" { @@ -285,6 +278,18 @@ func loadConfig(args []string, getEnv func(string) string, getHostname func() (s // reset temporary fields to their zero values cfg.rawAllowedRedirectDomains = "" cfg.rawUseRealHostname = false + + for _, envVar := range getEnviron() { + name, value, _ := strings.Cut(envVar, "=") + if !strings.HasPrefix(name, defaultEnvPrefix) { + continue + } + if cfg.Env == nil { + cfg.Env = make(map[string]string) + } + cfg.Env[name] = value + } + return cfg, nil } diff --git a/httpbin/cmd/cmd_test.go b/httpbin/cmd/cmd_test.go index 7a76e39..ecf7065 100644 --- a/httpbin/cmd/cmd_test.go +++ b/httpbin/cmd/cmd_test.go @@ -78,6 +78,49 @@ func TestLoadConfig(t *testing.T) { wantErr: flag.ErrHelp, }, + // env + "ok env with empty variables": { + env: map[string]string{}, + wantCfg: &config{ + Env: nil, + ListenHost: "0.0.0.0", + ListenPort: 8080, + MaxBodySize: httpbin.DefaultMaxBodySize, + MaxDuration: httpbin.DefaultMaxDuration, + LogFormat: defaultLogFormat, + }, + }, + "ok env with recognized variables": { + env: map[string]string{ + fmt.Sprintf("%sFOO", defaultEnvPrefix): "foo", + fmt.Sprintf("%s%sBAR", defaultEnvPrefix, defaultEnvPrefix): "bar", + fmt.Sprintf("%s123", defaultEnvPrefix): "123", + }, + wantCfg: &config{ + Env: map[string]string{ + fmt.Sprintf("%sFOO", defaultEnvPrefix): "foo", + fmt.Sprintf("%s%sBAR", defaultEnvPrefix, defaultEnvPrefix): "bar", + fmt.Sprintf("%s123", defaultEnvPrefix): "123", + }, + ListenHost: "0.0.0.0", + ListenPort: 8080, + MaxBodySize: httpbin.DefaultMaxBodySize, + MaxDuration: httpbin.DefaultMaxDuration, + LogFormat: defaultLogFormat, + }, + }, + "ok env with unrecognized variables": { + env: map[string]string{"HTTPBIN_FOO": "foo", "BAR": "bar"}, + wantCfg: &config{ + Env: nil, + ListenHost: "0.0.0.0", + ListenPort: 8080, + MaxBodySize: httpbin.DefaultMaxBodySize, + MaxDuration: httpbin.DefaultMaxDuration, + LogFormat: defaultLogFormat, + }, + }, + // max body size "invalid -max-body-size": { args: []string{"-max-body-size", "foo"}, @@ -516,7 +559,7 @@ func TestLoadConfig(t *testing.T) { if tc.getHostname == nil { tc.getHostname = getHostnameDefault } - cfg, err := loadConfig(tc.args, func(key string) string { return tc.env[key] }, tc.getHostname) + cfg, err := loadConfig(tc.args, func(key string) string { return tc.env[key] }, func() []string { return environSlice(tc.env) }, tc.getHostname) switch { case tc.wantErr != nil && err != nil: