Files
warmbly/internal/formserver/abuse.go
T

89 lines
2.0 KiB
Go

package formserver
import (
"sync"
"time"
)
// ipLimiter is a fixed-window in-memory submission limiter, the same shape
// the tracking service uses for pixels. Budgets are per forms-service
// instance; replicas each carry their own window, which only ever errs
// toward letting a real visitor through.
type ipLimiter struct {
mu sync.Mutex
limit int
window time.Duration
seen map[string]*ipWindow
}
type ipWindow struct {
n int
reset time.Time
}
// ipLimiterMax bounds the map against source-address spray; sweeping first,
// then dropping the table, costs a reset budget, not unbounded memory.
const ipLimiterMax = 100_000
func newIPLimiter(limit int, window time.Duration) *ipLimiter {
return &ipLimiter{limit: limit, window: window, seen: map[string]*ipWindow{}}
}
func (l *ipLimiter) Allow(ip string) bool {
now := time.Now()
l.mu.Lock()
defer l.mu.Unlock()
w, ok := l.seen[ip]
if !ok || now.After(w.reset) {
if len(l.seen) >= ipLimiterMax {
for k, e := range l.seen {
if now.After(e.reset) {
delete(l.seen, k)
}
}
if len(l.seen) >= ipLimiterMax {
l.seen = map[string]*ipWindow{}
}
}
l.seen[ip] = &ipWindow{n: 1, reset: now.Add(l.window)}
return true
}
w.n++
return w.n <= l.limit
}
// ttlSet remembers keys for a fixed duration; Add reports whether the key was
// new. It backs the per-visitor view dedupe.
type ttlSet struct {
mu sync.Mutex
ttl time.Duration
seen map[string]time.Time
}
const ttlSetMax = 200_000
func newTTLSet(ttl time.Duration) *ttlSet {
return &ttlSet{ttl: ttl, seen: map[string]time.Time{}}
}
func (s *ttlSet) Add(key string) bool {
now := time.Now()
s.mu.Lock()
defer s.mu.Unlock()
if exp, ok := s.seen[key]; ok && now.Before(exp) {
return false
}
if len(s.seen) >= ttlSetMax {
for k, exp := range s.seen {
if now.After(exp) {
delete(s.seen, k)
}
}
if len(s.seen) >= ttlSetMax {
s.seen = map[string]time.Time{}
}
}
s.seen[key] = now.Add(s.ttl)
return true
}