From ff15c7a65f61b41696efb2d7c974b31b3c17b44a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9F=90=99PiperYxzzy?= Date: Mon, 13 Oct 2025 20:53:49 +0200 Subject: [PATCH] Updating rate limits to also use TOML --- config/config.go | 30 ++++++++++--------- config/dev/conf.toml | 25 ++++++++-------- config/dev/ratelimit.auth.json | 17 ----------- config/dev/ratelimit.auth.toml | 29 +++++++++++++++++++ config/dev/ratelimit.unauth.json | 19 ------------- config/dev/ratelimit.unauth.toml | 35 +++++++++++++++++++++++ controllers/ratelimit.go | 49 +++++++++++++++++++++++--------- database/database.go | 10 +++---- main.go | 3 ++ 9 files changed, 138 insertions(+), 79 deletions(-) delete mode 100644 config/dev/ratelimit.auth.json create mode 100644 config/dev/ratelimit.auth.toml delete mode 100644 config/dev/ratelimit.unauth.json create mode 100644 config/dev/ratelimit.unauth.toml diff --git a/config/config.go b/config/config.go index 2e3fbef..59747aa 100644 --- a/config/config.go +++ b/config/config.go @@ -8,22 +8,26 @@ import ( "github.com/BurntSushi/toml" ) +type DbConfig struct { + Dialect string `toml:"dialect"` + Username string `toml:"username"` + PasswordSecret string `toml:"password-secret"` + Url string `toml:"url"` + Port string `toml:"port"` + Name string `toml:"name"` +} + type StackConfiguration struct { ConfigLoaded bool - AllowFreshAdminGeneration bool - AdminEmails []string - AdminHmacEnv string - UserHmacEnv string - AuthedRateLimitConfig string - UnauthedRateLimitConfig string + AllowFreshAdminGeneration bool `toml:"gen-fresh-admin"` + AdminEmails []string `toml:"admin-emails"` + AdminHmacEnv string `toml:"admin-hmac-env"` + UserHmacEnv string `toml:"user-hmac-env"` + AuthedRateLimitConfig string `toml:"auth-rate-limit-defs"` + UnauthedRateLimitConfig string `toml:"unauth-rate-limit-defs"` - DbDialect string - DbUsername string - DbPasswordSecret string - DbUrl string - DbPort string - DbName string + Db DbConfig `toml:"db"` } var Environment = os.Getenv("STACK_ENVIRONMENT") @@ -60,5 +64,5 @@ func LoadConfig() { configInternal.ConfigLoaded = true - log.Printf("Loaded Config for stack '%s': %+v", Environment, configInternal) + log.Printf("Loaded Config for stack '%s':\n%+v\n", Environment, configInternal) } diff --git a/config/dev/conf.toml b/config/dev/conf.toml index 73d86d4..3d39bec 100644 --- a/config/dev/conf.toml +++ b/config/dev/conf.toml @@ -1,13 +1,14 @@ -AllowFreshAdminGeneration = true -AdminEmails = ["admin@admin.invalid"] -AdminHmacEnv = "ADMIN_HMAC_ENV" -UserHmacEnv = "USER_HMAC_ENV" -AuthedRateLimitConfig = "ratelimit.auth.json" -UnauthedRateLimitConfig = "ratelimit.unauth.json" +gen-fresh-admin = true +admin-emails = ["admin@admin.invalid"] +admin-hmac-env = "ADMIN_HMAC_ENV" +user-hmac-env = "USER_HMAC_ENV" +auth-rate-limit-defs = "ratelimit.auth.toml" +unauth-rate-limit-defs = "ratelimit.unauth.toml" -DbDialect = "sqlite" -DbUrl = "prepack.db" -DbUsername = "" -DbPasswordSecret = "" -DbPort = "" -DbName = "" \ No newline at end of file +[db] +dialect = "sqlite" +url = "prepack.db" +username = "" +password-secret = "" +port = "" +name = "" \ No newline at end of file diff --git a/config/dev/ratelimit.auth.json b/config/dev/ratelimit.auth.json deleted file mode 100644 index 9b71b06..0000000 --- a/config/dev/ratelimit.auth.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "": {"seconds": 60, "max": 30, "_comment": "Global ratelimit."}, - - "GET:/v1/sec/doot": - {"seconds": 5, "max": 3, "_comment": "One DPS (Doot Per Second) for monitoring?"}, - - "GET:/v1/sec/2fa-doot": - {"seconds": 10, "max": 1, "_comment": "2FA doot probably doesn't need much usage at all, mainly exists as a proof of concept."}, - - "GET:/v1/adm/doot": - {"seconds": 5, "max": 3, "_comment": "One DPS (Doot Per Second) for monitoring?"}, - - "GET:/v1/adm/2fa-doot": - {"seconds": 10, "max": 1, "_comment": "2FA doot probably doesn't need much usage at all, mainly exists as a proof of concept."} - - -} \ No newline at end of file diff --git a/config/dev/ratelimit.auth.toml b/config/dev/ratelimit.auth.toml new file mode 100644 index 0000000..400038b --- /dev/null +++ b/config/dev/ratelimit.auth.toml @@ -0,0 +1,29 @@ +[[Rules]] +# Global Ratelimit +match = "" +seconds = 60 +max = 30 + +[[Rules]] +# One DPS (Doot Per Second) for monitoring +match = "GET:/v1/sec/doot" +seconds = 5 +max = 30 + +[[Rules]] +# 2FA doot probably doesn't need much usage at all, mainly exists as a proof of concept. +match = "GET:/v1/sec/2fa-doot" +seconds = 10 +max = 1 + +[[Rules]] +# One Admin DPS (Doot Per Second) for monitoring +match = "GET:/v1/adm/doot" +seconds = 5 +max = 3 + +[[Rules]] +# 2FA doot probably doesn't need much usage at all, mainly exists as a proof of concept. +match = "GET:/v1/adm/2fa-doot" +seconds = 10 +max = 1 diff --git a/config/dev/ratelimit.unauth.json b/config/dev/ratelimit.unauth.json deleted file mode 100644 index 299e44b..0000000 --- a/config/dev/ratelimit.unauth.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "": - {"seconds": 60, "max": 30, "_comment": "Global unauthenticated ratelimit."}, - - "GET:/v1/doot": - {"seconds": 5, "max": 5, "_comment": "Unauthenticated DOOT for server monitoring."}, - - "POST:/v1/login": - {"seconds": 60, "max": 3, "_comment": "Prevent bruteforce attacks on Login."}, - - "POST:/v1/admin": - {"seconds": 60, "max": 1, "_comment": "Prevent bruteforce attacks on Admin Login."}, - - "POST:/v1/signup": - {"seconds": 1800, "max": 1, "_comment": "Prevent spam account creation."}, - - "POST:/v1/forgot": - {"seconds": 60, "max": 1, "_comment": "Slow down 'forgot password' enumeration/spam."} -} \ No newline at end of file diff --git a/config/dev/ratelimit.unauth.toml b/config/dev/ratelimit.unauth.toml new file mode 100644 index 0000000..4afd734 --- /dev/null +++ b/config/dev/ratelimit.unauth.toml @@ -0,0 +1,35 @@ +[[Rules]] +# Global unauthenticated ratelimit. +match = "" +seconds = 60 +max = 30 + +[[Rules]] +# Unauthenticated DOOT for server monitoring. +match = "GET:/v1/doot" +seconds = 5 +max = 5 + +[[Rules]] +# Prevent bruteforce attacks on Login. +match = "POST:/v1/login" +seconds = 60 +max = 3 + +[[Rules]] +# Prevent bruteforce attacks on Admin Login. +match = "POST:/v1/admin" +seconds = 60 +max = 1 + +[[Rules]] +# Prevent spam account creation. +match = "GET:/v1/adm/2fa-doot" +seconds = 1800 +max = 1 + +[[Rules]] +# Slow down 'forgot password' enumeration/spam. +match = "POST:/v1/forgot" +seconds = 60 +max = 1 diff --git a/controllers/ratelimit.go b/controllers/ratelimit.go index 4a05e38..7198dc9 100644 --- a/controllers/ratelimit.go +++ b/controllers/ratelimit.go @@ -1,21 +1,28 @@ package controllers import ( - "encoding/json" + "fmt" + "io" "log" "net/http" "os" "regexp" "time" + "github.com/BurntSushi/toml" "github.com/gin-gonic/gin" "github.com/yxzzy-wtf/gin-gonic-prepack/config" "github.com/yxzzy-wtf/gin-gonic-prepack/util" ) +type RuleConfig struct { + Rules []ruleDescription +} + type ruleDescription struct { - seconds int - max int + Match string `toml:"match"` + Seconds int `toml:"seconds"` + Max int `toml:"max"` } type rule struct { @@ -80,16 +87,27 @@ type megabucket struct { } func (m *megabucket) loadFromConfig(filename string) { - file, _ := os.Open("conf.json") + file, err := os.Open(filename) + if err != nil { + panic(err) + } defer file.Close() - dec := json.NewDecoder(file) - ruleMap := map[string]ruleDescription{} - if err := dec.Decode(&ruleMap); err != nil { + + rules := RuleConfig{} + + b, err := io.ReadAll(file) + if err != nil { panic(err) } - for rkey, r := range ruleMap { - m.rules[rkey] = rule{duration: time.Second * time.Duration(r.seconds), limit: r.max} + err = toml.Unmarshal(b, &rules) + if err != nil { + panic(err) + } + + for _, r := range rules.Rules { + fmt.Printf("Loading ratelimit rule: %+v\n", r) + m.rules[r.Match] = rule{duration: time.Second * time.Duration(r.Seconds), limit: r.Max} } } @@ -119,8 +137,7 @@ var unauthLoaded = false func UnauthRateLimit() gin.HandlerFunc { return func(c *gin.Context) { if !unauthLoaded { - unauthed.loadFromConfig(config.GetConfigPath(config.Config().UnauthedRateLimitConfig)) - unauthLoaded = true + panic("Unauthed rate limits not loaded") } ip := c.ClientIP() @@ -145,8 +162,7 @@ var authLoaded = false func AuthedRateLimit() gin.HandlerFunc { return func(c *gin.Context) { if !authLoaded { - authed.loadFromConfig(config.GetConfigPath(config.Config().AuthedRateLimitConfig)) - authLoaded = true + panic("Authed rate limits not loaded") } pif, exists := c.Get("principal") @@ -162,3 +178,10 @@ func AuthedRateLimit() gin.HandlerFunc { } } } + +func LoadRateLimits() { + authed.loadFromConfig(config.GetConfigPath(config.Config().AuthedRateLimitConfig)) + authLoaded = true + unauthed.loadFromConfig(config.GetConfigPath(config.Config().UnauthedRateLimitConfig)) + unauthLoaded = true +} diff --git a/database/database.go b/database/database.go index 9741e98..f947c9f 100644 --- a/database/database.go +++ b/database/database.go @@ -18,13 +18,13 @@ var Db *gorm.DB var Dialect gorm.Dialector func InitDialect() gorm.Dialector { - if config.Config().DbDialect == "sqlite" { - return sqlite.Open(config.Config().DbUrl) - } else if config.Config().DbDialect == "postgres" { + if config.Config().Db.Dialect == "sqlite" { + return sqlite.Open(config.Config().Db.Url) + } else if config.Config().Db.Dialect == "postgres" { return postgres.New(postgres.Config{ DSN: fmt.Sprintf("user=%v password=%v dbname=%v port=%v sslmode=disable TimeZone=UTC", - config.Config().DbUsername, config.Config().DbPasswordSecret, config.Config().DbName, - config.Config().DbPort), + config.Config().Db.Username, config.Config().Db.PasswordSecret, config.Config().Db.Name, + config.Config().Db.Port), }) } else { panic("No valid DB config set up.") diff --git a/main.go b/main.go index 6dc46d9..73b08be 100644 --- a/main.go +++ b/main.go @@ -54,6 +54,9 @@ func main() { } } + // Pre-load rate limits + controllers.LoadRateLimits() + v1 := r.Group("/v1") v1.GET("/doot", controllers.UnauthRateLimit(), core.Doot())