error-pages/internal/cli/serve/command.go

280 lines
7.8 KiB
Go
Raw Normal View History

2021-09-29 15:38:50 +00:00
package serve
import (
"context"
"errors"
2023-01-29 10:54:56 +00:00
"fmt"
"net"
2021-09-29 15:38:50 +00:00
"os"
2023-01-29 10:54:56 +00:00
"strings"
2021-09-29 15:38:50 +00:00
"time"
2023-01-29 10:54:56 +00:00
"github.com/urfave/cli/v2"
"go.uber.org/zap"
"gh.tarampamp.am/error-pages/internal/breaker"
"gh.tarampamp.am/error-pages/internal/cli/shared"
"gh.tarampamp.am/error-pages/internal/config"
"gh.tarampamp.am/error-pages/internal/env"
appHttp "gh.tarampamp.am/error-pages/internal/http"
"gh.tarampamp.am/error-pages/internal/options"
"gh.tarampamp.am/error-pages/internal/pick"
2023-01-29 10:54:56 +00:00
)
type command struct {
c *cli.Command
}
const (
templateNameFlagName = "template-name"
defaultErrorPageFlagName = "default-error-page"
defaultHTTPCodeFlagName = "default-http-code"
showDetailsFlagName = "show-details"
proxyHTTPHeadersFlagName = "proxy-headers"
disableL10nFlagName = "disable-l10n"
catchAllFlagName = "catch-all"
2023-01-29 10:54:56 +00:00
)
const (
useRandomTemplate = "random"
useRandomTemplateOnEachRequest = "i-said-random"
useRandomTemplateDaily = "random-daily"
useRandomTemplateHourly = "random-hourly"
2021-09-29 15:38:50 +00:00
)
// NewCommand creates `serve` command.
2023-01-29 10:54:56 +00:00
func NewCommand(log *zap.Logger) *cli.Command { //nolint:funlen
var cmd = command{}
2021-09-29 15:38:50 +00:00
2023-01-29 10:54:56 +00:00
cmd.c = &cli.Command{
Name: "serve",
2021-09-29 15:38:50 +00:00
Aliases: []string{"s", "server"},
2023-01-29 10:54:56 +00:00
Usage: "Start HTTP server",
Action: func(c *cli.Context) error {
var cfg *config.Config
if configPath := c.String(shared.ConfigFileFlag.Name); configPath == "" { // load config from file
2021-09-29 15:38:50 +00:00
return errors.New("path to the config file is required for this command")
2023-01-29 10:54:56 +00:00
} else if loadedCfg, err := config.FromYamlFile(c.String(shared.ConfigFileFlag.Name)); err != nil {
return err
} else {
cfg = loadedCfg
2021-09-29 15:38:50 +00:00
}
2023-01-29 10:54:56 +00:00
var (
ip = c.String(shared.ListenAddrFlag.Name)
port = uint16(c.Uint(shared.ListenPortFlag.Name))
o options.ErrorPage
)
if net.ParseIP(ip) == nil {
return fmt.Errorf("wrong IP address [%s] for listening", ip)
2021-09-29 15:38:50 +00:00
}
2023-01-29 10:54:56 +00:00
{ // fill options
o.Template.Name = c.String(templateNameFlagName)
o.L10n.Disabled = c.Bool(disableL10nFlagName)
o.Default.PageCode = c.String(defaultErrorPageFlagName)
o.Default.HTTPCode = uint16(c.Uint(defaultHTTPCodeFlagName))
o.ShowDetails = c.Bool(showDetailsFlagName)
o.CatchAll = c.Bool(catchAllFlagName)
2023-01-29 10:54:56 +00:00
if headers := c.String(proxyHTTPHeadersFlagName); headers != "" { //nolint:nestif
var m = make(map[string]struct{})
// make unique and ignore empty strings
for _, header := range strings.Split(headers, ",") {
if h := strings.TrimSpace(header); h != "" {
if strings.ContainsRune(h, ' ') {
return fmt.Errorf("whitespaces in the HTTP headers for proxying [%s] are not allowed", header)
}
if _, ok := m[h]; !ok {
m[h] = struct{}{}
}
}
}
// convert map into slice
o.ProxyHTTPHeaders = make([]string, 0, len(m))
for h := range m {
o.ProxyHTTPHeaders = append(o.ProxyHTTPHeaders, h)
}
}
2021-09-29 15:38:50 +00:00
}
2023-01-29 10:54:56 +00:00
if o.Default.HTTPCode > 599 { //nolint:gomnd
return fmt.Errorf("wrong default HTTP response code [%d]", o.Default.HTTPCode)
}
return cmd.Run(c.Context, log, cfg, ip, port, o)
},
Flags: []cli.Flag{
shared.ConfigFileFlag,
shared.ListenPortFlag,
shared.ListenAddrFlag,
&cli.StringFlag{
Name: templateNameFlagName,
Aliases: []string{"t"},
Usage: fmt.Sprintf(
"template name (set \"%s\" to use a randomized or \"%s\" to use a randomized template on "+
"each request or \"%s/%s\" daily/hourly randomized)",
useRandomTemplate,
useRandomTemplateOnEachRequest,
useRandomTemplateDaily,
useRandomTemplateHourly,
),
EnvVars: []string{env.TemplateName.String()},
},
&cli.StringFlag{
Name: defaultErrorPageFlagName,
Value: "404",
Usage: "default error page",
EnvVars: []string{env.DefaultErrorPage.String()},
},
&cli.UintFlag{
Name: defaultHTTPCodeFlagName,
Value: 404, //nolint:gomnd
Usage: "default HTTP response code",
EnvVars: []string{env.DefaultHTTPCode.String()},
},
&cli.BoolFlag{
Name: showDetailsFlagName,
Usage: "show request details in response",
EnvVars: []string{env.ShowDetails.String()},
},
&cli.StringFlag{
Name: proxyHTTPHeadersFlagName,
Usage: "proxy HTTP request headers list (comma-separated)",
EnvVars: []string{env.ProxyHTTPHeaders.String()},
},
&cli.BoolFlag{
Name: disableL10nFlagName,
Usage: "disable error pages localization",
EnvVars: []string{env.DisableL10n.String()},
},
&cli.BoolFlag{
Name: catchAllFlagName,
Usage: "catch all pages",
EnvVars: []string{env.CatchAll.String()},
},
2021-09-29 15:38:50 +00:00
},
}
2023-01-29 10:54:56 +00:00
return cmd.c
2021-09-29 15:38:50 +00:00
}
2023-01-29 10:54:56 +00:00
// Run current command.
func (cmd *command) Run( //nolint:funlen
parentCtx context.Context, log *zap.Logger, cfg *config.Config, ip string, port uint16, opt options.ErrorPage,
) error {
2021-09-29 15:38:50 +00:00
var (
ctx, cancel = context.WithCancel(parentCtx) // serve context creation
oss = breaker.NewOSSignals(ctx) // OS signals listener
)
// subscribe for system signals
oss.Subscribe(func(sig os.Signal) {
log.Warn("Stopping by OS signal..", zap.String("signal", sig.String()))
cancel()
})
defer func() {
cancel() // call the cancellation function after all
oss.Stop() // stop system signals listening
}()
2021-10-06 17:38:00 +00:00
var (
templateNames = cfg.TemplateNames()
picker interface{ Pick() string }
2021-10-06 17:38:00 +00:00
)
2021-09-29 15:38:50 +00:00
switch opt.Template.Name {
2021-10-06 17:38:00 +00:00
case useRandomTemplate:
log.Info("A random template will be used")
picker = pick.NewStringsSlice(templateNames, pick.RandomOnce)
case useRandomTemplateOnEachRequest:
log.Info("A random template on EACH request will be used")
picker = pick.NewStringsSlice(templateNames, pick.RandomEveryTime)
case useRandomTemplateDaily:
log.Info("A random template will be used and changed once a day")
picker = pick.NewStringsSliceWithInterval(templateNames, pick.RandomEveryTime, time.Hour*24) //nolint:gomnd
case useRandomTemplateHourly:
log.Info("A random template will be used and changed hourly")
picker = pick.NewStringsSliceWithInterval(templateNames, pick.RandomEveryTime, time.Hour)
2021-10-06 17:38:00 +00:00
case "":
log.Info("The first template (ordered by name) will be used")
picker = pick.NewStringsSlice(templateNames, pick.First)
default:
if t, found := cfg.Template(opt.Template.Name); found {
log.Info("We will use the requested template", zap.String("name", t.Name()))
picker = pick.NewStringsSlice([]string{t.Name()}, pick.First)
} else {
return errors.New("requested nonexistent template: " + opt.Template.Name)
2021-10-06 17:38:00 +00:00
}
2021-09-29 15:38:50 +00:00
}
// create HTTP server
server := appHttp.NewServer(log)
// register server routes, middlewares, etc.
if err := server.Register(cfg, picker, opt); err != nil {
2022-01-28 15:42:08 +00:00
return err
}
2021-09-29 15:38:50 +00:00
2021-10-06 17:38:00 +00:00
startedAt, startingErrCh := time.Now(), make(chan error, 1) // channel for server starting error
2021-09-29 15:38:50 +00:00
// start HTTP server in separate goroutine
go func(errCh chan<- error) {
defer close(errCh)
log.Info("Server starting",
2023-01-29 10:54:56 +00:00
zap.String("addr", ip),
zap.Uint16("port", port),
zap.String("default error page", opt.Default.PageCode),
zap.Uint16("default HTTP response code", opt.Default.HTTPCode),
zap.Strings("proxy headers", opt.ProxyHTTPHeaders),
zap.Bool("show request details", opt.ShowDetails),
zap.Bool("localization disabled", opt.L10n.Disabled),
zap.Bool("catch all enabled", opt.CatchAll),
2021-09-29 15:38:50 +00:00
)
2023-01-29 10:54:56 +00:00
if err := server.Start(ip, port); err != nil {
2021-09-29 15:38:50 +00:00
errCh <- err
}
}(startingErrCh)
// and wait for...
select {
case err := <-startingErrCh: // ..server starting error
return err
case <-ctx.Done(): // ..or context cancellation
2021-10-06 17:38:00 +00:00
log.Info("Gracefully server stopping", zap.Duration("uptime", time.Since(startedAt)))
2021-09-29 15:38:50 +00:00
if p, ok := picker.(interface{ Close() error }); ok {
if err := p.Close(); err != nil {
return err
}
}
2021-09-29 15:38:50 +00:00
// stop the server using created context above
2021-10-06 17:38:00 +00:00
if err := server.Stop(); err != nil {
2021-09-29 15:38:50 +00:00
return err
}
}
return nil
}