From e857c0309b4e541f9554fdec086ca4510c3d23a1 Mon Sep 17 00:00:00 2001 From: Paramtamtam <7326800+tarampampam@users.noreply.github.com> Date: Wed, 23 Feb 2022 11:09:54 +0500 Subject: [PATCH] proxy headers (#67) --- .github/workflows/tests.yml | 2 +- CHANGELOG.md | 6 +++ docker-compose.yml | 1 + internal/cli/serve/command.go | 12 ++++- internal/cli/serve/flags.go | 56 +++++++++++++++++++++ internal/env/env.go | 1 + internal/env/env_test.go | 2 + internal/http/common/middlewares.go | 31 +++++++++--- internal/http/core/errorpage.go | 10 +++- internal/http/handlers/errorpage/handler.go | 10 +++- internal/http/handlers/index/handler.go | 3 +- internal/http/server.go | 5 +- test/hurl/proxy_headers.hurl | 13 +++++ 13 files changed, 137 insertions(+), 15 deletions(-) create mode 100644 test/hurl/proxy_headers.hurl diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index f59fd7c..137f4db 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -227,7 +227,7 @@ jobs: # Docs: run: sudo dpkg -i hurl.deb - name: Run container with the app - run: docker run --rm -d -p "8080:8080/tcp" -e "SHOW_DETAILS=true" --name app app:ci + run: docker run --rm -d -p "8080:8080/tcp" -e "SHOW_DETAILS=true" -e "PROXY_HTTP_HEADERS=X-Foo,Bar,Baz_blah" --name app app:ci - name: Wait for container "healthy" state run: until [[ "`docker inspect -f {{.State.Health.Status}} app`" == "healthy" ]]; do echo "wait 1 sec.."; sleep 1; done diff --git a/CHANGELOG.md b/CHANGELOG.md index 22aea84..01c2492 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,14 +6,20 @@ The format is based on [Keep a Changelog][keepachangelog] and this project adher ## UNRELEASED +### Changed + +- Logs includes request/response headers now [#67] + ### Added +- Possibility to proxy HTTP headers from the requests to the responses (can be enabled using `--proxy-headers` flag for the `serve` command or environment variable `PROXY_HTTP_HEADERS`, comma-separated) [#67] - Template `lost-in-space` [#68] ### Fixed - Template `l7-light` uses the dark colors in browsers with the preferred dark theme +[#67]:https://github.com/tarampampam/error-pages/pull/67 [#68]:https://github.com/tarampampam/error-pages/pull/68 ## v2.6.0 diff --git a/docker-compose.yml b/docker-compose.yml index bde5e9a..972d0b2 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -31,6 +31,7 @@ services: - --verbose - --port=8080 - --show-details + - --proxy-headers=X-Foo,Bar,Baz_blah healthcheck: test: ['CMD', 'wget', '--spider', '-q', 'http://127.0.0.1:8080/healthz'] interval: 5s diff --git a/internal/cli/serve/command.go b/internal/cli/serve/command.go index e73be58..2e84b16 100644 --- a/internal/cli/serve/command.go +++ b/internal/cli/serve/command.go @@ -107,11 +107,20 @@ func run(parentCtx context.Context, log *zap.Logger, f flags, cfg *config.Config } } + var proxyHTTPHeaders = f.HeadersToProxy() + // create HTTP server server := appHttp.NewServer(log) // register server routes, middlewares, etc. - if err := server.Register(cfg, picker, f.defaultErrorPage, f.defaultHTTPCode, f.showDetails); err != nil { + if err := server.Register( + cfg, + picker, + f.defaultErrorPage, + f.defaultHTTPCode, + f.showDetails, + proxyHTTPHeaders, + ); err != nil { return err } @@ -126,6 +135,7 @@ func run(parentCtx context.Context, log *zap.Logger, f flags, cfg *config.Config zap.Uint16("port", f.listen.port), zap.String("default error page", f.defaultErrorPage), zap.Uint16("default HTTP response code", f.defaultHTTPCode), + zap.Strings("proxy headers", proxyHTTPHeaders), zap.Bool("show request details", f.showDetails), ) diff --git a/internal/cli/serve/flags.go b/internal/cli/serve/flags.go index b474c99..bb40450 100644 --- a/internal/cli/serve/flags.go +++ b/internal/cli/serve/flags.go @@ -3,6 +3,7 @@ package serve import ( "fmt" "net" + "sort" "strconv" "strings" @@ -21,6 +22,45 @@ type flags struct { defaultErrorPage string defaultHTTPCode uint16 showDetails bool + proxyHTTPHeaders string // comma-separated +} + +// HeadersToProxy converts a comma-separated string with headers list into strings slice (with a sorting and without +// duplicates). +func (f *flags) HeadersToProxy() []string { + var raw = strings.Split(f.proxyHTTPHeaders, ",") + + if len(raw) == 0 { + return []string{} + } else if len(raw) == 1 { + if h := strings.TrimSpace(raw[0]); h != "" { + return []string{h} + } else { + return []string{} + } + } + + var m = make(map[string]struct{}, len(raw)) + + // make unique and ignore empty strings + for _, h := range raw { + if h = strings.TrimSpace(h); h != "" { + if _, ok := m[h]; !ok { + m[h] = struct{}{} + } + } + } + + // convert map into slice + var headers = make([]string, 0, len(m)) + for h := range m { + headers = append(headers, h) + } + + // make sort + sort.Strings(headers) + + return headers } const ( @@ -30,6 +70,7 @@ const ( defaultErrorPageFlagName = "default-error-page" defaultHTTPCodeFlagName = "default-http-code" showDetailsFlagName = "show-details" + proxyHTTPHeadersFlagName = "proxy-headers" ) const ( @@ -84,6 +125,12 @@ func (f *flags) init(flagSet *pflag.FlagSet) { false, fmt.Sprintf("show request details in response [$%s]", env.ShowDetails), ) + flagSet.StringVarP( + &f.proxyHTTPHeaders, + proxyHTTPHeadersFlagName, "", + "", + fmt.Sprintf("proxy HTTP request headers list (comma-separated) [$%s]", env.ProxyHTTPHeaders), + ) } func (f *flags) overrideUsingEnv(flagSet *pflag.FlagSet) (lastErr error) { //nolint:gocognit,gocyclo @@ -130,6 +177,11 @@ func (f *flags) overrideUsingEnv(flagSet *pflag.FlagSet) (lastErr error) { //nol f.showDetails = b } } + + case proxyHTTPHeadersFlagName: + if envVar, exists := env.ProxyHTTPHeaders.Lookup(); exists { + f.proxyHTTPHeaders = strings.TrimSpace(envVar) + } } } }) @@ -146,5 +198,9 @@ func (f *flags) validate() error { return fmt.Errorf("wrong default HTTP response code [%d]", f.defaultHTTPCode) } + if strings.ContainsRune(f.proxyHTTPHeaders, ' ') { + return fmt.Errorf("whitespaces in the HTTP headers for proxying [%s] are not allowed", f.proxyHTTPHeaders) + } + return nil } diff --git a/internal/env/env.go b/internal/env/env.go index f834f86..e2c3dae 100644 --- a/internal/env/env.go +++ b/internal/env/env.go @@ -13,6 +13,7 @@ const ( DefaultErrorPage envVariable = "DEFAULT_ERROR_PAGE" // default error page (code) DefaultHTTPCode envVariable = "DEFAULT_HTTP_CODE" // default HTTP response code ShowDetails envVariable = "SHOW_DETAILS" // show request details in response + ProxyHTTPHeaders envVariable = "PROXY_HTTP_HEADERS" // proxy HTTP request headers list (request -> response) ) // String returns environment variable name in the string representation. diff --git a/internal/env/env_test.go b/internal/env/env_test.go index a97dd7c..9ba607f 100644 --- a/internal/env/env_test.go +++ b/internal/env/env_test.go @@ -15,6 +15,7 @@ func TestConstants(t *testing.T) { assert.Equal(t, "DEFAULT_ERROR_PAGE", string(DefaultErrorPage)) assert.Equal(t, "DEFAULT_HTTP_CODE", string(DefaultHTTPCode)) assert.Equal(t, "SHOW_DETAILS", string(ShowDetails)) + assert.Equal(t, "PROXY_HTTP_HEADERS", string(ProxyHTTPHeaders)) } func TestEnvVariable_Lookup(t *testing.T) { @@ -28,6 +29,7 @@ func TestEnvVariable_Lookup(t *testing.T) { {giveEnv: DefaultErrorPage}, {giveEnv: DefaultHTTPCode}, {giveEnv: ShowDetails}, + {giveEnv: ProxyHTTPHeaders}, } for _, tt := range cases { diff --git a/internal/http/common/middlewares.go b/internal/http/common/middlewares.go index 79a14d1..5c087e4 100644 --- a/internal/http/common/middlewares.go +++ b/internal/http/common/middlewares.go @@ -9,17 +9,32 @@ import ( ) func LogRequest(h fasthttp.RequestHandler, log *zap.Logger) fasthttp.RequestHandler { + const headersSeparator = ": " + return func(ctx *fasthttp.RequestCtx) { - var ( - startedAt = time.Now() - ua = string(ctx.UserAgent()) - ) + var ua = string(ctx.UserAgent()) + + if strings.Contains(strings.ToLower(ua), "healthcheck") { // skip healthcheck requests logging + h(ctx) + + return + } + + var reqHeaders = make([]string, 0, 24) //nolint:gomnd + + ctx.Request.Header.VisitAll(func(key, value []byte) { + reqHeaders = append(reqHeaders, string(key)+headersSeparator+string(value)) + }) + + var startedAt = time.Now() h(ctx) - if strings.Contains(strings.ToLower(ua), "healthcheck") { // skip healthcheck requests logging - return - } + var respHeaders = make([]string, 0, 16) //nolint:gomnd + + ctx.Response.Header.VisitAll(func(key, value []byte) { + respHeaders = append(respHeaders, string(key)+headersSeparator+string(value)) + }) log.Info("HTTP request processed", zap.String("useragent", ua), @@ -30,6 +45,8 @@ func LogRequest(h fasthttp.RequestHandler, log *zap.Logger) fasthttp.RequestHand zap.String("content_type", string(ctx.Response.Header.ContentType())), zap.Bool("connection_close", ctx.Response.ConnectionClose()), zap.Duration("duration", time.Since(startedAt)), + zap.Strings("request_headers", reqHeaders), + zap.Strings("response_headers", respHeaders), ) } } diff --git a/internal/http/core/errorpage.go b/internal/http/core/errorpage.go index 1a1b58a..e9640c9 100644 --- a/internal/http/core/errorpage.go +++ b/internal/http/core/errorpage.go @@ -17,7 +17,7 @@ type renderer interface { Render(content []byte, props tpl.Properties) ([]byte, error) } -func RespondWithErrorPage( //nolint:funlen +func RespondWithErrorPage( //nolint:funlen,gocyclo ctx *fasthttp.RequestCtx, cfg *config.Config, p templatePicker, @@ -25,6 +25,7 @@ func RespondWithErrorPage( //nolint:funlen pageCode string, httpCode int, showRequestDetails bool, + proxyHeaders []string, ) { ctx.Response.Header.Set("X-Robots-Tag", "noindex") // block Search indexing @@ -64,6 +65,13 @@ func RespondWithErrorPage( //nolint:funlen return } + // proxy required HTTP headers from the request to the response + for _, headerToProxy := range proxyHeaders { + if reqHeader := ctx.Request.Header.Peek(headerToProxy); len(reqHeader) > 0 { + ctx.Response.Header.SetBytesV(headerToProxy, reqHeader) + } + } + switch { case clientWant == JSONContentType && canJSON: // JSON { diff --git a/internal/http/handlers/errorpage/handler.go b/internal/http/handlers/errorpage/handler.go index 176eb64..d9287e9 100644 --- a/internal/http/handlers/errorpage/handler.go +++ b/internal/http/handlers/errorpage/handler.go @@ -19,12 +19,18 @@ type ( ) // NewHandler creates handler for error pages serving. -func NewHandler(cfg *config.Config, p templatePicker, rdr renderer, showRequestDetails bool) fasthttp.RequestHandler { +func NewHandler( + cfg *config.Config, + p templatePicker, + rdr renderer, + showRequestDetails bool, + proxyHTTPHeaders []string, +) fasthttp.RequestHandler { return func(ctx *fasthttp.RequestCtx) { core.SetClientFormat(ctx, core.PlainTextContentType) // default content type if code, ok := ctx.UserValue("code").(string); ok { - core.RespondWithErrorPage(ctx, cfg, p, rdr, code, fasthttp.StatusOK, showRequestDetails) + core.RespondWithErrorPage(ctx, cfg, p, rdr, code, fasthttp.StatusOK, showRequestDetails, proxyHTTPHeaders) } else { // will never occur ctx.SetStatusCode(fasthttp.StatusInternalServerError) _, _ = ctx.WriteString("cannot extract requested code from the request") diff --git a/internal/http/handlers/index/handler.go b/internal/http/handlers/index/handler.go index 7b6e298..2844537 100644 --- a/internal/http/handlers/index/handler.go +++ b/internal/http/handlers/index/handler.go @@ -28,6 +28,7 @@ func NewHandler( defaultPageCode string, defaultHTTPCode uint16, showRequestDetails bool, + proxyHTTPHeaders []string, ) fasthttp.RequestHandler { return func(ctx *fasthttp.RequestCtx) { pageCode, httpCode := defaultPageCode, int(defaultHTTPCode) @@ -36,7 +37,7 @@ func NewHandler( pageCode, httpCode = strconv.Itoa(returnCode), returnCode } - core.RespondWithErrorPage(ctx, cfg, p, rdr, pageCode, httpCode, showRequestDetails) + core.RespondWithErrorPage(ctx, cfg, p, rdr, pageCode, httpCode, showRequestDetails, proxyHTTPHeaders) } } diff --git a/internal/http/server.go b/internal/http/server.go index 2e2ff4f..e7f90a2 100644 --- a/internal/http/server.go +++ b/internal/http/server.go @@ -72,6 +72,7 @@ func (s *Server) Register( defaultPageCode string, defaultHTTPCode uint16, showDetails bool, + proxyHTTPHeaders []string, ) error { reg, m := metrics.NewRegistry(), metrics.NewMetrics() @@ -81,8 +82,8 @@ func (s *Server) Register( s.fast.Handler = common.DurationMetrics(common.LogRequest(s.router.Handler, s.log), &m) - s.router.GET("/", indexHandler.NewHandler(cfg, templatePicker, s.rdr, defaultPageCode, defaultHTTPCode, showDetails)) //nolint:lll - s.router.GET("/{code}.html", errorpageHandler.NewHandler(cfg, templatePicker, s.rdr, showDetails)) + s.router.GET("/", indexHandler.NewHandler(cfg, templatePicker, s.rdr, defaultPageCode, defaultHTTPCode, showDetails, proxyHTTPHeaders)) //nolint:lll + s.router.GET("/{code}.html", errorpageHandler.NewHandler(cfg, templatePicker, s.rdr, showDetails, proxyHTTPHeaders)) //nolint:lll s.router.GET("/version", versionHandler.NewHandler(version.Version())) liveHandler := healthzHandler.NewHandler(checkers.NewLiveChecker()) diff --git a/test/hurl/proxy_headers.hurl b/test/hurl/proxy_headers.hurl new file mode 100644 index 0000000..42c2251 --- /dev/null +++ b/test/hurl/proxy_headers.hurl @@ -0,0 +1,13 @@ +GET http://{{ host }}:{{ port }}/502.html +X-Foo: foo +bar: BAR +Baz_blah: baz Baz +NonEx: skip + +HTTP/* 200 + +[Asserts] +header "X-Foo" == "foo" +header "Bar" == "BAR" +header "Baz_blah" == "baz Baz" +header "NonEx" not exists