// Package ratelimit implementiert ING-09: Token-Bucket-Rate-Limiting // auf Protokollebene für Login-Versuche (IMAP/POP3) und SMTP-Annahme, // je Mandant getrennt konfigurierbar (Akzeptanzkriterium 3). package ratelimit import ( "fmt" "sync" "time" ) // Config ist die Token-Bucket-Konfiguration EINES Limits // (Akzeptanzkriterium 1/2: begrenzt und konfigurierbar). Burst ist die // Anzahl sofort verfügbarer Versuche, RefillEvery die Zeit, die ein // neuer Versuch nachwächst. type Config struct { Burst int RefillEvery time.Duration } // ConfigProvider liefert die Rate-Limit-Konfiguration für einen // Mandanten (Akzeptanzkriterium 3: je Mandant getrennt konfigurierbar). type ConfigProvider interface { ConfigFor(tenant string) Config } // StaticConfig ist ein einfacher ConfigProvider: feste Konfiguration je // Mandant, mit Fallback auf Default für unbekannte/nicht gesondert // konfigurierte Mandanten. type StaticConfig struct { Default Config PerTenant map[string]Config } func (s StaticConfig) ConfigFor(tenant string) Config { if cfg, ok := s.PerTenant[tenant]; ok { return cfg } return s.Default } // tokenBucket ist EIN Token-Bucket-Zähler für einen Schlüssel // (Mandant+Quelle). type tokenBucket struct { mu sync.Mutex tokens float64 lastRefill time.Time cfg Config } func newTokenBucket(cfg Config) *tokenBucket { return &tokenBucket{tokens: float64(cfg.Burst), lastRefill: time.Now(), cfg: cfg} } // allow entscheidet über EINEN Versuch zum Zeitpunkt now. Bei // Ablehnung liefert retryAfter eine konkrete, positive Wartezeit // (Akzeptanzkriterium 1: definierte Ablehnung MIT Wartezeit-Hinweis, // nicht bloßer Verbindungsabbruch). func (b *tokenBucket) allow(now time.Time) (ok bool, retryAfter time.Duration) { b.mu.Lock() defer b.mu.Unlock() refillPerSecond := 1.0 / b.cfg.RefillEvery.Seconds() elapsed := now.Sub(b.lastRefill).Seconds() b.tokens += elapsed * refillPerSecond if b.tokens > float64(b.cfg.Burst) { b.tokens = float64(b.cfg.Burst) } b.lastRefill = now if b.tokens >= 1 { b.tokens-- return true, 0 } missing := 1 - b.tokens wait := time.Duration(missing / refillPerSecond * float64(time.Second)) if wait <= 0 { wait = time.Millisecond } return false, wait } // Limiter verwaltet Token-Buckets je (Mandant, Quelle)-Schlüssel — // EIN Limiter deckt EINEN Limit-Zweck ab (z. B. "Login-Versuche" oder // "SMTP-Annahme"); ein Server verwendet für unterschiedliche Zwecke // unterschiedliche Limiter-Instanzen. type Limiter struct { mu sync.Mutex buckets map[string]*tokenBucket provider ConfigProvider } func NewLimiter(provider ConfigProvider) *Limiter { return &Limiter{buckets: map[string]*tokenBucket{}, provider: provider} } // Allow prüft, ob EIN Versuch von source innerhalb des Mandanten // tenant aktuell erlaubt ist. func (l *Limiter) Allow(tenant, source string) (ok bool, retryAfter time.Duration) { key := fmt.Sprintf("%s|%s", tenant, source) l.mu.Lock() b, exists := l.buckets[key] if !exists { b = newTokenBucket(l.provider.ConfigFor(tenant)) l.buckets[key] = b } l.mu.Unlock() return b.allow(time.Now()) }