64 lines
1.3 KiB
Go
64 lines
1.3 KiB
Go
// Package ratelimit begrenzt Versuche je Schlüssel (z. B. Client-IP) –
|
||
// genutzt für die Enrollment-Endpunkte von Server und Accountant.
|
||
package ratelimit
|
||
|
||
import (
|
||
"sync"
|
||
"time"
|
||
)
|
||
|
||
// Limiter begrenzt Versuche je Schlüssel (Client-IP) auf Limit pro Window
|
||
// (gleitendes Fenster). Limit < 0 = unbegrenzt. Parallel nutzbar.
|
||
type Limiter struct {
|
||
Limit int
|
||
Window time.Duration
|
||
now func() time.Time
|
||
|
||
mu sync.Mutex
|
||
hits map[string][]time.Time
|
||
lastPurge time.Time
|
||
}
|
||
|
||
// Allow verbucht einen Versuch; bei Ablehnung liefert es die Wartezeit.
|
||
func (l *Limiter) Allow(key string) (bool, time.Duration) {
|
||
if l.Limit < 0 {
|
||
return true, 0
|
||
}
|
||
now := time.Now()
|
||
if l.now != nil {
|
||
now = l.now()
|
||
}
|
||
cutoff := now.Add(-l.Window)
|
||
l.mu.Lock()
|
||
defer l.mu.Unlock()
|
||
if l.hits == nil {
|
||
l.hits = map[string][]time.Time{}
|
||
}
|
||
if now.Sub(l.lastPurge) >= l.Window { // vergessen, was nicht mehr zählt
|
||
for k, ts := range l.hits {
|
||
if !ts[len(ts)-1].After(cutoff) {
|
||
delete(l.hits, k)
|
||
}
|
||
}
|
||
l.lastPurge = now
|
||
}
|
||
ts := l.hits[key]
|
||
i := 0
|
||
for i < len(ts) && !ts[i].After(cutoff) {
|
||
i++
|
||
}
|
||
ts = ts[i:]
|
||
if len(ts) >= l.Limit {
|
||
l.hits[key] = ts
|
||
return false, ts[0].Add(l.Window).Sub(now)
|
||
}
|
||
l.hits[key] = append(ts, now)
|
||
return true, 0
|
||
}
|
||
|
||
func (l *Limiter) size() int {
|
||
l.mu.Lock()
|
||
defer l.mu.Unlock()
|
||
return len(l.hits)
|
||
}
|