Add security headers and configurable rate limiting middleware
Change-Id: Id976e75e58d79ea38e7939afe7576a2295a1c5af
diff --git a/config/config.go b/config/config.go
index b7d2a16..2ad6c43 100644
--- a/config/config.go
+++ b/config/config.go
@@ -19,6 +19,7 @@
defaultCookieName = "km-config"
defaultPort = 5725
defaultLogLevel = "warn"
+ defaultRateLimit = 100
)
// MappingRule represents a single mapping rule in the configuration
@@ -96,6 +97,7 @@
CookieName string `yaml:"cookieName,omitempty"`
Port int `yaml:"port,omitempty"`
LogLevel string `yaml:"loglevel,omitempty"`
+ RateLimit int `yaml:"rateLimit,omitempty"` // max requests per minute per IP (0 = use default 100)
Lists []MappingList `yaml:"lists,omitempty"`
}
@@ -195,6 +197,7 @@
ServiceURL: globalConfig.ServiceURL,
Port: globalConfig.Port,
LogLevel: globalConfig.LogLevel,
+ RateLimit: globalConfig.RateLimit,
Lists: allLists,
}
@@ -227,6 +230,9 @@
if config.Port == 0 {
config.Port = defaultPort
}
+ if config.RateLimit == 0 {
+ config.RateLimit = defaultRateLimit
+ }
}
// ApplyEnvOverrides overrides configuration fields from environment variables.
@@ -253,6 +259,12 @@
config.Port = port
}
}
+
+ if val := os.Getenv("KORAL_MAPPER_RATE_LIMIT"); val != "" {
+ if rl, err := strconv.Atoi(val); err == nil {
+ config.RateLimit = rl
+ }
+ }
}
// validateMappingLists validates a slice of mapping lists (without duplicate ID checking)
diff --git a/config/config_test.go b/config/config_test.go
index 599191d..e2d02f3 100644
--- a/config/config_test.go
+++ b/config/config_test.go
@@ -1177,3 +1177,55 @@
assert.Equal(t, "textClass", and1.Operands[1].(*parser.CorpusField).Key)
assert.Equal(t, "musik", and1.Operands[1].(*parser.CorpusField).Value)
}
+
+func TestRateLimitConfigField(t *testing.T) {
+ content := `
+rateLimit: 50
+lists:
+ - id: test-mapper
+ mappings:
+ - "[A] <> [B]"
+`
+ tmpfile, err := os.CreateTemp("", "config-ratelimit-*.yaml")
+ require.NoError(t, err)
+ defer os.Remove(tmpfile.Name())
+
+ _, err = tmpfile.WriteString(content)
+ require.NoError(t, err)
+ require.NoError(t, tmpfile.Close())
+
+ cfg, err := LoadFromSources(tmpfile.Name(), nil)
+ require.NoError(t, err)
+ assert.Equal(t, 50, cfg.RateLimit, "rateLimit should be loaded from YAML")
+}
+
+func TestRateLimitDefaultApplied(t *testing.T) {
+ cfg := &MappingConfig{}
+ ApplyDefaults(cfg)
+ assert.Equal(t, defaultRateLimit, cfg.RateLimit,
+ "default rate limit should be applied when not specified")
+}
+
+func TestRateLimitEnvOverride(t *testing.T) {
+ t.Setenv("KORAL_MAPPER_RATE_LIMIT", "200")
+
+ content := `
+rateLimit: 50
+lists:
+ - id: test-mapper
+ mappings:
+ - "[A] <> [B]"
+`
+ tmpfile, err := os.CreateTemp("", "config-ratelimit-env-*.yaml")
+ require.NoError(t, err)
+ defer os.Remove(tmpfile.Name())
+
+ _, err = tmpfile.WriteString(content)
+ require.NoError(t, err)
+ require.NoError(t, tmpfile.Close())
+
+ cfg, err := LoadFromSources(tmpfile.Name(), nil)
+ require.NoError(t, err)
+ assert.Equal(t, 200, cfg.RateLimit,
+ "KORAL_MAPPER_RATE_LIMIT env var should override YAML value")
+}