proxy headers (#67)

This commit is contained in:
Paramtamtam 2022-02-23 11:09:54 +05:00 committed by GitHub
parent 06aff4ecb3
commit e857c0309b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 137 additions and 15 deletions

View File

@ -227,7 +227,7 @@ jobs: # Docs: <https://git.io/JvxXE>
run: sudo dpkg -i hurl.deb run: sudo dpkg -i hurl.deb
- name: Run container with the app - 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 - name: Wait for container "healthy" state
run: until [[ "`docker inspect -f {{.State.Health.Status}} app`" == "healthy" ]]; do echo "wait 1 sec.."; sleep 1; done run: until [[ "`docker inspect -f {{.State.Health.Status}} app`" == "healthy" ]]; do echo "wait 1 sec.."; sleep 1; done

View File

@ -6,14 +6,20 @@ The format is based on [Keep a Changelog][keepachangelog] and this project adher
## UNRELEASED ## UNRELEASED
### Changed
- Logs includes request/response headers now [#67]
### Added ### 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] - Template `lost-in-space` [#68]
### Fixed ### Fixed
- Template `l7-light` uses the dark colors in browsers with the preferred dark theme - 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 [#68]:https://github.com/tarampampam/error-pages/pull/68
## v2.6.0 ## v2.6.0

View File

@ -31,6 +31,7 @@ services:
- --verbose - --verbose
- --port=8080 - --port=8080
- --show-details - --show-details
- --proxy-headers=X-Foo,Bar,Baz_blah
healthcheck: healthcheck:
test: ['CMD', 'wget', '--spider', '-q', 'http://127.0.0.1:8080/healthz'] test: ['CMD', 'wget', '--spider', '-q', 'http://127.0.0.1:8080/healthz']
interval: 5s interval: 5s

View File

@ -107,11 +107,20 @@ func run(parentCtx context.Context, log *zap.Logger, f flags, cfg *config.Config
} }
} }
var proxyHTTPHeaders = f.HeadersToProxy()
// create HTTP server // create HTTP server
server := appHttp.NewServer(log) server := appHttp.NewServer(log)
// register server routes, middlewares, etc. // 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 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.Uint16("port", f.listen.port),
zap.String("default error page", f.defaultErrorPage), zap.String("default error page", f.defaultErrorPage),
zap.Uint16("default HTTP response code", f.defaultHTTPCode), zap.Uint16("default HTTP response code", f.defaultHTTPCode),
zap.Strings("proxy headers", proxyHTTPHeaders),
zap.Bool("show request details", f.showDetails), zap.Bool("show request details", f.showDetails),
) )

View File

