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"
|
|
|
|
|
2023-02-23 17:49:45 +00:00
|
|
|
"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"
|
2023-09-01 06:17:11 +00:00
|
|
|
catchAllFlagName = "catch-all"
|
2023-11-20 08:27:40 +00:00
|
|
|
readBufferSizeFlagName = "read-buffer-size"
|
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)
|
2023-09-01 06:17:11 +00:00
|
|
|
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)
|
|
|
|
}
|
|
|
|
|
2023-11-20 08:27:40 +00:00
|
|
|
return cmd.Run(c.Context, log, cfg, ip, port, c.Uint(readBufferSizeFlagName), o)
|
2023-01-29 10:54:56 +00:00
|
|
|
},
|
|
|
|
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()},
|
|
|
|
},
|
2023-09-01 06:17:11 +00:00
|
|
|
&cli.BoolFlag{
|
|
|
|
Name: catchAllFlagName,
|
|
|
|
Usage: "catch all pages",
|
|
|
|
EnvVars: []string{env.CatchAll.String()},
|
|
|
|
},
|
2023-11-20 08:27:40 +00:00
|
|
|
&cli.UintFlag{
|
|
|
|
Name: readBufferSizeFlagName,
|
|
|
|
Usage: "read buffer size (0 = use default value)",
|
|
|
|
EnvVars: []string{env.ReadBufferSize.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
|
2023-11-20 08:27:40 +00:00
|
|
|
parentCtx context.Context,
|
|
|
|
log *zap.Logger,
|
|
|
|
cfg *config.Config,
|
|
|
|
ip string,
|
|
|
|
port uint16,
|
|
|
|
readBufferSize uint,
|
|
|
|
opt options.ErrorPage,
|
2023-01-29 10:54:56 +00:00
|
|
|
) 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 (
|
2022-01-27 12:29:49 +00:00
|
|
|
templateNames = cfg.TemplateNames()
|
2022-02-01 14:39:50 +00:00
|
|
|
picker interface{ Pick() string }
|
2021-10-06 17:38:00 +00:00
|
|
|
)
|
2021-09-29 15:38:50 +00:00
|
|
|
|
2022-04-12 10:34:35 +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)
|
|
|
|
|
2022-02-01 14:39:50 +00:00
|
|
|
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:
|
2022-04-12 10:34:35 +00:00
|
|
|
if t, found := cfg.Template(opt.Template.Name); found {
|
2022-01-27 12:29:49 +00:00
|
|
|
log.Info("We will use the requested template", zap.String("name", t.Name()))
|
|
|
|
picker = pick.NewStringsSlice([]string{t.Name()}, pick.First)
|
|
|
|
} else {
|
2022-04-12 10:34:35 +00:00
|
|
|
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
|
2023-11-20 08:27:40 +00:00
|
|
|
server := appHttp.NewServer(log, readBufferSize)
|
2021-09-29 15:38:50 +00:00
|
|
|
|
|
|
|
// register server routes, middlewares, etc.
|
2022-04-12 10:34:35 +00:00
|
|
|
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)
|
|
|
|
|
2023-11-20 08:27:40 +00:00
|
|
|
var fields = []zap.Field{
|
2023-01-29 10:54:56 +00:00
|
|
|
zap.String("addr", ip),
|
|
|
|
zap.Uint16("port", port),
|
2022-04-12 10:34:35 +00:00
|
|
|
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),
|
2023-09-01 06:17:11 +00:00
|
|
|
zap.Bool("catch all enabled", opt.CatchAll),
|
2023-11-20 08:27:40 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if readBufferSize > 0 {
|
|
|
|
fields = append(fields, zap.Uint("read buffer size", readBufferSize))
|
|
|
|
}
|
|
|
|
|
|
|
|
log.Info("Server starting", fields...)
|
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
|
|
|
|
2022-02-01 14:39: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
|
|
|
|
}
|