From 146c5c125390aa35d20a34afbe076b5e2ccd2da5 Mon Sep 17 00:00:00 2001 From: Emil Jonathan Eriksson <8700261+ginger51011@users.noreply.github.com> Date: Thu, 18 Jan 2024 21:49:06 +0100 Subject: [PATCH 01/29] Add optional `-b`/`--book` flag to choose source file other than Nietzche While I do think all bots enjoy Nietzche (who doesn't?), I think we should take a stance to educate them. What better way than to be able to choose from any book! Personal suggestions include: - The Sorrows of Young Werther by Goethe - Any political manifesto - The Declaration of Independence etc. etc. --- README.md | 2 +- cmd/HellPot/HellPot.go | 2 +- heffalump/heffalump.go | 5 +- heffalump/markov.go | 3 +- internal/config/arguments.go | 7 ++ internal/config/config.go | 192 +++++++++++++++-------------------- internal/config/help.go | 3 +- internal/http/router.go | 27 ++++- 8 files changed, 122 insertions(+), 119 deletions(-) diff --git a/README.md b/README.md index 443dfba..0238084 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ Clients (hopefully bots) that disregard `robots.txt` and connect to your instanc HellPot will send an infinite stream of data that is _just close enough_ to being a real website that they might just stick around until their soul is ripped apart and they cease to exist. -Under the hood of this eternal suffering is a markov engine that chucks bits and pieces of [The Birth of Tragedy (Hellenism and Pessimism)](https://www.gutenberg.org/files/51356/51356-h/51356-h.htm) by Friedrich Nietzsche at the client using [fasthttp](https://github.com/valyala/fasthttp). +Under the hood of this eternal suffering is a markov engine that chucks bits and pieces of [The Birth of Tragedy (Hellenism and Pessimism)](https://www.gutenberg.org/files/51356/51356-h/51356-h.htm) by Friedrich Nietzsche at the client using [fasthttp](https://github.com/valyala/fasthttp), or optionally your (least?) favorite text using the `-b`/`--book` flag. ## Building From Source diff --git a/cmd/HellPot/HellPot.go b/cmd/HellPot/HellPot.go index b2b3055..54f53e3 100644 --- a/cmd/HellPot/HellPot.go +++ b/cmd/HellPot/HellPot.go @@ -38,7 +38,7 @@ func init() { extra.Banner() - log.Info().Str("caller", "config").Str("file", config.Filename).Msg(config.Filename) + log.Info().Str("caller", "config").Str("file", config.ConfigFilename).Msg(config.ConfigFilename) log.Info().Str("caller", "logger").Msg(config.CurrentLogFile) log.Debug().Str("caller", "logger").Msg("debug enabled") log.Trace().Str("caller", "logger").Msg("trace enabled") diff --git a/heffalump/heffalump.go b/heffalump/heffalump.go index 4e7ce3b..15b6365 100644 --- a/heffalump/heffalump.go +++ b/heffalump/heffalump.go @@ -15,7 +15,10 @@ import ( var log = config.GetLogger() // DefaultHeffalump represents a Heffalump type -var DefaultHeffalump *Heffalump +var ( + DefaultHeffalump *Heffalump + DefaultBuffSize int = 100 * 1 << 10 +) // Heffalump represents our buffer pool and markov map from Heffalump type Heffalump struct { diff --git a/heffalump/markov.go b/heffalump/markov.go index ca522ab..f6ce601 100644 --- a/heffalump/markov.go +++ b/heffalump/markov.go @@ -22,8 +22,9 @@ func init() { if len(src) < 1 { panic("failed to unpack source") } + DefaultMarkovMap = MakeMarkovMap(strings.NewReader(src)) - DefaultHeffalump = NewHeffalump(DefaultMarkovMap, 100*1<<10) + DefaultHeffalump = NewHeffalump(DefaultMarkovMap, DefaultBuffSize) } // ScanHTML is a basic split function for a Scanner that returns each diff --git a/internal/config/arguments.go b/internal/config/arguments.go index 89fe1fd..7628867 100644 --- a/internal/config/arguments.go +++ b/internal/config/arguments.go @@ -30,6 +30,13 @@ func argParse() { os.Exit(1) } loadCustomConfig(os.Args[i+1]) + case "-b", "--book": + if len(os.Args) < i+2 { + println("missing book file after -b/--book") + os.Exit(1) + } + BookFilename = os.Args[i+1] + UseCustomHeffalump = true default: continue } diff --git a/internal/config/config.go b/internal/config/config.go index 925bde3..d5c20b2 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -1,86 +1,61 @@ package config import ( - "errors" + "bytes" "fmt" + "io" "os" "path/filepath" "runtime" "strconv" "strings" - "github.com/knadh/koanf/providers/env" - "github.com/knadh/koanf/providers/file" "github.com/rs/zerolog" - - "github.com/knadh/koanf/parsers/toml" - viper "github.com/knadh/koanf/v2" + "github.com/spf13/viper" ) // generic vars var ( - noColorForce = false - customconfig = false - home string - snek = viper.New(".") + noColorForce = false + customconfig = false + home string + prefConfigLocation string + snek = viper.New() ) -func init() { - home, _ = os.UserHomeDir() - if home == "" { - home = os.Getenv("HOME") - } - if home == "" { - println("WARNING: could not determine home directory") - } -} - // exported generic vars var ( // Trace is the value of our trace (extra verbose) on/off toggle as per the current configuration. Trace bool // Debug is the value of our debug (verbose) on/off toggle as per the current configuration. Debug bool - // Filename returns the current location of our toml config file. - Filename string + // ConfigFilename returns the current location of our toml config file. + ConfigFilename string + // UseCustomHeffalump decides if a custom Heffalump is to be used + UseCustomHeffalump = false + // BookFilename returns the current location of a possible book file + BookFilename string ) -func writeConfig() string { - prefConfigLocation, _ := os.UserConfigDir() - if prefConfigLocation != "" { - prefConfigLocation = filepath.Join(prefConfigLocation, Title) - } - - if prefConfigLocation == "" { - home, _ = os.UserHomeDir() - prefConfigLocation = filepath.Join(home, ".config", Title) - } - +func writeConfig() { if _, err := os.Stat(prefConfigLocation); os.IsNotExist(err) { - if err = os.MkdirAll(prefConfigLocation, 0o750); err != nil && !errors.Is(err, os.ErrExist) { + if err = os.MkdirAll(prefConfigLocation, 0o750); err != nil { println("error writing new config: " + err.Error()) os.Exit(1) } } - - Filename = filepath.Join(prefConfigLocation, "config.toml") - - tomld, terr := toml.Parser().Marshal(snek.All()) - if terr != nil { - fmt.Println("Failed to marshal new configuration file: " + terr.Error()) - os.Exit(1) - } - - if err := os.WriteFile(Filename, tomld, 0o600); err != nil { - println("error writing new config: " + err.Error()) + ConfigFilename = prefConfigLocation + "/" + "config.toml" + if err := snek.SafeWriteConfigAs(ConfigFilename); err != nil { + fmt.Println("Failed to write new configuration file to '" + ConfigFilename + "': " + err.Error()) os.Exit(1) } - - return Filename } // Init will initialize our toml configuration engine and define our default configuration values which can be written to a new configuration file if desired func Init() { + snek.SetConfigType("toml") + snek.SetConfigName("config") + argParse() if customconfig { @@ -90,65 +65,70 @@ func Init() { setDefaults() - chosen := "" - - uconf, _ := os.UserConfigDir() - - switch runtime.GOOS { - case "windows": - // - default: - if _, err := os.Stat(filepath.Join("/etc/", Title, "config.toml")); err == nil { - chosen = filepath.Join("/etc/", Title, "config.toml") - } + for _, loc := range getConfigPaths() { + snek.AddConfigPath(loc) } - if chosen == "" && uconf == "" && home != "" { - uconf = filepath.Join(home, ".config") + if err := snek.MergeInConfig(); err != nil { + println("Error reading configuration file: " + err.Error()) + println("Writing new configuration file...") + writeConfig() } - if chosen == "" && uconf != "" { - _ = os.MkdirAll(filepath.Join(uconf, Title), 0750) - chosen = filepath.Join(uconf, Title, "config.toml") + if len(ConfigFilename) < 1 { + ConfigFilename = snek.ConfigFileUsed() } - if chosen == "" { - pwd, _ := os.Getwd() - if _, err := os.Stat("./config.toml"); err == nil { - chosen = "./config.toml" - } else { - if _, err := os.Stat(filepath.Join(pwd, "config.toml")); err == nil { - chosen = filepath.Join(pwd, "config.toml") - } - } - } + snek.SetEnvKeyReplacer(strings.NewReplacer(".", "_", "-", "_")) + snek.SetEnvPrefix(Title) - loadErr := snek.Load(file.Provider(chosen), toml.Parser()) + snek.AutomaticEnv() - if chosen == "" || loadErr != nil { - println("No configuration file found, writing new configuration file...") - chosen = writeConfig() + associateExportedVariables() +} + +func getConfigPaths() (paths []string) { + paths = append(paths, "./") + //goland:noinspection GoBoolExpressions + if runtime.GOOS != "windows" { + paths = append(paths, + prefConfigLocation, "/etc/"+Title+"/", "../", "../../") } - Filename = chosen + return +} - if loadErr = snek.Load(file.Provider(chosen), toml.Parser()); loadErr != nil { - fmt.Println("failed to load default config file: ", loadErr.Error()) +func loadCustomConfig(path string) { + /* #nosec */ + cf, err := os.Open(path) + if err != nil { + println("Error opening specified config file: " + path) + println(err.Error()) os.Exit(1) } - /* snek.SetEnvKeyReplacer(strings.NewReplacer(".", "_", "-", "_")) - snek.SetEnvPrefix(Title) - snek.AutomaticEnv() - */ - associateExportedVariables() -} + ConfigFilename, err = filepath.Abs(path) + if len(ConfigFilename) < 1 || err != nil { + ConfigFilename = path + } -func loadCustomConfig(path string) { - Filename, _ = filepath.Abs(path) + defer func(f *os.File) { + if fcerr := f.Close(); fcerr != nil { + fmt.Println("failed to close file handler for config file: ", fcerr.Error()) + } + }(cf) - if err := snek.Load(file.Provider(Filename), toml.Parser()); err != nil { - fmt.Println("failed to load specified config file: ", err.Error()) + buf, err1 := io.ReadAll(cf) + err2 := snek.ReadConfig(bytes.NewBuffer(buf)) + + switch { + case err1 != nil: + fmt.Println("config file read fatal error during i/o: ", err1.Error()) + os.Exit(1) + case err2 != nil: + fmt.Println("config file read fatal error during parse: ", err2.Error()) os.Exit(1) + default: + break } customconfig = true @@ -157,12 +137,11 @@ func loadCustomConfig(path string) { func processOpts() { // string options and their exported variables stringOpt := map[string]*string{ - "http.bind_addr": &HTTPBind, - "http.bind_port": &HTTPPort, - "http.real_ip_header": &HeaderName, - "logger.directory": &logDir, - "logger.console_time_format": &ConsoleTimeFormat, - "deception.server_name": &FakeServerName, + "http.bind_addr": &HTTPBind, + "http.bind_port": &HTTPPort, + "http.real_ip_header": &HeaderName, + "logger.directory": &logDir, + "deception.server_name": &FakeServerName, } // string slice options and their exported variables strSliceOpt := map[string]*[]string{ @@ -186,29 +165,20 @@ func processOpts() { } for key, opt := range stringOpt { - *opt = snek.String(key) + *opt = snek.GetString(key) } for key, opt := range strSliceOpt { - *opt = snek.Strings(key) + *opt = snek.GetStringSlice(key) } for key, opt := range boolOpt { - *opt = snek.Bool(key) + *opt = snek.GetBool(key) } for key, opt := range intOpt { - *opt = snek.Int(key) + *opt = snek.GetInt(key) } } func associateExportedVariables() { - _ = snek.Load(env.Provider("HELLPOT_", ".", func(s string) string { - s = strings.TrimPrefix(s, "HELLPOT_") - s = strings.ToLower(s) - s = strings.ReplaceAll(s, "__", " ") - s = strings.ReplaceAll(s, "_", ".") - s = strings.ReplaceAll(s, " ", "_") - return s - }), nil) - processOpts() if noColorForce { @@ -216,8 +186,8 @@ func associateExportedVariables() { } if UseUnixSocket { - UnixSocketPath = snek.String("http.unix_socket_path") - parsedPermissions, err := strconv.ParseUint(snek.String("http.unix_socket_permissions"), 8, 32) + UnixSocketPath = snek.GetString("http.unix_socket_path") + parsedPermissions, err := strconv.ParseUint(snek.GetString("http.unix_socket_permissions"), 8, 32) if err == nil { UnixSocketPermissions = uint32(parsedPermissions) } diff --git a/internal/config/help.go b/internal/config/help.go index 8c50059..3bbc357 100644 --- a/internal/config/help.go +++ b/internal/config/help.go @@ -22,7 +22,8 @@ var CLI = help{ 1: {0: "--nocolor", 1: "disable color and banner"}, 2: {0: "--banner", 1: "show banner + version and exit"}, 3: {0: "--genconfig", 1: "write default config to " + Title + ".toml then exit"}, - 4: {0: "--help", 1: "show this help and exit"}, + 4: {0: "--book", "", "Specify a custom file used for text generation"}, + 5: {0: "--help", 1: "show this help and exit"}, }, out: os.Stdout, } diff --git a/internal/http/router.go b/internal/http/router.go index a89284c..766c924 100644 --- a/internal/http/router.go +++ b/internal/http/router.go @@ -4,6 +4,7 @@ import ( "bufio" "fmt" "net/http" + "os" "runtime" "strings" "time" @@ -16,7 +17,10 @@ import ( "github.com/yunginnanet/HellPot/internal/config" ) -var log *zerolog.Logger +var ( + log *zerolog.Logger + hellpotHeffalump = heffalump.DefaultHeffalump +) func getRealRemote(ctx *fasthttp.RequestCtx) string { xrealip := string(ctx.Request.Header.Peek(config.HeaderName)) @@ -61,7 +65,7 @@ func hellPot(ctx *fasthttp.RequestCtx) { var wn int64 for { - wn, err = heffalump.DefaultHeffalump.WriteHell(bw) + wn, err = hellpotHeffalump.WriteHell(bw) n += wn if err != nil { slog.Trace().Err(err).Msg("END_ON_ERR") @@ -74,7 +78,6 @@ func hellPot(ctx *fasthttp.RequestCtx) { Dur("DURATION", time.Since(s)). Msg("FINISH") }) - } func getSrv(r *router.Router) fasthttp.Server { @@ -120,6 +123,24 @@ func getSrv(r *router.Router) fasthttp.Server { // Serve starts our HTTP server and request router func Serve() error { log = config.GetLogger() + + if config.UseCustomHeffalump { + content, err := os.ReadFile(config.BookFilename) + if err != nil { + panic(err) + } + // Wasteful, but only done once at startup + src := string(content) + log.Info().Msgf("Using custom book file '%s'", config.BookFilename) + + if len(src) < 1 { + panic("book file was empty!") + } + + markovMap := heffalump.MakeMarkovMap(strings.NewReader(src)) + hellpotHeffalump = heffalump.NewHeffalump(markovMap, heffalump.DefaultBuffSize) + } + l := config.HTTPBind + ":" + config.HTTPPort r := router.New() From 0ba61edbc070e844f0c1558089e08d9588844a7d Mon Sep 17 00:00:00 2001 From: Emil Jonathan Eriksson <8700261+ginger51011@users.noreply.github.com> Date: Fri, 19 Jan 2024 17:42:45 +0100 Subject: [PATCH 02/29] Remove globals from heffalump/*, add NewDefault{Heffalump,MarkovMap}() This removes globals from `heffalumpt/`, which are hard to reason about, easy to get wrong, and should not be created if they are never used. A possible drawbacks is if you would create multiple new defaults, but this should never be the case. --- heffalump/heffalump.go | 12 +++++++----- heffalump/markov.go | 9 +++------ internal/http/router.go | 5 ++++- 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/heffalump/heffalump.go b/heffalump/heffalump.go index 15b6365..cec193b 100644 --- a/heffalump/heffalump.go +++ b/heffalump/heffalump.go @@ -14,11 +14,7 @@ import ( var log = config.GetLogger() -// DefaultHeffalump represents a Heffalump type -var ( - DefaultHeffalump *Heffalump - DefaultBuffSize int = 100 * 1 << 10 -) +const DefaultBuffSize = 100 * 1 << 10 // Heffalump represents our buffer pool and markov map from Heffalump type Heffalump struct { @@ -39,6 +35,12 @@ func NewHeffalump(mm MarkovMap, buffsize int) *Heffalump { } } +// NewDefaultHeffalump instantiates a new default Heffalump from a MarkovMap created using +// using the default source text. +func NewDefaultHeffalump() *Heffalump { + return NewHeffalump(NewDefaultMarkovMap(), DefaultBuffSize) +} + // WriteHell writes markov chain heffalump hell to the provided io.Writer func (h *Heffalump) WriteHell(bw *bufio.Writer) (int64, error) { var n int64 diff --git a/heffalump/markov.go b/heffalump/markov.go index f6ce601..e4a565e 100644 --- a/heffalump/markov.go +++ b/heffalump/markov.go @@ -11,10 +11,8 @@ import ( "git.tcp.direct/kayos/common/squish" ) -var DefaultMarkovMap MarkovMap - -func init() { - // DefaultMarkovMap is a Markov chain based on src. +// NewDefaultMarkovMap creates a new MarkovMap from the default source text. +func NewDefaultMarkovMap() MarkovMap { src, err := squish.UnpackStr(srcGz) if err != nil { panic(err) @@ -23,8 +21,7 @@ func init() { panic("failed to unpack source") } - DefaultMarkovMap = MakeMarkovMap(strings.NewReader(src)) - DefaultHeffalump = NewHeffalump(DefaultMarkovMap, DefaultBuffSize) + return MakeMarkovMap(strings.NewReader(src)) } // ScanHTML is a basic split function for a Scanner that returns each diff --git a/internal/http/router.go b/internal/http/router.go index 766c924..bd597a6 100644 --- a/internal/http/router.go +++ b/internal/http/router.go @@ -19,7 +19,7 @@ import ( var ( log *zerolog.Logger - hellpotHeffalump = heffalump.DefaultHeffalump + hellpotHeffalump *heffalump.Heffalump ) func getRealRemote(ctx *fasthttp.RequestCtx) string { @@ -139,6 +139,9 @@ func Serve() error { markovMap := heffalump.MakeMarkovMap(strings.NewReader(src)) hellpotHeffalump = heffalump.NewHeffalump(markovMap, heffalump.DefaultBuffSize) + } else { + log.Info().Msg("Using default source text") + hellpotHeffalump = heffalump.NewDefaultHeffalump() } l := config.HTTPBind + ":" + config.HTTPPort From 3671a67924fe9edcc131dd7e63fce48517b0b760 Mon Sep 17 00:00:00 2001 From: Emil Jonathan Eriksson <8700261+ginger51011@users.noreply.github.com> Date: Sat, 20 Jan 2024 10:04:57 +0100 Subject: [PATCH 03/29] Add short flags to help, add `--help` flag This prints the short variants (like `-c` for `--config`) in the help. Also fixes bug where only `-h` flag works, not `--help`. --- internal/config/arguments.go | 2 +- internal/config/help.go | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/internal/config/arguments.go b/internal/config/arguments.go index 7628867..7ac340c 100644 --- a/internal/config/arguments.go +++ b/internal/config/arguments.go @@ -22,7 +22,7 @@ func argParse() { continue } switch arg { - case "-h": + case "-h", "--help": CLI.printUsage() case "-c", "--config": if len(os.Args) < i+2 { diff --git a/internal/config/help.go b/internal/config/help.go index 3bbc357..30b2d84 100644 --- a/internal/config/help.go +++ b/internal/config/help.go @@ -18,12 +18,12 @@ var CLI = help{ title: Title, version: Version, usage: map[int][]string{ - 0: {0: "--config", 1: "", 2: "Specify config file"}, + 0: {0: "-c, --config", 1: "", 2: "Specify config file"}, 1: {0: "--nocolor", 1: "disable color and banner"}, 2: {0: "--banner", 1: "show banner + version and exit"}, 3: {0: "--genconfig", 1: "write default config to " + Title + ".toml then exit"}, - 4: {0: "--book", "", "Specify a custom file used for text generation"}, - 5: {0: "--help", 1: "show this help and exit"}, + 4: {0: "-b, --book", 1: "", 2: "Specify a custom file used for text generation"}, + 5: {0: "-h, --help", 1: "show this help and exit"}, }, out: os.Stdout, } From 3024b2d0c35947ef44a08276c18c58b51e7aed84 Mon Sep 17 00:00:00 2001 From: Emil Jonathan Eriksson <8700261+ginger51011@users.noreply.github.com> Date: Fri, 2 Feb 2024 18:14:32 +0100 Subject: [PATCH 04/29] Rename --book to --grimoire, fix comments --- README.md | 2 +- cmd/HellPot/HellPot.go | 2 +- internal/config/arguments.go | 6 +-- internal/config/config.go | 24 ++++++------ internal/config/defaults.go | 72 +++++++++++++++++------------------- internal/config/help.go | 4 +- internal/http/router.go | 11 +++--- 7 files changed, 59 insertions(+), 62 deletions(-) diff --git a/README.md b/README.md index 0238084..b0ced5d 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ Clients (hopefully bots) that disregard `robots.txt` and connect to your instanc HellPot will send an infinite stream of data that is _just close enough_ to being a real website that they might just stick around until their soul is ripped apart and they cease to exist. -Under the hood of this eternal suffering is a markov engine that chucks bits and pieces of [The Birth of Tragedy (Hellenism and Pessimism)](https://www.gutenberg.org/files/51356/51356-h/51356-h.htm) by Friedrich Nietzsche at the client using [fasthttp](https://github.com/valyala/fasthttp), or optionally your (least?) favorite text using the `-b`/`--book` flag. +Under the hood of this eternal suffering is a markov engine that chucks bits and pieces of [The Birth of Tragedy (Hellenism and Pessimism)](https://www.gutenberg.org/files/51356/51356-h/51356-h.htm) by Friedrich Nietzsche at the client using [fasthttp](https://github.com/valyala/fasthttp), or optionally you may synchronize HellPot with your nightmares by using the `-g`/`--grimoire` flag ## Building From Source diff --git a/cmd/HellPot/HellPot.go b/cmd/HellPot/HellPot.go index 54f53e3..b2b3055 100644 --- a/cmd/HellPot/HellPot.go +++ b/cmd/HellPot/HellPot.go @@ -38,7 +38,7 @@ func init() { extra.Banner() - log.Info().Str("caller", "config").Str("file", config.ConfigFilename).Msg(config.ConfigFilename) + log.Info().Str("caller", "config").Str("file", config.Filename).Msg(config.Filename) log.Info().Str("caller", "logger").Msg(config.CurrentLogFile) log.Debug().Str("caller", "logger").Msg("debug enabled") log.Trace().Str("caller", "logger").Msg("trace enabled") diff --git a/internal/config/arguments.go b/internal/config/arguments.go index 7ac340c..d23e05e 100644 --- a/internal/config/arguments.go +++ b/internal/config/arguments.go @@ -30,12 +30,12 @@ func argParse() { os.Exit(1) } loadCustomConfig(os.Args[i+1]) - case "-b", "--book": + case "-g", "--grimoire": if len(os.Args) < i+2 { - println("missing book file after -b/--book") + println("missing source of suffering file after -g/--grimoire") os.Exit(1) } - BookFilename = os.Args[i+1] + Grimoire = os.Args[i+1] UseCustomHeffalump = true default: continue diff --git a/internal/config/config.go b/internal/config/config.go index d5c20b2..7ea53f1 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -29,12 +29,12 @@ var ( Trace bool // Debug is the value of our debug (verbose) on/off toggle as per the current configuration. Debug bool - // ConfigFilename returns the current location of our toml config file. - ConfigFilename string + // Filename returns the current location of our toml config file. + Filename string // UseCustomHeffalump decides if a custom Heffalump is to be used UseCustomHeffalump = false - // BookFilename returns the current location of a possible book file - BookFilename string + // Grimoire returns the current location of a possible source of suffering file + Grimoire string ) func writeConfig() { @@ -44,9 +44,9 @@ func writeConfig() { os.Exit(1) } } - ConfigFilename = prefConfigLocation + "/" + "config.toml" - if err := snek.SafeWriteConfigAs(ConfigFilename); err != nil { - fmt.Println("Failed to write new configuration file to '" + ConfigFilename + "': " + err.Error()) + Filename = prefConfigLocation + "/" + "config.toml" + if err := snek.SafeWriteConfigAs(Filename); err != nil { + fmt.Println("Failed to write new configuration file to '" + Filename + "': " + err.Error()) os.Exit(1) } } @@ -75,8 +75,8 @@ func Init() { writeConfig() } - if len(ConfigFilename) < 1 { - ConfigFilename = snek.ConfigFileUsed() + if len(Filename) < 1 { + Filename = snek.ConfigFileUsed() } snek.SetEnvKeyReplacer(strings.NewReplacer(".", "_", "-", "_")) @@ -106,9 +106,9 @@ func loadCustomConfig(path string) { os.Exit(1) } - ConfigFilename, err = filepath.Abs(path) - if len(ConfigFilename) < 1 || err != nil { - ConfigFilename = path + Filename, err = filepath.Abs(path) + if len(Filename) < 1 || err != nil { + Filename = path } defer func(f *os.File) { diff --git a/internal/config/defaults.go b/internal/config/defaults.go index 0d0241a..0f1ec14 100644 --- a/internal/config/defaults.go +++ b/internal/config/defaults.go @@ -1,15 +1,27 @@ package config import ( + "fmt" "io" "os" + "path" "runtime" - "time" - "github.com/knadh/koanf/parsers/toml" "github.com/spf13/afero" ) +func init() { + var err error + if home, err = os.UserHomeDir(); err != nil { + panic(err) + } + if len(defOpts) == 0 { + panic("default options map is empty") + } + defOpts["logger"]["directory"] = path.Join(home, ".local", "share", Title, "logs") + prefConfigLocation = path.Join(home, ".config", Title) +} + var ( configSections = []string{"logger", "http", "performance", "deception", "ssh"} defNoColor = false @@ -17,12 +29,11 @@ var ( var defOpts = map[string]map[string]interface{}{ "logger": { - "debug": true, - "trace": false, - "nocolor": defNoColor, - "use_date_filename": true, - "docker_logging": false, - "console_time_format": time.Kitchen, + "debug": true, + "trace": false, + "nocolor": defNoColor, + "use_date_filename": true, + "docker_logging": false, }, "http": { "use_unix_socket": false, @@ -54,28 +65,28 @@ var defOpts = map[string]map[string]interface{}{ } func gen(memfs afero.Fs) { - var ( - dat []byte - err error - f afero.File - ) - if dat, err = snek.Marshal(toml.Parser()); err != nil { + target := fmt.Sprintf("%s.toml", Title) + if err := snek.SafeWriteConfigAs("config.toml"); err != nil { + print(err.Error()) + os.Exit(1) + } + var f afero.File + var err error + f, err = memfs.Open("config.toml") + if err != nil { println(err.Error()) os.Exit(1) } - if f, err = memfs.Create("config.toml"); err != nil { + nf, err := os.Create(target) // #nosec G304 + if err != nil { println(err.Error()) os.Exit(1) } - var n int - if n, err = f.Write(dat); err != nil || n != len(dat) { - if err == nil { - err = io.ErrShortWrite - } + if _, err = io.Copy(nf, f); err != nil { println(err.Error()) os.Exit(1) } - println("Default config written to config.toml") + println("default configuration successfully written to " + target) os.Exit(0) } @@ -86,25 +97,10 @@ func setDefaults() { defNoColor = true } for _, def := range configSections { - for key, val := range defOpts[def] { - if _, ok := val.(map[string]interface{}); !ok { - if err := snek.Set(def+"."+key, val); err != nil { - println(err.Error()) - os.Exit(1) - } - continue - } - for k, v := range val.(map[string]interface{}) { - if err := snek.Set(def+"."+key+"."+k, v); err != nil { - println(err.Error()) - os.Exit(1) - } - } - continue - } + snek.SetDefault(def, defOpts[def]) } - if GenConfig { + snek.SetFs(memfs) gen(memfs) } } diff --git a/internal/config/help.go b/internal/config/help.go index 30b2d84..f5e8d37 100644 --- a/internal/config/help.go +++ b/internal/config/help.go @@ -22,8 +22,8 @@ var CLI = help{ 1: {0: "--nocolor", 1: "disable color and banner"}, 2: {0: "--banner", 1: "show banner + version and exit"}, 3: {0: "--genconfig", 1: "write default config to " + Title + ".toml then exit"}, - 4: {0: "-b, --book", 1: "", 2: "Specify a custom file used for text generation"}, - 5: {0: "-h, --help", 1: "show this help and exit"}, + 4: {0: "-g, --grimoire", 1: "", 2: "Specify a custom file used for text generation"}, + 5: {0: "-h,--help", 1: "show this help and exit"}, }, out: os.Stdout, } diff --git a/internal/http/router.go b/internal/http/router.go index bd597a6..18842c2 100644 --- a/internal/http/router.go +++ b/internal/http/router.go @@ -124,22 +124,23 @@ func getSrv(r *router.Router) fasthttp.Server { func Serve() error { log = config.GetLogger() - if config.UseCustomHeffalump { - content, err := os.ReadFile(config.BookFilename) + switch config.UseCustomHeffalump { + case true: + content, err := os.ReadFile(config.Grimoire) if err != nil { panic(err) } // Wasteful, but only done once at startup src := string(content) - log.Info().Msgf("Using custom book file '%s'", config.BookFilename) + log.Info().Msgf("Using custom grimoire file '%s'", config.Grimoire) if len(src) < 1 { - panic("book file was empty!") + panic("grimoire file was empty!") } markovMap := heffalump.MakeMarkovMap(strings.NewReader(src)) hellpotHeffalump = heffalump.NewHeffalump(markovMap, heffalump.DefaultBuffSize) - } else { + default: log.Info().Msg("Using default source text") hellpotHeffalump = heffalump.NewDefaultHeffalump() } From 0a18454ea058801876db46daf7db5a053d4dbb3c Mon Sep 17 00:00:00 2001 From: "kayos@tcp.direct" Date: Thu, 20 Jun 2024 21:42:24 -0700 Subject: [PATCH 05/29] TeeHee: Essentially rewrite HellPot :^) --- README.md | 2 +- cmd/HellPot/HellPot.go | 130 +++++++++++---- go.mod | 6 +- go.sum | 13 +- heffalump/heffalump.go | 8 +- internal/config/arguments.go | 44 ----- internal/config/client_rules.go | 83 ++++++++++ internal/config/command_line.go | 52 ++++++ internal/config/config.go | 206 ------------------------ internal/config/defaults.go | 117 ++++++-------- internal/config/defaults_test.go | 57 +++++++ internal/config/globals.go | 86 ---------- internal/config/help.go | 124 -------------- internal/config/logger.go | 71 -------- internal/config/models.go | 87 ++++++++++ internal/config/models_test.go | 47 ++++++ internal/config/setup.go | 63 ++++++++ internal/config/setup_test.go | 125 ++++++++++++++ internal/extra/banner.go | 33 ++-- internal/http/robots.go | 23 +-- internal/http/router.go | 59 ++++--- internal/http/router_unix.go | 3 +- internal/logger/logger.go | 63 ++++++++ internal/testutil/cmd/hellscope/main.go | 3 + internal/version/globals.go | 26 +++ 25 files changed, 820 insertions(+), 711 deletions(-) delete mode 100644 internal/config/arguments.go create mode 100644 internal/config/client_rules.go create mode 100644 internal/config/command_line.go delete mode 100644 internal/config/config.go create mode 100644 internal/config/defaults_test.go delete mode 100644 internal/config/globals.go delete mode 100644 internal/config/help.go delete mode 100644 internal/config/logger.go create mode 100644 internal/config/models.go create mode 100644 internal/config/models_test.go create mode 100644 internal/config/setup.go create mode 100644 internal/config/setup_test.go create mode 100644 internal/logger/logger.go create mode 100644 internal/testutil/cmd/hellscope/main.go create mode 100644 internal/version/globals.go diff --git a/README.md b/README.md index b0ced5d..a70da84 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ Clients (hopefully bots) that disregard `robots.txt` and connect to your instanc HellPot will send an infinite stream of data that is _just close enough_ to being a real website that they might just stick around until their soul is ripped apart and they cease to exist. -Under the hood of this eternal suffering is a markov engine that chucks bits and pieces of [The Birth of Tragedy (Hellenism and Pessimism)](https://www.gutenberg.org/files/51356/51356-h/51356-h.htm) by Friedrich Nietzsche at the client using [fasthttp](https://github.com/valyala/fasthttp), or optionally you may synchronize HellPot with your nightmares by using the `-g`/`--grimoire` flag +Under the hood of this eternal suffering is a markov engine that chucks bits and pieces of [The Birth of Tragedy (Hellenism and Pessimism)](https://www.gutenberg.org/files/51356/51356-h/51356-h.htm) by Friedrich Nietzsche at the client~~~~ using [fasthttp](https://github.com/valyala/fasthttp), or optionally you may synchronize HellPot with your nightmares by using the `-g`/`--grimoire` flag ## Building From Source diff --git a/cmd/HellPot/HellPot.go b/cmd/HellPot/HellPot.go index b2b3055..e2a81a5 100644 --- a/cmd/HellPot/HellPot.go +++ b/cmd/HellPot/HellPot.go @@ -1,59 +1,135 @@ package main import ( + "io" "os" "os/signal" + "path/filepath" + "strconv" "syscall" - - "github.com/rs/zerolog" + "time" "github.com/yunginnanet/HellPot/internal/config" "github.com/yunginnanet/HellPot/internal/extra" "github.com/yunginnanet/HellPot/internal/http" + "github.com/yunginnanet/HellPot/internal/logger" + "github.com/yunginnanet/HellPot/internal/version" ) var ( - log zerolog.Logger - version string // set by linker + runningConfig *config.Parameters ) -func init() { - if version != "" { - config.Version = version[1:] +func writeConfig(target string) bool { + var f *os.File + var err error + if f, err = os.Create(target); err != nil { + println("failed to create config file: " + err.Error()) + return false } - config.Init() - if config.BannerOnly { - extra.Banner() - os.Exit(0) + if _, err = io.Copy(f, config.Defaults.IO); err != nil { + println("failed to write default config to file: " + err.Error()) + _ = f.Close() + return false } + println("wrote default config to " + target) + runningConfig, _ = config.Setup(f) + _ = f.Close() + return true +} - switch config.DockerLogging { - case true: - config.CurrentLogFile = "/dev/stdout" - config.NoColor = true - log = config.StartLogger(false, os.Stdout) - default: - log = config.StartLogger(true) +func main() { + conf := config.CLIFlags.Lookup("config").Value + if conf.String() == "" { + conf = config.CLIFlags.Lookup("c").Value } - extra.Banner() + usingDefaults := true + resolvedConf := conf.String() - log.Info().Str("caller", "config").Str("file", config.Filename).Msg(config.Filename) - log.Info().Str("caller", "logger").Msg(config.CurrentLogFile) - log.Debug().Str("caller", "logger").Msg("debug enabled") - log.Trace().Str("caller", "logger").Msg("trace enabled") + uconf, _ := os.UserConfigDir() + if uconf == "" && os.Getenv("HOME") != "" { + uconf = filepath.Join(os.Getenv("HOME"), ".config") + } -} + if resolvedConf == "" { + for _, path := range []string{ + "/etc/HellPot/config.toml", + "/usr/local/etc/HellPot/config.toml", + "./config.toml", + filepath.Join(uconf, "HellPot", "config.toml"), + } { + if _, err := os.Stat(path); err == nil { + resolvedConf = path + break + } + } + } + + var setupErr error + var err error + var f *os.File + + f, err = os.Open(resolvedConf) + if err == nil { + runningConfig, setupErr = config.Setup(f) + } + switch { + case setupErr != nil: + println("failed to setup config: " + setupErr.Error()) + case err != nil: + println("failed to open config file for reading: " + err.Error()) + println("trying to create it....") + wroteOK := writeConfig(resolvedConf) + if wroteOK { + break + } + case runningConfig != nil: + usingDefaults = false + _ = f.Close() + } + + if runningConfig == nil { + if runningConfig, err = config.Setup(nil); err != nil || runningConfig == nil { + panic("failed to setup default config...\n" + err.Error()) + return // unreachable, but the linter doesn't seem to realize that + } + } + + log, _ := logger.New(runningConfig.Logger) + + if usingDefaults { + log.Warn().Msg("continuing with default configuration in ") + for i := 5; i > 0; i-- { + print(strconv.Itoa(i)) + for i := 0; i < 5; i++ { + time.Sleep(200 * time.Millisecond) + print(".") + } + } + } + + if !runningConfig.Logger.NoColor { + extra.Banner() + } + + log.Info().Msg("🔥 Starting HellPot 🔥") + log.Info().Msg("Version: " + version.Version) + log.Info().Msg("PID: " + strconv.Itoa(os.Getpid())) + log.Info().Msg("Using config file: " + resolvedConf) + if usingDefaults { + log.Warn().Msg("Using default configuration") + } + log.Debug().Msg("Debug logging enabled") + log.Trace().Msg("Trace logging enabled") -func main() { stopChan := make(chan os.Signal, 1) signal.Notify(stopChan, syscall.SIGINT, syscall.SIGTERM) go func() { - log.Fatal().Err(http.Serve()).Msg("HTTP error") + log.Fatal().Err(http.Serve(runningConfig)).Msg("HTTP error") }() <-stopChan // wait for SIGINT log.Warn().Msg("Shutting down server...") - } diff --git a/go.mod b/go.mod index 24d7bf2..96de733 100644 --- a/go.mod +++ b/go.mod @@ -7,17 +7,16 @@ require ( github.com/fasthttp/router v1.5.1 github.com/knadh/koanf/parsers/toml v0.1.0 github.com/knadh/koanf/providers/env v0.1.0 - github.com/knadh/koanf/providers/file v0.1.0 + github.com/knadh/koanf/providers/structs v0.1.0 github.com/knadh/koanf/v2 v2.1.1 github.com/rs/zerolog v1.33.0 - github.com/spf13/afero v1.11.0 github.com/valyala/fasthttp v1.55.0 golang.org/x/term v0.21.0 ) require ( github.com/andybalholm/brotli v1.1.0 // indirect - github.com/fsnotify/fsnotify v1.6.0 // indirect + github.com/fatih/structs v1.1.0 // indirect github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1 // indirect github.com/klauspost/compress v1.17.9 // indirect github.com/knadh/koanf/maps v0.1.1 // indirect @@ -29,5 +28,4 @@ require ( github.com/savsgio/gotils v0.0.0-20240303185622-093b76447511 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect golang.org/x/sys v0.21.0 // indirect - golang.org/x/text v0.16.0 // indirect ) diff --git a/go.sum b/go.sum index b0e7909..4b7a48a 100644 --- a/go.sum +++ b/go.sum @@ -6,8 +6,8 @@ github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSV github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/fasthttp/router v1.5.1 h1:uViy8UYYhm5npJSKEZ4b/ozM//NGzVCfJbh6VJ0VKr8= github.com/fasthttp/router v1.5.1/go.mod h1:WrmsLo3mrerZP2VEXRV1E8nL8ymJFYCDTr4HmnB8+Zs= -github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= -github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= +github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= +github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1 h1:TQcrn6Wq+sKGkpyPvppOz99zsMBaUOKXq6HSv655U1c= github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= @@ -19,8 +19,8 @@ github.com/knadh/koanf/parsers/toml v0.1.0 h1:S2hLqS4TgWZYj4/7mI5m1CQQcWurxUz6OD github.com/knadh/koanf/parsers/toml v0.1.0/go.mod h1:yUprhq6eo3GbyVXFFMdbfZSo928ksS+uo0FFqNMnO18= github.com/knadh/koanf/providers/env v0.1.0 h1:LqKteXqfOWyx5Ab9VfGHmjY9BvRXi+clwyZozgVRiKg= github.com/knadh/koanf/providers/env v0.1.0/go.mod h1:RE8K9GbACJkeEnkl8L/Qcj8p4ZyPXZIQ191HJi44ZaQ= -github.com/knadh/koanf/providers/file v0.1.0 h1:fs6U7nrV58d3CFAFh8VTde8TM262ObYf3ODrc//Lp+c= -github.com/knadh/koanf/providers/file v0.1.0/go.mod h1:rjJ/nHQl64iYCtAW2QQnF0eSmDEX/YZ/eNFj5yR6BvA= +github.com/knadh/koanf/providers/structs v0.1.0 h1:wJRteCNn1qvLtE5h8KQBvLJovidSdntfdyIbbCzEyE0= +github.com/knadh/koanf/providers/structs v0.1.0/go.mod h1:sw2YZ3txUcqA3Z27gPlmmBzWn1h8Nt9O6EP/91MkcWE= github.com/knadh/koanf/v2 v2.1.1 h1:/R8eXqasSTsmDCsAyYj+81Wteg8AqrV9CP6gvsTsOmM= github.com/knadh/koanf/v2 v2.1.1/go.mod h1:4mnTRbZCK+ALuBXHZMjDfG9y714L7TykVnZkXbMU3Es= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= @@ -41,22 +41,17 @@ github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8= github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= github.com/savsgio/gotils v0.0.0-20240303185622-093b76447511 h1:KanIMPX0QdEdB4R3CiimCAbxFrhB3j7h0/OvpYGVQa8= github.com/savsgio/gotils v0.0.0-20240303185622-093b76447511/go.mod h1:sM7Mt7uEoCeFSCBM+qBrqvEo+/9vdmj19wzp3yzUhmg= -github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= -github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasthttp v1.55.0 h1:Zkefzgt6a7+bVKHnu/YaYSOPfNYNisSVBo/unVCf8k8= github.com/valyala/fasthttp v1.55.0/go.mod h1:NkY9JtkrpPKmgwV3HTaS2HWaJss9RSIsRVfcxxoHiOM= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA= golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= -golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= -golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= nullprogram.com/x/rng v1.1.0 h1:SMU7DHaQSWtKJNTpNFIFt8Wd/KSmOuSDPXrMFp/UMro= diff --git a/heffalump/heffalump.go b/heffalump/heffalump.go index cec193b..d0d1b9f 100644 --- a/heffalump/heffalump.go +++ b/heffalump/heffalump.go @@ -8,12 +8,8 @@ import ( "bufio" "io" "sync" - - "github.com/yunginnanet/HellPot/internal/config" ) -var log = config.GetLogger() - const DefaultBuffSize = 100 * 1 << 10 // Heffalump represents our buffer pool and markov map from Heffalump @@ -46,11 +42,11 @@ func (h *Heffalump) WriteHell(bw *bufio.Writer) (int64, error) { var n int64 var err error - defer func() { + /* defer func() { if r := recover(); r != nil { log.Error().Interface("caller", r).Msg("panic recovered!") } - }() + }()*/ buf := h.pool.Get().([]byte) diff --git a/internal/config/arguments.go b/internal/config/arguments.go deleted file mode 100644 index d23e05e..0000000 --- a/internal/config/arguments.go +++ /dev/null @@ -1,44 +0,0 @@ -package config - -import ( - "os" -) - -var ( - forceDebug = false - forceTrace = false -) - -var argBoolMap = map[string]*bool{ - "--debug": &forceDebug, "-v": &forceDebug, "--trace": &forceTrace, "-vv": &forceTrace, - "--nocolor": &noColorForce, "--banner": &BannerOnly, "--genconfig": &GenConfig, -} - -// TODO: should probably just make a proper CLI with flags or something -func argParse() { - for i, arg := range os.Args { - if t, ok := argBoolMap[arg]; ok { - *t = true - continue - } - switch arg { - case "-h", "--help": - CLI.printUsage() - case "-c", "--config": - if len(os.Args) < i+2 { - println("missing config file after -c/--config") - os.Exit(1) - } - loadCustomConfig(os.Args[i+1]) - case "-g", "--grimoire": - if len(os.Args) < i+2 { - println("missing source of suffering file after -g/--grimoire") - os.Exit(1) - } - Grimoire = os.Args[i+1] - UseCustomHeffalump = true - default: - continue - } - } -} diff --git a/internal/config/client_rules.go b/internal/config/client_rules.go new file mode 100644 index 0000000..8a138a1 --- /dev/null +++ b/internal/config/client_rules.go @@ -0,0 +1,83 @@ +package config + +import ( + "bytes" + "fmt" + "regexp" +) + +type ClientRules struct { + // See: https://github.com/yunginnanet/HellPot/issues/23 + UseragentDisallowStrings []string `koanf:"user_agent_disallow_strings"` + useragentDisallowStrBytes [][]byte `koanf:"-"` + UseragentDisallowRegex []string `koanf:"user_agent_disallow_regex"` + useragentDisallowRegex []*regexp.Regexp `koanf:"-"` +} + +func NewClientRules(strs []string, regex []string) (*ClientRules, error) { + if strs == nil && regex == nil { + return &ClientRules{}, nil + } + if regex == nil { + regex = make([]string, 0) + } + if strs == nil { + strs = make([]string, 0) + } + cr := &ClientRules{ + UseragentDisallowStrings: strs, + UseragentDisallowRegex: regex, + } + return cr, cr.compile() +} + +func (c *ClientRules) compile() error { + dupes := make(map[string]struct{}) + for _, v := range c.UseragentDisallowRegex { + if v == "" { + continue + } + if _, ok := dupes[v]; ok { + continue + } + dupes[v] = struct{}{} + var compd *regexp.Regexp + var err error + if compd, err = regexp.Compile(v); err != nil { + return fmt.Errorf("failed to compile regex '%s': %w", v, err) + } + c.useragentDisallowRegex = append(c.useragentDisallowRegex, compd) + } + + newStrs := make([]string, 0) + for _, v := range c.UseragentDisallowStrings { + if v == "" { + continue + } + if _, ok := dupes[v]; ok { + continue + } + dupes[v] = struct{}{} + newStrs = append(newStrs, v) + } + c.UseragentDisallowStrings = newStrs + c.useragentDisallowStrBytes = make([][]byte, len(c.UseragentDisallowStrings)) + for i, v := range c.UseragentDisallowStrings { + c.useragentDisallowStrBytes[i] = []byte(v) + } + return nil +} + +func (c *ClientRules) MatchUseragent(ua []byte) bool { + for _, v := range c.useragentDisallowRegex { + if v.Match(ua) { + return true + } + } + for _, v := range c.useragentDisallowStrBytes { + if bytes.Contains(ua, v) { + return true + } + } + return false +} diff --git a/internal/config/command_line.go b/internal/config/command_line.go new file mode 100644 index 0000000..d3b44f5 --- /dev/null +++ b/internal/config/command_line.go @@ -0,0 +1,52 @@ +package config + +import ( + "flag" + "io" + "os" + + "github.com/yunginnanet/HellPot/internal/extra" + "github.com/yunginnanet/HellPot/internal/version" +) + +var CLIFlags = flag.NewFlagSet("cli", flag.ExitOnError) + +func init() { + CLIFlags.Bool("logger-debug", false, "force debug logging") + CLIFlags.Bool("logger-trace", false, "force trace logging") + CLIFlags.Bool("logger-nocolor", false, "force no color logging") + CLIFlags.String("bespoke-grimoire", "", "specify a custom file used for text generation") + CLIFlags.Bool("banner", false, "show banner and version then exit") + CLIFlags.Bool("genconfig", false, "write default config to stdout then exit") + CLIFlags.Bool("h", false, "show this help and exit") + CLIFlags.Bool("help", false, "show this help and exit") + CLIFlags.String("c", "", "specify config file") + CLIFlags.String("config", "", "specify config file") + CLIFlags.String("version", "", "show version and exit") + CLIFlags.String("v", "", "show version and exit") + if err := CLIFlags.Parse(os.Args[1:]); err != nil { + println(err.Error()) + // flag.ExitOnError will call os.Exit(2) + } + if CLIFlags.Lookup("h").Value.String() == "true" || CLIFlags.Lookup("help").Value.String() == "true" { + CLIFlags.Usage() + os.Exit(0) + } + if CLIFlags.Lookup("version").Value.String() == "true" || CLIFlags.Lookup("v").Value.String() == "true" { + _, _ = os.Stdout.WriteString("HellPot version: " + version.Version + "\n") + os.Exit(0) + } + if CLIFlags.Lookup("genconfig").Value.String() == "true" { + if n, err := io.Copy(os.Stdout, Defaults.IO); err != nil || n == 0 { + if err == nil { + err = io.EOF + } + panic(err) + } + os.Exit(0) + } + if CLIFlags.Lookup("banner").Value.String() == "true" { + extra.Banner() + os.Exit(0) + } +} diff --git a/internal/config/config.go b/internal/config/config.go deleted file mode 100644 index 7ea53f1..0000000 --- a/internal/config/config.go +++ /dev/null @@ -1,206 +0,0 @@ -package config - -import ( - "bytes" - "fmt" - "io" - "os" - "path/filepath" - "runtime" - "strconv" - "strings" - - "github.com/rs/zerolog" - "github.com/spf13/viper" -) - -// generic vars -var ( - noColorForce = false - customconfig = false - home string - prefConfigLocation string - snek = viper.New() -) - -// exported generic vars -var ( - // Trace is the value of our trace (extra verbose) on/off toggle as per the current configuration. - Trace bool - // Debug is the value of our debug (verbose) on/off toggle as per the current configuration. - Debug bool - // Filename returns the current location of our toml config file. - Filename string - // UseCustomHeffalump decides if a custom Heffalump is to be used - UseCustomHeffalump = false - // Grimoire returns the current location of a possible source of suffering file - Grimoire string -) - -func writeConfig() { - if _, err := os.Stat(prefConfigLocation); os.IsNotExist(err) { - if err = os.MkdirAll(prefConfigLocation, 0o750); err != nil { - println("error writing new config: " + err.Error()) - os.Exit(1) - } - } - Filename = prefConfigLocation + "/" + "config.toml" - if err := snek.SafeWriteConfigAs(Filename); err != nil { - fmt.Println("Failed to write new configuration file to '" + Filename + "': " + err.Error()) - os.Exit(1) - } -} - -// Init will initialize our toml configuration engine and define our default configuration values which can be written to a new configuration file if desired -func Init() { - snek.SetConfigType("toml") - snek.SetConfigName("config") - - argParse() - - if customconfig { - associateExportedVariables() - return - } - - setDefaults() - - for _, loc := range getConfigPaths() { - snek.AddConfigPath(loc) - } - - if err := snek.MergeInConfig(); err != nil { - println("Error reading configuration file: " + err.Error()) - println("Writing new configuration file...") - writeConfig() - } - - if len(Filename) < 1 { - Filename = snek.ConfigFileUsed() - } - - snek.SetEnvKeyReplacer(strings.NewReplacer(".", "_", "-", "_")) - snek.SetEnvPrefix(Title) - - snek.AutomaticEnv() - - associateExportedVariables() -} - -func getConfigPaths() (paths []string) { - paths = append(paths, "./") - //goland:noinspection GoBoolExpressions - if runtime.GOOS != "windows" { - paths = append(paths, - prefConfigLocation, "/etc/"+Title+"/", "../", "../../") - } - return -} - -func loadCustomConfig(path string) { - /* #nosec */ - cf, err := os.Open(path) - if err != nil { - println("Error opening specified config file: " + path) - println(err.Error()) - os.Exit(1) - } - - Filename, err = filepath.Abs(path) - if len(Filename) < 1 || err != nil { - Filename = path - } - - defer func(f *os.File) { - if fcerr := f.Close(); fcerr != nil { - fmt.Println("failed to close file handler for config file: ", fcerr.Error()) - } - }(cf) - - buf, err1 := io.ReadAll(cf) - err2 := snek.ReadConfig(bytes.NewBuffer(buf)) - - switch { - case err1 != nil: - fmt.Println("config file read fatal error during i/o: ", err1.Error()) - os.Exit(1) - case err2 != nil: - fmt.Println("config file read fatal error during parse: ", err2.Error()) - os.Exit(1) - default: - break - } - - customconfig = true -} - -func processOpts() { - // string options and their exported variables - stringOpt := map[string]*string{ - "http.bind_addr": &HTTPBind, - "http.bind_port": &HTTPPort, - "http.real_ip_header": &HeaderName, - "logger.directory": &logDir, - "deception.server_name": &FakeServerName, - } - // string slice options and their exported variables - strSliceOpt := map[string]*[]string{ - "http.router.paths": &Paths, - "http.uagent_string_blacklist": &UseragentBlacklistMatchers, - } - // bool options and their exported variables - boolOpt := map[string]*bool{ - "performance.restrict_concurrency": &RestrictConcurrency, - "http.use_unix_socket": &UseUnixSocket, - "logger.debug": &Debug, - "logger.trace": &Trace, - "logger.nocolor": &NoColor, - "logger.docker_logging": &DockerLogging, - "http.router.makerobots": &MakeRobots, - "http.router.catchall": &CatchAll, - } - // integer options and their exported variables - intOpt := map[string]*int{ - "performance.max_workers": &MaxWorkers, - } - - for key, opt := range stringOpt { - *opt = snek.GetString(key) - } - for key, opt := range strSliceOpt { - *opt = snek.GetStringSlice(key) - } - for key, opt := range boolOpt { - *opt = snek.GetBool(key) - } - for key, opt := range intOpt { - *opt = snek.GetInt(key) - } -} - -func associateExportedVariables() { - processOpts() - - if noColorForce { - NoColor = true - } - - if UseUnixSocket { - UnixSocketPath = snek.GetString("http.unix_socket_path") - parsedPermissions, err := strconv.ParseUint(snek.GetString("http.unix_socket_permissions"), 8, 32) - if err == nil { - UnixSocketPermissions = uint32(parsedPermissions) - } - } - - // We set exported variables here so that it tracks when accessed from other packages. - - if Debug || forceDebug { - zerolog.SetGlobalLevel(zerolog.DebugLevel) - Debug = true - } - if Trace || forceTrace { - zerolog.SetGlobalLevel(zerolog.TraceLevel) - Trace = true - } -} diff --git a/internal/config/defaults.go b/internal/config/defaults.go index 0f1ec14..f70d192 100644 --- a/internal/config/defaults.go +++ b/internal/config/defaults.go @@ -1,46 +1,66 @@ package config import ( - "fmt" - "io" - "os" - "path" + "bytes" "runtime" + "time" - "github.com/spf13/afero" + "github.com/knadh/koanf/parsers/toml" ) +var Defaults = &Preset{val: defOpts} + func init() { - var err error - if home, err = os.UserHomeDir(); err != nil { - panic(err) + Defaults.IO = &PresetIO{p: Defaults} +} + +type Preset struct { + val map[string]interface{} + IO *PresetIO +} + +type PresetIO struct { + p *Preset + buf *bytes.Buffer +} + +func (pre *Preset) ReadBytes() ([]byte, error) { + return toml.Parser().Marshal(pre.val) //nolint:wrapcheck +} + +func (shim *PresetIO) Read(p []byte) (int, error) { + if shim.buf.Len() > 0 { + return shim.buf.Read(p) //nolint:wrapcheck + } + data, err := shim.p.ReadBytes() + if err != nil { + return 0, err } - if len(defOpts) == 0 { - panic("default options map is empty") + if shim.buf == nil { + shim.buf = bytes.NewBuffer(data) } - defOpts["logger"]["directory"] = path.Join(home, ".local", "share", Title, "logs") - prefConfigLocation = path.Join(home, ".config", Title) + return shim.buf.Read(p) //nolint:wrapcheck } -var ( - configSections = []string{"logger", "http", "performance", "deception", "ssh"} - defNoColor = false -) +func (pre *Preset) Read() (map[string]interface{}, error) { + return pre.val, nil +} -var defOpts = map[string]map[string]interface{}{ - "logger": { - "debug": true, - "trace": false, - "nocolor": defNoColor, - "use_date_filename": true, - "docker_logging": false, +var defOpts = map[string]interface{}{ + "logger": map[string]interface{}{ + "debug": true, + "trace": false, + "nocolor": runtime.GOOS == "windows", + "use_date_filename": true, + "docker_logging": false, + "console_time_format": time.Kitchen, }, - "http": { + "http": map[string]interface{}{ "use_unix_socket": false, "unix_socket_path": "/var/run/hellpot", "unix_socket_permissions": "0666", "bind_addr": "127.0.0.1", - "bind_port": "8080", + "bind_port": int64(8080), //nolint:gomnd "real_ip_header": "X-Real-IP", "router": map[string]interface{}{ @@ -55,52 +75,11 @@ var defOpts = map[string]map[string]interface{}{ "Cloudflare-Traffic-Manager", }, }, - "performance": { + "performance": map[string]interface{}{ "restrict_concurrency": false, - "max_workers": 256, + "max_workers": 256, //nolint:gomnd }, - "deception": { + "deception": map[string]interface{}{ "server_name": "nginx", }, } - -func gen(memfs afero.Fs) { - target := fmt.Sprintf("%s.toml", Title) - if err := snek.SafeWriteConfigAs("config.toml"); err != nil { - print(err.Error()) - os.Exit(1) - } - var f afero.File - var err error - f, err = memfs.Open("config.toml") - if err != nil { - println(err.Error()) - os.Exit(1) - } - nf, err := os.Create(target) // #nosec G304 - if err != nil { - println(err.Error()) - os.Exit(1) - } - if _, err = io.Copy(nf, f); err != nil { - println(err.Error()) - os.Exit(1) - } - println("default configuration successfully written to " + target) - os.Exit(0) -} - -func setDefaults() { - memfs := afero.NewMemMapFs() - //goland:noinspection GoBoolExpressions - if runtime.GOOS == "windows" { - defNoColor = true - } - for _, def := range configSections { - snek.SetDefault(def, defOpts[def]) - } - if GenConfig { - snek.SetFs(memfs) - gen(memfs) - } -} diff --git a/internal/config/defaults_test.go b/internal/config/defaults_test.go new file mode 100644 index 0000000..fa31a6d --- /dev/null +++ b/internal/config/defaults_test.go @@ -0,0 +1,57 @@ +package config + +import ( + "bytes" + "testing" +) + +func TestDefaults(t *testing.T) { + t.Run("ReadBytes", func(t *testing.T) { + t.Parallel() + bs, err := Defaults.ReadBytes() + if err != nil { + t.Fatal(err) + } + if len(bs) == 0 { + t.Fatal("expected non-empty byte slice") + } + total := 0 + for _, needle := range []string{ + "logger", + "http", + "performance", + "deception", + } { + total += bytes.Count(bs, []byte(needle)) + 3 // name plus brackets and newline + if !bytes.Contains(bs, []byte(needle)) { + t.Errorf("expected %q in byte slice", needle) + } + } + if len(bs) <= total { + t.Errorf("default byte slice seems too short to contain any default values") + } + }) + t.Run("Read", func(t *testing.T) { + t.Parallel() + m, err := Defaults.Read() + if err != nil { + t.Fatal(err) + } + if len(m) == 0 { + t.Fatal("expected non-empty map") + } + for _, needle := range []string{ + "logger", + "http", + "performance", + "deception", + } { + if _, ok := m[needle]; !ok { + t.Errorf("expected %q in map", needle) + } + if m[needle] == nil { + t.Errorf("expected non-nil value for %q", needle) + } + } + }) +} diff --git a/internal/config/globals.go b/internal/config/globals.go deleted file mode 100644 index 1c82dfb..0000000 --- a/internal/config/globals.go +++ /dev/null @@ -1,86 +0,0 @@ -package config - -import ( - "runtime/debug" -) - -// Title is the name of the application used throughout the configuration process. -const Title = "HellPot" - -var Version = "dev" - -func init() { - if Version != "dev" { - return - } - binInfo := make(map[string]string) - info, ok := debug.ReadBuildInfo() - if !ok { - return - } - for _, v := range info.Settings { - binInfo[v.Key] = v.Value - } - if gitrev, ok := binInfo["vcs.revision"]; ok { - Version = gitrev[:7] - } -} - -var ( - // BannerOnly when toggled causes HellPot to only print the banner and version then exit. - BannerOnly = false - // GenConfig when toggled causes HellPot to write its default config to the cwd and then exit. - GenConfig = false - // NoColor when true will disable the banner and any colored console output. - NoColor bool - // DockerLogging when true will disable the banner and any colored console output, as well as disable the log file. - // Assumes NoColor == true. - DockerLogging bool - // MakeRobots when false will not respond to requests for robots.txt. - MakeRobots bool - // CatchAll when true will cause HellPot to respond to all paths. - // Note that this will override MakeRobots. - CatchAll bool - // ConsoleTimeFormat sets the time format for the console. The string is passed to time.Format() down the line. - ConsoleTimeFormat string -) - -// "http" -var ( - // HTTPBind is defined via our toml configuration file. It is the address that HellPot listens on. - HTTPBind string - // HTTPPort is defined via our toml configuration file. It is the port that HellPot listens on. - HTTPPort string - // HeaderName is defined via our toml configuration file. It is the HTTP Header containing the original IP of the client, - // in traditional reverse Proxy deployments. - HeaderName string - - // Paths are defined via our toml configuration file. These are the paths that HellPot will present for "robots.txt" - // These are also the paths that HellPot will respond for. Other paths will throw a warning and will serve a 404. - Paths []string - - // UseUnixSocket determines if we will listen for HTTP connections on a unix socket. - UseUnixSocket bool - - // UnixSocketPath is defined via our toml configuration file. It is the path of the socket HellPot listens on - // if UseUnixSocket, also defined via our toml configuration file, is set to true. - UnixSocketPath = "" - UnixSocketPermissions uint32 - - // UseragentBlacklistMatchers contains useragent matches checked for with strings.Contains() that - // prevent HellPot from firing off. - // See: https://github.com/yunginnanet/HellPot/issues/23 - UseragentBlacklistMatchers []string -) - -// "performance" -var ( - RestrictConcurrency bool - MaxWorkers int -) - -// "deception" -var ( - // FakeServerName is our configured value for the "Server: " response header when serving HTTP clients - FakeServerName string -) diff --git a/internal/config/help.go b/internal/config/help.go deleted file mode 100644 index f5e8d37..0000000 --- a/internal/config/help.go +++ /dev/null @@ -1,124 +0,0 @@ -package config - -import ( - "io" - "os" - "strings" - - "golang.org/x/term" -) - -type help struct { - title, version string - usage map[int][]string - out io.Writer -} - -var CLI = help{ - title: Title, - version: Version, - usage: map[int][]string{ - 0: {0: "-c, --config", 1: "", 2: "Specify config file"}, - 1: {0: "--nocolor", 1: "disable color and banner"}, - 2: {0: "--banner", 1: "show banner + version and exit"}, - 3: {0: "--genconfig", 1: "write default config to " + Title + ".toml then exit"}, - 4: {0: "-g, --grimoire", 1: "", 2: "Specify a custom file used for text generation"}, - 5: {0: "-h,--help", 1: "show this help and exit"}, - }, - out: os.Stdout, -} - -func (cli help) secondColStart(index int) (max int) { - l := cli.firstColEnd() + 2 - if len(cli.usage[index]) > 2 && cli.usage[index][2] != "" { - l -= len(cli.usage[index][1]) - } - if l > max { - max = l - } - return max -} - -func (cli help) firstColEnd() (max int) { - for n := range cli.usage { - l := len(cli.usage[n][0]) - if l > max { - max = l - } - } - return max -} - -func (cli help) stdout(s ...string) { - for _, v := range s { - _, _ = cli.out.Write([]byte(v)) - } -} - -func (cli help) lb(n int) { - for n > 0 { - cli.stdout("\n") - n-- - } -} - -func (cli help) printUsage() { - if !term.IsTerminal(int(os.Stdout.Fd())) { - os.Exit(1) - } - cli.header() - - for n := 0; n < len(cli.usage); n++ { - line := &strings.Builder{} - buf := &strings.Builder{} - usageAt := 1 - tlen := cli.secondColStart(n) - switch { - case cli.usage[n][0] == "": - cli.lb(1) - case cli.usage[n][1] == "": - cli.stdout(cli.usage[n][0]) - cli.lb(2) - case len(cli.usage[n]) > 2 && cli.usage[n][2] != "": - tlen = cli.firstColEnd() - len(cli.usage[n][1]) - usageAt = 2 - fallthrough - default: - buf.WriteString(cli.usage[n][0]) - } - if tlen < 0 { - tlen = 2 - } - tab := strings.Repeat(" ", tlen) - line.WriteString(" ") - if buf.Len() < cli.firstColEnd() { - line.WriteString(strings.Repeat(" ", cli.firstColEnd()-buf.Len())) - } - if usageAt == 2 { - buf.WriteString(strings.Repeat(" ", tlen/2)) - buf.WriteString(cli.usage[n][1]) - } - buf.WriteString(tab) - buf.Write([]byte(" (" + cli.usage[n][usageAt] + ")")) - buf.Write([]byte{'\n'}) - line.Write([]byte(buf.String())) - cli.stdout(line.String()) - } - os.Exit(0) - -} - -func (cli help) header() { - cli.stdout("\n") - s := &strings.Builder{} - s.Write([]byte(cli.title)) - s.Write([]byte(" v[")) - s.Write([]byte(cli.version)) - s.Write([]byte("]")) - tab := cli.firstColEnd() - (s.Len() % 2) + 1 - if tab > 0 { - cli.stdout(strings.Repeat(" ", tab)) - } - cli.stdout(s.String()) - cli.lb(2) -} diff --git a/internal/config/logger.go b/internal/config/logger.go deleted file mode 100644 index a9aba64..0000000 --- a/internal/config/logger.go +++ /dev/null @@ -1,71 +0,0 @@ -package config - -import ( - "io" - "os" - "path" - "path/filepath" - "strings" - "time" - - "github.com/rs/zerolog" -) - -var ( - // CurrentLogFile is used for accessing the location of the currently used log file across packages. - CurrentLogFile string - logFile io.Writer - logDir string - logger zerolog.Logger -) - -func prepLogDir() { - logDir = snek.String("logger.directory") - if logDir == "" { - logDir = filepath.Join(home, ".local", "share", Title, "logs") - } - _ = os.MkdirAll(logDir, 0750) -} - -// StartLogger instantiates an instance of our zerolog loggger so we can hook it in our main package. -// While this does return a logger, it should not be used for additional retrievals of the logger. Use GetLogger(). -func StartLogger(pretty bool, targets ...io.Writer) zerolog.Logger { - logFileName := "HellPot" - - if snek.Bool("logger.use_date_filename") { - tn := strings.ReplaceAll(time.Now().Format(time.RFC822), " ", "_") - tn = strings.ReplaceAll(tn, ":", "-") - logFileName = logFileName + "_" + tn - } - - var err error - - switch { - case len(targets) > 0: - logFile = io.MultiWriter(targets...) - default: - prepLogDir() - CurrentLogFile = path.Join(logDir, logFileName+".log") - //nolint:lll - logFile, err = os.OpenFile(CurrentLogFile, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0o666) // #nosec G304 G302 -- we are not using user input to create the file - if err != nil { - println("cannot create log file: " + err.Error()) - os.Exit(1) - } - } - - var logWriter = logFile - - if pretty { - logWriter = zerolog.MultiLevelWriter(zerolog.ConsoleWriter{TimeFormat: ConsoleTimeFormat, NoColor: NoColor, Out: os.Stdout}, logFile) - } - - logger = zerolog.New(logWriter).With().Timestamp().Logger() - return logger -} - -// GetLogger retrieves our global logger object. -func GetLogger() *zerolog.Logger { - // future logic here - return &logger -} diff --git a/internal/config/models.go b/internal/config/models.go new file mode 100644 index 0000000..f648d84 --- /dev/null +++ b/internal/config/models.go @@ -0,0 +1,87 @@ +package config + +import ( + "sync" + + "github.com/knadh/koanf/v2" + "github.com/rs/zerolog" + + "github.com/yunginnanet/HellPot/internal/logger" +) + +// Parameters represents the configuration for HellPot. +type Parameters struct { + HTTP HTTP `koanf:"http"` + Logger *logger.Configuration `koanf:"logger"` + Bespoke Customization `koanf:"bespoke"` + Perf Performance `koanf:"performance"` + Liar Deception `koanf:"deception"` + + source *koanf.Koanf `koanf:"-"` + logger *zerolog.Logger +} + +var once = &sync.Once{} + +func (p *Parameters) GetLogger() *zerolog.Logger { + once.Do(func() { + p.logger = logger.GetLoggerOnce() + }) + return p.logger +} + +type Deception struct { + // FakeServerName is our configured value for the "Server: " response header when serving HTTP clients + FakeServerName string `koanf:"fake_server_name"` +} + +type Performance struct { + ConcurrencyCap bool `koanf:"limit_concurrency"` + MaxWorkers int `koanf:"max_workers"` +} + +// UnixSocket represents the configuration for the Unix socket. +type UnixSocket struct { + // UnixSocketPath is the path to the Unix socket that HellPot will listen on if UseUnixSocket is set to true. + UnixSocketPath string `koanf:"unix_socket_path"` + // UseUnixSocket determines if we will listen for HTTP connections on a unix socket. + UseUnixSocket bool `koanf:"use_unix_socket"` + // UnixSocketPermissions are the octal permissions for the Unix socket. + UnixSocketPermissions uint32 `koanf:"unix_socket_permissions"` +} + +// Router represents the configuration for the HTTP router. +type Router struct { + // Paths are defined via our toml configuration file. These are the paths that HellPot will present for "robots.txt" + // These are also the paths that HellPot will respond for. Other paths will throw a warning and will serve a 404. + Paths []string `koanf:"paths"` + CatchAll bool `koanf:"catchall"` + MakeRobots bool `koanf:"makerobots"` + ClientRules ClientRules `koanf:"client_rules"` +} + +// HTTP represents the configuration for the HTTP server. +type HTTP struct { + Bind string `koanf:"bind_addr"` + Port int64 `koanf:"port"` + // ProxiedIPHeader is the HTTP Header containing the original IP of the client + // for usage by traditional reverse Proxy deployments. + ProxiedIPHeader string `koanf:"proxied_ip_header"` + Router Router `koanf:"router"` + UnixSocket UnixSocket `koanf:"unix_socket"` + ClientRules ClientRules `koanf:"client_rules"` + Experimental DevilsPlaythings `koanf:"experimental"` +} + +// DevilsPlaythings - nothing to see here, move along. +type DevilsPlaythings struct { + // POSTMimicry when true will cause HellPot to respond to POST requests to the configured roads to hell + // with the content of the POST request entangled within the response. (Experimental) + POSTMimicry bool `koanf:"post_mimicry"` +} + +// Customization represents the configuration for the customizations. +type Customization struct { + CustomHeffalump bool `koanf:"custom_heffalump"` + Grimoire string `koanf:"grimoire"` +} diff --git a/internal/config/models_test.go b/internal/config/models_test.go new file mode 100644 index 0000000..b7e9c51 --- /dev/null +++ b/internal/config/models_test.go @@ -0,0 +1,47 @@ +package config + +import "testing" + +func TestCompileRules(t *testing.T) { + if _, err := NewClientRules(nil, nil); err != nil { + t.Error(err) + } + rules, err := NewClientRules([]string{"test", "test"}, nil) + if err != nil { + t.Error(err) + } + if len(rules.UseragentDisallowStrings) != 1 { + t.Error("expected 1 got", len(rules.UseragentDisallowStrings)) + } + if rules.UseragentDisallowStrings[0] != "test" { + t.Error("expected test got", rules.UseragentDisallowStrings[0]) + } + rules, err = NewClientRules( + []string{"yeeterson", "", "", "", "yeeterson", "mc", "mc", "geeterson"}, + []string{"^y..terson$", "^mc", "^mc", "^g..ters.n$"}, + ) + if err != nil { + t.Error(err) + } + if len(rules.useragentDisallowRegex) != 3 { + t.Error("expected 3 got", len(rules.useragentDisallowRegex)) + } + if len(rules.UseragentDisallowStrings) != 3 { + t.Error("expected 3 got", len(rules.UseragentDisallowStrings)) + } + if !rules.MatchUseragent("yeeterson") { + t.Error("expected true got false") + } + if !rules.MatchUseragent("mc") { + t.Error("expected true got false") + } + if !rules.MatchUseragent("yooterson") { + t.Error("expected true got false") + } + if !rules.MatchUseragent("gooters%n") { + t.Error("expected true got false") + } + if rules.MatchUseragent("yootersongooterson") { + t.Error("expected false got true") + } +} diff --git a/internal/config/setup.go b/internal/config/setup.go new file mode 100644 index 0000000..20cd26a --- /dev/null +++ b/internal/config/setup.go @@ -0,0 +1,63 @@ +package config + +import ( + "fmt" + "io" + "os" + "strings" + + "github.com/knadh/koanf/parsers/toml" + "github.com/knadh/koanf/providers/env" + "github.com/knadh/koanf/v2" +) + +type readerProvider struct { + source io.Reader +} + +func (r *readerProvider) ReadBytes() ([]byte, error) { + return io.ReadAll(r.source) +} + +func (r *readerProvider) Read() (map[string]interface{}, error) { + b, err := r.ReadBytes() + if err != nil { + return nil, err + } + return toml.Parser().Unmarshal(b) //nolint:wrapcheck +} + +func Setup(source io.Reader) (*Parameters, error) { + k := koanf.New(".") + + if err := k.Load(Defaults, nil); err != nil { + return nil, fmt.Errorf("failed to load defaults: %w", err) + } + + if source != nil { + if err := k.Load(&readerProvider{source}, toml.Parser()); err != nil { + return nil, fmt.Errorf("failed to read config: %w", err) + } + } + + _ = k.Load(env.Provider("HELLPOT_", ".", func(s string) string { + s = strings.TrimPrefix(s, "HELLPOT_") + s = strings.ToLower(s) + s = strings.ReplaceAll(s, "__", " ") + s = strings.ReplaceAll(s, "_", ".") + s = strings.ReplaceAll(s, " ", "_") + return s + }), nil) + + p := &Parameters{ + source: k, + } + + if err := k.Unmarshal("", p); err != nil { + return nil, fmt.Errorf("failed to unmarshal config: %w", err) + } + + p.Logger.Outputs = append(p.Logger.Outputs, os.Stdout) + + return p, nil +} diff --git a/internal/config/setup_test.go b/internal/config/setup_test.go new file mode 100644 index 0000000..1562f20 --- /dev/null +++ b/internal/config/setup_test.go @@ -0,0 +1,125 @@ +package config + +import ( + "bytes" + "testing" +) + +func TestSetup(t *testing.T) { + t.Run("Success", SetupSuccess) + t.Run("NoFailureOnNilSource", SetupNoFailureOnNilSource) + t.Run("FailureOnReadConfig", SetupFailureOnReadConfig) +} + +func SetupSuccess(t *testing.T) { + source := bytes.NewBufferString(` +[http] +port = 55 +bind_addr = "5.5.5.5" + +[http.router] +catchall = true +makerobots = false +`) + + params, err := Setup(source) + if err != nil { + t.Errorf("Expected no error, got %v", err) + } + + if params == nil { + t.Fatal("Expected params to be not nil") + } + + if params.source.Get("http.port") != int64(55) { + t.Errorf("Expected 55, got (%T) %v", params.source.Get("http.port"), params.source.Get("http.port")) + } + if params.HTTP.Port != int64(55) { + t.Errorf("Expected 55, got %v", params.HTTP.Port) + } + if params.source.Get("http.bind_addr") != "5.5.5.5" { + t.Errorf("Expected 5.5.5.5, got %v", params.source.Get("http.bind_addr")) + } + if params.HTTP.Bind != "5.5.5.5" { + t.Errorf("Expected 5.5.5.5, got %v", params.HTTP.Bind) + } + if params.source.Get("http.router.catchall") != true { + t.Errorf("Expected true, got %v", params.source.Get("http.router.catchall")) + } + if params.HTTP.Router.CatchAll != true { + t.Errorf("Expected true, got %v", params.HTTP.Router.CatchAll) + } + +} + +func SetupNoFailureOnNilSource(t *testing.T) { + params, err := Setup(nil) + if err != nil { + t.Errorf("Expected no error, got %v", err) + } + + if params == nil { + t.Fatal("Expected params to be not nil") + } + + t.Run("DefaultsWithNilSource", func(t *testing.T) { + for _, needle := range []string{ + "logger", + "http", + "performance", + "deception", + "http.router", + "http.router.paths", + } { + if params.source.Get(needle) == nil { + t.Errorf("Expected %q in map", needle) + } + } + + // nolint:forcetypeassert + if params.HTTP.Port != Defaults.val["http"].(map[string]interface{})["port"].(int64) { + t.Errorf("Expected %v, got %v", + // nolint:forcetypeassert + Defaults.val["http"].(map[string]interface{})["port"].(int64), params.HTTP.Port, + ) + } + // nolint:forcetypeassert + if params.HTTP.Bind != Defaults.val["http"].(map[string]interface{})["bind_addr"].(string) { + t.Errorf("Expected %v, got %v", + // nolint:forcetypeassert + Defaults.val["http"].(map[string]interface{})["bind_addr"].(string), params.HTTP.Bind, + ) + } + if params.HTTP.Router.CatchAll != + // nolint:forcetypeassert + Defaults.val["http"].(map[string]interface{})["router"].(map[string]interface{})["catchall"].(bool) { + t.Errorf("Expected %v, got %v", + // nolint:forcetypeassert + Defaults.val["http"].(map[string]interface{})["router"].(map[string]interface{})["catchall"].(bool), + params.HTTP.Router.CatchAll, + ) + } + if len(params.HTTP.Router.Paths) != + // nolint:forcetypeassert + len(Defaults.val["http"].(map[string]interface{})["router"].(map[string]interface{})["paths"].([]string)) { + t.Errorf("Expected %v, got %v", + // nolint:forcetypeassert + Defaults.val["http"].(map[string]interface{})["router"].(map[string]interface{})["paths"].([]string), + params.HTTP.Router.Paths, + ) + } + }) +} + +func SetupFailureOnReadConfig(t *testing.T) { + source := bytes.NewBufferString(`{eeeeeeeeeeeeeeeeeeEE: 1}`) + + params, err := Setup(source) + if err == nil { + t.Error("Expected error, got nil") + } + + if params != nil { + t.Error("Expected params to be nil") + } +} diff --git a/internal/extra/banner.go b/internal/extra/banner.go index 1dd80a0..09e0745 100644 --- a/internal/extra/banner.go +++ b/internal/extra/banner.go @@ -5,13 +5,12 @@ import ( "encoding/binary" "fmt" "os" - "runtime" "strings" "time" "git.tcp.direct/kayos/common/squish" - "github.com/yunginnanet/HellPot/internal/config" + "github.com/yunginnanet/HellPot/internal/version" ) const hellpot = "H4sIAAAAAAACA8VXvW7bQAze9QpZOGQUZNXntBD6Ahm7Gx1cx0jdRnKRKAUCdPDgQavOgB/QTxLZ1P3oRJ5Obo0CtnE5feSR30fylOhmfjv9PEtzwIXIj4dds/xw2jsequNB2gizXd3Mxad2O81PX7AAe+UNGneuR8aUOuTsqQUDXAMv1cJE5Tfbn6GaKz45kpid+lQc3zoNY5zmEUEt+jCGNZUjeYr0StZYmbwtwNavuCaUFWA8MjxVIImjNas6TPQT9Tnq4MnYJF0zkhVU4rLvqflscU/ox0Lg45qKTjoSmiLQPA+ZuTT7BbrckpfWKMkUquTErIPEYbPoKjamy6SjR0feGssPUMYTCDWEnrR8c0m7hJ2B4jekK2KUsBfa7bpTD0ftnmKPE9nN2IzcLc99vxhIUbszlwqrJoklpQWlI6AeQh9nDHXj2ldOvyat/vZdDxVfzZdbSuspRUe/+IKZtxq2GWlbZzS6jnrnDEXGCkXUGnahuTgAA+DY9HU8FUoYH3ji/q84HetDWmT/Y3ml6oX21/eCNzB46+6UuVTSQHXgGmzUTJT/zeNQ3zCvysEBuH3hER9CbhNa6FoLHSBfT2gmK/rFKCj/K1nTfcBduKHVwgjo+Y+HilXBEAqhKg1X6lQzMaIF6ZK6ipVILR0Awh16SWy9KsxvZXWbL34oGpNmMcPNdYFmiE40+qV9cg4Logjm2uXjukzK5a/kYf28WpaTn4u3zcvkfvX09GVTnuFfEYzBNujvr9+S5SafvL0Wj+uiWBSrsov/I6axmMXiLhYf40zE2TTOZnF2F2fNn2n0DpcvBxhQEAAA" @@ -21,23 +20,23 @@ func rc(s []string) string { } func process(in string) (s string) { - var v = strings.Split(config.Version, "") + var v = strings.Split(version.Version, "") var maj, min, smin = "", "", "" - if len(config.Version) > 0 { + if len(version.Version) > 0 { maj = v[0] } - if len(config.Version) > 2 { + if len(version.Version) > 2 { min = v[2] } - if len(config.Version) > 4 { + if len(version.Version) > 4 { smin = v[4] } defl8, _ := squish.UnpackStr(in) sp := strings.Split(defl8, "|") s = sp[0] - if smin == "" || len(config.Version) == 7 || config.Version == "dev" { + if smin == "" || len(version.Version) == 7 || version.Version == "dev" { s = strings.ReplaceAll(s, "$1;40m.", "$1;40m") - if len(config.Version) == 7 || config.Version == "dev" { + if len(version.Version) == 7 || version.Version == "dev" { s = strings.ReplaceAll(s, "$3;40m.", "$3;40m") } } @@ -53,8 +52,8 @@ func process(in string) (s string) { for n := 1; n < 5; n++ { s = cproc(s, fmt.Sprintf("%d", n)) } - if len(config.Version) == 7 || config.Version == "dev" { - maj = "[" + config.Version + "]" + if len(version.Version) == 7 || version.Version == "dev" { + maj = "[" + version.Version + "]" min = "" smin = "" } @@ -72,8 +71,8 @@ func ru() uint32 { return binary.LittleEndian.Uint32(b) } -// printBanner prints our entropic banner -func printBanner() { +// Banner prints our entropic banner +func Banner() { time.Sleep(5 * time.Millisecond) println("\n" + process(hellpot) + "\n\n") time.Sleep(5 * time.Millisecond) @@ -88,13 +87,3 @@ func bannerFail(errs ...error) { } os.Exit(1) } - -// Banner prints out our banner (using spooky magic) -func Banner() { - //goland:noinspection GoBoolExpressions - if runtime.GOOS == "windows" || config.NoColor { - _, _ = os.Stdout.Write([]byte(config.Title + " " + config.Version + "\n\n")) - return - } - printBanner() -} diff --git a/internal/http/robots.go b/internal/http/robots.go index 08039c4..829a64b 100644 --- a/internal/http/robots.go +++ b/internal/http/robots.go @@ -2,32 +2,35 @@ package http import ( "fmt" - "strings" + "git.tcp.direct/kayos/common/pool" "github.com/valyala/fasthttp" - - "github.com/yunginnanet/HellPot/internal/config" ) +var strs = pool.NewStringFactory() + func robotsTXT(ctx *fasthttp.RequestCtx) { + config := runningConfig.HTTP.Router slog := log.With(). Str("USERAGENT", string(ctx.UserAgent())). Str("REMOTE_ADDR", getRealRemote(ctx)). Interface("URL", string(ctx.RequestURI())).Logger() - paths := &strings.Builder{} - paths.WriteString("User-agent: *\r\n") + pathBuf := strs.Get() + pathBuf.MustWriteString("User-agent: *\r\n") for _, p := range config.Paths { - paths.WriteString("Disallow: ") - paths.WriteString(p) - paths.WriteString("\r\n") + pathBuf.MustWriteString("Disallow: ") + pathBuf.MustWriteString(p) + pathBuf.MustWriteString("\r\n") } - paths.WriteString("\r\n") + pathBuf.MustWriteString("\r\n") + paths := pathBuf.String() + strs.MustPut(pathBuf) slog.Debug(). Strs("PATHS", config.Paths). Msg("SERVE_ROBOTS") - if _, err := fmt.Fprintf(ctx, paths.String()); err != nil { + if _, err := fmt.Fprintf(ctx, paths); err != nil { slog.Error().Err(err).Msg("SERVE_ROBOTS_ERROR") } } diff --git a/internal/http/router.go b/internal/http/router.go index 18842c2..74084fa 100644 --- a/internal/http/router.go +++ b/internal/http/router.go @@ -6,6 +6,7 @@ import ( "net/http" "os" "runtime" + "strconv" "strings" "time" @@ -20,10 +21,11 @@ import ( var ( log *zerolog.Logger hellpotHeffalump *heffalump.Heffalump + runningConfig *config.Parameters ) func getRealRemote(ctx *fasthttp.RequestCtx) string { - xrealip := string(ctx.Request.Header.Peek(config.HeaderName)) + xrealip := string(ctx.Request.Header.Peek(runningConfig.HTTP.ProxiedIPHeader)) if len(xrealip) > 0 { return xrealip } @@ -43,15 +45,13 @@ func hellPot(ctx *fasthttp.RequestCtx) { Str("REMOTE_ADDR", remoteAddr). Interface("URL", string(ctx.RequestURI())).Logger() - for _, denied := range config.UseragentBlacklistMatchers { - if strings.Contains(string(ctx.UserAgent()), denied) { - slog.Trace().Msg("Ignoring useragent") - ctx.Error("Not found", http.StatusNotFound) - return - } + if runningConfig.HTTP.ClientRules.MatchUseragent(ctx.UserAgent()) { + slog.Trace().Msg("Ignoring useragent") + ctx.Error("Not found", http.StatusNotFound) + return } - if config.Trace { + if runningConfig.Logger.Trace { slog = slog.With().Str("caller", path).Logger() } @@ -81,31 +81,29 @@ func hellPot(ctx *fasthttp.RequestCtx) { } func getSrv(r *router.Router) fasthttp.Server { - if !config.RestrictConcurrency { - config.MaxWorkers = fasthttp.DefaultConcurrency + if !runningConfig.Perf.ConcurrencyCap { + runningConfig.Perf.MaxWorkers = fasthttp.DefaultConcurrency } - log = config.GetLogger() + log = runningConfig.GetLogger() return fasthttp.Server{ // User defined server name // Likely not useful if behind a reverse proxy without additional configuration of the proxy server. - Name: config.FakeServerName, + Name: runningConfig.Liar.FakeServerName, /* from fasthttp docs: "By default request read timeout is unlimited." - My thinking here is avoiding some sort of weird oversized GET query just in case. + Nope. */ ReadTimeout: 5 * time.Second, MaxRequestBodySize: 1 * 1024 * 1024, - // Help curb abuse of HellPot (we've always needed this badly) - MaxConnsPerIP: 10, + MaxConnsPerIP: 3, MaxRequestsPerConn: 2, - Concurrency: config.MaxWorkers, + Concurrency: runningConfig.Perf.MaxWorkers, - // only accept GET requests - GetOnly: true, + // GetOnly: true, // we don't care if a request ends up being handled by a different handler (in fact it probably will) KeepHijackedConns: true, @@ -121,18 +119,19 @@ func getSrv(r *router.Router) fasthttp.Server { } // Serve starts our HTTP server and request router -func Serve() error { +func Serve(config *config.Parameters) error { log = config.GetLogger() + runningConfig = config - switch config.UseCustomHeffalump { + switch config.Bespoke.CustomHeffalump { case true: - content, err := os.ReadFile(config.Grimoire) + content, err := os.ReadFile(config.Bespoke.Grimoire) if err != nil { panic(err) } // Wasteful, but only done once at startup src := string(content) - log.Info().Msgf("Using custom grimoire file '%s'", config.Grimoire) + log.Info().Msgf("Using custom grimoire file '%s'", config.Bespoke.Grimoire) if len(src) < 1 { panic("grimoire file was empty!") @@ -145,16 +144,16 @@ func Serve() error { hellpotHeffalump = heffalump.NewDefaultHeffalump() } - l := config.HTTPBind + ":" + config.HTTPPort + l := config.HTTP.Bind + ":" + strconv.Itoa(int(config.HTTP.Port)) r := router.New() - if config.MakeRobots && !config.CatchAll { + if config.HTTP.Router.MakeRobots && !config.HTTP.Router.CatchAll { r.GET("/robots.txt", robotsTXT) } - if !config.CatchAll { - for _, p := range config.Paths { + if !config.HTTP.Router.CatchAll { + for _, p := range config.HTTP.Router.Paths { log.Trace().Str("caller", "router").Msgf("Add route: %s", p) r.GET(fmt.Sprintf("/%s", p), hellPot) } @@ -166,15 +165,15 @@ func Serve() error { srv := getSrv(r) //goland:noinspection GoBoolExpressions - if !config.UseUnixSocket || runtime.GOOS == "windows" { + if !config.HTTP.UnixSocket.UseUnixSocket || runtime.GOOS == "windows" { log.Info().Str("caller", l).Msg("Listening and serving HTTP...") return srv.ListenAndServe(l) } - if len(config.UnixSocketPath) < 1 { + if len(config.HTTP.UnixSocket.UnixSocketPath) < 1 { log.Fatal().Msg("unix_socket_path configuration directive appears to be empty") } - log.Info().Str("caller", config.UnixSocketPath).Msg("Listening and serving HTTP...") - return listenOnUnixSocket(config.UnixSocketPath, r) + log.Info().Str("caller", config.HTTP.UnixSocket.UnixSocketPath).Msg("Listening and serving HTTP...") + return listenOnUnixSocket(config.HTTP.UnixSocket.UnixSocketPath, r) } diff --git a/internal/http/router_unix.go b/internal/http/router_unix.go index 978ad48..3b60880 100644 --- a/internal/http/router_unix.go +++ b/internal/http/router_unix.go @@ -9,11 +9,10 @@ import ( "github.com/fasthttp/router" "github.com/valyala/fasthttp" - - "github.com/yunginnanet/HellPot/internal/config" ) func listenOnUnixSocket(addr string, r *router.Router) error { + config := runningConfig.HTTP.UnixSocket var err error var unixAddr *net.UnixAddr var unixListener *net.UnixListener diff --git a/internal/logger/logger.go b/internal/logger/logger.go new file mode 100644 index 0000000..bc822ff --- /dev/null +++ b/internal/logger/logger.go @@ -0,0 +1,63 @@ +package logger + +import ( + "errors" + "io" + "os" + "sync" + + "github.com/rs/zerolog" +) + +// Configuration represents the configuration for the logger. +type Configuration struct { + Directory string `koanf:"directory"` + Debug bool `koanf:"debug"` + Trace bool `koanf:"trace"` + NoColor bool `koanf:"nocolor"` + DockerLogging bool `koanf:"docker_logging"` + // ConsoleTimeFormat sets the time format for the console. + // The string is passed to time.Format() down the line. + ConsoleTimeFormat string + + Outputs []io.Writer `koanf:"-"` +} + +var once = &sync.Once{} + +func GetLoggerOnce() *zerolog.Logger { + var ret *zerolog.Logger + once.Do(func() { + ret = &_log + }) + if ret == nil { + panic("i said once you fool") + } + return ret +} + +var ErrNoOutputs = errors.New("no outputs provided") + +var _log zerolog.Logger + +func New(conf *Configuration) (zerolog.Logger, error) { + if len(conf.Outputs) == 0 { + return zerolog.Logger{}, ErrNoOutputs + } + for i, output := range conf.Outputs { + if output == os.Stdout || output == os.Stderr { + cw := zerolog.ConsoleWriter{Out: output, TimeFormat: conf.ConsoleTimeFormat, NoColor: conf.NoColor} + conf.Outputs = append(conf.Outputs[:i], conf.Outputs[i+1:]...) + conf.Outputs = append(conf.Outputs, cw) + } + } + _log = zerolog.New(zerolog.MultiLevelWriter(conf.Outputs...)).With().Timestamp().Logger() + _log = _log.Level(zerolog.InfoLevel) + if conf.Debug { + _log = _log.Level(zerolog.DebugLevel) + } + if conf.Trace { + _log = _log.Level(zerolog.TraceLevel) + } + return _log, nil +} diff --git a/internal/testutil/cmd/hellscope/main.go b/internal/testutil/cmd/hellscope/main.go new file mode 100644 index 0000000..28716e1 --- /dev/null +++ b/internal/testutil/cmd/hellscope/main.go @@ -0,0 +1,3 @@ +package hellscope + +func Measured diff --git a/internal/version/globals.go b/internal/version/globals.go new file mode 100644 index 0000000..ae9837f --- /dev/null +++ b/internal/version/globals.go @@ -0,0 +1,26 @@ +package version + +import ( + "runtime/debug" +) + +const HP = "HellPot" + +var Version = "dev" + +func init() { + if Version != "dev" { + return + } + binInfo := make(map[string]string) + info, ok := debug.ReadBuildInfo() + if !ok { + return + } + for _, v := range info.Settings { + binInfo[v.Key] = v.Value + } + if gitrev, ok := binInfo["vcs.revision"]; ok { + Version = gitrev[:7] + } +} From f02f6bfc20dbffe1643c901f6e2d83241c0da7bb Mon Sep 17 00:00:00 2001 From: "kayos@tcp.direct" Date: Thu, 20 Jun 2024 21:45:43 -0700 Subject: [PATCH 06/29] Fix: build and vet errors --- cmd/HellPot/HellPot.go | 1 - internal/config/models_test.go | 10 +++++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/cmd/HellPot/HellPot.go b/cmd/HellPot/HellPot.go index e2a81a5..d562f82 100644 --- a/cmd/HellPot/HellPot.go +++ b/cmd/HellPot/HellPot.go @@ -92,7 +92,6 @@ func main() { if runningConfig == nil { if runningConfig, err = config.Setup(nil); err != nil || runningConfig == nil { panic("failed to setup default config...\n" + err.Error()) - return // unreachable, but the linter doesn't seem to realize that } } diff --git a/internal/config/models_test.go b/internal/config/models_test.go index b7e9c51..c8b080f 100644 --- a/internal/config/models_test.go +++ b/internal/config/models_test.go @@ -29,19 +29,19 @@ func TestCompileRules(t *testing.T) { if len(rules.UseragentDisallowStrings) != 3 { t.Error("expected 3 got", len(rules.UseragentDisallowStrings)) } - if !rules.MatchUseragent("yeeterson") { + if !rules.MatchUseragent([]byte("yeeterson")) { t.Error("expected true got false") } - if !rules.MatchUseragent("mc") { + if !rules.MatchUseragent([]byte("mc")) { t.Error("expected true got false") } - if !rules.MatchUseragent("yooterson") { + if !rules.MatchUseragent([]byte("yooterson")) { t.Error("expected true got false") } - if !rules.MatchUseragent("gooters%n") { + if !rules.MatchUseragent([]byte("gooters%n")) { t.Error("expected true got false") } - if rules.MatchUseragent("yootersongooterson") { + if rules.MatchUseragent([]byte("yootersongooterson")) { t.Error("expected false got true") } } From b9702c28cdd12435b18fceacfdd766122db1e624 Mon Sep 17 00:00:00 2001 From: "kayos@tcp.direct" Date: Thu, 20 Jun 2024 21:49:07 -0700 Subject: [PATCH 07/29] Fix: Remove unused git stash lint --- internal/testutil/cmd/hellscope/main.go | 3 --- 1 file changed, 3 deletions(-) delete mode 100644 internal/testutil/cmd/hellscope/main.go diff --git a/internal/testutil/cmd/hellscope/main.go b/internal/testutil/cmd/hellscope/main.go deleted file mode 100644 index 28716e1..0000000 --- a/internal/testutil/cmd/hellscope/main.go +++ /dev/null @@ -1,3 +0,0 @@ -package hellscope - -func Measured From a9a3e52a86ef077b8a157d994b070b94589d53d0 Mon Sep 17 00:00:00 2001 From: "kayos@tcp.direct" Date: Thu, 20 Jun 2024 21:59:47 -0700 Subject: [PATCH 08/29] SAST: Resolve gosec G304 --- cmd/HellPot/HellPot.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/cmd/HellPot/HellPot.go b/cmd/HellPot/HellPot.go index d562f82..7ca4956 100644 --- a/cmd/HellPot/HellPot.go +++ b/cmd/HellPot/HellPot.go @@ -23,7 +23,8 @@ var ( func writeConfig(target string) bool { var f *os.File var err error - if f, err = os.Create(target); err != nil { + f, err = os.Create(target) // #nosec G304 -- go home gosec, you're drunk + if err != nil { println("failed to create config file: " + err.Error()) return false } @@ -70,7 +71,7 @@ func main() { var err error var f *os.File - f, err = os.Open(resolvedConf) + f, err = os.Open(resolvedConf) // #nosec G304 go home gosec, you're drunk if err == nil { runningConfig, setupErr = config.Setup(f) } From f0fcb56d0482af916fec07d613abe31140c5261d Mon Sep 17 00:00:00 2001 From: "kayos@tcp.direct" Date: Thu, 20 Jun 2024 22:06:57 -0700 Subject: [PATCH 09/29] Fix: don't parse flags during unit tests --- internal/config/command_line.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/internal/config/command_line.go b/internal/config/command_line.go index d3b44f5..c875ab9 100644 --- a/internal/config/command_line.go +++ b/internal/config/command_line.go @@ -4,6 +4,7 @@ import ( "flag" "io" "os" + "strings" "github.com/yunginnanet/HellPot/internal/extra" "github.com/yunginnanet/HellPot/internal/version" @@ -12,6 +13,13 @@ import ( var CLIFlags = flag.NewFlagSet("cli", flag.ExitOnError) func init() { + for _, arg := range os.Args { + if strings.Contains(arg, "test.testlogfile") { + // we're in a unit test, bail + return + } + } + CLIFlags.Bool("logger-debug", false, "force debug logging") CLIFlags.Bool("logger-trace", false, "force trace logging") CLIFlags.Bool("logger-nocolor", false, "force no color logging") From a09adb646b7f1d88ab1714de0d613653637fa460 Mon Sep 17 00:00:00 2001 From: "kayos@tcp.direct" Date: Thu, 20 Jun 2024 22:08:06 -0700 Subject: [PATCH 10/29] Chore: prune deps Tacking this on to trigger github issue notifs: Related to pull request GH-162: closes #140 closes #139 --- go.mod | 3 --- go.sum | 6 ------ 2 files changed, 9 deletions(-) diff --git a/go.mod b/go.mod index 96de733..97fdf8b 100644 --- a/go.mod +++ b/go.mod @@ -7,16 +7,13 @@ require ( github.com/fasthttp/router v1.5.1 github.com/knadh/koanf/parsers/toml v0.1.0 github.com/knadh/koanf/providers/env v0.1.0 - github.com/knadh/koanf/providers/structs v0.1.0 github.com/knadh/koanf/v2 v2.1.1 github.com/rs/zerolog v1.33.0 github.com/valyala/fasthttp v1.55.0 - golang.org/x/term v0.21.0 ) require ( github.com/andybalholm/brotli v1.1.0 // indirect - github.com/fatih/structs v1.1.0 // indirect github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1 // indirect github.com/klauspost/compress v1.17.9 // indirect github.com/knadh/koanf/maps v0.1.1 // indirect diff --git a/go.sum b/go.sum index 4b7a48a..4ab95a0 100644 --- a/go.sum +++ b/go.sum @@ -6,8 +6,6 @@ github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSV github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/fasthttp/router v1.5.1 h1:uViy8UYYhm5npJSKEZ4b/ozM//NGzVCfJbh6VJ0VKr8= github.com/fasthttp/router v1.5.1/go.mod h1:WrmsLo3mrerZP2VEXRV1E8nL8ymJFYCDTr4HmnB8+Zs= -github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= -github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1 h1:TQcrn6Wq+sKGkpyPvppOz99zsMBaUOKXq6HSv655U1c= github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= @@ -19,8 +17,6 @@ github.com/knadh/koanf/parsers/toml v0.1.0 h1:S2hLqS4TgWZYj4/7mI5m1CQQcWurxUz6OD github.com/knadh/koanf/parsers/toml v0.1.0/go.mod h1:yUprhq6eo3GbyVXFFMdbfZSo928ksS+uo0FFqNMnO18= github.com/knadh/koanf/providers/env v0.1.0 h1:LqKteXqfOWyx5Ab9VfGHmjY9BvRXi+clwyZozgVRiKg= github.com/knadh/koanf/providers/env v0.1.0/go.mod h1:RE8K9GbACJkeEnkl8L/Qcj8p4ZyPXZIQ191HJi44ZaQ= -github.com/knadh/koanf/providers/structs v0.1.0 h1:wJRteCNn1qvLtE5h8KQBvLJovidSdntfdyIbbCzEyE0= -github.com/knadh/koanf/providers/structs v0.1.0/go.mod h1:sw2YZ3txUcqA3Z27gPlmmBzWn1h8Nt9O6EP/91MkcWE= github.com/knadh/koanf/v2 v2.1.1 h1:/R8eXqasSTsmDCsAyYj+81Wteg8AqrV9CP6gvsTsOmM= github.com/knadh/koanf/v2 v2.1.1/go.mod h1:4mnTRbZCK+ALuBXHZMjDfG9y714L7TykVnZkXbMU3Es= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= @@ -51,7 +47,5 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA= -golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= nullprogram.com/x/rng v1.1.0 h1:SMU7DHaQSWtKJNTpNFIFt8Wd/KSmOuSDPXrMFp/UMro= From 5c0d3457a0a4c212c0918b4b08a00d889c82f37f Mon Sep 17 00:00:00 2001 From: "kayos@tcp.direct" Date: Fri, 21 Jun 2024 00:16:22 -0700 Subject: [PATCH 11/29] Resolve https://github.com/yunginnanet/HellPot/pull/162#discussion_r1648508932 --- heffalump/heffalump.go | 6 ------ 1 file changed, 6 deletions(-) diff --git a/heffalump/heffalump.go b/heffalump/heffalump.go index d0d1b9f..cedfb32 100644 --- a/heffalump/heffalump.go +++ b/heffalump/heffalump.go @@ -42,12 +42,6 @@ func (h *Heffalump) WriteHell(bw *bufio.Writer) (int64, error) { var n int64 var err error - /* defer func() { - if r := recover(); r != nil { - log.Error().Interface("caller", r).Msg("panic recovered!") - } - }()*/ - buf := h.pool.Get().([]byte) if _, err = bw.WriteString("\n\n"); err != nil { From 51ff751c286b205422af797bb241990fbb2b77d3 Mon Sep 17 00:00:00 2001 From: "kayos@tcp.direct" Date: Fri, 21 Jun 2024 00:16:27 -0700 Subject: [PATCH 12/29] Fix: nil check on buffer pointer --- internal/config/defaults.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/config/defaults.go b/internal/config/defaults.go index f70d192..eaab38d 100644 --- a/internal/config/defaults.go +++ b/internal/config/defaults.go @@ -29,7 +29,7 @@ func (pre *Preset) ReadBytes() ([]byte, error) { } func (shim *PresetIO) Read(p []byte) (int, error) { - if shim.buf.Len() > 0 { + if shim.buf != nil && shim.buf.Len() > 0 { return shim.buf.Read(p) //nolint:wrapcheck } data, err := shim.p.ReadBytes() From c1977ba9d560334ef514993782995a275ada64b9 Mon Sep 17 00:00:00 2001 From: "kayos@tcp.direct" Date: Tue, 25 Jun 2024 22:03:04 -0700 Subject: [PATCH 13/29] WIP rewrite progress, config loading broken atm --- cmd/HellPot/HellPot.go | 152 ++++++++++++++++++++++------ cmd/HellPot/HellPot_test.go | 21 ++++ go.mod | 3 + go.sum | 6 ++ heffalump/heffalump.go | 9 +- internal/config/defaults.go | 2 +- internal/config/models.go | 9 +- internal/config/setup.go | 25 ++++- internal/http/router.go | 41 ++++++-- internal/http/router_unix.go | 11 +- internal/logger/logger.go | 190 +++++++++++++++++++++++++++++++++-- 11 files changed, 401 insertions(+), 68 deletions(-) create mode 100644 cmd/HellPot/HellPot_test.go diff --git a/cmd/HellPot/HellPot.go b/cmd/HellPot/HellPot.go index 7ca4956..167e217 100644 --- a/cmd/HellPot/HellPot.go +++ b/cmd/HellPot/HellPot.go @@ -1,6 +1,8 @@ package main import ( + "errors" + "fmt" "io" "os" "os/signal" @@ -9,6 +11,8 @@ import ( "syscall" "time" + "github.com/rs/zerolog" + "github.com/yunginnanet/HellPot/internal/config" "github.com/yunginnanet/HellPot/internal/extra" "github.com/yunginnanet/HellPot/internal/http" @@ -39,38 +43,36 @@ func writeConfig(target string) bool { return true } -func main() { - conf := config.CLIFlags.Lookup("config").Value - if conf.String() == "" { - conf = config.CLIFlags.Lookup("c").Value - } - - usingDefaults := true - resolvedConf := conf.String() - +func searchConfig() string { + var resolvedConf string uconf, _ := os.UserConfigDir() if uconf == "" && os.Getenv("HOME") != "" { uconf = filepath.Join(os.Getenv("HOME"), ".config") } - if resolvedConf == "" { - for _, path := range []string{ - "/etc/HellPot/config.toml", - "/usr/local/etc/HellPot/config.toml", - "./config.toml", - filepath.Join(uconf, "HellPot", "config.toml"), - } { - if _, err := os.Stat(path); err == nil { - resolvedConf = path - break - } + for _, path := range []string{ + "/etc/HellPot/config.toml", + "/usr/local/etc/HellPot/config.toml", + "./config.toml", + filepath.Join(uconf, "HellPot", "config.toml"), + } { + if _, err := os.Stat(path); err == nil { + resolvedConf = path + break } } + return resolvedConf +} - var setupErr error +func readConfig(resolvedConf string) error { var err error + var setupErr error var f *os.File + if resolvedConf == "" { + return fmt.Errorf("%w: provided config file is an empty string", io.EOF) + } + f, err = os.Open(resolvedConf) // #nosec G304 go home gosec, you're drunk if err == nil { runningConfig, setupErr = config.Setup(f) @@ -78,6 +80,7 @@ func main() { switch { case setupErr != nil: println("failed to setup config: " + setupErr.Error()) + err = setupErr case err != nil: println("failed to open config file for reading: " + err.Error()) println("trying to create it....") @@ -85,20 +88,65 @@ func main() { if wroteOK { break } + println("failed to create config file, cannot continue") + return fmt.Errorf("failed to create config file: %w", err) case runningConfig != nil: - usingDefaults = false _ = f.Close() } + return err +} + +func resolveConfig() (usingDefaults bool, resolvedConf string, err error) { + if config.CLIFlags != nil { + confRoot := config.CLIFlags.Lookup("config") + if confRoot != nil && confRoot.Value.String() != "" { + resolvedConf = confRoot.Value.String() + } + } + + if resolvedConf == "" && os.Getenv("HELLPOT_CONFIG_FILE") != "" { + resolvedConf = os.Getenv("HELLPOT_CONFIG_FILE") + } + + if resolvedConf == "" { + resolvedConf = searchConfig() + } + + if err = readConfig(resolvedConf); err != nil && !errors.Is(err, io.EOF) { + return false, "", err + } + if runningConfig == nil { + println("still no config, trying defaults...") if runningConfig, err = config.Setup(nil); err != nil || runningConfig == nil { - panic("failed to setup default config...\n" + err.Error()) + if err == nil { + err = errors.New("unknown failure resulting in missing configuration, cannot continue") + } + return false, "", err } + return true, "", nil } - log, _ := logger.New(runningConfig.Logger) + return false, resolvedConf, nil +} + +func setup(stopChan chan os.Signal) (log zerolog.Logger, logFile string, resolvedConf string, err error) { + var usingDefaults bool + + if usingDefaults, resolvedConf, err = resolveConfig(); err != nil { + return + } + + //goland:noinspection GoNilness // we check for nil above + if log, err = logger.New(runningConfig.Logger); err != nil { + return + } + + logFile = runningConfig.Logger.ActiveLogFileName if usingDefaults { + runningConfig.UsingDefaults = true log.Warn().Msg("continuing with default configuration in ") for i := 5; i > 0; i-- { print(strconv.Itoa(i)) @@ -109,27 +157,69 @@ func main() { } } - if !runningConfig.Logger.NoColor { + if //goland:noinspection GoNilness + !runningConfig.Logger.NoColor { extra.Banner() } + signal.Notify(stopChan, syscall.SIGINT, syscall.SIGTERM) + return +} + +func testMain(t interface{ Error(...any) }) (string, string, chan os.Signal, error) { + stopChan := make(chan os.Signal, 1) + log, logFile, resolvedConf, err := setup(stopChan) + if err == nil { + log.Info().Msg("🔥 Starting HellPot 🔥") + go func() { + terr := http.Serve(runningConfig) + if terr != nil { + t.Error("failed to serve HTTP: " + terr.Error()) + close(stopChan) + } + }() + } + //goland:noinspection GoNilness + return resolvedConf, logFile, stopChan, err +} + +func main() { + stopChan := make(chan os.Signal, 1) + log, _, resolvedConf, err := setup(stopChan) + + if err != nil { + println("failed to start: " + err.Error()) + os.Exit(1) + } + log.Info().Msg("🔥 Starting HellPot 🔥") log.Info().Msg("Version: " + version.Version) log.Info().Msg("PID: " + strconv.Itoa(os.Getpid())) log.Info().Msg("Using config file: " + resolvedConf) - if usingDefaults { + if runningConfig.UsingDefaults { log.Warn().Msg("Using default configuration") } + if runningConfig.Logger.RSyslog != "" { + log.Info().Msg("Logging to syslog: " + runningConfig.Logger.RSyslog) + } + if runningConfig.Logger.ActiveLogFileName != "" { + log.Info().Msg("Logging to file: " + runningConfig.Logger.ActiveLogFileName) + } + if runningConfig.Logger.DockerLogging && + runningConfig.Logger.File == "" && + runningConfig.Logger.Directory == "" && + runningConfig.Logger.RSyslog == "" { + log.Info().Msg("Only logging to standard output") + } + log.Debug().Msg("Debug logging enabled") log.Trace().Msg("Trace logging enabled") - stopChan := make(chan os.Signal, 1) - signal.Notify(stopChan, syscall.SIGINT, syscall.SIGTERM) - go func() { log.Fatal().Err(http.Serve(runningConfig)).Msg("HTTP error") }() - <-stopChan // wait for SIGINT - log.Warn().Msg("Shutting down server...") + sig := <-stopChan // wait for SIGINT + log.Warn().Interface("signal_received", sig). + Msg("Shutting down server...") } diff --git a/cmd/HellPot/HellPot_test.go b/cmd/HellPot/HellPot_test.go new file mode 100644 index 0000000..2fab2e8 --- /dev/null +++ b/cmd/HellPot/HellPot_test.go @@ -0,0 +1,21 @@ +package main + +import ( + "os" + "testing" + "time" +) + +func TestHellPot(t *testing.T) { + resolvedConf, logFile, stopChan, err := testMain(t) + if err != nil { + t.Fatal(err) + } + if stopChan == nil { + t.Fatal("stopChan is nil") + } + t.Log("resolvedConf: ", resolvedConf) + t.Log("logFile: ", logFile) + time.Sleep(100 * time.Millisecond) + stopChan <- os.Interrupt +} diff --git a/go.mod b/go.mod index 97fdf8b..fe05b5d 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.19 require ( git.tcp.direct/kayos/common v0.9.7 github.com/fasthttp/router v1.5.1 + github.com/google/go-cmdtest v0.4.0 github.com/knadh/koanf/parsers/toml v0.1.0 github.com/knadh/koanf/providers/env v0.1.0 github.com/knadh/koanf/v2 v2.1.1 @@ -15,6 +16,8 @@ require ( require ( github.com/andybalholm/brotli v1.1.0 // indirect github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1 // indirect + github.com/google/go-cmp v0.3.1 // indirect + github.com/google/renameio v0.1.0 // indirect github.com/klauspost/compress v1.17.9 // indirect github.com/knadh/koanf/maps v0.1.1 // indirect github.com/mattn/go-colorable v0.1.13 // indirect diff --git a/go.sum b/go.sum index 4ab95a0..9d33cb2 100644 --- a/go.sum +++ b/go.sum @@ -9,6 +9,12 @@ github.com/fasthttp/router v1.5.1/go.mod h1:WrmsLo3mrerZP2VEXRV1E8nL8ymJFYCDTr4H github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1 h1:TQcrn6Wq+sKGkpyPvppOz99zsMBaUOKXq6HSv655U1c= github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/google/go-cmdtest v0.4.0 h1:ToXh6W5spLp3npJV92tk6d5hIpUPYEzHLkD+rncbyhI= +github.com/google/go-cmdtest v0.4.0/go.mod h1:apVn/GCasLZUVpAJ6oWAuyP7Ne7CEsQbTnc0plM3m+o= +github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/renameio v0.1.0 h1:GOZbcHa3HfsPKPlmyPyN2KEohoMXOhdMbHrvbpl2QaA= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= github.com/knadh/koanf/maps v0.1.1 h1:G5TjmUh2D7G2YWf5SQQqSiHRJEjaicvU0KpypqB3NIs= diff --git a/heffalump/heffalump.go b/heffalump/heffalump.go index cedfb32..ed97f21 100644 --- a/heffalump/heffalump.go +++ b/heffalump/heffalump.go @@ -6,6 +6,7 @@ package heffalump import ( "bufio" + "fmt" "io" "sync" ) @@ -31,8 +32,7 @@ func NewHeffalump(mm MarkovMap, buffsize int) *Heffalump { } } -// NewDefaultHeffalump instantiates a new default Heffalump from a MarkovMap created using -// using the default source text. +// NewDefaultHeffalump instantiates a new default Heffalump from a MarkovMap created using using the default source text. func NewDefaultHeffalump() *Heffalump { return NewHeffalump(NewDefaultMarkovMap(), DefaultBuffSize) } @@ -42,7 +42,10 @@ func (h *Heffalump) WriteHell(bw *bufio.Writer) (int64, error) { var n int64 var err error - buf := h.pool.Get().([]byte) + buf, ok := h.pool.Get().([]byte) + if !ok { + panic("buffer pool type assertion failed, retrieved type is a " + fmt.Sprintf("%T", buf)) + } if _, err = bw.WriteString("\n\n"); err != nil { h.pool.Put(buf) diff --git a/internal/config/defaults.go b/internal/config/defaults.go index eaab38d..8e150b0 100644 --- a/internal/config/defaults.go +++ b/internal/config/defaults.go @@ -51,7 +51,7 @@ var defOpts = map[string]interface{}{ "debug": true, "trace": false, "nocolor": runtime.GOOS == "windows", - "use_date_filename": true, + "use_date_filename": false, "docker_logging": false, "console_time_format": time.Kitchen, }, diff --git a/internal/config/models.go b/internal/config/models.go index f648d84..f06c929 100644 --- a/internal/config/models.go +++ b/internal/config/models.go @@ -17,8 +17,11 @@ type Parameters struct { Perf Performance `koanf:"performance"` Liar Deception `koanf:"deception"` - source *koanf.Koanf `koanf:"-"` - logger *zerolog.Logger + IdleHands DevilsPlaythings `koanf:"experimental"` + + source *koanf.Koanf `koanf:"-"` + logger *zerolog.Logger `koanf:"-"` + UsingDefaults bool `koanf:"-"` } var once = &sync.Once{} @@ -63,7 +66,7 @@ type Router struct { // HTTP represents the configuration for the HTTP server. type HTTP struct { Bind string `koanf:"bind_addr"` - Port int64 `koanf:"port"` + Port int64 `koanf:"bind_port"` // ProxiedIPHeader is the HTTP Header containing the original IP of the client // for usage by traditional reverse Proxy deployments. ProxiedIPHeader string `koanf:"proxied_ip_header"` diff --git a/internal/config/setup.go b/internal/config/setup.go index 20cd26a..193e449 100644 --- a/internal/config/setup.go +++ b/internal/config/setup.go @@ -3,7 +3,6 @@ package config import ( "fmt" "io" - "os" "strings" "github.com/knadh/koanf/parsers/toml" @@ -24,7 +23,17 @@ func (r *readerProvider) Read() (map[string]interface{}, error) { if err != nil { return nil, err } - return toml.Parser().Unmarshal(b) //nolint:wrapcheck + vals, err := toml.Parser().Unmarshal(b) + if len(vals) == 0 { + err = io.ErrUnexpectedEOF + } + if err != nil { + println(err.Error()) + } + for k, v := range vals { + println(k, v) + } + return vals, err //nolint:wrapcheck } func Setup(source io.Reader) (*Parameters, error) { @@ -40,7 +49,9 @@ func Setup(source io.Reader) (*Parameters, error) { } } - _ = k.Load(env.Provider("HELLPOT_", ".", func(s string) string { + envK := koanf.New(".") + + envErr := envK.Load(env.Provider("HELLPOT_", ".", func(s string) string { s = strings.TrimPrefix(s, "HELLPOT_") s = strings.ToLower(s) s = strings.ReplaceAll(s, "__", " ") @@ -49,6 +60,12 @@ func Setup(source io.Reader) (*Parameters, error) { return s }), nil) + if envErr == nil && envK != nil && len(envK.All()) > 0 { + if err := k.Merge(envK); err != nil { + return nil, fmt.Errorf("failed to merge env config: %w", err) + } + } + p := &Parameters{ source: k, } @@ -57,7 +74,5 @@ func Setup(source io.Reader) (*Parameters, error) { return nil, fmt.Errorf("failed to unmarshal config: %w", err) } - p.Logger.Outputs = append(p.Logger.Outputs, os.Stdout) - return p, nil } diff --git a/internal/http/router.go b/internal/http/router.go index 74084fa..d2e06c8 100644 --- a/internal/http/router.go +++ b/internal/http/router.go @@ -3,6 +3,7 @@ package http import ( "bufio" "fmt" + "io" "net/http" "os" "runtime" @@ -97,13 +98,13 @@ func getSrv(r *router.Router) fasthttp.Server { Nope. */ ReadTimeout: 5 * time.Second, - MaxRequestBodySize: 1 * 1024 * 1024, + MaxRequestBodySize: 0.5 * 1024 * 1024, MaxConnsPerIP: 3, MaxRequestsPerConn: 2, Concurrency: runningConfig.Perf.MaxWorkers, - // GetOnly: true, + GetOnly: !runningConfig.IdleHands.POSTMimicry, // we don't care if a request ends up being handled by a different handler (in fact it probably will) KeepHijackedConns: true, @@ -118,23 +119,19 @@ func getSrv(r *router.Router) fasthttp.Server { } } -// Serve starts our HTTP server and request router -func Serve(config *config.Parameters) error { - log = config.GetLogger() - runningConfig = config - +func setupHeffalump(config *config.Parameters) error { switch config.Bespoke.CustomHeffalump { case true: content, err := os.ReadFile(config.Bespoke.Grimoire) if err != nil { - panic(err) + return fmt.Errorf("failed to read grimoire file '%s': %w", config.Bespoke.Grimoire, err) } // Wasteful, but only done once at startup src := string(content) log.Info().Msgf("Using custom grimoire file '%s'", config.Bespoke.Grimoire) if len(src) < 1 { - panic("grimoire file was empty!") + return fmt.Errorf("%w: grimoire file '%s' appears to be empty", io.EOF, config.Bespoke.Grimoire) } markovMap := heffalump.MakeMarkovMap(strings.NewReader(src)) @@ -143,6 +140,16 @@ func Serve(config *config.Parameters) error { log.Info().Msg("Using default source text") hellpotHeffalump = heffalump.NewDefaultHeffalump() } + if hellpotHeffalump == nil { + panic("failed to initialize heffalump") + } + return nil +} + +// Serve starts our HTTP server and request router +func Serve(config *config.Parameters) error { + log = config.GetLogger() + runningConfig = config l := config.HTTP.Bind + ":" + strconv.Itoa(int(config.HTTP.Port)) @@ -167,7 +174,10 @@ func Serve(config *config.Parameters) error { //goland:noinspection GoBoolExpressions if !config.HTTP.UnixSocket.UseUnixSocket || runtime.GOOS == "windows" { log.Info().Str("caller", l).Msg("Listening and serving HTTP...") - return srv.ListenAndServe(l) + err := srv.ListenAndServe(l) + if err != nil { + return fmt.Errorf("failed to start HTTP server: %w", err) + } } if len(config.HTTP.UnixSocket.UnixSocketPath) < 1 { @@ -175,5 +185,14 @@ func Serve(config *config.Parameters) error { } log.Info().Str("caller", config.HTTP.UnixSocket.UnixSocketPath).Msg("Listening and serving HTTP...") - return listenOnUnixSocket(config.HTTP.UnixSocket.UnixSocketPath, r) + listener, err := listenOnUnixSocket(config.HTTP.UnixSocket.UnixSocketPath, r) + if err != nil { + return fmt.Errorf("failed to start unix listener: %w", err) + } + + err = srv.Serve(listener) + if err != nil { + err = fmt.Errorf("failed to serve HTTP: %w", err) + } + return err } diff --git a/internal/http/router_unix.go b/internal/http/router_unix.go index 3b60880..011ec5c 100644 --- a/internal/http/router_unix.go +++ b/internal/http/router_unix.go @@ -8,17 +8,16 @@ import ( "syscall" "github.com/fasthttp/router" - "github.com/valyala/fasthttp" ) -func listenOnUnixSocket(addr string, r *router.Router) error { +func listenOnUnixSocket(addr string, r *router.Router) (net.Listener, error) { config := runningConfig.HTTP.UnixSocket var err error var unixAddr *net.UnixAddr var unixListener *net.UnixListener unixAddr, err = net.ResolveUnixAddr("unix", addr) if err != nil { - return err + return nil, err } // Always unlink sockets before listening on them _ = syscall.Unlink(addr) @@ -28,14 +27,14 @@ func listenOnUnixSocket(addr string, r *router.Router) error { unixListener, err = net.ListenUnix("unix", unixAddr) syscall.Umask(oldmask) if err != nil { - return err + return nil, err } if err = os.Chmod( unixAddr.Name, os.FileMode(config.UnixSocketPermissions), ); err != nil { - return err + return nil, err } - return fasthttp.Serve(unixListener, r.Handler) + return unixListener, nil } diff --git a/internal/logger/logger.go b/internal/logger/logger.go index bc822ff..c54bcb5 100644 --- a/internal/logger/logger.go +++ b/internal/logger/logger.go @@ -2,27 +2,191 @@ package logger import ( "errors" + "fmt" "io" + "log/syslog" "os" + "path/filepath" + "strings" "sync" + "time" "github.com/rs/zerolog" ) // Configuration represents the configuration for the logger. type Configuration struct { - Directory string `koanf:"directory"` - Debug bool `koanf:"debug"` - Trace bool `koanf:"trace"` - NoColor bool `koanf:"nocolor"` - DockerLogging bool `koanf:"docker_logging"` + Debug bool `koanf:"debug"` + Trace bool `koanf:"trace"` + NoColor bool `koanf:"nocolor"` + DockerLogging bool `koanf:"docker_logging"` // ConsoleTimeFormat sets the time format for the console. // The string is passed to time.Format() down the line. ConsoleTimeFormat string + // TimeDateFilename sets the log file name to include the date and time. + TimeDateFilename bool `koanf:"use_date_filename"` + + Directory string `koanf:"directory"` + File string `koanf:"log_file"` + RSyslog string `koanf:"rsyslog_address"` + + ActiveLogFileName string `koanf:"active_log_file_name"` + Outputs []io.Writer `koanf:"-"` } +func (c *Configuration) findFallbackDir() error { + locs := []string{"/var/log"} + uconf, err := os.UserHomeDir() + if err == nil { + locs = append(locs, filepath.Join(uconf, ".local", "share")) + } + + var errs []error + + for _, loc := range locs { + println("checking", loc) + if _, err = os.Stat(loc); err == nil { + var locStat os.FileInfo + if locStat, err = os.Stat(filepath.Join(loc, "HellPot")); err == nil { + if locStat.IsDir() { + c.Directory = filepath.Join(loc, "HellPot") + c.File = "HellPot.log" + return nil + } + errs = append(errs, fmt.Errorf("HellPot directory exists but is not a directory")) + } else if !errors.Is(err, os.ErrNotExist) { + errs = append(errs, err) + } + + if err = os.MkdirAll(filepath.Join(loc, "HellPot"), 0755); err == nil { + c.Directory = filepath.Join(loc, "HellPot") + c.File = "HellPot.log" + return nil + } + errs = append(errs, fmt.Errorf("failed to create HellPot directory")) + } + } + + return errors.Join(errs...) +} + +func (c *Configuration) Validate() error { + if c.DockerLogging { + c.Outputs = []io.Writer{os.Stdout} + } + if c.Directory == "" && c.File == "" && c.ActiveLogFileName == "" && c.RSyslog == "" && !c.DockerLogging { + if err := c.findFallbackDir(); err != nil { + return fmt.Errorf("failed to find a fallback log directory: %w", err) + } + } + if len(c.Outputs) == 0 && c.Directory == "" && c.File == "" && c.RSyslog == "" { + return ErrNoOutputs + } + if c.File != "" && c.Directory != "" && filepath.Dir(c.File) != c.Directory && filepath.Base(c.File) != c.File { + return ErrBothFileAndDirectory + } + return nil +} + +func (c *Configuration) setupDirAndFile() error { + switch { + case c.Directory != "": + println(c.Directory) + stat, err := os.Stat(c.Directory) + if err != nil && !errors.Is(err, os.ErrNotExist) { + return fmt.Errorf("failed to access specified log directory: %w", err) + } + if errors.Is(err, os.ErrNotExist) { + if err = os.MkdirAll(c.Directory, 0755); err != nil { + return fmt.Errorf("failed to create specified log directory: %w", err) + } + } + if stat != nil && !stat.IsDir() { + return fmt.Errorf("specified log directory is not a directory") + } + if c.File == "" { + c.File = "HellPot.log" + } + case c.Directory == "" && c.File != "": + println(c.File) + stat, err := os.Stat(filepath.Dir(c.File)) + if err != nil && !errors.Is(err, os.ErrNotExist) { + return fmt.Errorf("failed to access specified log directory: %w", err) + } + if errors.Is(err, os.ErrNotExist) { + if err = os.MkdirAll(filepath.Dir(c.File), 0755); err != nil { + return fmt.Errorf("failed to create specified log directory: %w", err) + } + } + if stat != nil && !stat.IsDir() { + panic("specified log directory is not a directory, but it should be...? please report this issue on github") + } + case c.Directory == "" && c.File == "" && c.ActiveLogFileName == "" && c.RSyslog == "" && !c.DockerLogging: + return fmt.Errorf("no log directory or file specified") + } + var f *os.File + var err error + if c.TimeDateFilename { + og := filepath.Base(c.File) + ext := filepath.Ext(og) + og = strings.TrimSuffix(og, ext) + c.File = filepath.Join( + filepath.Dir(c.File), + fmt.Sprintf("%s-%s%s", og, time.Now().Format("2006-01-02-15-04-05"), ext), + ) + } + if f, err = os.OpenFile(filepath.Join(c.Directory, c.File), os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644); err != nil { + return fmt.Errorf("failed to open log file: %w", err) + } + c.Outputs = append(c.Outputs, f) + c.ActiveLogFileName = f.Name() + return nil +} + +func (c *Configuration) setupSyslog() error { + var ( + err error + proto string + addr string + conn *syslog.Writer + ) + switch { + case strings.Contains(c.RSyslog, "://"): + proto = strings.Split(c.RSyslog, "://")[0] + addr = strings.Split(c.RSyslog, "://")[1] + case strings.Contains(c.RSyslog, ":"): + proto = "udp" + addr = c.RSyslog + default: + proto = "udp" + addr = c.RSyslog + ":514" + } + if conn, err = syslog.Dial(proto, addr, syslog.LOG_INFO, "HellPot"); err != nil { + return fmt.Errorf("failed to dial syslog server: %w", err) + } + + c.Outputs = append(c.Outputs, zerolog.SyslogLevelWriter(conn)) + + return nil +} + +func (c *Configuration) SetupOutputs() error { + if c.Directory != "" || c.File != "" { + if err := c.setupDirAndFile(); err != nil { + return fmt.Errorf("failed to setup log file: %w", err) + } + } + if c.RSyslog != "" { + if err := c.setupSyslog(); err != nil { + return fmt.Errorf("failed to setup syslog: %w", err) + } + } + + return nil +} + var once = &sync.Once{} func GetLoggerOnce() *zerolog.Logger { @@ -36,13 +200,23 @@ func GetLoggerOnce() *zerolog.Logger { return ret } -var ErrNoOutputs = errors.New("no outputs provided") +var ( + ErrNoOutputs = errors.New( + "no outputs provided, if console only logging is desired, set docker_logging to true", + ) + ErrBothFileAndDirectory = errors.New( + "cannot specify both file and directory unless file is a child of directory", + ) +) var _log zerolog.Logger func New(conf *Configuration) (zerolog.Logger, error) { - if len(conf.Outputs) == 0 { - return zerolog.Logger{}, ErrNoOutputs + if err := conf.Validate(); err != nil { + return zerolog.Logger{}, fmt.Errorf("invalid logger configuration: %w", err) + } + if err := conf.SetupOutputs(); err != nil { + return zerolog.Logger{}, fmt.Errorf("failed to setup logger outputs: %w", err) } for i, output := range conf.Outputs { if output == os.Stdout || output == os.Stderr { From 072ea6fa6c31148eee9b1c398d8bc88b88ade678 Mon Sep 17 00:00:00 2001 From: "kayos@tcp.direct" Date: Tue, 25 Jun 2024 23:47:54 -0700 Subject: [PATCH 14/29] Fix: fix configuration file loading --- internal/config/command_line.go | 4 ++++ internal/config/models.go | 6 +++--- internal/config/setup_test.go | 6 +++--- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/internal/config/command_line.go b/internal/config/command_line.go index c875ab9..be6f279 100644 --- a/internal/config/command_line.go +++ b/internal/config/command_line.go @@ -14,10 +14,14 @@ var CLIFlags = flag.NewFlagSet("cli", flag.ExitOnError) func init() { for _, arg := range os.Args { + // check for unit test flags if strings.Contains(arg, "test.testlogfile") { // we're in a unit test, bail return } + if strings.Contains(arg, "test.v") { + return + } } CLIFlags.Bool("logger-debug", false, "force debug logging") diff --git a/internal/config/models.go b/internal/config/models.go index f06c929..c6b9dec 100644 --- a/internal/config/models.go +++ b/internal/config/models.go @@ -19,9 +19,9 @@ type Parameters struct { IdleHands DevilsPlaythings `koanf:"experimental"` - source *koanf.Koanf `koanf:"-"` - logger *zerolog.Logger `koanf:"-"` - UsingDefaults bool `koanf:"-"` + source *koanf.Koanf + logger *zerolog.Logger + UsingDefaults bool } var once = &sync.Once{} diff --git a/internal/config/setup_test.go b/internal/config/setup_test.go index 1562f20..7f902a2 100644 --- a/internal/config/setup_test.go +++ b/internal/config/setup_test.go @@ -14,7 +14,7 @@ func TestSetup(t *testing.T) { func SetupSuccess(t *testing.T) { source := bytes.NewBufferString(` [http] -port = 55 +bind_port = 55 bind_addr = "5.5.5.5" [http.router] @@ -31,7 +31,7 @@ makerobots = false t.Fatal("Expected params to be not nil") } - if params.source.Get("http.port") != int64(55) { + if params.source.Get("http.bind_port") != int64(55) { t.Errorf("Expected 55, got (%T) %v", params.source.Get("http.port"), params.source.Get("http.port")) } if params.HTTP.Port != int64(55) { @@ -77,7 +77,7 @@ func SetupNoFailureOnNilSource(t *testing.T) { } // nolint:forcetypeassert - if params.HTTP.Port != Defaults.val["http"].(map[string]interface{})["port"].(int64) { + if params.HTTP.Port != Defaults.val["http"].(map[string]interface{})["bind_port"].(int64) { t.Errorf("Expected %v, got %v", // nolint:forcetypeassert Defaults.val["http"].(map[string]interface{})["port"].(int64), params.HTTP.Port, From 982a0764d20260003fec9641d20f938c81eaea88 Mon Sep 17 00:00:00 2001 From: "kayos@tcp.direct" Date: Tue, 25 Jun 2024 23:49:45 -0700 Subject: [PATCH 15/29] Security (logger): Resolve gosec G301 (CWE-276) --- internal/logger/logger.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/logger/logger.go b/internal/logger/logger.go index c54bcb5..0983089 100644 --- a/internal/logger/logger.go +++ b/internal/logger/logger.go @@ -60,7 +60,7 @@ func (c *Configuration) findFallbackDir() error { errs = append(errs, err) } - if err = os.MkdirAll(filepath.Join(loc, "HellPot"), 0755); err == nil { + if err = os.MkdirAll(filepath.Join(loc, "HellPot"), 0750); err == nil { c.Directory = filepath.Join(loc, "HellPot") c.File = "HellPot.log" return nil @@ -99,7 +99,7 @@ func (c *Configuration) setupDirAndFile() error { return fmt.Errorf("failed to access specified log directory: %w", err) } if errors.Is(err, os.ErrNotExist) { - if err = os.MkdirAll(c.Directory, 0755); err != nil { + if err = os.MkdirAll(c.Directory, 0750); err != nil { return fmt.Errorf("failed to create specified log directory: %w", err) } } @@ -116,7 +116,7 @@ func (c *Configuration) setupDirAndFile() error { return fmt.Errorf("failed to access specified log directory: %w", err) } if errors.Is(err, os.ErrNotExist) { - if err = os.MkdirAll(filepath.Dir(c.File), 0755); err != nil { + if err = os.MkdirAll(filepath.Dir(c.File), 0750); err != nil { return fmt.Errorf("failed to create specified log directory: %w", err) } } From 4977844cac0d18f2c1247ac6d1a87d48145a6073 Mon Sep 17 00:00:00 2001 From: "kayos@tcp.direct" Date: Tue, 25 Jun 2024 23:52:05 -0700 Subject: [PATCH 16/29] Security (logger): Resolve gosec G302 (CWE-276) --- internal/logger/logger.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/logger/logger.go b/internal/logger/logger.go index 0983089..b7121ca 100644 --- a/internal/logger/logger.go +++ b/internal/logger/logger.go @@ -137,7 +137,7 @@ func (c *Configuration) setupDirAndFile() error { fmt.Sprintf("%s-%s%s", og, time.Now().Format("2006-01-02-15-04-05"), ext), ) } - if f, err = os.OpenFile(filepath.Join(c.Directory, c.File), os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644); err != nil { + if f, err = os.OpenFile(filepath.Join(c.Directory, c.File), os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0600); err != nil { return fmt.Errorf("failed to open log file: %w", err) } c.Outputs = append(c.Outputs, f) From d662ee97ee8bb8f2ffa982487a21629f84494f7f Mon Sep 17 00:00:00 2001 From: "kayos@tcp.direct" Date: Tue, 25 Jun 2024 23:55:19 -0700 Subject: [PATCH 17/29] Chore: Tidy up, split up files in main package --- cmd/HellPot/HellPot.go | 170 ------------------------------------ cmd/HellPot/HellPot_test.go | 19 ++++ cmd/HellPot/boot.go | 160 +++++++++++++++++++++++++++++++++ internal/config/setup.go | 12 +-- internal/logger/logger.go | 1 - 5 files changed, 180 insertions(+), 182 deletions(-) create mode 100644 cmd/HellPot/boot.go diff --git a/cmd/HellPot/HellPot.go b/cmd/HellPot/HellPot.go index 167e217..c5118e1 100644 --- a/cmd/HellPot/HellPot.go +++ b/cmd/HellPot/HellPot.go @@ -1,22 +1,11 @@ package main import ( - "errors" - "fmt" - "io" "os" - "os/signal" - "path/filepath" "strconv" - "syscall" - "time" - - "github.com/rs/zerolog" "github.com/yunginnanet/HellPot/internal/config" - "github.com/yunginnanet/HellPot/internal/extra" "github.com/yunginnanet/HellPot/internal/http" - "github.com/yunginnanet/HellPot/internal/logger" "github.com/yunginnanet/HellPot/internal/version" ) @@ -24,165 +13,6 @@ var ( runningConfig *config.Parameters ) -func writeConfig(target string) bool { - var f *os.File - var err error - f, err = os.Create(target) // #nosec G304 -- go home gosec, you're drunk - if err != nil { - println("failed to create config file: " + err.Error()) - return false - } - if _, err = io.Copy(f, config.Defaults.IO); err != nil { - println("failed to write default config to file: " + err.Error()) - _ = f.Close() - return false - } - println("wrote default config to " + target) - runningConfig, _ = config.Setup(f) - _ = f.Close() - return true -} - -func searchConfig() string { - var resolvedConf string - uconf, _ := os.UserConfigDir() - if uconf == "" && os.Getenv("HOME") != "" { - uconf = filepath.Join(os.Getenv("HOME"), ".config") - } - - for _, path := range []string{ - "/etc/HellPot/config.toml", - "/usr/local/etc/HellPot/config.toml", - "./config.toml", - filepath.Join(uconf, "HellPot", "config.toml"), - } { - if _, err := os.Stat(path); err == nil { - resolvedConf = path - break - } - } - return resolvedConf -} - -func readConfig(resolvedConf string) error { - var err error - var setupErr error - var f *os.File - - if resolvedConf == "" { - return fmt.Errorf("%w: provided config file is an empty string", io.EOF) - } - - f, err = os.Open(resolvedConf) // #nosec G304 go home gosec, you're drunk - if err == nil { - runningConfig, setupErr = config.Setup(f) - } - switch { - case setupErr != nil: - println("failed to setup config: " + setupErr.Error()) - err = setupErr - case err != nil: - println("failed to open config file for reading: " + err.Error()) - println("trying to create it....") - wroteOK := writeConfig(resolvedConf) - if wroteOK { - break - } - println("failed to create config file, cannot continue") - return fmt.Errorf("failed to create config file: %w", err) - case runningConfig != nil: - _ = f.Close() - } - - return err -} - -func resolveConfig() (usingDefaults bool, resolvedConf string, err error) { - if config.CLIFlags != nil { - confRoot := config.CLIFlags.Lookup("config") - if confRoot != nil && confRoot.Value.String() != "" { - resolvedConf = confRoot.Value.String() - } - } - - if resolvedConf == "" && os.Getenv("HELLPOT_CONFIG_FILE") != "" { - resolvedConf = os.Getenv("HELLPOT_CONFIG_FILE") - } - - if resolvedConf == "" { - resolvedConf = searchConfig() - } - - if err = readConfig(resolvedConf); err != nil && !errors.Is(err, io.EOF) { - return false, "", err - } - - if runningConfig == nil { - println("still no config, trying defaults...") - if runningConfig, err = config.Setup(nil); err != nil || runningConfig == nil { - if err == nil { - err = errors.New("unknown failure resulting in missing configuration, cannot continue") - } - return false, "", err - } - return true, "", nil - } - - return false, resolvedConf, nil -} - -func setup(stopChan chan os.Signal) (log zerolog.Logger, logFile string, resolvedConf string, err error) { - var usingDefaults bool - - if usingDefaults, resolvedConf, err = resolveConfig(); err != nil { - return - } - - //goland:noinspection GoNilness // we check for nil above - if log, err = logger.New(runningConfig.Logger); err != nil { - return - } - - logFile = runningConfig.Logger.ActiveLogFileName - - if usingDefaults { - runningConfig.UsingDefaults = true - log.Warn().Msg("continuing with default configuration in ") - for i := 5; i > 0; i-- { - print(strconv.Itoa(i)) - for i := 0; i < 5; i++ { - time.Sleep(200 * time.Millisecond) - print(".") - } - } - } - - if //goland:noinspection GoNilness - !runningConfig.Logger.NoColor { - extra.Banner() - } - - signal.Notify(stopChan, syscall.SIGINT, syscall.SIGTERM) - return -} - -func testMain(t interface{ Error(...any) }) (string, string, chan os.Signal, error) { - stopChan := make(chan os.Signal, 1) - log, logFile, resolvedConf, err := setup(stopChan) - if err == nil { - log.Info().Msg("🔥 Starting HellPot 🔥") - go func() { - terr := http.Serve(runningConfig) - if terr != nil { - t.Error("failed to serve HTTP: " + terr.Error()) - close(stopChan) - } - }() - } - //goland:noinspection GoNilness - return resolvedConf, logFile, stopChan, err -} - func main() { stopChan := make(chan os.Signal, 1) log, _, resolvedConf, err := setup(stopChan) diff --git a/cmd/HellPot/HellPot_test.go b/cmd/HellPot/HellPot_test.go index 2fab2e8..67a1506 100644 --- a/cmd/HellPot/HellPot_test.go +++ b/cmd/HellPot/HellPot_test.go @@ -4,8 +4,27 @@ import ( "os" "testing" "time" + + "github.com/yunginnanet/HellPot/internal/http" ) +func testMain(t interface{ Error(...any) }) (string, string, chan os.Signal, error) { + stopChan := make(chan os.Signal, 1) + log, logFile, resolvedConf, err := setup(stopChan) + if err == nil { + log.Info().Msg("🔥 Starting HellPot 🔥") + go func() { + terr := http.Serve(runningConfig) + if terr != nil { + t.Error("failed to serve HTTP: " + terr.Error()) + close(stopChan) + } + }() + } + //goland:noinspection GoNilness + return resolvedConf, logFile, stopChan, err +} + func TestHellPot(t *testing.T) { resolvedConf, logFile, stopChan, err := testMain(t) if err != nil { diff --git a/cmd/HellPot/boot.go b/cmd/HellPot/boot.go new file mode 100644 index 0000000..4a8a7d0 --- /dev/null +++ b/cmd/HellPot/boot.go @@ -0,0 +1,160 @@ +package main + +import ( + "errors" + "fmt" + "io" + "os" + "os/signal" + "path/filepath" + "strconv" + "syscall" + "time" + + "github.com/rs/zerolog" + + "github.com/yunginnanet/HellPot/internal/config" + "github.com/yunginnanet/HellPot/internal/extra" + "github.com/yunginnanet/HellPot/internal/logger" +) + +func writeConfig(target string) bool { + var f *os.File + var err error + f, err = os.Create(target) // #nosec G304 -- go home gosec, you're drunk + if err != nil { + println("failed to create config file: " + err.Error()) + return false + } + if _, err = io.Copy(f, config.Defaults.IO); err != nil { + println("failed to write default config to file: " + err.Error()) + _ = f.Close() + return false + } + println("wrote default config to " + target) + runningConfig, _ = config.Setup(f) + _ = f.Close() + return true +} + +func searchConfig() string { + var resolvedConf string + uconf, _ := os.UserConfigDir() + if uconf == "" && os.Getenv("HOME") != "" { + uconf = filepath.Join(os.Getenv("HOME"), ".config") + } + + for _, path := range []string{ + "/etc/HellPot/config.toml", + "/usr/local/etc/HellPot/config.toml", + "./config.toml", + filepath.Join(uconf, "HellPot", "config.toml"), + } { + if _, err := os.Stat(path); err == nil { + resolvedConf = path + break + } + } + return resolvedConf +} + +func readConfig(resolvedConf string) error { + var err error + var setupErr error + var f *os.File + + if resolvedConf == "" { + return fmt.Errorf("%w: provided config file is an empty string", io.EOF) + } + + f, err = os.Open(resolvedConf) // #nosec G304 go home gosec, you're drunk + if err == nil { + runningConfig, setupErr = config.Setup(f) + } + switch { + case setupErr != nil: + println("failed to setup config: " + setupErr.Error()) + err = setupErr + case err != nil: + println("failed to open config file for reading: " + err.Error()) + println("trying to create it....") + wroteOK := writeConfig(resolvedConf) + if wroteOK { + break + } + println("failed to create config file, cannot continue") + return fmt.Errorf("failed to create config file: %w", err) + case runningConfig != nil: + _ = f.Close() + } + + return err +} + +func resolveConfig() (usingDefaults bool, resolvedConf string, err error) { + if config.CLIFlags != nil { + confRoot := config.CLIFlags.Lookup("config") + if confRoot != nil && confRoot.Value.String() != "" { + resolvedConf = confRoot.Value.String() + } + } + + if resolvedConf == "" && os.Getenv("HELLPOT_CONFIG_FILE") != "" { + resolvedConf = os.Getenv("HELLPOT_CONFIG_FILE") + } + + if resolvedConf == "" { + resolvedConf = searchConfig() + } + + if err = readConfig(resolvedConf); err != nil && !errors.Is(err, io.EOF) { + return false, "", err + } + + if runningConfig == nil { + if runningConfig, err = config.Setup(nil); err != nil || runningConfig == nil { + if err == nil { + err = errors.New("unknown failure resulting in missing configuration, cannot continue") + } + return false, "", err + } + return true, "", nil + } + + return false, resolvedConf, nil +} + +func setup(stopChan chan os.Signal) (log zerolog.Logger, logFile string, resolvedConf string, err error) { + var usingDefaults bool + + if usingDefaults, resolvedConf, err = resolveConfig(); err != nil { + return + } + + //goland:noinspection GoNilness // we check for nil above + if log, err = logger.New(runningConfig.Logger); err != nil { + return + } + + logFile = runningConfig.Logger.ActiveLogFileName + + if usingDefaults { + runningConfig.UsingDefaults = true + log.Warn().Msg("continuing with default configuration in ") + for i := 5; i > 0; i-- { + print(strconv.Itoa(i)) + for i := 0; i < 5; i++ { + time.Sleep(200 * time.Millisecond) + print(".") + } + } + } + + if //goland:noinspection GoNilness + !runningConfig.Logger.NoColor { + extra.Banner() + } + + signal.Notify(stopChan, syscall.SIGINT, syscall.SIGTERM) + return +} diff --git a/internal/config/setup.go b/internal/config/setup.go index 193e449..ceb6c2b 100644 --- a/internal/config/setup.go +++ b/internal/config/setup.go @@ -23,17 +23,7 @@ func (r *readerProvider) Read() (map[string]interface{}, error) { if err != nil { return nil, err } - vals, err := toml.Parser().Unmarshal(b) - if len(vals) == 0 { - err = io.ErrUnexpectedEOF - } - if err != nil { - println(err.Error()) - } - for k, v := range vals { - println(k, v) - } - return vals, err //nolint:wrapcheck + return toml.Parser().Unmarshal(b) //nolint:wrapcheck } func Setup(source io.Reader) (*Parameters, error) { diff --git a/internal/logger/logger.go b/internal/logger/logger.go index b7121ca..1e37cfb 100644 --- a/internal/logger/logger.go +++ b/internal/logger/logger.go @@ -46,7 +46,6 @@ func (c *Configuration) findFallbackDir() error { var errs []error for _, loc := range locs { - println("checking", loc) if _, err = os.Stat(loc); err == nil { var locStat os.FileInfo if locStat, err = os.Stat(filepath.Join(loc, "HellPot")); err == nil { From 8b9603b7592b1ab7de594e8ecb3d00dd26b08068 Mon Sep 17 00:00:00 2001 From: "kayos@tcp.direct" Date: Wed, 26 Jun 2024 00:04:15 -0700 Subject: [PATCH 18/29] Chore: go mod tidy --- go.mod | 3 --- go.sum | 6 ------ 2 files changed, 9 deletions(-) diff --git a/go.mod b/go.mod index fe05b5d..97fdf8b 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,6 @@ go 1.19 require ( git.tcp.direct/kayos/common v0.9.7 github.com/fasthttp/router v1.5.1 - github.com/google/go-cmdtest v0.4.0 github.com/knadh/koanf/parsers/toml v0.1.0 github.com/knadh/koanf/providers/env v0.1.0 github.com/knadh/koanf/v2 v2.1.1 @@ -16,8 +15,6 @@ require ( require ( github.com/andybalholm/brotli v1.1.0 // indirect github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1 // indirect - github.com/google/go-cmp v0.3.1 // indirect - github.com/google/renameio v0.1.0 // indirect github.com/klauspost/compress v1.17.9 // indirect github.com/knadh/koanf/maps v0.1.1 // indirect github.com/mattn/go-colorable v0.1.13 // indirect diff --git a/go.sum b/go.sum index 9d33cb2..4ab95a0 100644 --- a/go.sum +++ b/go.sum @@ -9,12 +9,6 @@ github.com/fasthttp/router v1.5.1/go.mod h1:WrmsLo3mrerZP2VEXRV1E8nL8ymJFYCDTr4H github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1 h1:TQcrn6Wq+sKGkpyPvppOz99zsMBaUOKXq6HSv655U1c= github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= -github.com/google/go-cmdtest v0.4.0 h1:ToXh6W5spLp3npJV92tk6d5hIpUPYEzHLkD+rncbyhI= -github.com/google/go-cmdtest v0.4.0/go.mod h1:apVn/GCasLZUVpAJ6oWAuyP7Ne7CEsQbTnc0plM3m+o= -github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg= -github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/renameio v0.1.0 h1:GOZbcHa3HfsPKPlmyPyN2KEohoMXOhdMbHrvbpl2QaA= -github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= github.com/knadh/koanf/maps v0.1.1 h1:G5TjmUh2D7G2YWf5SQQqSiHRJEjaicvU0KpypqB3NIs= From 92818e4ea77dc649ba566cd027b8a091ee2dfb12 Mon Sep 17 00:00:00 2001 From: "kayos@tcp.direct" Date: Wed, 26 Jun 2024 00:48:37 -0700 Subject: [PATCH 19/29] Fix: cli arg, config, and logger fixes additionally: - fix `-c` flag - make main package test better --- cmd/HellPot/HellPot_test.go | 27 +++++++++++++++++- cmd/HellPot/boot.go | 49 +++++++++++++++++++++++++++------ internal/config/command_line.go | 23 ++++++++++------ internal/config/defaults.go | 1 + internal/config/setup.go | 2 +- internal/logger/logger.go | 18 ++++++++++-- 6 files changed, 98 insertions(+), 22 deletions(-) diff --git a/cmd/HellPot/HellPot_test.go b/cmd/HellPot/HellPot_test.go index 67a1506..e8db70d 100644 --- a/cmd/HellPot/HellPot_test.go +++ b/cmd/HellPot/HellPot_test.go @@ -2,14 +2,16 @@ package main import ( "os" + "path/filepath" "testing" "time" "github.com/yunginnanet/HellPot/internal/http" ) -func testMain(t interface{ Error(...any) }) (string, string, chan os.Signal, error) { +func testMain(t *testing.T) (string, string, chan os.Signal, error) { stopChan := make(chan os.Signal, 1) + log, logFile, resolvedConf, err := setup(stopChan) if err == nil { log.Info().Msg("🔥 Starting HellPot 🔥") @@ -26,6 +28,14 @@ func testMain(t interface{ Error(...any) }) (string, string, chan os.Signal, err } func TestHellPot(t *testing.T) { + logDir := filepath.Join(t.TempDir(), "logs") + if err := os.MkdirAll(logDir, 0755); err != nil { + t.Fatal(err) + } + confFile := filepath.Join(t.TempDir(), "HellPot_test.toml") + t.Setenv("HELLPOT_LOGGER_DIRECTORY", logDir) + t.Setenv("HELLPOT_CONFIG", confFile) + resolvedConf, logFile, stopChan, err := testMain(t) if err != nil { t.Fatal(err) @@ -33,6 +43,21 @@ func TestHellPot(t *testing.T) { if stopChan == nil { t.Fatal("stopChan is nil") } + if resolvedConf == "" { + t.Fatal("resolvedConf is empty") + } + if logFile == "" { + t.Fatal("logFile is empty") + } + if _, err = os.Stat(logFile); err != nil { + t.Fatal(err) + } + if resolvedConf != confFile { + t.Errorf("expected %s, got %s", confFile, resolvedConf) + } + if logFile != filepath.Join(logDir, "HellPot.log") { + t.Errorf("expected %s, got %s", filepath.Join(logDir, "HellPot.log"), logFile) + } t.Log("resolvedConf: ", resolvedConf) t.Log("logFile: ", logFile) time.Sleep(100 * time.Millisecond) diff --git a/cmd/HellPot/boot.go b/cmd/HellPot/boot.go index 4a8a7d0..16ca6a2 100644 --- a/cmd/HellPot/boot.go +++ b/cmd/HellPot/boot.go @@ -2,6 +2,7 @@ package main import ( "errors" + "flag" "fmt" "io" "os" @@ -18,23 +19,32 @@ import ( "github.com/yunginnanet/HellPot/internal/logger" ) -func writeConfig(target string) bool { +func writeConfig(target string) (*config.Parameters, bool) { var f *os.File var err error f, err = os.Create(target) // #nosec G304 -- go home gosec, you're drunk if err != nil { println("failed to create config file: " + err.Error()) - return false + return nil, false } if _, err = io.Copy(f, config.Defaults.IO); err != nil { println("failed to write default config to file: " + err.Error()) _ = f.Close() - return false + return nil, false + } + if err = f.Sync(); err != nil { + panic(err) } println("wrote default config to " + target) - runningConfig, _ = config.Setup(f) + var newConf *config.Parameters + newConf, err = config.Setup(f) + if err != nil { + println("failed to setup config with newly written file: " + err.Error()) + _ = f.Close() + return nil, false + } _ = f.Close() - return true + return newConf, true } func searchConfig() string { @@ -74,13 +84,15 @@ func readConfig(resolvedConf string) error { switch { case setupErr != nil: println("failed to setup config: " + setupErr.Error()) + _ = f.Close() err = setupErr case err != nil: println("failed to open config file for reading: " + err.Error()) println("trying to create it....") - wroteOK := writeConfig(resolvedConf) + newRunningConfig, wroteOK := writeConfig(resolvedConf) if wroteOK { - break + runningConfig = newRunningConfig + return nil } println("failed to create config file, cannot continue") return fmt.Errorf("failed to create config file: %w", err) @@ -92,10 +104,18 @@ func readConfig(resolvedConf string) error { } func resolveConfig() (usingDefaults bool, resolvedConf string, err error) { - if config.CLIFlags != nil { - confRoot := config.CLIFlags.Lookup("config") + setIfPresent := func(confRoot *flag.Flag) (ok bool) { if confRoot != nil && confRoot.Value.String() != "" { resolvedConf = confRoot.Value.String() + return true + } + return false + } + if config.CLIFlags != nil { + confRoot := config.CLIFlags.Lookup("config") + if !setIfPresent(confRoot) { + confRoot = config.CLIFlags.Lookup("c") + setIfPresent(confRoot) } } @@ -125,12 +145,18 @@ func resolveConfig() (usingDefaults bool, resolvedConf string, err error) { } func setup(stopChan chan os.Signal) (log zerolog.Logger, logFile string, resolvedConf string, err error) { + config.InitCLI() var usingDefaults bool if usingDefaults, resolvedConf, err = resolveConfig(); err != nil { return } + if runningConfig == nil { + err = errors.New("running configuration is nil, cannot continue") + return + } + //goland:noinspection GoNilness // we check for nil above if log, err = logger.New(runningConfig.Logger); err != nil { return @@ -156,5 +182,10 @@ func setup(stopChan chan os.Signal) (log zerolog.Logger, logFile string, resolve } signal.Notify(stopChan, syscall.SIGINT, syscall.SIGTERM) + + if absResolvedConf, err := filepath.Abs(resolvedConf); err == nil { + resolvedConf = absResolvedConf + } + return } diff --git a/internal/config/command_line.go b/internal/config/command_line.go index be6f279..c794ef6 100644 --- a/internal/config/command_line.go +++ b/internal/config/command_line.go @@ -10,17 +10,14 @@ import ( "github.com/yunginnanet/HellPot/internal/version" ) -var CLIFlags = flag.NewFlagSet("cli", flag.ExitOnError) +var CLIFlags = flag.NewFlagSet("config", flag.ExitOnError) -func init() { +func InitCLI() { + newArgs := make([]string, 0) for _, arg := range os.Args { // check for unit test flags - if strings.Contains(arg, "test.testlogfile") { - // we're in a unit test, bail - return - } - if strings.Contains(arg, "test.v") { - return + if !strings.HasPrefix(arg, "-test.") { + newArgs = append(newArgs, arg) } } @@ -36,10 +33,18 @@ func init() { CLIFlags.String("config", "", "specify config file") CLIFlags.String("version", "", "show version and exit") CLIFlags.String("v", "", "show version and exit") - if err := CLIFlags.Parse(os.Args[1:]); err != nil { + if err := CLIFlags.Parse(newArgs[1:]); err != nil { println(err.Error()) // flag.ExitOnError will call os.Exit(2) } + if os.Getenv("HELLPOT_CONFIG") != "" { + if err := CLIFlags.Set("config", os.Getenv("HELLPOT_CONFIG")); err != nil { + panic(err) + } + if err := CLIFlags.Set("c", os.Getenv("HELLPOT_CONFIG")); err != nil { + panic(err) + } + } if CLIFlags.Lookup("h").Value.String() == "true" || CLIFlags.Lookup("help").Value.String() == "true" { CLIFlags.Usage() os.Exit(0) diff --git a/internal/config/defaults.go b/internal/config/defaults.go index 8e150b0..06d8e05 100644 --- a/internal/config/defaults.go +++ b/internal/config/defaults.go @@ -51,6 +51,7 @@ var defOpts = map[string]interface{}{ "debug": true, "trace": false, "nocolor": runtime.GOOS == "windows", + "noconsole": false, "use_date_filename": false, "docker_logging": false, "console_time_format": time.Kitchen, diff --git a/internal/config/setup.go b/internal/config/setup.go index ceb6c2b..a86c3c3 100644 --- a/internal/config/setup.go +++ b/internal/config/setup.go @@ -15,7 +15,7 @@ type readerProvider struct { } func (r *readerProvider) ReadBytes() ([]byte, error) { - return io.ReadAll(r.source) + return io.ReadAll(r.source) //nolint:wrapcheck } func (r *readerProvider) Read() (map[string]interface{}, error) { diff --git a/internal/logger/logger.go b/internal/logger/logger.go index 1e37cfb..bd278dd 100644 --- a/internal/logger/logger.go +++ b/internal/logger/logger.go @@ -19,6 +19,7 @@ type Configuration struct { Debug bool `koanf:"debug"` Trace bool `koanf:"trace"` NoColor bool `koanf:"nocolor"` + NoConsole bool `koanf:"noconsole"` DockerLogging bool `koanf:"docker_logging"` // ConsoleTimeFormat sets the time format for the console. // The string is passed to time.Format() down the line. @@ -92,7 +93,6 @@ func (c *Configuration) Validate() error { func (c *Configuration) setupDirAndFile() error { switch { case c.Directory != "": - println(c.Directory) stat, err := os.Stat(c.Directory) if err != nil && !errors.Is(err, os.ErrNotExist) { return fmt.Errorf("failed to access specified log directory: %w", err) @@ -109,7 +109,6 @@ func (c *Configuration) setupDirAndFile() error { c.File = "HellPot.log" } case c.Directory == "" && c.File != "": - println(c.File) stat, err := os.Stat(filepath.Dir(c.File)) if err != nil && !errors.Is(err, os.ErrNotExist) { return fmt.Errorf("failed to access specified log directory: %w", err) @@ -183,6 +182,21 @@ func (c *Configuration) SetupOutputs() error { } } + consoleSeen := false + + for _, out := range c.Outputs { + if out == nil { + return fmt.Errorf("nil output provided") + } + if out == os.Stdout || out == os.Stderr { + consoleSeen = true + } + } + + if !consoleSeen && !c.NoConsole { + c.Outputs = append(c.Outputs, os.Stdout) + } + return nil } From 578aea0f3a2cedd748db990b5ac22f167bf656b0 Mon Sep 17 00:00:00 2001 From: "kayos@tcp.direct" Date: Wed, 26 Jun 2024 01:42:34 -0700 Subject: [PATCH 20/29] Fix: Embolden main package test, improve default config warning --- cmd/HellPot/HellPot.go | 34 ++------------------ cmd/HellPot/HellPot_test.go | 39 ++++++++++++++++++----- cmd/HellPot/boot.go | 63 +++++++++++++++++++++++++------------ internal/config/setup.go | 4 +++ 4 files changed, 81 insertions(+), 59 deletions(-) diff --git a/cmd/HellPot/HellPot.go b/cmd/HellPot/HellPot.go index c5118e1..001cb8b 100644 --- a/cmd/HellPot/HellPot.go +++ b/cmd/HellPot/HellPot.go @@ -2,51 +2,23 @@ package main import ( "os" - "strconv" - "github.com/yunginnanet/HellPot/internal/config" "github.com/yunginnanet/HellPot/internal/http" - "github.com/yunginnanet/HellPot/internal/version" -) - -var ( - runningConfig *config.Parameters ) func main() { stopChan := make(chan os.Signal, 1) - log, _, resolvedConf, err := setup(stopChan) + log, _, resolvedConf, realConf, err := setup(stopChan) if err != nil { println("failed to start: " + err.Error()) os.Exit(1) } - log.Info().Msg("🔥 Starting HellPot 🔥") - log.Info().Msg("Version: " + version.Version) - log.Info().Msg("PID: " + strconv.Itoa(os.Getpid())) - log.Info().Msg("Using config file: " + resolvedConf) - if runningConfig.UsingDefaults { - log.Warn().Msg("Using default configuration") - } - if runningConfig.Logger.RSyslog != "" { - log.Info().Msg("Logging to syslog: " + runningConfig.Logger.RSyslog) - } - if runningConfig.Logger.ActiveLogFileName != "" { - log.Info().Msg("Logging to file: " + runningConfig.Logger.ActiveLogFileName) - } - if runningConfig.Logger.DockerLogging && - runningConfig.Logger.File == "" && - runningConfig.Logger.Directory == "" && - runningConfig.Logger.RSyslog == "" { - log.Info().Msg("Only logging to standard output") - } - - log.Debug().Msg("Debug logging enabled") - log.Trace().Msg("Trace logging enabled") + printInfo(log, resolvedConf, realConf) go func() { - log.Fatal().Err(http.Serve(runningConfig)).Msg("HTTP error") + log.Fatal().Err(http.Serve(realConf)).Msg("HTTP error") }() sig := <-stopChan // wait for SIGINT diff --git a/cmd/HellPot/HellPot_test.go b/cmd/HellPot/HellPot_test.go index e8db70d..0b43fea 100644 --- a/cmd/HellPot/HellPot_test.go +++ b/cmd/HellPot/HellPot_test.go @@ -3,20 +3,24 @@ package main import ( "os" "path/filepath" + "strconv" + "strings" "testing" "time" + "github.com/yunginnanet/HellPot/internal/config" "github.com/yunginnanet/HellPot/internal/http" ) -func testMain(t *testing.T) (string, string, chan os.Signal, error) { +func testMain(t *testing.T) (string, string, chan os.Signal, *config.Parameters, error) { + t.Helper() stopChan := make(chan os.Signal, 1) - log, logFile, resolvedConf, err := setup(stopChan) + log, logFile, resolvedConf, realConfig, err := setup(stopChan) if err == nil { - log.Info().Msg("🔥 Starting HellPot 🔥") + printInfo(log, resolvedConf, realConfig) go func() { - terr := http.Serve(runningConfig) + terr := http.Serve(realConfig) if terr != nil { t.Error("failed to serve HTTP: " + terr.Error()) close(stopChan) @@ -24,19 +28,20 @@ func testMain(t *testing.T) (string, string, chan os.Signal, error) { }() } //goland:noinspection GoNilness - return resolvedConf, logFile, stopChan, err + return resolvedConf, logFile, stopChan, realConfig, err } func TestHellPot(t *testing.T) { - logDir := filepath.Join(t.TempDir(), "logs") + tDir := filepath.Join(t.TempDir(), strconv.Itoa(int(time.Now().Unix()))) + logDir := filepath.Join(tDir, "logs") if err := os.MkdirAll(logDir, 0755); err != nil { t.Fatal(err) } - confFile := filepath.Join(t.TempDir(), "HellPot_test.toml") + confFile := filepath.Join(tDir, "HellPot_test.toml") t.Setenv("HELLPOT_LOGGER_DIRECTORY", logDir) t.Setenv("HELLPOT_CONFIG", confFile) - resolvedConf, logFile, stopChan, err := testMain(t) + resolvedConf, logFile, stopChan, realConfig, err := testMain(t) if err != nil { t.Fatal(err) } @@ -58,8 +63,26 @@ func TestHellPot(t *testing.T) { if logFile != filepath.Join(logDir, "HellPot.log") { t.Errorf("expected %s, got %s", filepath.Join(logDir, "HellPot.log"), logFile) } + time.Sleep(25 * time.Millisecond) // sync maybe + logDat, err := os.ReadFile(logFile) + if err != nil { + t.Error(err) + } + if !strings.Contains(string(logDat), "🔥 Starting HellPot 🔥") { + t.Errorf("expected log to contain '🔥 Starting HellPot 🔥', got %s", logDat) + } + if !strings.Contains(string(logDat), logFile) { + t.Errorf("expected log to contain '%s'", logFile) + } + if !strings.Contains(string(logDat), resolvedConf) { + t.Errorf("expected log to contain '%s'", resolvedConf) + } + if !strings.Contains(string(logDat), "PID: "+strconv.Itoa(os.Getpid())) { + t.Errorf("expected log to contain 'PID: %d', got %s", os.Getpid(), logDat) + } t.Log("resolvedConf: ", resolvedConf) t.Log("logFile: ", logFile) + t.Log("realConfig: ", realConfig) time.Sleep(100 * time.Millisecond) stopChan <- os.Interrupt } diff --git a/cmd/HellPot/boot.go b/cmd/HellPot/boot.go index 16ca6a2..02b760d 100644 --- a/cmd/HellPot/boot.go +++ b/cmd/HellPot/boot.go @@ -19,6 +19,12 @@ import ( "github.com/yunginnanet/HellPot/internal/logger" ) +const ( + defaultConfigWarningDelaySecs = 10 + red = "\033[31m" + reset = "\033[0m" +) + func writeConfig(target string) (*config.Parameters, bool) { var f *os.File var err error @@ -37,13 +43,13 @@ func writeConfig(target string) (*config.Parameters, bool) { } println("wrote default config to " + target) var newConf *config.Parameters - newConf, err = config.Setup(f) - if err != nil { + if newConf, err = config.Setup(f); err != nil { println("failed to setup config with newly written file: " + err.Error()) _ = f.Close() return nil, false } _ = f.Close() + newConf.UsingDefaults = true return newConf, true } @@ -68,15 +74,17 @@ func searchConfig() string { return resolvedConf } -func readConfig(resolvedConf string) error { +func readConfig(resolvedConf string) (*config.Parameters, error) { var err error var setupErr error var f *os.File if resolvedConf == "" { - return fmt.Errorf("%w: provided config file is an empty string", io.EOF) + return nil, fmt.Errorf("%w: provided config file is an empty string", io.EOF) } + var runningConfig *config.Parameters + f, err = os.Open(resolvedConf) // #nosec G304 go home gosec, you're drunk if err == nil { runningConfig, setupErr = config.Setup(f) @@ -84,26 +92,27 @@ func readConfig(resolvedConf string) error { switch { case setupErr != nil: println("failed to setup config: " + setupErr.Error()) - _ = f.Close() + if f != nil { + _ = f.Close() + } err = setupErr case err != nil: println("failed to open config file for reading: " + err.Error()) println("trying to create it....") newRunningConfig, wroteOK := writeConfig(resolvedConf) if wroteOK { - runningConfig = newRunningConfig - return nil + return newRunningConfig, nil } println("failed to create config file, cannot continue") - return fmt.Errorf("failed to create config file: %w", err) + return nil, fmt.Errorf("failed to create config file: %w", err) case runningConfig != nil: _ = f.Close() } - return err + return runningConfig, err } -func resolveConfig() (usingDefaults bool, resolvedConf string, err error) { +func resolveConfig() (runningConfig *config.Parameters, usingDefaults bool, resolvedConf string, err error) { setIfPresent := func(confRoot *flag.Flag) (ok bool) { if confRoot != nil && confRoot.Value.String() != "" { resolvedConf = confRoot.Value.String() @@ -127,8 +136,8 @@ func resolveConfig() (usingDefaults bool, resolvedConf string, err error) { resolvedConf = searchConfig() } - if err = readConfig(resolvedConf); err != nil && !errors.Is(err, io.EOF) { - return false, "", err + if runningConfig, err = readConfig(resolvedConf); err != nil && !errors.Is(err, io.EOF) { + return runningConfig, false, "", err } if runningConfig == nil { @@ -136,19 +145,23 @@ func resolveConfig() (usingDefaults bool, resolvedConf string, err error) { if err == nil { err = errors.New("unknown failure resulting in missing configuration, cannot continue") } - return false, "", err + return runningConfig, false, "", err } - return true, "", nil + return runningConfig, true, "", nil } - return false, resolvedConf, nil + return runningConfig, false, resolvedConf, nil } -func setup(stopChan chan os.Signal) (log zerolog.Logger, logFile string, resolvedConf string, err error) { +func setup(stopChan chan os.Signal) (log zerolog.Logger, logFile string, + resolvedConf string, realConf *config.Parameters, err error) { + config.InitCLI() + var usingDefaults bool + var runningConfig *config.Parameters - if usingDefaults, resolvedConf, err = resolveConfig(); err != nil { + if runningConfig, usingDefaults, resolvedConf, err = resolveConfig(); err != nil { return } @@ -157,6 +170,15 @@ func setup(stopChan chan os.Signal) (log zerolog.Logger, logFile string, resolve return } + // TODO: jesus bro r u ok + realConf = runningConfig + if usingDefaults && !realConf.UsingDefaults { + realConf.UsingDefaults = true + } + if realConf.UsingDefaults && !usingDefaults { + usingDefaults = true + } + //goland:noinspection GoNilness // we check for nil above if log, err = logger.New(runningConfig.Logger); err != nil { return @@ -165,15 +187,16 @@ func setup(stopChan chan os.Signal) (log zerolog.Logger, logFile string, resolve logFile = runningConfig.Logger.ActiveLogFileName if usingDefaults { - runningConfig.UsingDefaults = true - log.Warn().Msg("continuing with default configuration in ") - for i := 5; i > 0; i-- { + log.Warn().Msg("using default configuration!") + print(red + "continuing with default configuration in ") + for i := defaultConfigWarningDelaySecs; i > 0; i-- { print(strconv.Itoa(i)) for i := 0; i < 5; i++ { time.Sleep(200 * time.Millisecond) print(".") } } + print(reset + "\n") } if //goland:noinspection GoNilness diff --git a/internal/config/setup.go b/internal/config/setup.go index a86c3c3..885da52 100644 --- a/internal/config/setup.go +++ b/internal/config/setup.go @@ -60,6 +60,10 @@ func Setup(source io.Reader) (*Parameters, error) { source: k, } + if source == nil { + p.UsingDefaults = true + } + if err := k.Unmarshal("", p); err != nil { return nil, fmt.Errorf("failed to unmarshal config: %w", err) } From 23c65b94d76388b38f03882360beb48e37d34326 Mon Sep 17 00:00:00 2001 From: "kayos@tcp.direct" Date: Wed, 26 Jun 2024 01:44:10 -0700 Subject: [PATCH 21/29] Fix: add missing `util.go` --- cmd/HellPot/util.go | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 cmd/HellPot/util.go diff --git a/cmd/HellPot/util.go b/cmd/HellPot/util.go new file mode 100644 index 0000000..cc1918e --- /dev/null +++ b/cmd/HellPot/util.go @@ -0,0 +1,35 @@ +package main + +import ( + "os" + "strconv" + + "github.com/rs/zerolog" + + "github.com/yunginnanet/HellPot/internal/config" + "github.com/yunginnanet/HellPot/internal/version" +) + +func printInfo(log zerolog.Logger, resolvedConf string, realConfig *config.Parameters) { + log.Info().Msg("🔥 Starting HellPot 🔥") + if realConfig.UsingDefaults { + log.Warn().Msg("Using default configuration! Please edit the configuration file to suit your needs.") + } + log.Info().Msg("Version: " + version.Version) + log.Info().Msg("PID: " + strconv.Itoa(os.Getpid())) + log.Info().Msg("Using config file: " + resolvedConf) + if realConfig.Logger.RSyslog != "" { + log.Info().Msg("Logging to syslog: " + realConfig.Logger.RSyslog) + } + if realConfig.Logger.ActiveLogFileName != "" { + log.Info().Msg("Logging to file: " + realConfig.Logger.ActiveLogFileName) + } + if realConfig.Logger.DockerLogging && + realConfig.Logger.File == "" && + realConfig.Logger.Directory == "" && + realConfig.Logger.RSyslog == "" { + log.Info().Msg("Only logging to standard output") + } + log.Debug().Msg("Debug logging enabled") + log.Trace().Msg("Trace logging enabled") +} From 93dcb98817b95f8dba9548f4b7c21a5beba05b68 Mon Sep 17 00:00:00 2001 From: "kayos@tcp.direct" Date: Wed, 26 Jun 2024 03:58:55 -0700 Subject: [PATCH 22/29] Fix: fix CLI arg <-> config <-> env parity --- go.mod | 1 + go.sum | 2 + internal/config/command_line.go | 73 ++++++++++++++--- internal/config/defaults.go | 4 + internal/config/models.go | 4 +- internal/config/setup.go | 141 +++++++++++++++++++++++++++++--- internal/http/router.go | 6 +- 7 files changed, 207 insertions(+), 24 deletions(-) diff --git a/go.mod b/go.mod index 97fdf8b..f7f0b67 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( git.tcp.direct/kayos/common v0.9.7 github.com/fasthttp/router v1.5.1 github.com/knadh/koanf/parsers/toml v0.1.0 + github.com/knadh/koanf/providers/basicflag v1.0.0 github.com/knadh/koanf/providers/env v0.1.0 github.com/knadh/koanf/v2 v2.1.1 github.com/rs/zerolog v1.33.0 diff --git a/go.sum b/go.sum index 4ab95a0..dd8dbf7 100644 --- a/go.sum +++ b/go.sum @@ -15,6 +15,8 @@ github.com/knadh/koanf/maps v0.1.1 h1:G5TjmUh2D7G2YWf5SQQqSiHRJEjaicvU0KpypqB3NI github.com/knadh/koanf/maps v0.1.1/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI= github.com/knadh/koanf/parsers/toml v0.1.0 h1:S2hLqS4TgWZYj4/7mI5m1CQQcWurxUz6ODgOub/6LCI= github.com/knadh/koanf/parsers/toml v0.1.0/go.mod h1:yUprhq6eo3GbyVXFFMdbfZSo928ksS+uo0FFqNMnO18= +github.com/knadh/koanf/providers/basicflag v1.0.0 h1:qB0es/9fYsLuYnrKazxNCuWtkv3JFX1lI1druUsDDvY= +github.com/knadh/koanf/providers/basicflag v1.0.0/go.mod h1:n0NlnaxXUCER/WIzRroT9q3Np+FiZ9pSjrC6A/OozI8= github.com/knadh/koanf/providers/env v0.1.0 h1:LqKteXqfOWyx5Ab9VfGHmjY9BvRXi+clwyZozgVRiKg= github.com/knadh/koanf/providers/env v0.1.0/go.mod h1:RE8K9GbACJkeEnkl8L/Qcj8p4ZyPXZIQ191HJi44ZaQ= github.com/knadh/koanf/v2 v2.1.1 h1:/R8eXqasSTsmDCsAyYj+81Wteg8AqrV9CP6gvsTsOmM= diff --git a/internal/config/command_line.go b/internal/config/command_line.go index c794ef6..c0f083f 100644 --- a/internal/config/command_line.go +++ b/internal/config/command_line.go @@ -4,38 +4,88 @@ import ( "flag" "io" "os" + "slices" "strings" "github.com/yunginnanet/HellPot/internal/extra" "github.com/yunginnanet/HellPot/internal/version" ) -var CLIFlags = flag.NewFlagSet("config", flag.ExitOnError) +var CLIFlags = flag.NewFlagSet("config", flag.ContinueOnError) + +var ( + sliceDefs = make(map[string][]string) + slicePtrs = make(map[string]*string) +) + +func addCLIFlags() { + parse := func(k string, v interface{}, nestedName string) { + switch casted := v.(type) { + case bool: + CLIFlags.Bool(nestedName, casted, "set "+k) + case string: + CLIFlags.String(nestedName, casted, "set "+k) + case int: + CLIFlags.Int(nestedName, casted, "set "+k) + case float64: + CLIFlags.Float64(nestedName, casted, "set "+k) + case []string: + sliceDefs[nestedName] = casted + joined := strings.Join(sliceDefs[nestedName], ",") + slicePtrs[nestedName] = CLIFlags.String(nestedName, joined, "set "+k) + } + } + + for key, val := range Defaults.val { + if _, ok := val.(map[string]interface{}); !ok { + parse(key, val, key) + continue + } + nested, ok := val.(map[string]interface{}) + if !ok { + // linter was confused by the above check + panic("unreachable, if you see this you have entered a real life HellPot") + } + for k, v := range nested { + nestedName := key + "." + k + parse(k, v, nestedName) + } + } +} + +var replacer = map[string][]string{ + "-h": {"-help"}, + "-v": {"-version"}, + "-c": {"-config"}, + "-g": {"-bespoke.enable_grimoire", "true", "-bespoke.grimoire_file"}, +} func InitCLI() { newArgs := make([]string, 0) for _, arg := range os.Args { + if repl, ok := replacer[arg]; ok { + newArgs = append(newArgs, repl...) + continue + } // check for unit test flags if !strings.HasPrefix(arg, "-test.") { newArgs = append(newArgs, arg) } } - CLIFlags.Bool("logger-debug", false, "force debug logging") - CLIFlags.Bool("logger-trace", false, "force trace logging") - CLIFlags.Bool("logger-nocolor", false, "force no color logging") - CLIFlags.String("bespoke-grimoire", "", "specify a custom file used for text generation") + newArgs = slices.Compact(newArgs) + CLIFlags.Bool("banner", false, "show banner and version then exit") CLIFlags.Bool("genconfig", false, "write default config to stdout then exit") - CLIFlags.Bool("h", false, "show this help and exit") CLIFlags.Bool("help", false, "show this help and exit") - CLIFlags.String("c", "", "specify config file") CLIFlags.String("config", "", "specify config file") CLIFlags.String("version", "", "show version and exit") - CLIFlags.String("v", "", "show version and exit") + + addCLIFlags() + if err := CLIFlags.Parse(newArgs[1:]); err != nil { println(err.Error()) - // flag.ExitOnError will call os.Exit(2) + os.Exit(2) } if os.Getenv("HELLPOT_CONFIG") != "" { if err := CLIFlags.Set("config", os.Getenv("HELLPOT_CONFIG")); err != nil { @@ -45,11 +95,11 @@ func InitCLI() { panic(err) } } - if CLIFlags.Lookup("h").Value.String() == "true" || CLIFlags.Lookup("help").Value.String() == "true" { + if CLIFlags.Lookup("help").Value.String() == "true" { CLIFlags.Usage() os.Exit(0) } - if CLIFlags.Lookup("version").Value.String() == "true" || CLIFlags.Lookup("v").Value.String() == "true" { + if CLIFlags.Lookup("version").Value.String() == "true" { _, _ = os.Stdout.WriteString("HellPot version: " + version.Version + "\n") os.Exit(0) } @@ -66,4 +116,5 @@ func InitCLI() { extra.Banner() os.Exit(0) } + } diff --git a/internal/config/defaults.go b/internal/config/defaults.go index 06d8e05..509d886 100644 --- a/internal/config/defaults.go +++ b/internal/config/defaults.go @@ -83,4 +83,8 @@ var defOpts = map[string]interface{}{ "deception": map[string]interface{}{ "server_name": "nginx", }, + "bespoke": map[string]interface{}{ + "grimoire_file": "", + "enable_grimoire": false, + }, } diff --git a/internal/config/models.go b/internal/config/models.go index c6b9dec..c3d9a32 100644 --- a/internal/config/models.go +++ b/internal/config/models.go @@ -85,6 +85,6 @@ type DevilsPlaythings struct { // Customization represents the configuration for the customizations. type Customization struct { - CustomHeffalump bool `koanf:"custom_heffalump"` - Grimoire string `koanf:"grimoire"` + CustomHeffalump bool `koanf:"enable_grimoire"` + Grimoire string `koanf:"grimoire_file"` } diff --git a/internal/config/setup.go b/internal/config/setup.go index 885da52..bdbf6a7 100644 --- a/internal/config/setup.go +++ b/internal/config/setup.go @@ -3,9 +3,11 @@ package config import ( "fmt" "io" + "slices" "strings" "github.com/knadh/koanf/parsers/toml" + flags "github.com/knadh/koanf/providers/basicflag" "github.com/knadh/koanf/providers/env" "github.com/knadh/koanf/v2" ) @@ -26,19 +28,89 @@ func (r *readerProvider) Read() (map[string]interface{}, error) { return toml.Parser().Unmarshal(b) //nolint:wrapcheck } -func Setup(source io.Reader) (*Parameters, error) { - k := koanf.New(".") +func normalizeMap(m map[string]interface{}) map[string]interface{} { + for k, v := range m { + ogk := k + k = strings.ToLower(k) - if err := k.Load(Defaults, nil); err != nil { - return nil, fmt.Errorf("failed to load defaults: %w", err) + var sslice []string + var sliceOK bool + + if sslice, sliceOK = v.([]string); !sliceOK { + goto justLower + } + for i, s := range sslice { + sslice[i] = strings.ToLower(s) + } + slices.Sort(sslice) + m[k] = sslice + justLower: + if k != ogk { + delete(m, ogk) + } } + return m +} - if source != nil { - if err := k.Load(&readerProvider{source}, toml.Parser()); err != nil { - return nil, fmt.Errorf("failed to read config: %w", err) +func (p *Parameters) merge(ogk *koanf.Koanf, newk *koanf.Koanf, friendlyName string) error { + if ogk == nil { + panic("original koanf is nil") + } + if newk == nil { + return nil + } + dirty := false + + newKeys := normalizeMap(newk.All()) + + if len(newk.All()) == 0 || len(newKeys) == 0 { + return nil + } + + for k, v := range newKeys { + if !ogk.Exists(k) { + if err := ogk.Set(k, v); err != nil { + panic(fmt.Sprintf("failed to set key %s: %v", k, err)) + } + dirty = true + continue + } + + ogv := ogk.Get(k) + if ogv == nil { + if err := ogk.Set(k, v); err != nil { + panic(fmt.Sprintf("failed to set key %s: %v", k, err)) + } + dirty = true + continue + } + + if _, hasDefault := Defaults.val[k]; !hasDefault { + continue + } + + if ogv == Defaults.val[k] && v != ogv { + if err := ogk.Set(k, v); err != nil { + panic(fmt.Sprintf("failed to set key %s: %v", k, err)) + } + dirty = true } } + if !dirty { + return nil + } + + println("found configuration overrides in " + friendlyName) + + if err := ogk.Merge(newk); err != nil { + return fmt.Errorf("failed to merge env config: %w", err) + } + + return nil +} + +func (p *Parameters) LoadEnv(k *koanf.Koanf) error { envK := koanf.New(".") envErr := envK.Load(env.Provider("HELLPOT_", ".", func(s string) string { @@ -50,9 +122,50 @@ func Setup(source io.Reader) (*Parameters, error) { return s }), nil) - if envErr == nil && envK != nil && len(envK.All()) > 0 { - if err := k.Merge(envK); err != nil { - return nil, fmt.Errorf("failed to merge env config: %w", err) + if envErr != nil { + return fmt.Errorf("failed to load env: %w", envErr) + } + + if err := p.merge(k, envK, "environment variables"); err != nil { + return err + } + + return nil +} + +func parseCLISlice(key string, value string) (string, interface{}) { + if _, ok := slicePtrs[key]; !ok { + return key, value + } + split := strings.Split(value, ",") + slices.Sort(split) + return key, split +} + +func (p *Parameters) LoadFlags(k *koanf.Koanf) error { + flagsK := koanf.New(".") + + if err := flagsK.Load(flags.ProviderWithValue(CLIFlags, ".", parseCLISlice), nil); err != nil { + return fmt.Errorf("failed to load flags: %w", err) + } + + if err := p.merge(k, flagsK, "cli arguments"); err != nil { + return err + } + + return nil +} + +func Setup(source io.Reader) (*Parameters, error) { + k := koanf.New(".") + + if err := k.Load(Defaults, nil); err != nil { + return nil, fmt.Errorf("failed to load defaults: %w", err) + } + + if source != nil { + if err := k.Load(&readerProvider{source}, toml.Parser()); err != nil { + return nil, fmt.Errorf("failed to read config: %w", err) } } @@ -64,6 +177,14 @@ func Setup(source io.Reader) (*Parameters, error) { p.UsingDefaults = true } + if err := p.LoadFlags(k); err != nil { + return nil, err + } + + if err := p.LoadEnv(k); err != nil { + return nil, err + } + if err := k.Unmarshal("", p); err != nil { return nil, fmt.Errorf("failed to unmarshal config: %w", err) } diff --git a/internal/http/router.go b/internal/http/router.go index d2e06c8..e63167b 100644 --- a/internal/http/router.go +++ b/internal/http/router.go @@ -119,7 +119,7 @@ func getSrv(r *router.Router) fasthttp.Server { } } -func setupHeffalump(config *config.Parameters) error { +func SetupHeffalump(config *config.Parameters) error { switch config.Bespoke.CustomHeffalump { case true: content, err := os.ReadFile(config.Bespoke.Grimoire) @@ -151,6 +151,10 @@ func Serve(config *config.Parameters) error { log = config.GetLogger() runningConfig = config + if err := SetupHeffalump(config); err != nil { + return fmt.Errorf("failed to setup heffalump: %w", err) + } + l := config.HTTP.Bind + ":" + strconv.Itoa(int(config.HTTP.Port)) r := router.New() From 5724075eff9916129c8eda29eb35ccf5a5426c1c Mon Sep 17 00:00:00 2001 From: "kayos@tcp.direct" Date: Wed, 26 Jun 2024 04:12:18 -0700 Subject: [PATCH 23/29] Fix (cli): dupe/invalid flag alias, fix test case send on closed channel --- cmd/HellPot/HellPot_test.go | 2 +- internal/config/command_line.go | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/cmd/HellPot/HellPot_test.go b/cmd/HellPot/HellPot_test.go index 0b43fea..710dee1 100644 --- a/cmd/HellPot/HellPot_test.go +++ b/cmd/HellPot/HellPot_test.go @@ -23,7 +23,7 @@ func testMain(t *testing.T) (string, string, chan os.Signal, *config.Parameters, terr := http.Serve(realConfig) if terr != nil { t.Error("failed to serve HTTP: " + terr.Error()) - close(stopChan) + return } }() } diff --git a/internal/config/command_line.go b/internal/config/command_line.go index c0f083f..fc8e073 100644 --- a/internal/config/command_line.go +++ b/internal/config/command_line.go @@ -91,9 +91,6 @@ func InitCLI() { if err := CLIFlags.Set("config", os.Getenv("HELLPOT_CONFIG")); err != nil { panic(err) } - if err := CLIFlags.Set("c", os.Getenv("HELLPOT_CONFIG")); err != nil { - panic(err) - } } if CLIFlags.Lookup("help").Value.String() == "true" { CLIFlags.Usage() From e0b4c0ac5981986f1edadd85d080da76d8369396 Mon Sep 17 00:00:00 2001 From: "kayos@tcp.direct" Date: Wed, 26 Jun 2024 04:12:44 -0700 Subject: [PATCH 24/29] Fix (grimoire): check for opening and closing brackets in source --- internal/http/router.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/internal/http/router.go b/internal/http/router.go index e63167b..453ed00 100644 --- a/internal/http/router.go +++ b/internal/http/router.go @@ -134,6 +134,11 @@ func SetupHeffalump(config *config.Parameters) error { return fmt.Errorf("%w: grimoire file '%s' appears to be empty", io.EOF, config.Bespoke.Grimoire) } + if !strings.Contains(src, "<") || !strings.Contains(src, ">") { + return fmt.Errorf("%w: grimoire file '%s' does not appear to be a valid source text, missing brackets", + io.ErrUnexpectedEOF, config.Bespoke.Grimoire) + } + markovMap := heffalump.MakeMarkovMap(strings.NewReader(src)) hellpotHeffalump = heffalump.NewHeffalump(markovMap, heffalump.DefaultBuffSize) default: From 0bc411a64ab77474ed85b6ced3515a4f3962f831 Mon Sep 17 00:00:00 2001 From: "kayos@tcp.direct" Date: Wed, 26 Jun 2024 04:26:49 -0700 Subject: [PATCH 25/29] Entropy: improve markov map randomness, decrease detectability --- go.mod | 1 + go.sum | 1 + heffalump/markov.go | 26 +++++++++++++++++++++----- 3 files changed, 23 insertions(+), 5 deletions(-) diff --git a/go.mod b/go.mod index f7f0b67..c477423 100644 --- a/go.mod +++ b/go.mod @@ -26,4 +26,5 @@ require ( github.com/savsgio/gotils v0.0.0-20240303185622-093b76447511 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect golang.org/x/sys v0.21.0 // indirect + nullprogram.com/x/rng v1.1.0 // indirect ) diff --git a/go.sum b/go.sum index dd8dbf7..0f4f395 100644 --- a/go.sum +++ b/go.sum @@ -51,3 +51,4 @@ golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= nullprogram.com/x/rng v1.1.0 h1:SMU7DHaQSWtKJNTpNFIFt8Wd/KSmOuSDPXrMFp/UMro= +nullprogram.com/x/rng v1.1.0/go.mod h1:glGw6V87vyfawxCzqOABL3WfL95G65az9Z2JZCylCkg= diff --git a/heffalump/markov.go b/heffalump/markov.go index e4a565e..b7f6f01 100644 --- a/heffalump/markov.go +++ b/heffalump/markov.go @@ -5,12 +5,19 @@ import ( "io" "math/rand" "strings" + "time" "unicode" "unicode/utf8" + "git.tcp.direct/kayos/common/entropy" "git.tcp.direct/kayos/common/squish" ) +const ( + sp = " " + nl = "\n" +) + // NewDefaultMarkovMap creates a new MarkovMap from the default source text. func NewDefaultMarkovMap() MarkovMap { src, err := squish.UnpackStr(srcGz) @@ -70,11 +77,15 @@ func ScanHTML(data []byte, atEOF bool) (advance int, token []byte, err error) { type tokenPair [2]string // MarkovMap is a map that acts as a Markov chain generator. -type MarkovMap map[tokenPair][]string +type MarkovMap struct { + m map[tokenPair][]string + r *rand.Rand +} // MakeMarkovMap makes an empty MakeMarkov and fills it with r. func MakeMarkovMap(r io.Reader) MarkovMap { - m := MarkovMap{} + m := MarkovMap{m: make(map[tokenPair][]string)} + m.r = rand.New(rand.NewSource(entropy.GetOptimizedRand().Int63())) m.Fill(r) return m } @@ -97,13 +108,13 @@ func (mm MarkovMap) Fill(r io.Reader) { // Add adds a three token sequence to the map. func (mm MarkovMap) Add(w1, w2, w3 string) { p := tokenPair{w1, w2} - mm[p] = append(mm[p], w3) + mm.m[p] = append(mm.m[p], w3) } // Get pseudo-randomly chooses a possible suffix to w1 and w2. func (mm MarkovMap) Get(w1, w2 string) string { p := tokenPair{w1, w2} - suffix, ok := mm[p] + suffix, ok := mm.m[p] if !ok { return "" } @@ -119,10 +130,15 @@ func (mm MarkovMap) Read(p []byte) (n int, err error) { for { w3 = mm.Get(w1, w2) if n+len(w3)+1 >= len(p) { + n += copy(p[n:], nl) break } n += copy(p[n:], w3) - n += copy(p[n:], "\n") + if time.Now().UnixNano()%10 == 0 { + n += copy(p[n:], nl) + } else { + n += copy(p[n:], sp) + } w1, w2 = w2, w3 } return From e43c60396fc08b08d63dc62a9b410ba2f05c9c6a Mon Sep 17 00:00:00 2001 From: "kayos@tcp.direct" Date: Wed, 26 Jun 2024 04:30:18 -0700 Subject: [PATCH 26/29] Security (heffalump): nosec gosec G404 (entropy) --- heffalump/markov.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/heffalump/markov.go b/heffalump/markov.go index b7f6f01..ef70066 100644 --- a/heffalump/markov.go +++ b/heffalump/markov.go @@ -85,7 +85,7 @@ type MarkovMap struct { // MakeMarkovMap makes an empty MakeMarkov and fills it with r. func MakeMarkovMap(r io.Reader) MarkovMap { m := MarkovMap{m: make(map[tokenPair][]string)} - m.r = rand.New(rand.NewSource(entropy.GetOptimizedRand().Int63())) + m.r = rand.New(rand.NewSource(entropy.GetOptimizedRand().Int63())) // #nosec: G404 : speed not security m.Fill(r) return m } From e31113b06287b01d81cd0bebb55615f81847c94f Mon Sep 17 00:00:00 2001 From: "kayos@tcp.direct" Date: Wed, 26 Jun 2024 16:03:03 -0700 Subject: [PATCH 27/29] Fix (config): Logger config persistence, implement syslog (tested working) --- Makefile | 12 +- cmd/HellPot/HellPot_test.go | 2 + cmd/HellPot/boot.go | 123 ++++++++--------- internal/config/client_rules.go | 8 +- .../{models_test.go => client_rules_test.go} | 0 internal/config/command_line.go | 25 ++++ internal/config/defaults.go | 1 + internal/config/defaults_test.go | 1 + internal/config/models.go | 10 +- internal/config/setup.go | 33 +++-- internal/config/setup_test.go | 24 +++- internal/logger/logger.go | 130 +++++++++++++++--- 12 files changed, 268 insertions(+), 101 deletions(-) rename internal/config/{models_test.go => client_rules_test.go} (100%) diff --git a/Makefile b/Makefile index c2663e0..9e6ccef 100644 --- a/Makefile +++ b/Makefile @@ -1,11 +1,13 @@ all: deps check build -format: +format : find . -iname "*.go" -exec gofmt -s -l -w {} \; -check: +check : go vet ./... -run: +run : go run cmd/HellPot/*.go deps: go mod tidy -v -build: - go build -trimpath -ldflags "-s -w -X main.version=`git tag --sort=-version:refname | head -n 1`" cmd/HellPot/*.go +test : + go test -v ./... +build : + go build -x -trimpath -ldflags "-s -w -X main.version=`git tag --sort=-version:refname | head -n 1`" cmd/HellPot/*.go diff --git a/cmd/HellPot/HellPot_test.go b/cmd/HellPot/HellPot_test.go index 710dee1..b415a16 100644 --- a/cmd/HellPot/HellPot_test.go +++ b/cmd/HellPot/HellPot_test.go @@ -13,6 +13,7 @@ import ( ) func testMain(t *testing.T) (string, string, chan os.Signal, *config.Parameters, error) { + t.Setenv("HELLPOT_TEST_MODE", "true") t.Helper() stopChan := make(chan os.Signal, 1) @@ -39,6 +40,7 @@ func TestHellPot(t *testing.T) { } confFile := filepath.Join(tDir, "HellPot_test.toml") t.Setenv("HELLPOT_LOGGER_DIRECTORY", logDir) + t.Setenv("HELLPOT_LOGGER_RSYSLOG_ADDRESS", "local") t.Setenv("HELLPOT_CONFIG", confFile) resolvedConf, logFile, stopChan, realConfig, err := testMain(t) diff --git a/cmd/HellPot/boot.go b/cmd/HellPot/boot.go index 02b760d..0c62f39 100644 --- a/cmd/HellPot/boot.go +++ b/cmd/HellPot/boot.go @@ -2,7 +2,6 @@ package main import ( "errors" - "flag" "fmt" "io" "os" @@ -86,15 +85,17 @@ func readConfig(resolvedConf string) (*config.Parameters, error) { var runningConfig *config.Parameters f, err = os.Open(resolvedConf) // #nosec G304 go home gosec, you're drunk + defer func() { + if f != nil { + _ = f.Close() + } + }() if err == nil { runningConfig, setupErr = config.Setup(f) } switch { case setupErr != nil: println("failed to setup config: " + setupErr.Error()) - if f != nil { - _ = f.Close() - } err = setupErr case err != nil: println("failed to open config file for reading: " + err.Error()) @@ -105,26 +106,22 @@ func readConfig(resolvedConf string) (*config.Parameters, error) { } println("failed to create config file, cannot continue") return nil, fmt.Errorf("failed to create config file: %w", err) - case runningConfig != nil: - _ = f.Close() + case runningConfig == nil: + err = errors.New("unknown failure resulting in missing configuration, cannot continue") + return nil, err } return runningConfig, err } func resolveConfig() (runningConfig *config.Parameters, usingDefaults bool, resolvedConf string, err error) { - setIfPresent := func(confRoot *flag.Flag) (ok bool) { - if confRoot != nil && confRoot.Value.String() != "" { - resolvedConf = confRoot.Value.String() - return true - } - return false - } if config.CLIFlags != nil { - confRoot := config.CLIFlags.Lookup("config") - if !setIfPresent(confRoot) { - confRoot = config.CLIFlags.Lookup("c") - setIfPresent(confRoot) + conf := config.CLIFlags.Lookup("config") + if conf != nil && conf.Value.String() != "" { + resolvedConf = conf.Value.String() + if runningConfig, err = readConfig(resolvedConf); err != nil { + return runningConfig, false, resolvedConf, err + } } } @@ -153,62 +150,64 @@ func resolveConfig() (runningConfig *config.Parameters, usingDefaults bool, reso return runningConfig, false, resolvedConf, nil } -func setup(stopChan chan os.Signal) (log zerolog.Logger, logFile string, - resolvedConf string, realConf *config.Parameters, err error) { - - config.InitCLI() - - var usingDefaults bool - var runningConfig *config.Parameters - - if runningConfig, usingDefaults, resolvedConf, err = resolveConfig(); err != nil { +func initLogger(runningConfig *config.Parameters) (log zerolog.Logger, logFile string, err error) { + if log, err = logger.New(&runningConfig.Logger); err != nil { return } - if runningConfig == nil { - err = errors.New("running configuration is nil, cannot continue") - return - } + logFile = runningConfig.Logger.ActiveLogFileName - // TODO: jesus bro r u ok - realConf = runningConfig - if usingDefaults && !realConf.UsingDefaults { - realConf.UsingDefaults = true - } - if realConf.UsingDefaults && !usingDefaults { - usingDefaults = true - } + return +} - //goland:noinspection GoNilness // we check for nil above - if log, err = logger.New(runningConfig.Logger); err != nil { - return - } +func setup(stopChan chan os.Signal) (log zerolog.Logger, logFile string, + resolvedConf string, runningConfig *config.Parameters, err error) { - logFile = runningConfig.Logger.ActiveLogFileName + config.InitCLI() + + var usingDefaults bool - if usingDefaults { - log.Warn().Msg("using default configuration!") - print(red + "continuing with default configuration in ") - for i := defaultConfigWarningDelaySecs; i > 0; i-- { - print(strconv.Itoa(i)) - for i := 0; i < 5; i++ { - time.Sleep(200 * time.Millisecond) - print(".") + defer func() { + if runningConfig == nil && err == nil { + err = errors.New("running configuration is nil, cannot continue") + return + } + if usingDefaults && os.Getenv("HELLPOT_TEST_MODE") == "" { + log.Warn().Msg("using default configuration!") + print(red + "continuing with default configuration in ") + for i := defaultConfigWarningDelaySecs; i > 0; i-- { + print(strconv.Itoa(i)) + for i := 0; i < 5; i++ { + time.Sleep(200 * time.Millisecond) + print(".") + } } + print(reset + "\n") } - print(reset + "\n") - } + signal.Notify(stopChan, syscall.SIGINT, syscall.SIGTERM) - if //goland:noinspection GoNilness - !runningConfig.Logger.NoColor { - extra.Banner() - } - - signal.Notify(stopChan, syscall.SIGINT, syscall.SIGTERM) + if //goland:noinspection GoNilness + !runningConfig.Logger.NoColor { + extra.Banner() + } + if absResolvedConf, err := filepath.Abs(resolvedConf); err == nil { + resolvedConf = absResolvedConf + } + }() - if absResolvedConf, err := filepath.Abs(resolvedConf); err == nil { - resolvedConf = absResolvedConf + switch { + case config.CLIFlags.Lookup("config") == nil, config.CLIFlags.Lookup("genconfig").Value.String() == "": + if runningConfig, usingDefaults, resolvedConf, err = resolveConfig(); err != nil { + return + } + log, logFile, err = initLogger(runningConfig) + default: + println("loading configuration file: " + config.CLIFlags.Lookup("config").Value.String()) + if runningConfig, err = readConfig(config.CLIFlags.Lookup("config").Value.String()); err != nil { + return + } + resolvedConf = config.CLIFlags.Lookup("config").Value.String() + log, logFile, err = initLogger(runningConfig) } - return } diff --git a/internal/config/client_rules.go b/internal/config/client_rules.go index 8a138a1..7b880c3 100644 --- a/internal/config/client_rules.go +++ b/internal/config/client_rules.go @@ -8,10 +8,10 @@ import ( type ClientRules struct { // See: https://github.com/yunginnanet/HellPot/issues/23 - UseragentDisallowStrings []string `koanf:"user_agent_disallow_strings"` - useragentDisallowStrBytes [][]byte `koanf:"-"` - UseragentDisallowRegex []string `koanf:"user_agent_disallow_regex"` - useragentDisallowRegex []*regexp.Regexp `koanf:"-"` + UseragentDisallowStrings []string `koanf:"user_agent_disallow_strings"` + useragentDisallowStrBytes [][]byte + UseragentDisallowRegex []string `koanf:"user_agent_disallow_regex"` + useragentDisallowRegex []*regexp.Regexp } func NewClientRules(strs []string, regex []string) (*ClientRules, error) { diff --git a/internal/config/models_test.go b/internal/config/client_rules_test.go similarity index 100% rename from internal/config/models_test.go rename to internal/config/client_rules_test.go diff --git a/internal/config/command_line.go b/internal/config/command_line.go index fc8e073..a74361e 100644 --- a/internal/config/command_line.go +++ b/internal/config/command_line.go @@ -5,6 +5,7 @@ import ( "io" "os" "slices" + "strconv" "strings" "github.com/yunginnanet/HellPot/internal/extra" @@ -87,6 +88,30 @@ func InitCLI() { println(err.Error()) os.Exit(2) } + + for defaultKey, defaultVal := range Defaults.val { + switch defaultVal.(type) { + case bool: + parsedBool, pErr := strconv.ParseBool(CLIFlags.Lookup(defaultKey).Value.String()) + if pErr != nil { + continue + } + if parsedBool == Defaults.val[defaultKey].(bool) { + fl := CLIFlags.Lookup(defaultKey) + *fl = flag.Flag{} + fl = nil + } + case string: + if CLIFlags.Lookup(defaultKey).Value.String() == Defaults.val[defaultKey].(string) || + CLIFlags.Lookup(defaultKey).Value.String() == "" { + fl := CLIFlags.Lookup(defaultKey) + *fl = flag.Flag{} + fl = nil + } + } + + } + if os.Getenv("HELLPOT_CONFIG") != "" { if err := CLIFlags.Set("config", os.Getenv("HELLPOT_CONFIG")); err != nil { panic(err) diff --git a/internal/config/defaults.go b/internal/config/defaults.go index 509d886..4e4c6f7 100644 --- a/internal/config/defaults.go +++ b/internal/config/defaults.go @@ -54,6 +54,7 @@ var defOpts = map[string]interface{}{ "noconsole": false, "use_date_filename": false, "docker_logging": false, + "rsyslog_address": "", "console_time_format": time.Kitchen, }, "http": map[string]interface{}{ diff --git a/internal/config/defaults_test.go b/internal/config/defaults_test.go index fa31a6d..535f8e1 100644 --- a/internal/config/defaults_test.go +++ b/internal/config/defaults_test.go @@ -21,6 +21,7 @@ func TestDefaults(t *testing.T) { "http", "performance", "deception", + "bespoke", } { total += bytes.Count(bs, []byte(needle)) + 3 // name plus brackets and newline if !bytes.Contains(bs, []byte(needle)) { diff --git a/internal/config/models.go b/internal/config/models.go index c3d9a32..aa39ea2 100644 --- a/internal/config/models.go +++ b/internal/config/models.go @@ -11,11 +11,11 @@ import ( // Parameters represents the configuration for HellPot. type Parameters struct { - HTTP HTTP `koanf:"http"` - Logger *logger.Configuration `koanf:"logger"` - Bespoke Customization `koanf:"bespoke"` - Perf Performance `koanf:"performance"` - Liar Deception `koanf:"deception"` + HTTP HTTP `koanf:"http"` + Logger logger.Configuration `koanf:"logger"` + Bespoke Customization `koanf:"bespoke"` + Perf Performance `koanf:"performance"` + Liar Deception `koanf:"deception"` IdleHands DevilsPlaythings `koanf:"experimental"` diff --git a/internal/config/setup.go b/internal/config/setup.go index bdbf6a7..3051a44 100644 --- a/internal/config/setup.go +++ b/internal/config/setup.go @@ -10,6 +10,8 @@ import ( flags "github.com/knadh/koanf/providers/basicflag" "github.com/knadh/koanf/providers/env" "github.com/knadh/koanf/v2" + + "github.com/yunginnanet/HellPot/internal/logger" ) type readerProvider struct { @@ -67,17 +69,21 @@ func (p *Parameters) merge(ogk *koanf.Koanf, newk *koanf.Koanf, friendlyName str return nil } - for k, v := range newKeys { - if !ogk.Exists(k) { - if err := ogk.Set(k, v); err != nil { - panic(fmt.Sprintf("failed to set key %s: %v", k, err)) - } - dirty = true - continue + valIsEmpty := func(v any) bool { + switch v.(type) { + case string: + return v.(string) == "" + case []string: + return len(v.([]string)) == 0 + default: + return false } + } + for k, v := range newKeys { ogv := ogk.Get(k) - if ogv == nil { + if ogk.Exists(k) && valIsEmpty(ogv) && !valIsEmpty(v) { + println("setting newer value for key " + k + " to " + fmt.Sprintf("%v", v) + " from " + friendlyName) if err := ogk.Set(k, v); err != nil { panic(fmt.Sprintf("failed to set key %s: %v", k, err)) } @@ -189,5 +195,16 @@ func Setup(source io.Reader) (*Parameters, error) { return nil, fmt.Errorf("failed to unmarshal config: %w", err) } + // for some reason the logger config pointer is not getting populated, do it manually + p.Logger = logger.Configuration{} + for k, v := range k.All() { + if strings.HasPrefix(k, "logger.") { + println("setting logger config key " + k + " to " + fmt.Sprintf("%v", v)) + if err := p.Logger.Set(k, v); err != nil { + return nil, fmt.Errorf("failed to set logger config: %w", err) + } + } + } + return p, nil } diff --git a/internal/config/setup_test.go b/internal/config/setup_test.go index 7f902a2..3f20608 100644 --- a/internal/config/setup_test.go +++ b/internal/config/setup_test.go @@ -6,6 +6,7 @@ import ( ) func TestSetup(t *testing.T) { + t.Setenv("HELLPOT_TEST_MODE", "true") t.Run("Success", SetupSuccess) t.Run("NoFailureOnNilSource", SetupNoFailureOnNilSource) t.Run("FailureOnReadConfig", SetupFailureOnReadConfig) @@ -20,6 +21,10 @@ bind_addr = "5.5.5.5" [http.router] catchall = true makerobots = false + +[logger] +debug = true +rsyslog_address = "local" `) params, err := Setup(source) @@ -49,7 +54,24 @@ makerobots = false if params.HTTP.Router.CatchAll != true { t.Errorf("Expected true, got %v", params.HTTP.Router.CatchAll) } - + if params.source.Get("http.router.makerobots") != false { + t.Errorf("Expected false, got %v", params.source.Get("http.router.makerobots")) + } + if params.HTTP.Router.MakeRobots != false { + t.Errorf("Expected false, got %v", params.HTTP.Router.MakeRobots) + } + if params.source.Get("logger.debug") != true { + t.Errorf("Expected true, got %v", params.source.Get("logger.debug")) + } + if params.Logger.Debug != true { + t.Errorf("Expected true, got %v", params.Logger.Debug) + } + if params.source.Get("logger.rsyslog_address") != "local" { + t.Errorf("Expected local, got %v", params.source.Get("logger.rsyslog_address")) + } + if params.Logger.RSyslog != "local" { + t.Errorf("Expected local, got %v", params.Logger.RSyslog) + } } func SetupNoFailureOnNilSource(t *testing.T) { diff --git a/internal/logger/logger.go b/internal/logger/logger.go index bd278dd..5525e0a 100644 --- a/internal/logger/logger.go +++ b/internal/logger/logger.go @@ -7,6 +7,8 @@ import ( "log/syslog" "os" "path/filepath" + "reflect" + "strconv" "strings" "sync" "time" @@ -23,20 +25,90 @@ type Configuration struct { DockerLogging bool `koanf:"docker_logging"` // ConsoleTimeFormat sets the time format for the console. // The string is passed to time.Format() down the line. - ConsoleTimeFormat string + ConsoleTimeFormat string `koanf:"console_time_format"` // TimeDateFilename sets the log file name to include the date and time. TimeDateFilename bool `koanf:"use_date_filename"` - Directory string `koanf:"directory"` - File string `koanf:"log_file"` - RSyslog string `koanf:"rsyslog_address"` + Directory string `koanf:"directory"` + File string `koanf:"log_file"` + RSyslog string `koanf:"rsyslog_address"` + rsyslogTarget string ActiveLogFileName string `koanf:"active_log_file_name"` Outputs []io.Writer `koanf:"-"` } +func (c *Configuration) Set(k string, v any) error { + k = strings.ToLower(k) + k = strings.TrimPrefix(k, "logger.") + k = strings.ReplaceAll(k, "__", "_") + k = strings.ReplaceAll(k, ".", "_") + + ref := reflect.ValueOf(c) + if ref.Kind() != reflect.Ptr { + panic("not a pointer") + } + ref = ref.Elem() + if ref.Kind() != reflect.Struct { + panic("not a struct") + } + + var field reflect.Value + + for i := 0; i < ref.NumField(); i++ { + strutTag := ref.Type().Field(i).Tag.Get("koanf") + if strings.ToLower(strutTag) == strings.ToLower(k) { + field = ref.Field(i) + break + } + } + + if field == (reflect.Value{}) { + return fmt.Errorf("field %s does not exist", k) + } + + if !field.CanSet() { + return fmt.Errorf("field %s cannot be set", k) + } + + switch field.Kind() { + case reflect.Bool: + if vstr, vstrok := v.(string); vstrok { + if vb, err := strconv.ParseBool(vstr); err == nil { + field.SetBool(vb) + return nil + } + } + if b, ok := v.(bool); ok { + field.SetBool(b) + return nil + } + return fmt.Errorf("field %s is not a bool", k) + case reflect.String: + if s, ok := v.(string); ok { + field.SetString(s) + return nil + } + return fmt.Errorf("field %s is not a string", k) + case reflect.Slice: + if s, ok := v.([]string); ok { + field.Set(reflect.ValueOf(s)) + return nil + } + return fmt.Errorf("field %s is not a slice", k) + case reflect.Int: + if i, ok := v.(int); ok { + field.SetInt(int64(i)) + return nil + } + return fmt.Errorf("field %s is not an int", k) + default: + return fmt.Errorf("field %s is not a supported type (%T)", k, v) + } +} + func (c *Configuration) findFallbackDir() error { locs := []string{"/var/log"} uconf, err := os.UserHomeDir() @@ -143,7 +215,10 @@ func (c *Configuration) setupDirAndFile() error { return nil } -func (c *Configuration) setupSyslog() error { +func (c *Configuration) setupSyslog() (error, bool) { + if c.RSyslog == "" { + return nil, false + } var ( err error proto string @@ -151,6 +226,9 @@ func (c *Configuration) setupSyslog() error { conn *syslog.Writer ) switch { + case strings.ToLower(c.RSyslog) == "local": + proto = "" + addr = "" case strings.Contains(c.RSyslog, "://"): proto = strings.Split(c.RSyslog, "://")[0] addr = strings.Split(c.RSyslog, "://")[1] @@ -161,24 +239,37 @@ func (c *Configuration) setupSyslog() error { proto = "udp" addr = c.RSyslog + ":514" } - if conn, err = syslog.Dial(proto, addr, syslog.LOG_INFO, "HellPot"); err != nil { - return fmt.Errorf("failed to dial syslog server: %w", err) + + if proto != "" && addr != "" { + println("dialing syslog server: " + c.rsyslogTarget + "...") + } + + syslogLogLevel := syslog.LOG_INFO + if c.Debug || c.Trace { + syslogLogLevel = syslog.LOG_DEBUG + } + + if conn, err = syslog.Dial(proto, addr, syslogLogLevel, "HellPot"); err != nil { + return fmt.Errorf("failed to dial syslog server: %w", err), false } + c.rsyslogTarget = proto + "://" + addr + c.Outputs = append(c.Outputs, zerolog.SyslogLevelWriter(conn)) - return nil + return nil, true } -func (c *Configuration) SetupOutputs() error { +func (c *Configuration) SetupOutputs() (err error, rsyslogEnabled bool) { if c.Directory != "" || c.File != "" { - if err := c.setupDirAndFile(); err != nil { - return fmt.Errorf("failed to setup log file: %w", err) + if err = c.setupDirAndFile(); err != nil { + return fmt.Errorf("failed to setup log file: %w", err), false } } + if c.RSyslog != "" { - if err := c.setupSyslog(); err != nil { - return fmt.Errorf("failed to setup syslog: %w", err) + if err, rsyslogEnabled = c.setupSyslog(); err != nil { + return fmt.Errorf("failed to setup syslog: %w", err), false } } @@ -186,7 +277,7 @@ func (c *Configuration) SetupOutputs() error { for _, out := range c.Outputs { if out == nil { - return fmt.Errorf("nil output provided") + return fmt.Errorf("nil output provided"), false } if out == os.Stdout || out == os.Stderr { consoleSeen = true @@ -197,7 +288,7 @@ func (c *Configuration) SetupOutputs() error { c.Outputs = append(c.Outputs, os.Stdout) } - return nil + return nil, rsyslogEnabled } var once = &sync.Once{} @@ -228,7 +319,9 @@ func New(conf *Configuration) (zerolog.Logger, error) { if err := conf.Validate(); err != nil { return zerolog.Logger{}, fmt.Errorf("invalid logger configuration: %w", err) } - if err := conf.SetupOutputs(); err != nil { + var err error + var rsyslogEnabled bool + if err, rsyslogEnabled = conf.SetupOutputs(); err != nil { return zerolog.Logger{}, fmt.Errorf("failed to setup logger outputs: %w", err) } for i, output := range conf.Outputs { @@ -246,5 +339,10 @@ func New(conf *Configuration) (zerolog.Logger, error) { if conf.Trace { _log = _log.Level(zerolog.TraceLevel) } + + if rsyslogEnabled { + _log.Info().Str("target", conf.rsyslogTarget).Msg("remote syslog connection established") + } + return _log, nil } From 38b0e05ca04469bcb126849855cbb2b92a467788 Mon Sep 17 00:00:00 2001 From: "kayos@tcp.direct" Date: Thu, 27 Jun 2024 17:13:32 -0700 Subject: [PATCH 28/29] Fix: Logger and main package init fixes --- cmd/HellPot/HellPot.go | 2 +- cmd/HellPot/HellPot_test.go | 24 ++++++++--- cmd/HellPot/boot.go | 85 ++++++++++++++++++++++++++---------- cmd/HellPot/util.go | 41 ++++++++++-------- internal/config/models.go | 5 +-- internal/config/setup.go | 68 +++++++++++++---------------- internal/http/router.go | 4 +- internal/logger/logger.go | 86 +++++++++++++++++++++++-------------- 8 files changed, 194 insertions(+), 121 deletions(-) diff --git a/cmd/HellPot/HellPot.go b/cmd/HellPot/HellPot.go index 001cb8b..1f23794 100644 --- a/cmd/HellPot/HellPot.go +++ b/cmd/HellPot/HellPot.go @@ -8,7 +8,7 @@ import ( func main() { stopChan := make(chan os.Signal, 1) - log, _, resolvedConf, realConf, err := setup(stopChan) + log, resolvedConf, realConf, err := setup(stopChan) if err != nil { println("failed to start: " + err.Error()) diff --git a/cmd/HellPot/HellPot_test.go b/cmd/HellPot/HellPot_test.go index b415a16..86eb303 100644 --- a/cmd/HellPot/HellPot_test.go +++ b/cmd/HellPot/HellPot_test.go @@ -10,14 +10,15 @@ import ( "github.com/yunginnanet/HellPot/internal/config" "github.com/yunginnanet/HellPot/internal/http" + "github.com/yunginnanet/HellPot/internal/logger" ) -func testMain(t *testing.T) (string, string, chan os.Signal, *config.Parameters, error) { +func testMain(t *testing.T) (string, chan os.Signal, *logger.Log, *config.Parameters, error) { t.Setenv("HELLPOT_TEST_MODE", "true") t.Helper() stopChan := make(chan os.Signal, 1) - log, logFile, resolvedConf, realConfig, err := setup(stopChan) + log, resolvedConf, realConfig, err := setup(stopChan) if err == nil { printInfo(log, resolvedConf, realConfig) go func() { @@ -29,7 +30,7 @@ func testMain(t *testing.T) (string, string, chan os.Signal, *config.Parameters, }() } //goland:noinspection GoNilness - return resolvedConf, logFile, stopChan, realConfig, err + return resolvedConf, stopChan, log, realConfig, err } func TestHellPot(t *testing.T) { @@ -40,13 +41,24 @@ func TestHellPot(t *testing.T) { } confFile := filepath.Join(tDir, "HellPot_test.toml") t.Setenv("HELLPOT_LOGGER_DIRECTORY", logDir) - t.Setenv("HELLPOT_LOGGER_RSYSLOG_ADDRESS", "local") + t.Setenv("HELLPOT_LOGGER_RSYSLOG__ADDRESS", "local") + t.Setenv("HELLPOT_LOGGER_DEBUG", "true") t.Setenv("HELLPOT_CONFIG", confFile) - resolvedConf, logFile, stopChan, realConfig, err := testMain(t) + resolvedConf, stopChan, log, realConfig, err := testMain(t) + if err != nil { t.Fatal(err) } + if log == nil { + t.Fatal("log is nil") + } + if realConfig == nil { + t.Fatal("realConfig is nil") + } + + logFile := log.Config.ActiveLogFileName + if stopChan == nil { t.Fatal("stopChan is nil") } @@ -79,7 +91,7 @@ func TestHellPot(t *testing.T) { if !strings.Contains(string(logDat), resolvedConf) { t.Errorf("expected log to contain '%s'", resolvedConf) } - if !strings.Contains(string(logDat), "PID: "+strconv.Itoa(os.Getpid())) { + if !strings.Contains(string(logDat), strconv.Itoa(os.Getpid())+",") { t.Errorf("expected log to contain 'PID: %d', got %s", os.Getpid(), logDat) } t.Log("resolvedConf: ", resolvedConf) diff --git a/cmd/HellPot/boot.go b/cmd/HellPot/boot.go index 0c62f39..968e0f8 100644 --- a/cmd/HellPot/boot.go +++ b/cmd/HellPot/boot.go @@ -11,8 +11,6 @@ import ( "syscall" "time" - "github.com/rs/zerolog" - "github.com/yunginnanet/HellPot/internal/config" "github.com/yunginnanet/HellPot/internal/extra" "github.com/yunginnanet/HellPot/internal/logger" @@ -114,23 +112,64 @@ func readConfig(resolvedConf string) (*config.Parameters, error) { return runningConfig, err } +type configDetail struct { + runningConfig *config.Parameters + usingDefaults bool + resolvedConf string +} + +func loadCLIConfig() (detail configDetail, err error) { + conf := config.CLIFlags.Lookup("config") + detail = configDetail{} + detail.resolvedConf = conf.Value.String() + detail.runningConfig, err = readConfig(detail.resolvedConf) + detail.usingDefaults = false + return +} + +func loadEnvConfig() (detail configDetail, err error) { + detail = configDetail{} + detail.resolvedConf = os.Getenv("HELLPOT_CONFIG_FILE") + detail.runningConfig, err = readConfig(detail.resolvedConf) + detail.usingDefaults = false + return +} + func resolveConfig() (runningConfig *config.Parameters, usingDefaults bool, resolvedConf string, err error) { - if config.CLIFlags != nil { - conf := config.CLIFlags.Lookup("config") - if conf != nil && conf.Value.String() != "" { - resolvedConf = conf.Value.String() - if runningConfig, err = readConfig(resolvedConf); err != nil { - return runningConfig, false, resolvedConf, err - } - } + var cliConfigPath string + if config.CLIFlags != nil && config.CLIFlags.Lookup("config") != nil { + cliConfigPath = config.CLIFlags.Lookup("config").Value.String() } - if resolvedConf == "" && os.Getenv("HELLPOT_CONFIG_FILE") != "" { - resolvedConf = os.Getenv("HELLPOT_CONFIG_FILE") - } + var detail configDetail - if resolvedConf == "" { - resolvedConf = searchConfig() +try: + switch { + case cliConfigPath != "": + detail, err = loadCLIConfig() + if err == nil { + runningConfig = detail.runningConfig + usingDefaults = detail.usingDefaults + resolvedConf = detail.resolvedConf + return + } + println("failed to load config from CLI path: " + err.Error()) + cliConfigPath = "" + goto try + case os.Getenv("HELLPOT_CONFIG_FILE") != "": + detail, err = loadEnvConfig() + if err == nil { + runningConfig = detail.runningConfig + usingDefaults = detail.usingDefaults + resolvedConf = detail.resolvedConf + return + } + println("failed to load config from env path: " + err.Error()) + fallthrough + default: + if resolvedConf = searchConfig(); resolvedConf != "" { + usingDefaults = false + } } if runningConfig, err = readConfig(resolvedConf); err != nil && !errors.Is(err, io.EOF) { @@ -150,17 +189,15 @@ func resolveConfig() (runningConfig *config.Parameters, usingDefaults bool, reso return runningConfig, false, resolvedConf, nil } -func initLogger(runningConfig *config.Parameters) (log zerolog.Logger, logFile string, err error) { +func initLogger(runningConfig *config.Parameters) (log *logger.Log, err error) { if log, err = logger.New(&runningConfig.Logger); err != nil { return } - logFile = runningConfig.Logger.ActiveLogFileName - return } -func setup(stopChan chan os.Signal) (log zerolog.Logger, logFile string, +func setup(stopChan chan os.Signal) (log *logger.Log, resolvedConf string, runningConfig *config.Parameters, err error) { config.InitCLI() @@ -172,6 +209,10 @@ func setup(stopChan chan os.Signal) (log zerolog.Logger, logFile string, err = errors.New("running configuration is nil, cannot continue") return } + if (runningConfig.GetLogger() == nil || runningConfig.GetLogger().Config == nil) && err == nil { + err = errors.New("running configuration logger is nil, cannot continue") + return + } if usingDefaults && os.Getenv("HELLPOT_TEST_MODE") == "" { log.Warn().Msg("using default configuration!") print(red + "continuing with default configuration in ") @@ -187,7 +228,7 @@ func setup(stopChan chan os.Signal) (log zerolog.Logger, logFile string, signal.Notify(stopChan, syscall.SIGINT, syscall.SIGTERM) if //goland:noinspection GoNilness - !runningConfig.Logger.NoColor { + !runningConfig.GetLogger().Config.NoColor { extra.Banner() } if absResolvedConf, err := filepath.Abs(resolvedConf); err == nil { @@ -200,14 +241,14 @@ func setup(stopChan chan os.Signal) (log zerolog.Logger, logFile string, if runningConfig, usingDefaults, resolvedConf, err = resolveConfig(); err != nil { return } - log, logFile, err = initLogger(runningConfig) + log, err = initLogger(runningConfig) default: println("loading configuration file: " + config.CLIFlags.Lookup("config").Value.String()) if runningConfig, err = readConfig(config.CLIFlags.Lookup("config").Value.String()); err != nil { return } resolvedConf = config.CLIFlags.Lookup("config").Value.String() - log, logFile, err = initLogger(runningConfig) + log, err = initLogger(runningConfig) } return } diff --git a/cmd/HellPot/util.go b/cmd/HellPot/util.go index cc1918e..7c123b6 100644 --- a/cmd/HellPot/util.go +++ b/cmd/HellPot/util.go @@ -2,34 +2,41 @@ package main import ( "os" - "strconv" "github.com/rs/zerolog" "github.com/yunginnanet/HellPot/internal/config" + "github.com/yunginnanet/HellPot/internal/logger" "github.com/yunginnanet/HellPot/internal/version" ) -func printInfo(log zerolog.Logger, resolvedConf string, realConfig *config.Parameters) { - log.Info().Msg("🔥 Starting HellPot 🔥") +func printInfo(log *logger.Log, resolvedConf string, realConfig *config.Parameters) { + log.Info(). + Str("version", version.Version). + Int("pid", os.Getpid()). + Str("config_file", resolvedConf). + Str("log_file", log.Config.ActiveLogFileName). + Msg("🔥 Starting HellPot 🔥") if realConfig.UsingDefaults { log.Warn().Msg("Using default configuration! Please edit the configuration file to suit your needs.") } - log.Info().Msg("Version: " + version.Version) - log.Info().Msg("PID: " + strconv.Itoa(os.Getpid())) - log.Info().Msg("Using config file: " + resolvedConf) - if realConfig.Logger.RSyslog != "" { - log.Info().Msg("Logging to syslog: " + realConfig.Logger.RSyslog) + + var eventer func() *zerolog.Event + + if realConfig.Logger.Debug && !realConfig.Logger.Trace { + eventer = log.Debug } - if realConfig.Logger.ActiveLogFileName != "" { - log.Info().Msg("Logging to file: " + realConfig.Logger.ActiveLogFileName) + if realConfig.Logger.Trace { + eventer = log.Trace } - if realConfig.Logger.DockerLogging && - realConfig.Logger.File == "" && - realConfig.Logger.Directory == "" && - realConfig.Logger.RSyslog == "" { - log.Info().Msg("Only logging to standard output") + + if eventer == nil { + return } - log.Debug().Msg("Debug logging enabled") - log.Trace().Msg("Trace logging enabled") + + eventer().Bool("docker_logging", true). + Bool("debug", true).Bool("trace", false). + Str("rsyslog", realConfig.Logger.RSyslog). + Str("log_file", realConfig.Logger.ActiveLogFileName). + Msg("logging engine started") } diff --git a/internal/config/models.go b/internal/config/models.go index aa39ea2..b73801f 100644 --- a/internal/config/models.go +++ b/internal/config/models.go @@ -4,7 +4,6 @@ import ( "sync" "github.com/knadh/koanf/v2" - "github.com/rs/zerolog" "github.com/yunginnanet/HellPot/internal/logger" ) @@ -20,13 +19,13 @@ type Parameters struct { IdleHands DevilsPlaythings `koanf:"experimental"` source *koanf.Koanf - logger *zerolog.Logger + logger *logger.Log UsingDefaults bool } var once = &sync.Once{} -func (p *Parameters) GetLogger() *zerolog.Logger { +func (p *Parameters) GetLogger() *logger.Log { once.Do(func() { p.logger = logger.GetLoggerOnce() }) diff --git a/internal/config/setup.go b/internal/config/setup.go index 3051a44..978ab27 100644 --- a/internal/config/setup.go +++ b/internal/config/setup.go @@ -30,30 +30,6 @@ func (r *readerProvider) Read() (map[string]interface{}, error) { return toml.Parser().Unmarshal(b) //nolint:wrapcheck } -func normalizeMap(m map[string]interface{}) map[string]interface{} { - for k, v := range m { - ogk := k - k = strings.ToLower(k) - - var sslice []string - var sliceOK bool - - if sslice, sliceOK = v.([]string); !sliceOK { - goto justLower - } - for i, s := range sslice { - sslice[i] = strings.ToLower(s) - } - slices.Sort(sslice) - m[k] = sslice - justLower: - if k != ogk { - delete(m, ogk) - } - } - return m -} - func (p *Parameters) merge(ogk *koanf.Koanf, newk *koanf.Koanf, friendlyName string) error { if ogk == nil { panic("original koanf is nil") @@ -63,13 +39,18 @@ func (p *Parameters) merge(ogk *koanf.Koanf, newk *koanf.Koanf, friendlyName str } dirty := false - newKeys := normalizeMap(newk.All()) + newKeys := newk.All() + ogKeys := ogk.All() if len(newk.All()) == 0 || len(newKeys) == 0 { + // println("no configuration overrides found in " + friendlyName) return nil } valIsEmpty := func(v any) bool { + if v == nil { + return true + } switch v.(type) { case string: return v.(string) == "" @@ -80,11 +61,22 @@ func (p *Parameters) merge(ogk *koanf.Koanf, newk *koanf.Koanf, friendlyName str } } - for k, v := range newKeys { - ogv := ogk.Get(k) - if ogk.Exists(k) && valIsEmpty(ogv) && !valIsEmpty(v) { - println("setting newer value for key " + k + " to " + fmt.Sprintf("%v", v) + " from " + friendlyName) - if err := ogk.Set(k, v); err != nil { + for k, v := range ogKeys { + // println("checking key " + k + " for overrides from " + friendlyName) + if strings.HasPrefix(k, "logger") && !valIsEmpty(newKeys[k]) { + lg := p.Logger + lgptr := &lg + // println("setting logger config key " + k + " to " + fmt.Sprintf("%v", newKeys[k]) + " from " + friendlyName) + if err := lgptr.Set(k, newKeys[k]); err != nil { + panic(fmt.Sprintf("failed to set logger config: %v", err)) + } + p.Logger = *lgptr + dirty = true + continue + } + if valIsEmpty(v) && !valIsEmpty(newKeys[k]) { + // println("setting newer value for key " + k + " to " + fmt.Sprintf("%v", newKeys[k]) + " from " + friendlyName) + if err := ogk.Set(k, newKeys[k]); err != nil { panic(fmt.Sprintf("failed to set key %s: %v", k, err)) } dirty = true @@ -95,19 +87,20 @@ func (p *Parameters) merge(ogk *koanf.Koanf, newk *koanf.Koanf, friendlyName str continue } - if ogv == Defaults.val[k] && v != ogv { + if v == Defaults.val[k] && newKeys[k] != Defaults.val[k] { if err := ogk.Set(k, v); err != nil { panic(fmt.Sprintf("failed to set key %s: %v", k, err)) } dirty = true } + } if !dirty { return nil } - println("found configuration overrides in " + friendlyName) + // println("found configuration overrides in " + friendlyName) if err := ogk.Merge(newk); err != nil { return fmt.Errorf("failed to merge env config: %w", err) @@ -151,7 +144,7 @@ func parseCLISlice(key string, value string) (string, interface{}) { func (p *Parameters) LoadFlags(k *koanf.Koanf) error { flagsK := koanf.New(".") - if err := flagsK.Load(flags.ProviderWithValue(CLIFlags, ".", parseCLISlice), nil); err != nil { + if err := flagsK.Load(flags.ProviderWithValue(CLIFlags, "-", parseCLISlice, k), nil); err != nil { return fmt.Errorf("failed to load flags: %w", err) } @@ -177,17 +170,18 @@ func Setup(source io.Reader) (*Parameters, error) { p := &Parameters{ source: k, + Logger: logger.Configuration{}, } if source == nil { p.UsingDefaults = true } - if err := p.LoadFlags(k); err != nil { + if err := p.LoadEnv(k); err != nil { return nil, err } - if err := p.LoadEnv(k); err != nil { + if err := p.LoadFlags(k); err != nil { return nil, err } @@ -195,11 +189,9 @@ func Setup(source io.Reader) (*Parameters, error) { return nil, fmt.Errorf("failed to unmarshal config: %w", err) } - // for some reason the logger config pointer is not getting populated, do it manually - p.Logger = logger.Configuration{} for k, v := range k.All() { if strings.HasPrefix(k, "logger.") { - println("setting logger config key " + k + " to " + fmt.Sprintf("%v", v)) + // println("setting logger config key " + k + " to " + fmt.Sprintf("%v", v)) if err := p.Logger.Set(k, v); err != nil { return nil, fmt.Errorf("failed to set logger config: %w", err) } diff --git a/internal/http/router.go b/internal/http/router.go index 453ed00..3636649 100644 --- a/internal/http/router.go +++ b/internal/http/router.go @@ -12,15 +12,15 @@ import ( "time" "github.com/fasthttp/router" - "github.com/rs/zerolog" "github.com/valyala/fasthttp" "github.com/yunginnanet/HellPot/heffalump" "github.com/yunginnanet/HellPot/internal/config" + "github.com/yunginnanet/HellPot/internal/logger" ) var ( - log *zerolog.Logger + log *logger.Log hellpotHeffalump *heffalump.Heffalump runningConfig *config.Parameters ) diff --git a/internal/logger/logger.go b/internal/logger/logger.go index 5525e0a..a15e51c 100644 --- a/internal/logger/logger.go +++ b/internal/logger/logger.go @@ -16,6 +16,11 @@ import ( "github.com/rs/zerolog" ) +type Log struct { + zerolog.Logger + Config *Configuration +} + // Configuration represents the configuration for the logger. type Configuration struct { Debug bool `koanf:"debug"` @@ -50,9 +55,15 @@ func (c *Configuration) Set(k string, v any) error { if ref.Kind() != reflect.Ptr { panic("not a pointer") } + ref = ref.Elem() + + if !ref.IsValid() { + panic("invalid pointer") + } + if ref.Kind() != reflect.Struct { - panic("not a struct") + panic("not a struct: " + ref.Kind().String()) } var field reflect.Value @@ -162,20 +173,20 @@ func (c *Configuration) Validate() error { return nil } -func (c *Configuration) setupDirAndFile() error { +func (c *Configuration) setupDirAndFile() (string, error) { switch { case c.Directory != "": stat, err := os.Stat(c.Directory) if err != nil && !errors.Is(err, os.ErrNotExist) { - return fmt.Errorf("failed to access specified log directory: %w", err) + return "", fmt.Errorf("failed to access specified log directory: %w", err) } if errors.Is(err, os.ErrNotExist) { if err = os.MkdirAll(c.Directory, 0750); err != nil { - return fmt.Errorf("failed to create specified log directory: %w", err) + return "", fmt.Errorf("failed to create specified log directory: %w", err) } } if stat != nil && !stat.IsDir() { - return fmt.Errorf("specified log directory is not a directory") + return "", fmt.Errorf("specified log directory is not a directory") } if c.File == "" { c.File = "HellPot.log" @@ -183,18 +194,18 @@ func (c *Configuration) setupDirAndFile() error { case c.Directory == "" && c.File != "": stat, err := os.Stat(filepath.Dir(c.File)) if err != nil && !errors.Is(err, os.ErrNotExist) { - return fmt.Errorf("failed to access specified log directory: %w", err) + return "", fmt.Errorf("failed to access specified log directory: %w", err) } if errors.Is(err, os.ErrNotExist) { if err = os.MkdirAll(filepath.Dir(c.File), 0750); err != nil { - return fmt.Errorf("failed to create specified log directory: %w", err) + return "", fmt.Errorf("failed to create specified log directory: %w", err) } } if stat != nil && !stat.IsDir() { panic("specified log directory is not a directory, but it should be...? please report this issue on github") } case c.Directory == "" && c.File == "" && c.ActiveLogFileName == "" && c.RSyslog == "" && !c.DockerLogging: - return fmt.Errorf("no log directory or file specified") + return "", fmt.Errorf("no log directory or file specified") } var f *os.File var err error @@ -208,11 +219,11 @@ func (c *Configuration) setupDirAndFile() error { ) } if f, err = os.OpenFile(filepath.Join(c.Directory, c.File), os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0600); err != nil { - return fmt.Errorf("failed to open log file: %w", err) + return "", fmt.Errorf("failed to open log file: %w", err) } c.Outputs = append(c.Outputs, f) c.ActiveLogFileName = f.Name() - return nil + return f.Name(), nil } func (c *Configuration) setupSyslog() (error, bool) { @@ -253,23 +264,28 @@ func (c *Configuration) setupSyslog() (error, bool) { return fmt.Errorf("failed to dial syslog server: %w", err), false } - c.rsyslogTarget = proto + "://" + addr + c.rsyslogTarget = "syslog" + if proto != "" && addr != "" { + c.rsyslogTarget = proto + "://" + addr + } c.Outputs = append(c.Outputs, zerolog.SyslogLevelWriter(conn)) return nil, true } -func (c *Configuration) SetupOutputs() (err error, rsyslogEnabled bool) { +func (c *Configuration) SetupOutputs() (rsyslogEnabled bool, err error) { + var logPath string if c.Directory != "" || c.File != "" { - if err = c.setupDirAndFile(); err != nil { - return fmt.Errorf("failed to setup log file: %w", err), false + if logPath, err = c.setupDirAndFile(); err != nil { + return false, fmt.Errorf("failed to setup log file: %w", err) } + c.ActiveLogFileName = logPath } if c.RSyslog != "" { if err, rsyslogEnabled = c.setupSyslog(); err != nil { - return fmt.Errorf("failed to setup syslog: %w", err), false + return false, fmt.Errorf("failed to setup syslog: %w", err) } } @@ -277,10 +293,11 @@ func (c *Configuration) SetupOutputs() (err error, rsyslogEnabled bool) { for _, out := range c.Outputs { if out == nil { - return fmt.Errorf("nil output provided"), false + return false, fmt.Errorf("nil output provided") } if out == os.Stdout || out == os.Stderr { consoleSeen = true + break } } @@ -288,15 +305,18 @@ func (c *Configuration) SetupOutputs() (err error, rsyslogEnabled bool) { c.Outputs = append(c.Outputs, os.Stdout) } - return nil, rsyslogEnabled + return rsyslogEnabled, nil } var once = &sync.Once{} -func GetLoggerOnce() *zerolog.Logger { - var ret *zerolog.Logger +func GetLoggerOnce() *Log { + var ret *Log once.Do(func() { - ret = &_log + if _log == nil { + panic("early access to logger") + } + ret = _log }) if ret == nil { panic("i said once you fool") @@ -313,35 +333,37 @@ var ( ) ) -var _log zerolog.Logger +var _log *Log -func New(conf *Configuration) (zerolog.Logger, error) { +func New(conf *Configuration) (*Log, error) { if err := conf.Validate(); err != nil { - return zerolog.Logger{}, fmt.Errorf("invalid logger configuration: %w", err) + return nil, fmt.Errorf("invalid logger configuration: %w", err) } var err error var rsyslogEnabled bool - if err, rsyslogEnabled = conf.SetupOutputs(); err != nil { - return zerolog.Logger{}, fmt.Errorf("failed to setup logger outputs: %w", err) + if rsyslogEnabled, err = conf.SetupOutputs(); err != nil { + return nil, fmt.Errorf("failed to setup logger outputs: %w", err) } for i, output := range conf.Outputs { if output == os.Stdout || output == os.Stderr { cw := zerolog.ConsoleWriter{Out: output, TimeFormat: conf.ConsoleTimeFormat, NoColor: conf.NoColor} - conf.Outputs = append(conf.Outputs[:i], conf.Outputs[i+1:]...) - conf.Outputs = append(conf.Outputs, cw) + conf.Outputs[i] = cw } } - _log = zerolog.New(zerolog.MultiLevelWriter(conf.Outputs...)).With().Timestamp().Logger() - _log = _log.Level(zerolog.InfoLevel) + _log = &Log{Logger: zerolog.New(zerolog.MultiLevelWriter(conf.Outputs...)).With().Timestamp().Logger(), Config: conf} + _log.Logger = _log.Level(zerolog.InfoLevel) + zerolog.SetGlobalLevel(zerolog.InfoLevel) if conf.Debug { - _log = _log.Level(zerolog.DebugLevel) + _log.Logger = _log.Level(zerolog.DebugLevel) + zerolog.SetGlobalLevel(zerolog.DebugLevel) } if conf.Trace { - _log = _log.Level(zerolog.TraceLevel) + _log.Logger = _log.Level(zerolog.TraceLevel) + zerolog.SetGlobalLevel(zerolog.TraceLevel) } if rsyslogEnabled { - _log.Info().Str("target", conf.rsyslogTarget).Msg("remote syslog connection established") + _log.Info().Str("target", conf.rsyslogTarget).Msg("syslog connection established") } return _log, nil From 42c3b2a2bdc4f2a69b10899950be7905f681c22f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Jul 2024 22:56:52 +0000 Subject: [PATCH 29/29] Bump github.com/knadh/koanf/providers/file from 0.1.0 to 1.0.0 Bumps [github.com/knadh/koanf/providers/file](https://github.com/knadh/koanf) from 0.1.0 to 1.0.0. - [Release notes](https://github.com/knadh/koanf/releases) - [Commits](https://github.com/knadh/koanf/compare/v0.1.0...v1.0.0) --- updated-dependencies: - dependency-name: github.com/knadh/koanf/providers/file dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot]