62 lines
1.2 KiB
Go
62 lines
1.2 KiB
Go
package server
|
|
|
|
import (
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// rateLimiter begrenzt Versuche je Schlüssel (Client-IP) auf limit pro
|
|
// window (gleitendes Fenster). limit < 0 = unbegrenzt.
|
|
type rateLimiter 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 *rateLimiter) 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 *rateLimiter) size() int {
|
|
l.mu.Lock()
|
|
defer l.mu.Unlock()
|
|
return len(l.hits)
|
|
}
|