mirror of
https://github.com/tarampampam/error-pages.git
synced 2024-08-30 18:22:40 +00:00
proxy headers (#67)
This commit is contained in:
parent
06aff4ecb3
commit
e857c0309b
2
.github/workflows/tests.yml
vendored
2
.github/workflows/tests.yml
vendored
@ -227,7 +227,7 @@ jobs: # Docs: <https://git.io/JvxXE>
|
||||
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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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),
|
||||
)
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
1
internal/env/env.go
vendored
1
internal/env/env.go
vendored
@ -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.
|
||||
|
2
internal/env/env_test.go
vendored
2
internal/env/env_test.go
vendored
@ -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 {
|
||||
|
@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
{
|
||||
|
@ -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")
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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())
|
||||
|
13
test/hurl/proxy_headers.hurl
Normal file
13
test/hurl/proxy_headers.hurl
Normal 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
|
Loading…
Reference in New Issue
Block a user