@ -3,6 +3,7 @@ package serve
import ( import (
"fmt" "fmt"
"net" "net"
"sort"
"strconv" "strconv"
"strings" "strings"
@ -21,6 +22,45 @@ type flags struct {
defaultErrorPage string defaultErrorPage string
defaultHTTPCode uint16 defaultHTTPCode uint16
showDetails bool 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 ( const (
@ -30,6 +70,7 @@ const (
defaultErrorPageFlagName = "default-error-page" defaultErrorPageFlagName = "default-error-page"
defaultHTTPCodeFlagName = "default-http-code" defaultHTTPCodeFlagName = "default-http-code"
showDetailsFlagName = "show-details" showDetailsFlagName = "show-details"
proxyHTTPHeadersFlagName = "proxy-headers"
) )
const ( const (
@ -84,6 +125,12 @@ func (f *flags) init(flagSet *pflag.FlagSet) {
false, false,
fmt.Sprintf("show request details in response [$%s]", env.ShowDetails), 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 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 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) 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 return nil
} }

1
internal/env/env.go vendored
View File

@ -13,6 +13,7 @@ const (
DefaultErrorPage envVariable = "DEFAULT_ERROR_PAGE" // default error page (code) DefaultErrorPage envVariable = "DEFAULT_ERROR_PAGE" // default error page (code)
DefaultHTTPCode envVariable = "DEFAULT_HTTP_CODE" // default HTTP response code DefaultHTTPCode envVariable = "DEFAULT_HTTP_CODE" // default HTTP response code
ShowDetails envVariable = "SHOW_DETAILS" // show request details in response 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. // String returns environment variable name in the string representation.

View File

@ -15,6 +15,7 @@ func TestConstants(t *testing.T) {
assert.Equal(t, "DEFAULT_ERROR_PAGE", string(DefaultErrorPage)) assert.Equal(t, "DEFAULT_ERROR_PAGE", string(DefaultErrorPage))
assert.Equal(t, "DEFAULT_HTTP_CODE", string(DefaultHTTPCode)) assert.Equal(t, "DEFAULT_HTTP_CODE", string(DefaultHTTPCode))
assert.Equal(t, "SHOW_DETAILS", string(ShowDetails)) assert.Equal(t, "SHOW_DETAILS", string(ShowDetails))
assert.Equal(t, "PROXY_HTTP_HEADERS", string(ProxyHTTPHeaders))
} }
func TestEnvVariable_Lookup(t *testing.T) { func TestEnvVariable_Lookup(t *testing.T) {
@ -28,6 +29,7 @@ func TestEnvVariable_Lookup(t *testing.T) {
{giveEnv: DefaultErrorPage}, {giveEnv: DefaultErrorPage},
{giveEnv: DefaultHTTPCode}, {giveEnv: DefaultHTTPCode},
{giveEnv: ShowDetails}, {giveEnv: ShowDetails},
{giveEnv: ProxyHTTPHeaders},
} }
for _, tt := range cases { for _, tt := range cases {

View File

@ -9,17 +9,32 @@ import (
) )
func LogRequest(h fasthttp.RequestHandler, log *zap.Logger) fasthttp.RequestHandler { func LogRequest(h fasthttp.RequestHandler, log *zap.Logger) fasthttp.RequestHandler {
const headersSeparator = ": "
return func(ctx *fasthttp.RequestCtx) { return func(ctx *fasthttp.RequestCtx) {
var ( var ua = string(ctx.UserAgent())
startedAt = time.Now()
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) h(ctx)
if strings.Contains(strings.ToLower(ua), "healthcheck") { // skip healthcheck requests logging var respHeaders = make([]string, 0, 16) //nolint:gomnd
return
} ctx.Response.Header.VisitAll(func(key, value []byte) {
respHeaders = append(respHeaders, string(key)+headersSeparator+string(value))
})
log.Info("HTTP request processed", log.Info("HTTP request processed",
zap.String("useragent", ua), 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.String("content_type", string(ctx.Response.Header.ContentType())),
zap.Bool("connection_close", ctx.Response.ConnectionClose()), zap.Bool("connection_close", ctx.Response.ConnectionClose()),
zap.Duration("duration", time.Since(startedAt)), zap.Duration("duration", time.Since(startedAt)),
zap.Strings("request_headers", reqHeaders),
zap.Strings("response_headers", respHeaders),
) )
} }
} }

View File

@ -17,7 +17,7 @@ type renderer interface {
Render(content []byte, props tpl.Properties) ([]byte, error) Render(content []byte, props tpl.Properties) ([]byte, error)
} }
func RespondWithErrorPage( //nolint:funlen func RespondWithErrorPage( //nolint:funlen,gocyclo
ctx *fasthttp.RequestCtx, ctx *fasthttp.RequestCtx,
cfg *config.Config, cfg *config.Config,
p templatePicker, p templatePicker,
@ -25,6 +25,7 @@ func RespondWithErrorPage( //nolint:funlen
pageCode string, pageCode string,
httpCode int, httpCode int,
showRequestDetails bool, showRequestDetails bool,
proxyHeaders []string,
) { ) {
ctx.Response.Header.Set("X-Robots-Tag", "noindex") // block Search indexing ctx.Response.Header.Set("X-Robots-Tag", "noindex") // block Search indexing
@ -64,6 +65,13 @@ func RespondWithErrorPage( //nolint:funlen
return 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 { switch {
case clientWant == JSONContentType && canJSON: // JSON case clientWant == JSONContentType && canJSON: // JSON
{ {

View File

@ -19,12 +19,18 @@ type (
) )
// NewHandler creates handler for error pages serving. // 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) { return func(ctx *fasthttp.RequestCtx) {
core.SetClientFormat(ctx, core.PlainTextContentType) // default content type core.SetClientFormat(ctx, core.PlainTextContentType) // default content type
if code, ok := ctx.UserValue("code").(string); ok { 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 } else { // will never occur
ctx.SetStatusCode(fasthttp.StatusInternalServerError) ctx.SetStatusCode(fasthttp.StatusInternalServerError)
_, _ = ctx.WriteString("cannot extract requested code from the request") _, _ = ctx.WriteString("cannot extract requested code from the request")

View File

@ -28,6 +28,7 @@ func NewHandler(
defaultPageCode string, defaultPageCode string,
defaultHTTPCode uint16, defaultHTTPCode uint16,
showRequestDetails bool, showRequestDetails bool,
proxyHTTPHeaders []string,
) fasthttp.RequestHandler { ) fasthttp.RequestHandler {
return func(ctx *fasthttp.RequestCtx) { return func(ctx *fasthttp.RequestCtx) {
pageCode, httpCode := defaultPageCode, int(defaultHTTPCode) pageCode, httpCode := defaultPageCode, int(defaultHTTPCode)
@ -36,7 +37,7 @@ func NewHandler(
pageCode, httpCode = strconv.Itoa(returnCode), returnCode 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)
} }
} }

View File

@ -72,6 +72,7 @@ func (s *Server) Register(
defaultPageCode string, defaultPageCode string,
defaultHTTPCode uint16, defaultHTTPCode uint16,
showDetails bool, showDetails bool,
proxyHTTPHeaders []string,
) error { ) error {
reg, m := metrics.NewRegistry(), metrics.NewMetrics() 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.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("/", indexHandler.NewHandler(cfg, templatePicker, s.rdr, defaultPageCode, defaultHTTPCode, showDetails, proxyHTTPHeaders)) //nolint:lll
s.router.GET("/{code}.html", errorpageHandler.NewHandler(cfg, templatePicker, s.rdr, showDetails)) s.router.GET("/{code}.html", errorpageHandler.NewHandler(cfg, templatePicker, s.rdr, showDetails, proxyHTTPHeaders)) //nolint:lll
s.router.GET("/version", versionHandler.NewHandler(version.Version())) s.router.GET("/version", versionHandler.NewHandler(version.Version()))
liveHandler := healthzHandler.NewHandler(checkers.NewLiveChecker()) liveHandler := healthzHandler.NewHandler(checkers.NewLiveChecker())

View File

@ -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