niko_trust/internal/server/ratelimit.go
Niko Marmeladkov 9d66003689
Initial commit: signed-object trust relay, verifier, and docs
- server: relay storing signed objects (PUT/GET), per-IP rate limiting,
  per-subject quota (1000), one-response-per-request, pagination,
  /v1/healthz /v1/readyz /v1/metrics
- verify: signature-verifying trust evaluator; every object is checked via
  env.Verify(), approvals via VerifyApprovalResponse, revocations via
  VerifyRevocationOf; k-of-n approval quorum
- docs: TRUST-MODEL.md and API.md describing issuer-anchored signatures and
  the endpoint/status-code contract
- tests: server, verify, and ratelimit packages
2026-08-12 22:36:49 +03:00

60 lines
1.3 KiB
Go

package server
import (
"net"
"net/http"
"sync"
"time"
)
// ipLimiter is a fixed-window per-client rate limiter keyed by remote IP. It is
// deliberately minimal: no bursting, no warm-up, just a hard cap per window to
// keep an open relay from being flooded.
type ipLimiter struct {
mu sync.Mutex
limit int
window time.Duration
hits map[string]int
reset map[string]time.Time
}
func newIPLimiter(limit int, window time.Duration) *ipLimiter {
return &ipLimiter{
limit: limit,
window: window,
hits: make(map[string]int),
reset: make(map[string]time.Time),
}
}
// allow reports whether the client may proceed, incrementing its windowed count.
func (l *ipLimiter) allow(ip string) bool {
l.mu.Lock()
defer l.mu.Unlock()
now := time.Now()
until, ok := l.reset[ip]
if !ok || now.After(until) {
l.hits[ip] = 0
l.reset[ip] = now.Add(l.window)
} else if now.Sub(until) > l.window*2 {
// Lazy cleanup of long-idle entries.
delete(l.hits, ip)
delete(l.reset, ip)
l.hits[ip] = 0
l.reset[ip] = now.Add(l.window)
}
if l.hits[ip] >= l.limit {
return false
}
l.hits[ip]++
return true
}
// clientIP returns the request's remote IP, stripping a port if present.
func clientIP(r *http.Request) string {
host, _, err := net.SplitHostPort(r.RemoteAddr)
if err != nil {
return r.RemoteAddr
}
return host
}