diff --git a/README.md b/README.md index 443dfba..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). +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..001cb8b 100644 --- a/cmd/HellPot/HellPot.go +++ b/cmd/HellPot/HellPot.go @@ -2,58 +2,26 @@ package main import ( "os" - "os/signal" - "syscall" - "github.com/rs/zerolog" - - "github.com/yunginnanet/HellPot/internal/config" - "github.com/yunginnanet/HellPot/internal/extra" "github.com/yunginnanet/HellPot/internal/http" ) -var ( - log zerolog.Logger - version string // set by linker -) - -func init() { - if version != "" { - config.Version = version[1:] - } - config.Init() - if config.BannerOnly { - extra.Banner() - os.Exit(0) - } +func main() { + stopChan := make(chan os.Signal, 1) + log, _, resolvedConf, realConf, err := setup(stopChan) - switch config.DockerLogging { - case true: - config.CurrentLogFile = "/dev/stdout" - config.NoColor = true - log = config.StartLogger(false, os.Stdout) - default: - log = config.StartLogger(true) + if err != nil { + println("failed to start: " + err.Error()) + os.Exit(1) } - 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") - -} - -func main() { - stopChan := make(chan os.Signal, 1) - signal.Notify(stopChan, syscall.SIGINT, syscall.SIGTERM) + printInfo(log, resolvedConf, realConf) go func() { - log.Fatal().Err(http.Serve()).Msg("HTTP error") + log.Fatal().Err(http.Serve(realConf)).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..0b43fea --- /dev/null +++ b/cmd/HellPot/HellPot_test.go @@ -0,0 +1,88 @@ +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, *config.Parameters, error) { + t.Helper() + stopChan := make(chan os.Signal, 1) + + log, logFile, resolvedConf, realConfig, err := setup(stopChan) + if err == nil { + printInfo(log, resolvedConf, realConfig) + go func() { + terr := http.Serve(realConfig) + if terr != nil { + t.Error("failed to serve HTTP: " + terr.Error()) + close(stopChan) + } + }() + } + //goland:noinspection GoNilness + return resolvedConf, logFile, stopChan, realConfig, err +} + +func TestHellPot(t *testing.T) { + 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(tDir, "HellPot_test.toml") + t.Setenv("HELLPOT_LOGGER_DIRECTORY", logDir) + t.Setenv("HELLPOT_CONFIG", confFile) + + resolvedConf, logFile, stopChan, realConfig, err := testMain(t) + if err != nil { + t.Fatal(err) + } + 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) + } + 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 new file mode 100644 index 0000000..02b760d --- /dev/null +++ b/cmd/HellPot/boot.go @@ -0,0 +1,214 @@ +package main + +import ( + "errors" + "flag" + "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" +) + +const ( + defaultConfigWarningDelaySecs = 10 + red = "\033[31m" + reset = "\033[0m" +) + +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 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 nil, false + } + if err = f.Sync(); err != nil { + panic(err) + } + println("wrote default config to " + target) + var newConf *config.Parameters + 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 +} + +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) (*config.Parameters, error) { + var err error + var setupErr error + var f *os.File + + if resolvedConf == "" { + 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) + } + 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()) + println("trying to create it....") + newRunningConfig, wroteOK := writeConfig(resolvedConf) + if wroteOK { + return newRunningConfig, nil + } + println("failed to create config file, cannot continue") + return nil, fmt.Errorf("failed to create config file: %w", err) + case runningConfig != nil: + _ = f.Close() + } + + 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) + } + } + + if resolvedConf == "" && os.Getenv("HELLPOT_CONFIG_FILE") != "" { + resolvedConf = os.Getenv("HELLPOT_CONFIG_FILE") + } + + if resolvedConf == "" { + resolvedConf = searchConfig() + } + + if runningConfig, err = readConfig(resolvedConf); err != nil && !errors.Is(err, io.EOF) { + return runningConfig, 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 runningConfig, false, "", err + } + return runningConfig, true, "", nil + } + + 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 { + return + } + + if runningConfig == nil { + err = errors.New("running configuration is nil, cannot continue") + 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 + } + + logFile = runningConfig.Logger.ActiveLogFileName + + 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(".") + } + } + print(reset + "\n") + } + + if //goland:noinspection GoNilness + !runningConfig.Logger.NoColor { + extra.Banner() + } + + signal.Notify(stopChan, syscall.SIGINT, syscall.SIGTERM) + + if absResolvedConf, err := filepath.Abs(resolvedConf); err == nil { + resolvedConf = absResolvedConf + } + + return +} 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") +} diff --git a/go.mod b/go.mod index 24d7bf2..97fdf8b 100644 --- a/go.mod +++ b/go.mod @@ -7,17 +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/file 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/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 +25,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..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/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= -github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= 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/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/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 +37,15 @@ 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 4e7ce3b..c91e4e0 100644 --- a/heffalump/heffalump.go +++ b/heffalump/heffalump.go @@ -6,16 +6,12 @@ package heffalump import ( "bufio" + "fmt" "io" "sync" - - "github.com/yunginnanet/HellPot/internal/config" ) -var log = config.GetLogger() - -// DefaultHeffalump represents a Heffalump type -var DefaultHeffalump *Heffalump +const DefaultBuffSize = 100 * 1 << 10 // Heffalump represents our buffer pool and markov map from Heffalump type Heffalump struct { @@ -36,21 +32,22 @@ func NewHeffalump(mm MarkovMap, buffsize int) *Heffalump { } } +// NewDefaultHeffalump instantiates a new default Heffalump from a MarkovMap created 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 var err error - defer func() { - if r := recover(); r != nil { - log.Error().Interface("caller", r).Msg("panic recovered!") - } - }() - - 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) return n, err } if n, err = io.CopyBuffer(bw, h.mm, buf); err != nil { diff --git a/heffalump/markov.go b/heffalump/markov.go index ca522ab..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) @@ -22,8 +20,8 @@ func init() { if len(src) < 1 { panic("failed to unpack source") } - DefaultMarkovMap = MakeMarkovMap(strings.NewReader(src)) - DefaultHeffalump = NewHeffalump(DefaultMarkovMap, 100*1<<10) + + return MakeMarkovMap(strings.NewReader(src)) } // ScanHTML is a basic split function for a Scanner that returns each diff --git a/internal/config/arguments.go b/internal/config/arguments.go deleted file mode 100644 index 89fe1fd..0000000 --- a/internal/config/arguments.go +++ /dev/null @@ -1,37 +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": - 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]) - 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..c794ef6 --- /dev/null +++ b/internal/config/command_line.go @@ -0,0 +1,69 @@ +package config + +import ( + "flag" + "io" + "os" + "strings" + + "github.com/yunginnanet/HellPot/internal/extra" + "github.com/yunginnanet/HellPot/internal/version" +) + +var CLIFlags = flag.NewFlagSet("config", flag.ExitOnError) + +func InitCLI() { + newArgs := make([]string, 0) + for _, arg := range os.Args { + // 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") + 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(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) + } + 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 925bde3..0000000 --- a/internal/config/config.go +++ /dev/null @@ -1,236 +0,0 @@ -package config - -import ( - "errors" - "fmt" - "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" -) - -// generic vars -var ( - noColorForce = false - customconfig = false - home 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 -) - -func writeConfig() string { - prefConfigLocation, _ := os.UserConfigDir() - if prefConfigLocation != "" { - prefConfigLocation = filepath.Join(prefConfigLocation, Title) - } - - if prefConfigLocation == "" { - home, _ = os.UserHomeDir() - prefConfigLocation = filepath.Join(home, ".config", Title) - } - - if _, err := os.Stat(prefConfigLocation); os.IsNotExist(err) { - if err = os.MkdirAll(prefConfigLocation, 0o750); err != nil && !errors.Is(err, os.ErrExist) { - 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()) - 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() { - argParse() - - if customconfig { - associateExportedVariables() - return - } - - 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") - } - } - - if chosen == "" && uconf == "" && home != "" { - uconf = filepath.Join(home, ".config") - } - - if chosen == "" && uconf != "" { - _ = os.MkdirAll(filepath.Join(uconf, Title), 0750) - chosen = filepath.Join(uconf, Title, "config.toml") - } - - 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") - } - } - } - - loadErr := snek.Load(file.Provider(chosen), toml.Parser()) - - if chosen == "" || loadErr != nil { - println("No configuration file found, writing new configuration file...") - chosen = writeConfig() - } - Filename = chosen - - if loadErr = snek.Load(file.Provider(chosen), toml.Parser()); loadErr != nil { - fmt.Println("failed to load default config file: ", loadErr.Error()) - os.Exit(1) - } - - /* snek.SetEnvKeyReplacer(strings.NewReplacer(".", "_", "-", "_")) - snek.SetEnvPrefix(Title) - snek.AutomaticEnv() - */ - associateExportedVariables() -} - -func loadCustomConfig(path string) { - Filename, _ = filepath.Abs(path) - - if err := snek.Load(file.Provider(Filename), toml.Parser()); err != nil { - fmt.Println("failed to load specified config file: ", err.Error()) - os.Exit(1) - } - - 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, - "logger.console_time_format": &ConsoleTimeFormat, - "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.String(key) - } - for key, opt := range strSliceOpt { - *opt = snek.Strings(key) - } - for key, opt := range boolOpt { - *opt = snek.Bool(key) - } - for key, opt := range intOpt { - *opt = snek.Int(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 { - NoColor = true - } - - if UseUnixSocket { - UnixSocketPath = snek.String("http.unix_socket_path") - parsedPermissions, err := strconv.ParseUint(snek.String("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 0d0241a..06d8e05 100644 --- a/internal/config/defaults.go +++ b/internal/config/defaults.go @@ -1,35 +1,67 @@ package config import ( - "io" - "os" + "bytes" "runtime" "time" "github.com/knadh/koanf/parsers/toml" - "github.com/spf13/afero" ) -var ( - configSections = []string{"logger", "http", "performance", "deception", "ssh"} - defNoColor = false -) +var Defaults = &Preset{val: defOpts} + +func init() { + 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 != nil && 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 +} -var defOpts = map[string]map[string]interface{}{ - "logger": { +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": defNoColor, - "use_date_filename": true, + "nocolor": runtime.GOOS == "windows", + "noconsole": false, + "use_date_filename": false, "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{}{ @@ -44,67 +76,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) { - var ( - dat []byte - err error - f afero.File - ) - if dat, err = snek.Marshal(toml.Parser()); err != nil { - println(err.Error()) - os.Exit(1) - } - if f, err = memfs.Create("config.toml"); 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 - } - println(err.Error()) - os.Exit(1) - } - println("Default config written to config.toml") - os.Exit(0) -} - -func setDefaults() { - memfs := afero.NewMemMapFs() - //goland:noinspection GoBoolExpressions - if runtime.GOOS == "windows" { - 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 - } - } - - if GenConfig { - 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 8c50059..0000000 --- a/internal/config/help.go +++ /dev/null @@ -1,123 +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: "--config", 1: "