diff --git a/conf.json b/conf.json deleted file mode 100644 index 7017f33..0000000 --- a/conf.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "AllowFreshAdminGeneration": true, - "AdminEmails": ["admin@admin.invalid"] -} \ No newline at end of file diff --git a/config/config.go b/config/config.go index cb3b330..4004582 100644 --- a/config/config.go +++ b/config/config.go @@ -2,21 +2,37 @@ package config import ( "encoding/json" + "log" "os" ) type StackConfiguration struct { - ConfigLoaded bool + ConfigLoaded bool + AllowFreshAdminGeneration bool AdminEmails []string AdminHmacEnv string UserHmacEnv string + AuthedRateLimitConfig string + UnauthedRateLimitConfig string } +var Environment = os.Getenv("STACK_ENVIRONMENT") + var Config = StackConfiguration{} +func GetConfigPath(filename string) string { + if Environment == "" { + Environment = "dev" + } + return Environment + "/" + filename +} + func LoadConfig() { - file, _ := os.Open("conf.json") + file, err := os.Open(GetConfigPath("conf.json")) + if err != nil { + panic(err) + } defer file.Close() dec := json.NewDecoder(file) if err := dec.Decode(&Config); err != nil { @@ -24,4 +40,6 @@ func LoadConfig() { } Config.ConfigLoaded = true + + log.Printf("Loaded Config for stack " + Environment) } diff --git a/config/config_test.go b/config/config_test.go new file mode 100644 index 0000000..b933c86 --- /dev/null +++ b/config/config_test.go @@ -0,0 +1,68 @@ +package config + +import ( + "testing" +) + +func TestAllConfigs(t *testing.T) { + SingleStackTest(t, "dev", StackConfiguration{ + AllowFreshAdminGeneration: true, + AdminEmails: []string{"admin@admin.invalid"}, + AdminHmacEnv: "ADMIN_HMAC_ENV", + UserHmacEnv: "USER_HMAC_ENV", + AuthedRateLimitConfig: "ratelimit.auth.json", + UnauthedRateLimitConfig: "ratelimit.unauth.json", + }) +} + +func SingleStackTest(t *testing.T, stack string, expected StackConfiguration) { + Config = StackConfiguration{} + + if Config.ConfigLoaded { + t.Errorf("Config.ConfigLoaded should be false before any processing") + } + + if len(Config.AdminEmails) > 0 || + Config.AdminHmacEnv != "" || + Config.UserHmacEnv != "" || + Config.AllowFreshAdminGeneration || + Config.AuthedRateLimitConfig != "" || + Config.UnauthedRateLimitConfig != "" { // Extend this IF for any other config values + t.Errorf("Config already has values before loading") + } + + Environment = stack + LoadConfig() + + if !Config.ConfigLoaded { + t.Errorf("Config was not set to loaded") + } + + // Finally test values + if Config.AllowFreshAdminGeneration != expected.AllowFreshAdminGeneration { + t.Errorf("AllowFreshAdminGeneration value not set properly") + } + + for i, email := range Config.AdminEmails { + if expected.AdminEmails[i] != email { + t.Errorf("AdminEmails value not set properly, expected %v at %v, was %v", expected.AdminEmails[i], i, email) + } + } + + if Config.AdminHmacEnv != expected.AdminHmacEnv { + t.Errorf("AdminHmacEnv value not set properly") + } + + if Config.UserHmacEnv != expected.UserHmacEnv { + t.Errorf("UserHmacEnv value not set properly") + } + + if Config.AuthedRateLimitConfig != expected.AuthedRateLimitConfig { + t.Errorf("AuthedRateLimitConfig value not set properly") + } + + if Config.UnauthedRateLimitConfig != expected.UnauthedRateLimitConfig { + t.Errorf("UnauthedRateLimitConfig value not set properly") + } + +} diff --git a/config/dev/conf.json b/config/dev/conf.json new file mode 100644 index 0000000..22d295f --- /dev/null +++ b/config/dev/conf.json @@ -0,0 +1,8 @@ +{ + "AllowFreshAdminGeneration": true, + "AdminEmails": ["admin@admin.invalid"], + "AdminHmacEnv": "ADMIN_HMAC_ENV", + "UserHmacEnv": "USER_HMAC_ENV", + "AuthedRateLimitConfig": "ratelimit.auth.json", + "UnauthedRateLimitConfig": "ratelimit.unauth.json" +} \ No newline at end of file diff --git a/config/dev/ratelimit.auth.json b/config/dev/ratelimit.auth.json new file mode 100644 index 0000000..9a8c07e --- /dev/null +++ b/config/dev/ratelimit.auth.json @@ -0,0 +1,17 @@ +{ + "": {"seconds": 60, "max": 30, "_comment": "Global ratelimit."}, + + "/v1/sec/doot": + {"seconds": 5, "max": 3, "_comment": "One DPS (Doot Per Second) for monitoring?"}, + + "/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."}, + + "/v1/adm/doot": + {"seconds": 5, "max": 3, "_comment": "One DPS (Doot Per Second) for monitoring?"}, + + "/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.unauth.json b/config/dev/ratelimit.unauth.json new file mode 100644 index 0000000..ff74971 --- /dev/null +++ b/config/dev/ratelimit.unauth.json @@ -0,0 +1,19 @@ +{ + "": + {"seconds": 60, "max": 30, "_comment": "Global unauthenticated ratelimit."}, + + "/v1/doot": + {"seconds": 5, "max": 5, "_comment": "Unauthenticated DOOT for server monitoring."}, + + "/v1/login": + {"seconds": 60, "max": 3, "_comment": "Prevent bruteforce attacks on Login."}, + + "/v1/admin": + {"seconds": 60, "max": 1, "_comment": "Prevent bruteforce attacks on Admin Login."}, + + "/v1/signup": + {"seconds": 1800, "max": 1, "_comment": "Prevent spam account creation."}, + + "/v1/forgot": + {"seconds": 60, "max": 1, "_comment": "Slow down 'forgot password' enumeration/spam."} +} \ No newline at end of file diff --git a/controllers/core/core.go b/controllers/core/core.go index 63b5e18..ab954c5 100644 --- a/controllers/core/core.go +++ b/controllers/core/core.go @@ -301,8 +301,7 @@ func UserResetForgottenPassword() gin.HandlerFunc { resetting.SetPassword(resetVals.NewPassword) if err := resetting.Save(); err != nil { - log. - log.Error("could not save user", err) + log.Println("could not save user", err) c.AbortWithStatus(http.StatusUnauthorized) return } diff --git a/controllers/ratelimit.go b/controllers/ratelimit.go index 00f0e9c..7e26eeb 100644 --- a/controllers/ratelimit.go +++ b/controllers/ratelimit.go @@ -1,15 +1,23 @@ package controllers import ( + "encoding/json" "log" "net/http" + "os" "regexp" "time" "github.com/gin-gonic/gin" + "github.com/yxzzy-wtf/gin-gonic-prepack/config" "github.com/yxzzy-wtf/gin-gonic-prepack/util" ) +type ruleDescription struct { + seconds int + max int +} + type rule struct { duration time.Duration limit int @@ -71,6 +79,20 @@ type megabucket struct { rules map[string]rule } +func (m *megabucket) loadFromConfig(filename string) { + file, _ := os.Open("conf.json") + defer file.Close() + dec := json.NewDecoder(file) + ruleMap := map[string]ruleDescription{} + if err := dec.Decode(&ruleMap); err != nil { + panic(err) + } + + for rkey, r := range ruleMap { + m.rules[rkey] = rule{duration: time.Second * time.Duration(r.seconds), limit: r.max} + } +} + func (m *megabucket) take(signature string, resource string) bool { b, ex := m.buckets[signature] if !ex { @@ -86,10 +108,9 @@ func (m *megabucket) take(signature string, resource string) bool { var unauthed = megabucket{ buckets: map[string]bucket{}, - rules: map[string]rule{ - "*": {duration: time.Second * 10, limit: 20}, - }, + rules: map[string]rule{}, } +var unauthLoaded = false /** * Applies rate limiting to unauthorized actors based on their IP address. @@ -97,6 +118,11 @@ var unauthed = megabucket{ */ func UnauthRateLimit() gin.HandlerFunc { return func(c *gin.Context) { + if !unauthLoaded { + unauthed.loadFromConfig(config.GetConfigPath(config.Config.UnauthedRateLimitConfig)) + unauthLoaded = true + } + ip := c.ClientIP() if !unauthed.take(ip, "") { @@ -108,10 +134,9 @@ func UnauthRateLimit() gin.HandlerFunc { var authed = megabucket{ buckets: map[string]bucket{}, - rules: map[string]rule{ - "*": {duration: time.Second * 10, limit: 5}, - }, + rules: map[string]rule{}, } +var authLoaded = false /** * Authorized rate limit. Using the UID of the authorized user as the @@ -119,6 +144,11 @@ var authed = megabucket{ */ func AuthedRateLimit() gin.HandlerFunc { return func(c *gin.Context) { + if !authLoaded { + authed.loadFromConfig(config.GetConfigPath(config.Config.AuthedRateLimitConfig)) + authLoaded = true + } + pif, exists := c.Get("principal") p := pif.(util.PrincipalInfo) if !exists { diff --git a/main.go b/main.go index 4368d5f..cc8c2ea 100644 --- a/main.go +++ b/main.go @@ -75,6 +75,7 @@ func main() { v1Admin := v1.Group("/adm", core.AdminAuth()) v1Admin.GET("/doot", core.Doot()) + v1Admin.GET("/2fa-doot", core.LiveTwoFactor(), core.Doot()) // Start server if err := http.ListenAndServe(":9091", r); err != nil {