Skip to content

Commit

Permalink
proxy headers (#67)
Browse files Browse the repository at this point in the history
  • Loading branch information
tarampampam authored Feb 23, 2022
1 parent 06aff4e commit e857c03
Show file tree
Hide file tree
Showing 13 changed files with 136 additions and 14 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 11 additions & 1 deletion internal/cli/serve/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand All @@ -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),
)

Expand Down
56 changes: 56 additions & 0 deletions internal/cli/serve/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package serve
import (
"fmt"
"net"
"sort"
"strconv"
"strings"

Expand All @@ -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 (
Expand All @@ -30,6 +70,7 @@ const (
defaultErrorPageFlagName = "default-error-page"
defaultHTTPCodeFlagName = "default-http-code"
showDetailsFlagName = "show-details"
proxyHTTPHeadersFlagName = "proxy-headers"
)

const (
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
}
}
})
Expand All @@ -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 change: 1 addition & 0 deletions internal/env/env.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 2 additions & 0 deletions internal/env/env_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -28,6 +29,7 @@ func TestEnvVariable_Lookup(t *testing.T) {
{giveEnv: DefaultErrorPage},
{giveEnv: DefaultHTTPCode},
{giveEnv: ShowDetails},
{giveEnv: ProxyHTTPHeaders},
}

for _, tt := range cases {
Expand Down
29 changes: 23 additions & 6 deletions internal/http/common/middlewares.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,33 @@ import (
)

func LogRequest(h fasthttp.RequestHandler, log *zap.Logger) fasthttp.RequestHandler {
return func(ctx *fasthttp.RequestCtx) {
var (
startedAt = time.Now()
ua = string(ctx.UserAgent())
)
const headersSeparator = ": "

h(ctx)
return func(ctx *fasthttp.RequestCtx) {
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)

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),
zap.String("method", string(ctx.Method())),
Expand All @@ -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),
)
}
}
Expand Down
10 changes: 9 additions & 1 deletion internal/http/core/errorpage.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,15 @@ 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,
rdr renderer,
pageCode string,
httpCode int,
showRequestDetails bool,
proxyHeaders []string,
) {
ctx.Response.Header.Set("X-Robots-Tag", "noindex") // block Search indexing

Expand Down Expand Up @@ -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
{
Expand Down
10 changes: 8 additions & 2 deletions internal/http/handlers/errorpage/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
3 changes: 2 additions & 1 deletion internal/http/handlers/index/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
}
}

Expand Down
5 changes: 3 additions & 2 deletions internal/http/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ func (s *Server) Register(
defaultPageCode string,
defaultHTTPCode uint16,
showDetails bool,
proxyHTTPHeaders []string,
) error {
reg, m := metrics.NewRegistry(), metrics.NewMetrics()

Expand All @@ -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())
Expand Down
13 changes: 13 additions & 0 deletions test/hurl/proxy_headers.hurl
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit e857c03

Please sign in to comment.