From 0a18454ea058801876db46daf7db5a053d4dbb3c Mon Sep 17 00:00:00 2001 From: "kayos@tcp.direct" Date: Thu, 20 Jun 2024 21:42:24 -0700 Subject: [PATCH] TeeHee: Essentially rewrite HellPot :^) --- README.md | 2 +- cmd/HellPot/HellPot.go | 136 ++++++++++++---- 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 | 121 ++++++-------- 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, 825 insertions(+), 716 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 } - - switch config.DockerLogging { - case true: - config.CurrentLogFile = "/dev/stdout" - config.NoColor = true - log = config.StartLogger(false, os.Stdout) - default: - log = config.StartLogger(true) - } - - extra.Banner() - - 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") - + println("wrote default config to " + target) + runningConfig, _ = config.Setup(f) + _ = f.Close() + return true } func main() { + conf := config.CLIFlags.Lookup("config").Value + if conf.String() == "" { + conf = config.CLIFlags.Lookup("c").Value + } + + usingDefaults := true + resolvedConf := conf.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 + } + } + } + + 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") + 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) - } - 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) + Defaults.IO = &PresetIO{p: Defaults} } -var ( - configSections = []string{"logger", "http", "performance", "deception", "ssh"} - defNoColor = false -) +type Preset struct { + val map[string]interface{} + IO *PresetIO +} -var defOpts = map[string]map[string]interface{}{ - "logger": { - "debug": true, - "trace": false, - "nocolor": defNoColor, - "use_date_filename": true, - "docker_logging": false, +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 shim.buf == nil { + shim.buf = bytes.NewBuffer(data) + } + return shim.buf.Read(p) //nolint:wrapcheck +} + +func (pre *Preset) Read() (map[string]interface{}, error) { + return pre.val, nil +} + +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] + } +}