Updating rate limits to also use TOML

This commit is contained in:
🐙PiperYxzzy
2025-10-13 20:53:49 +02:00
parent acd23c2f45
commit ff15c7a65f
9 changed files with 138 additions and 79 deletions

View File

@@ -8,22 +8,26 @@ import (
"github.com/BurntSushi/toml" "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 { type StackConfiguration struct {
ConfigLoaded bool ConfigLoaded bool
AllowFreshAdminGeneration bool AllowFreshAdminGeneration bool `toml:"gen-fresh-admin"`
AdminEmails []string AdminEmails []string `toml:"admin-emails"`
AdminHmacEnv string AdminHmacEnv string `toml:"admin-hmac-env"`
UserHmacEnv string UserHmacEnv string `toml:"user-hmac-env"`
AuthedRateLimitConfig string AuthedRateLimitConfig string `toml:"auth-rate-limit-defs"`
UnauthedRateLimitConfig string UnauthedRateLimitConfig string `toml:"unauth-rate-limit-defs"`
DbDialect string Db DbConfig `toml:"db"`
DbUsername string
DbPasswordSecret string
DbUrl string
DbPort string
DbName string
} }
var Environment = os.Getenv("STACK_ENVIRONMENT") var Environment = os.Getenv("STACK_ENVIRONMENT")
@@ -60,5 +64,5 @@ func LoadConfig() {
configInternal.ConfigLoaded = true 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)
} }

View File

@@ -1,13 +1,14 @@
AllowFreshAdminGeneration = true gen-fresh-admin = true
AdminEmails = ["admin@admin.invalid"] admin-emails = ["admin@admin.invalid"]
AdminHmacEnv = "ADMIN_HMAC_ENV" admin-hmac-env = "ADMIN_HMAC_ENV"
UserHmacEnv = "USER_HMAC_ENV" user-hmac-env = "USER_HMAC_ENV"
AuthedRateLimitConfig = "ratelimit.auth.json" auth-rate-limit-defs = "ratelimit.auth.toml"
UnauthedRateLimitConfig = "ratelimit.unauth.json" unauth-rate-limit-defs = "ratelimit.unauth.toml"
DbDialect = "sqlite" [db]
DbUrl = "prepack.db" dialect = "sqlite"
DbUsername = "" url = "prepack.db"
DbPasswordSecret = "" username = ""
DbPort = "" password-secret = ""
DbName = "" port = ""
name = ""

View File

@@ -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."}
}

View File

@@ -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

View File

@@ -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."}
}

View File

@@ -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

View File

@@ -1,21 +1,28 @@
package controllers package controllers
import ( import (
"encoding/json" "fmt"
"io"
"log" "log"
"net/http" "net/http"
"os" "os"
"regexp" "regexp"
"time" "time"
"github.com/BurntSushi/toml"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/yxzzy-wtf/gin-gonic-prepack/config" "github.com/yxzzy-wtf/gin-gonic-prepack/config"
"github.com/yxzzy-wtf/gin-gonic-prepack/util" "github.com/yxzzy-wtf/gin-gonic-prepack/util"
) )
type RuleConfig struct {
Rules []ruleDescription
}
type ruleDescription struct { type ruleDescription struct {
seconds int Match string `toml:"match"`
max int Seconds int `toml:"seconds"`
Max int `toml:"max"`
} }
type rule struct { type rule struct {
@@ -80,16 +87,27 @@ type megabucket struct {
} }
func (m *megabucket) loadFromConfig(filename string) { func (m *megabucket) loadFromConfig(filename string) {
file, _ := os.Open("conf.json") file, err := os.Open(filename)
if err != nil {
panic(err)
}
defer file.Close() defer file.Close()
dec := json.NewDecoder(file)
ruleMap := map[string]ruleDescription{} rules := RuleConfig{}
if err := dec.Decode(&ruleMap); err != nil {
b, err := io.ReadAll(file)
if err != nil {
panic(err) panic(err)
} }
for rkey, r := range ruleMap { err = toml.Unmarshal(b, &rules)
m.rules[rkey] = rule{duration: time.Second * time.Duration(r.seconds), limit: r.max} 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 { func UnauthRateLimit() gin.HandlerFunc {
return func(c *gin.Context) { return func(c *gin.Context) {
if !unauthLoaded { if !unauthLoaded {
unauthed.loadFromConfig(config.GetConfigPath(config.Config().UnauthedRateLimitConfig)) panic("Unauthed rate limits not loaded")
unauthLoaded = true
} }
ip := c.ClientIP() ip := c.ClientIP()
@@ -145,8 +162,7 @@ var authLoaded = false
func AuthedRateLimit() gin.HandlerFunc { func AuthedRateLimit() gin.HandlerFunc {
return func(c *gin.Context) { return func(c *gin.Context) {
if !authLoaded { if !authLoaded {
authed.loadFromConfig(config.GetConfigPath(config.Config().AuthedRateLimitConfig)) panic("Authed rate limits not loaded")
authLoaded = true
} }
pif, exists := c.Get("principal") 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
}

View File

@@ -18,13 +18,13 @@ var Db *gorm.DB
var Dialect gorm.Dialector var Dialect gorm.Dialector
func InitDialect() gorm.Dialector { func InitDialect() gorm.Dialector {
if config.Config().DbDialect == "sqlite" { if config.Config().Db.Dialect == "sqlite" {
return sqlite.Open(config.Config().DbUrl) return sqlite.Open(config.Config().Db.Url)
} else if config.Config().DbDialect == "postgres" { } else if config.Config().Db.Dialect == "postgres" {
return postgres.New(postgres.Config{ return postgres.New(postgres.Config{
DSN: fmt.Sprintf("user=%v password=%v dbname=%v port=%v sslmode=disable TimeZone=UTC", 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().Db.Username, config.Config().Db.PasswordSecret, config.Config().Db.Name,
config.Config().DbPort), config.Config().Db.Port),
}) })
} else { } else {
panic("No valid DB config set up.") panic("No valid DB config set up.")

View File

@@ -54,6 +54,9 @@ func main() {
} }
} }
// Pre-load rate limits
controllers.LoadRateLimits()
v1 := r.Group("/v1") v1 := r.Group("/v1")
v1.GET("/doot", controllers.UnauthRateLimit(), core.Doot()) v1.GET("/doot", controllers.UnauthRateLimit(), core.Doot())