wip: 🔕 temporary commit

This commit is contained in:
Paramtamtam 2024-06-22 12:05:01 +04:00
parent 71f8cfc162
commit b71475fcf7
No known key found for this signature in database
GPG Key ID: 366371698FAD0A2B
4 changed files with 201 additions and 16 deletions

View File

@ -27,11 +27,21 @@ $ error-pages [GLOBAL FLAGS] serve [COMMAND FLAGS] [ARGUMENTS...]
The following flags are supported:
| Name | Description | Default value | Environment variables |
|-----------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------|:-------------:|:---------------------:|
| `--port="…"` (`-p`) | TCP port number | `8080` | `LISTEN_PORT` |
| `--listen="…"` (`-l`) | IP (v4 or v6) address to listen on | `0.0.0.0` | `LISTEN_ADDR` |
| `--add-template="…"` | to add a new template, provide the path to the file here (may be specified multiple times; the filename without the extension will be used as the template name) | `[]` | *none* |
| Name | Description | Default value | Environment variables |
|------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:-----------------------------------------:|:----------------------:|
| `--listen="…"` (`-l`) | IP (v4 or v6) address to listen on | `0.0.0.0` | `LISTEN_ADDR` |
| `--port="…"` (`-p`) | TCP port number | `8080` | `LISTEN_PORT` |
| `--add-template="…"` | to add a new template, provide the path to the file using this flag (the filename without the extension will be used as the template name) | `[]` | *none* |
| `--add-http-code="…"` | to add a new HTTP status code, provide the code and its message/description using this flag (the format should be '%code%=%message%/%description%'; the code may contain a wildcard '*' to cover multiple codes at once, for example, '4**' will cover all 4xx codes, unless a more specific code was described previously) | `map[]` | *none* |
| `--json-format="…"` | override the default error page response in JSON format (Go templates are supported) | | `RESPONSE_JSON_FORMAT` |
| `--xml-format="…"` | override the default error page response in XML format (Go templates are supported) | | `RESPONSE_XML_FORMAT` |
| `--template-name="…"` (`-t`) | name of the template to use for rendering error pages | `template-1` | `TEMPLATE_NAME` |
| `--disable-l10n` | disable localization of error pages (if the template supports localization) | `false` | `DISABLE_L10N` |
| `--default-error-page="…"` | the code of the default (index page, when a code is not specified) error page to render | `404` | `DEFAULT_ERROR_PAGE` |
| `--default-http-code="…"` | the default (index page, when a code is not specified) HTTP response code | `404` | `DEFAULT_HTTP_CODE` |
| `--show-details` | show request details in the error page response (if supported by the template) | `false` | `SHOW_DETAILS` |
| `--proxy-headers="…"` | listed here HTTP headers will be proxied from the original request to the error page response (comma-separated list) | `X-Request-Id,X-Trace-Id,X-Amzn-Trace-Id` | `PROXY_HTTP_HEADERS` |
| `--read-buffer-size="…"` | customize the HTTP read buffer size (set per connection for reading requests, also limits the maximum header size; consider increasing it if your clients send multi-KB request URIs or multi-KB headers, such as large cookies) | `0` | `READ_BUFFER_SIZE` |
### `healthcheck` command (aliases: `chk`, `health`, `check`)

View File

