Pulse/internal/cloudcp/ratelimit.go
rcourtman c7e50d5602 Harden provider-hosted MSP isolation
Broker provider control-plane Docker access through a socket proxy, remove broad host mounts, align audit and rate-limit proxy trust, harden tenant runtime containers, restrict workspace report logo paths, and update provider deploy guardrails.
2026-06-02 21:10:13 +01:00

88 lines
1.9 KiB
Go

package cloudcp
import (
"math"
"net/http"
"strconv"
"sync"
"time"
"github.com/rcourtman/pulse-go-rewrite/internal/cloudcp/proxytrust"
)
const (
defaultCPRateLimit = 120
defaultCPRateWindow = time.Minute
)
// CPRateLimiter provides simple IP-based rate limiting for control plane endpoints.
type CPRateLimiter struct {
mu sync.Mutex
attempts map[string][]time.Time
limit int
window time.Duration
}
// NewCPRateLimiter creates a rate limiter with the given limit per window.
func NewCPRateLimiter(limit int, window time.Duration) *CPRateLimiter {
if limit <= 0 {
limit = defaultCPRateLimit
}
if window <= 0 {
window = defaultCPRateWindow
}
return &CPRateLimiter{
attempts: make(map[string][]time.Time),
limit: limit,
window: window,
}
}
// Allow checks whether the given IP is within the rate limit.
func (rl *CPRateLimiter) Allow(ip string) bool {
rl.mu.Lock()
defer rl.mu.Unlock()
now := time.Now()
cutoff := now.Add(-rl.window)
// Filter expired entries
valid := rl.attempts[ip][:0]
for _, t := range rl.attempts[ip] {
if t.After(cutoff) {
valid = append(valid, t)
}
}
if len(valid) >= rl.limit {
rl.attempts[ip] = valid
return false
}
rl.attempts[ip] = append(valid, now)
return true
}
// Middleware wraps an http.Handler with rate limiting.
func (rl *CPRateLimiter) Middleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ip := clientIP(r)
if !rl.Allow(ip) {
retryAfter := int(math.Ceil(rl.window.Seconds()))
if retryAfter < 1 {
retryAfter = 1
}
w.Header().Set("Retry-After", strconv.Itoa(retryAfter))
w.Header().Set("X-RateLimit-Limit", strconv.Itoa(rl.limit))
w.Header().Set("X-RateLimit-Remaining", "0")
http.Error(w, "Too Many Requests", http.StatusTooManyRequests)
return
}
next.ServeHTTP(w, r)
})
}
func clientIP(r *http.Request) string {
return proxytrust.ClientIP(r)
}