2024-06-21 23:32:10 +00:00
|
|
|
package serve
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"fmt"
|
2024-06-22 08:05:01 +00:00
|
|
|
"net/http"
|
2024-06-21 23:32:10 +00:00
|
|
|
"strings"
|
|
|
|
|
|
|
|
"github.com/urfave/cli/v3"
|
|
|
|
"go.uber.org/zap"
|
|
|
|
|
|
|
|
"gh.tarampamp.am/error-pages/internal/cli/shared"
|
|
|
|
"gh.tarampamp.am/error-pages/internal/config"
|
|
|
|
)
|
|
|
|
|
|
|
|
type command struct {
|
|
|
|
c *cli.Command
|
|
|
|
|
2024-06-22 08:05:01 +00:00
|
|
|
opt struct {
|
|
|
|
http struct { // our HTTP server
|
|
|
|
addr string
|
|
|
|
port uint16
|
|
|
|
readBufferSize uint
|
|
|
|
}
|
|
|
|
}
|
2024-06-21 23:32:10 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// NewCommand creates `serve` command.
|
2024-06-22 08:05:01 +00:00
|
|
|
func NewCommand(log *zap.Logger) *cli.Command { //nolint:funlen,gocognit,gocyclo
|
2024-06-21 23:32:10 +00:00
|
|
|
var (
|
2024-06-22 08:05:01 +00:00
|
|
|
cmd command
|
|
|
|
cfg = config.New()
|
|
|
|
)
|
2024-06-21 23:32:10 +00:00
|
|
|
|
2024-06-22 08:05:01 +00:00
|
|
|
var (
|
|
|
|
addrFlag = shared.ListenAddrFlag
|
|
|
|
portFlag = shared.ListenPortFlag
|
|
|
|
addTplFlag = shared.AddTemplateFlag
|
|
|
|
addCodeFlag = shared.AddHTTPCodeFlag
|
2024-06-21 23:32:10 +00:00
|
|
|
jsonFormatFlag = cli.StringFlag{
|
|
|
|
Name: "json-format",
|
|
|
|
Usage: "override the default error page response in JSON format (Go templates are supported)",
|
|
|
|
Sources: cli.EnvVars("RESPONSE_JSON_FORMAT"),
|
|
|
|
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)",
|
|
|
|
Sources: cli.EnvVars("RESPONSE_XML_FORMAT"),
|
|
|
|
OnlyOnce: true,
|
|
|
|
Config: cli.StringConfig{TrimSpace: true},
|
|
|
|
}
|
2024-06-22 08:05:01 +00:00
|
|
|
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,
|
|
|
|
}
|
2024-06-21 23:32:10 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
cmd.c = &cli.Command{
|
|
|
|
Name: "serve",
|
|
|
|
Aliases: []string{"s", "server", "http"},
|
|
|
|
Usage: "Start HTTP server",
|
|
|
|
Suggest: true,
|
|
|
|
Action: func(ctx context.Context, c *cli.Command) error {
|
2024-06-22 08:05:01 +00:00
|
|
|
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)
|
|
|
|
}
|
|
|
|
}
|
2024-06-21 23:32:10 +00:00
|
|
|
|
|
|
|
if add := c.StringSlice(addTplFlag.Name); len(add) > 0 { // add templates from files to the config
|
|
|
|
for _, templatePath := range add {
|
|
|
|
if addedName, err := cfg.Templates.AddFromFile(templatePath); err != nil {
|
|
|
|
return fmt.Errorf("cannot add template from file %s: %w", templatePath, err)
|
|
|
|
} else {
|
|
|
|
log.Info("Template added",
|
|
|
|
zap.String("name", addedName),
|
|
|
|
zap.String("path", templatePath),
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if add := c.StringMap(addCodeFlag.Name); len(add) > 0 { // add custom HTTP codes
|
|
|
|
for code, msgAndDesc := range add {
|
|
|
|
var (
|
|
|
|
parts = strings.SplitN(msgAndDesc, "/", 2) //nolint:mnd
|
|
|
|
desc config.CodeDescription
|
|
|
|
)
|
|
|
|
|
|
|
|
if len(parts) > 0 {
|
|
|
|
desc.Message = strings.TrimSpace(parts[0])
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(parts) > 1 {
|
|
|
|
desc.Description = strings.TrimSpace(parts[1])
|
|
|
|
}
|
|
|
|
|
|
|
|
cfg.Codes[code] = desc
|
|
|
|
|
|
|
|
log.Info("HTTP code added",
|
|
|
|
zap.String("code", code),
|
|
|
|
zap.String("message", desc.Message),
|
|
|
|
zap.String("description", desc.Description),
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
{ // override default JSON and XML formats
|
|
|
|
if c.IsSet(jsonFormatFlag.Name) {
|
|
|
|
cfg.Formats.JSON = c.String(jsonFormatFlag.Name)
|
|
|
|
}
|
|
|
|
|
|
|
|
if c.IsSet(xmlFormatFlag.Name) {
|
|
|
|
cfg.Formats.XML = c.String(xmlFormatFlag.Name)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
log.Debug("Configuration",
|
|
|
|
zap.Strings("loaded templates", cfg.Templates.Names()),
|
|
|
|
zap.Strings("described HTTP codes", cfg.Codes.Codes()),
|
|
|
|
zap.String("JSON format", cfg.Formats.JSON),
|
|
|
|
zap.String("XML format", cfg.Formats.XML),
|
2024-06-22 08:05:01 +00:00
|
|
|
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),
|
2024-06-21 23:32:10 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
return cmd.Run(ctx, log, &cfg)
|
|
|
|
},
|
|
|
|
Flags: []cli.Flag{
|
|
|
|
&addrFlag,
|
2024-06-22 08:05:01 +00:00
|
|
|
&portFlag,
|
2024-06-21 23:32:10 +00:00
|
|
|
&addTplFlag,
|
|
|
|
&addCodeFlag,
|
|
|
|
&jsonFormatFlag,
|
|
|
|
&xmlFormatFlag,
|
2024-06-22 08:05:01 +00:00
|
|
|
&templateNameFlag,
|
|
|
|
&disableL10nFlag,
|
|
|
|
&defaultCodeToRenderFlag,
|
|
|
|
&defaultHTTPCodeFlag,
|
|
|
|
&showDetailsFlag,
|
|
|
|
&proxyHeadersListFlag,
|
|
|
|
&readBufferSizeFlag,
|
2024-06-21 23:32:10 +00:00
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
return cmd.c
|
|
|
|
}
|
|
|
|
|
|
|
|
// Run current command.
|
|
|
|
func (cmd *command) Run(ctx context.Context, log *zap.Logger, cfg *config.Config) error {
|
|
|
|
return nil // TODO: implement
|
|
|
|
}
|