@ -3,6 +3,7 @@ package serve
import (
"context"
"fmt"
"net/http"
"strings"
"github.com/urfave/cli/v3"
@ -15,19 +16,27 @@ import (
type command struct {
c *cli.Command
opt struct{}
opt struct {
http struct { // our HTTP server
addr string
port uint16
readBufferSize uint
}
}
}
// NewCommand creates `serve` command.
func NewCommand(log *zap.Logger) *cli.Command { //nolint:funlen
var cmd command
func NewCommand(log *zap.Logger) *cli.Command { //nolint:funlen,gocognit,gocyclo
var (
cmd command
cfg = config.New()
)
var (
portFlag = shared.ListenPortFlag
addrFlag = shared.ListenAddrFlag
addTplFlag = shared.AddTemplateFlag
addCodeFlag = shared.AddHTTPCodeFlag
addrFlag = shared.ListenAddrFlag
portFlag = shared.ListenPortFlag
addTplFlag = shared.AddTemplateFlag
addCodeFlag = shared.AddHTTPCodeFlag
jsonFormatFlag = cli.StringFlag{
Name: "json-format",
Usage: "override the default error page response in JSON format (Go templates are supported)",
@ -35,7 +44,6 @@ func NewCommand(log *zap.Logger) *cli.Command { //nolint:funlen
OnlyOnce: true,
Config: cli.StringConfig{TrimSpace: true},
}
xmlFormatFlag = cli.StringFlag{
Name: "xml-format",
Usage: "override the default error page response in XML format (Go templates are supported)",
@ -43,6 +51,78 @@ func NewCommand(log *zap.Logger) *cli.Command { //nolint:funlen
OnlyOnce: true,
Config: cli.StringConfig{TrimSpace: true},
}
templateNameFlag = cli.StringFlag{
Name: "template-name",
Aliases: []string{"t"},
Value: cfg.TemplateName,
Usage: "name of the template to use for rendering error pages",
Sources: cli.EnvVars("TEMPLATE_NAME"),
OnlyOnce: true,
Config: cli.StringConfig{TrimSpace: true},
}
disableL10nFlag = cli.BoolFlag{
Name: "disable-l10n",
Usage: "disable localization of error pages (if the template supports localization)",
Value: cfg.L10n.Disable,
Sources: cli.EnvVars("DISABLE_L10N"),
OnlyOnce: true,
}
defaultCodeToRenderFlag = cli.UintFlag{
Name: "default-error-page",
Usage: "the code of the default (index page, when a code is not specified) error page to render",
Value: uint64(cfg.Default.CodeToRender),
Sources: cli.EnvVars("DEFAULT_ERROR_PAGE"),
Validator: func(code uint64) error {
if code > 999 { //nolint:mnd
return fmt.Errorf("wrong HTTP code [%d] for the default error page", code)
}
return nil
},
OnlyOnce: true,
}
defaultHTTPCodeFlag = cli.UintFlag{
Name: "default-http-code",
Usage: "the default (index page, when a code is not specified) HTTP response code",
Value: uint64(cfg.Default.HttpCode),
Sources: cli.EnvVars("DEFAULT_HTTP_CODE"),
Validator: defaultCodeToRenderFlag.Validator,
OnlyOnce: true,
}
showDetailsFlag = cli.BoolFlag{
Name: "show-details",
Usage: "show request details in the error page response (if supported by the template)",
Value: cfg.ShowDetails,
Sources: cli.EnvVars("SHOW_DETAILS"),
OnlyOnce: true,
}
proxyHeadersListFlag = cli.StringFlag{
Name: "proxy-headers",
Usage: "listed here HTTP headers will be proxied from the original request to the error page response " +
"(comma-separated list)",
Value: strings.Join(cfg.ProxyHeaders, ","),
Sources: cli.EnvVars("PROXY_HTTP_HEADERS"),
Validator: func(s string) error {
for _, raw := range strings.Split(s, ",") {
if clean := strings.TrimSpace(raw); strings.ContainsRune(clean, ' ') {
return fmt.Errorf("whitespaces in the HTTP headers are not allowed: %s", clean)
}
}
return nil
},
OnlyOnce: true,
Config: cli.StringConfig{TrimSpace: true},
}
readBufferSizeFlag = cli.UintFlag{
Name: "read-buffer-size",
Usage: "customize the HTTP read buffer size (set per connection for reading requests, also limits the " +
"maximum header size; consider increasing it if your clients send multi-KB request URIs or multi-KB " +
"headers, such as large cookies)",
DefaultText: "not set",
Sources: cli.EnvVars("READ_BUFFER_SIZE"),
OnlyOnce: true,
}
)
cmd.c = &cli.Command{
@ -51,7 +131,29 @@ func NewCommand(log *zap.Logger) *cli.Command { //nolint:funlen
Usage: "Start HTTP server",
Suggest: true,
Action: func(ctx context.Context, c *cli.Command) error {
var cfg = config.New()
cmd.opt.http.addr = c.String(addrFlag.Name)
cmd.opt.http.port = uint16(c.Uint(portFlag.Name))
cmd.opt.http.readBufferSize = uint(c.Uint(readBufferSizeFlag.Name))
cfg.TemplateName = c.String(templateNameFlag.Name)
cfg.L10n.Disable = c.Bool(disableL10nFlag.Name)
cfg.Default.CodeToRender = uint16(c.Uint(defaultCodeToRenderFlag.Name))
cfg.Default.HttpCode = uint16(c.Uint(defaultHTTPCodeFlag.Name))
cfg.ShowDetails = c.Bool(showDetailsFlag.Name)
if c.IsSet(proxyHeadersListFlag.Name) {
var m = make(map[string]struct{}) // map is used to avoid duplicates
for _, header := range strings.Split(c.String(proxyHeadersListFlag.Name), ",") {
m[http.CanonicalHeaderKey(strings.TrimSpace(header))] = struct{}{}
}
clear(cfg.ProxyHeaders) // clear the list before adding new headers
for header := range m {
cfg.ProxyHeaders = append(cfg.ProxyHeaders, header)
}
}
if add := c.StringSlice(addTplFlag.Name); len(add) > 0 { // add templates from files to the config
for _, templatePath := range add {
@ -106,17 +208,30 @@ func NewCommand(log *zap.Logger) *cli.Command { //nolint:funlen
zap.Strings("described HTTP codes", cfg.Codes.Codes()),
zap.String("JSON format", cfg.Formats.JSON),
zap.String("XML format", cfg.Formats.XML),
zap.String("template name", cfg.TemplateName),
zap.Bool("disable localization", cfg.L10n.Disable),
zap.Uint16("default code to render", cfg.Default.CodeToRender),
zap.Uint16("default HTTP code", cfg.Default.HttpCode),
zap.Bool("show details", cfg.ShowDetails),
zap.Strings("proxy HTTP headers", cfg.ProxyHeaders),
)
return cmd.Run(ctx, log, &cfg)
},
Flags: []cli.Flag{
&portFlag,
&addrFlag,
&portFlag,
&addTplFlag,
&addCodeFlag,
&jsonFormatFlag,
&xmlFormatFlag,
&templateNameFlag,
&disableL10nFlag,
&defaultCodeToRenderFlag,
&defaultHTTPCodeFlag,
&showDetailsFlag,
&proxyHeadersListFlag,
&readBufferSizeFlag,
},
}

View File

@ -2,6 +2,8 @@ package config
import (
"maps"
"net/http"
"slices"
builtinTemplates "gh.tarampamp.am/error-pages/templates"
)
@ -20,6 +22,34 @@ type Config struct {
// Codes hold descriptions for HTTP codes (e.g., 404: "Not Found / The server can not find the requested page").
Codes Codes
// TemplateName is the name of the template to use for rendering error pages. The template must be present in the
// Templates map.
TemplateName string
// ProxyHeaders contains a list of HTTP headers that will be proxied from the incoming request to the
// error page response.
ProxyHeaders []string
// L10n contains localization settings.
L10n struct {
// Disable the localization of error pages.
Disable bool
}
// Default contains default settings.
Default struct {
// CodeToRender is the code for the default error page to be displayed. It is used when the requested
// code is not defined in the incoming request (i.e., the code to render as the index page).
CodeToRender uint16
// HTTPCode is the HTTP code to return when the requested code is not defined in the incoming request.
HttpCode uint16
}
// ShowDetails determines whether to show additional details in the error response, extracted from the
// incoming request (if supported by the template).
ShowDetails bool
}
const defaultJSONFormat string = `{
@ -82,6 +112,14 @@ var defaultCodes = Codes{ //nolint:gochecknoglobals
"505": {"HTTP Version Not Supported", "The server does not support the \"http protocol\" version"},
}
var defaultProxyHeaders = []string{ //nolint:gochecknoglobals
// "Traceparent", // W3C Trace Context
// "Tracestate", // W3C Trace Context
"X-Request-Id", // unofficial HTTP header, used to trace individual HTTP requests
"X-Trace-Id", // same as above
"X-Amzn-Trace-Id", // to track HTTP requests from clients to targets or other AWS services
}
// New creates a new configuration with default values.
func New() Config {
var cfg = Config{
@ -97,5 +135,19 @@ func New() Config {
cfg.Templates[name] = content
}
// set first template as default
for _, name := range cfg.Templates.Names() {
cfg.TemplateName = name
break
}
// set default HTTP headers to proxy
cfg.ProxyHeaders = slices.Clone(defaultProxyHeaders)
// set defaults
cfg.Default.CodeToRender = 404
cfg.Default.HttpCode = http.StatusNotFound
return cfg
}

View File

@ -18,6 +18,10 @@ func TestNew(t *testing.T) {
assert.NotEmpty(t, cfg.Formats.JSON)
assert.True(t, len(cfg.Codes) >= 19)
assert.True(t, len(cfg.Templates) >= 2)
assert.NotEmpty(t, cfg.TemplateName)
assert.True(t, cfg.Templates.Has(cfg.TemplateName))
assert.Equal(t, uint16(404), cfg.Default.CodeToRender)
assert.Equal(t, uint16(404), cfg.Default.HttpCode)
})
t.Run("changing cfg1 should not affect cfg2", func(t *testing.T) {
@ -26,5 +30,9 @@ func TestNew(t *testing.T) {
cfg1.Codes["400"] = config.CodeDescription{Message: "foo", Description: "bar"}
assert.NotEqual(t, cfg1.Codes["400"], cfg2.Codes["400"])
cfg1.ProxyHeaders = append(cfg1.ProxyHeaders, "foo")
assert.NotEqual(t, cfg1.ProxyHeaders, cfg2.ProxyHeaders)
})
}