mirror of
https://github.com/phishingclub/phishingclub.git
synced 2026-07-07 04:47:55 +02:00
61ecc4c505
Signed-off-by: Ronni Skansing <rskansing@gmail.com>
126 lines
4.0 KiB
Go
126 lines
4.0 KiB
Go
package middleware
|
|
|
|
import (
|
|
"net/http"
|
|
"sync"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"golang.org/x/time/rate"
|
|
)
|
|
|
|
// limiterEntry holds a rate limiter and the last time it was accessed as
|
|
// unix nanoseconds. lastAccess is accessed atomically to avoid data races
|
|
// between GetLimiter (writer) and cleanup (reader) under concurrent requests.
|
|
type limiterEntry struct {
|
|
limiter *rate.Limiter
|
|
lastAccess atomic.Int64
|
|
}
|
|
|
|
// NewIPRateLimiterMiddleware creates a middleware that limits the number of requests per IP
|
|
// limit is the number of requests per second
|
|
// burst is the maximum burst size, the maximum number of requests that can be made in a burst without being limited
|
|
func NewIPRateLimiterMiddleware(limit float64, burst int) gin.HandlerFunc {
|
|
ipLimiter := NewKeyRateLimiter(rate.Limit(limit), burst, 10*time.Minute)
|
|
return func(c *gin.Context) {
|
|
limiter := ipLimiter.GetLimiter(c.ClientIP())
|
|
if !limiter.Allow() {
|
|
c.AbortWithStatus(http.StatusTooManyRequests)
|
|
return
|
|
}
|
|
c.Next()
|
|
}
|
|
}
|
|
|
|
// NewScimRateLimiterMiddleware limits SCIM requests per company rather than per
|
|
// IP. cloud identity providers (Microsoft Entra, Okta) send every tenant's SCIM
|
|
// traffic from a shared pool of source IPs, so an IP based limit would throttle
|
|
// all companies together. keying on the companyID path param gives each company
|
|
// its own bucket. requests without a companyID fall back to the client IP.
|
|
// limit is requests per second, burst is the maximum burst size.
|
|
func NewScimRateLimiterMiddleware(limit float64, burst int) gin.HandlerFunc {
|
|
companyLimiter := NewKeyRateLimiter(rate.Limit(limit), burst, 10*time.Minute)
|
|
return func(c *gin.Context) {
|
|
key := c.Param("companyID")
|
|
if key == "" {
|
|
key = c.ClientIP()
|
|
}
|
|
limiter := companyLimiter.GetLimiter(key)
|
|
if !limiter.Allow() {
|
|
c.AbortWithStatus(http.StatusTooManyRequests)
|
|
return
|
|
}
|
|
c.Next()
|
|
}
|
|
}
|
|
|
|
// KeyRateLimiter is a rate limiter for key such as username, email or IP
|
|
type KeyRateLimiter struct {
|
|
// key is a map of key to limiterEntry
|
|
key sync.Map
|
|
// limiter is the rate limit, e.g. 1 request per second
|
|
limiter rate.Limit
|
|
// burst is the maximum burst size, the maximum number of requests that can be made in a burst without being limited
|
|
burst int
|
|
// cleanupInterval is the interval at which idle entries are evicted
|
|
cleanupInterval time.Duration
|
|
}
|
|
|
|
// NewKeyRateLimiter creates a new key rate limiter
|
|
// limiter is the rate limit, e.g. 1 request per second
|
|
// burst is the maximum burst size, the maximum number of requests that can be made in a burst without being limited
|
|
func NewKeyRateLimiter(
|
|
limiter rate.Limit,
|
|
burst int,
|
|
cleanupInterval time.Duration,
|
|
) *KeyRateLimiter {
|
|
rl := &KeyRateLimiter{
|
|
limiter: limiter,
|
|
burst: burst,
|
|
cleanupInterval: cleanupInterval,
|
|
}
|
|
go rl.cleanup()
|
|
return rl
|
|
}
|
|
|
|
// cleanup evicts entries that have not been accessed within the cleanup interval,
|
|
// preventing unbounded memory growth from the sync.Map
|
|
func (r *KeyRateLimiter) cleanup() {
|
|
for range time.Tick(r.cleanupInterval) {
|
|
threshold := time.Now().Add(-r.cleanupInterval).UnixNano()
|
|
r.key.Range(func(key, value interface{}) bool {
|
|
entry, ok := value.(*limiterEntry)
|
|
if !ok {
|
|
// remove any entry with an unexpected type
|
|
r.key.Delete(key)
|
|
return true
|
|
}
|
|
if entry.lastAccess.Load() < threshold {
|
|
r.key.Delete(key)
|
|
}
|
|
return true
|
|
})
|
|
}
|
|
}
|
|
|
|
// GetLimiter gets the limiter for a key or creates one if it does not exist
|
|
func (r *KeyRateLimiter) GetLimiter(key string) *rate.Limiter {
|
|
entry := &limiterEntry{
|
|
limiter: rate.NewLimiter(r.limiter, r.burst),
|
|
}
|
|
entry.lastAccess.Store(time.Now().UnixNano())
|
|
|
|
// LoadOrStore atomically either stores our new entry or returns the
|
|
// existing one — correctly handles both the common case and concurrent
|
|
// goroutines racing to create an entry for the same key
|
|
actual, loaded := r.key.LoadOrStore(key, entry)
|
|
if loaded {
|
|
existing := actual.(*limiterEntry)
|
|
existing.lastAccess.Store(time.Now().UnixNano())
|
|
return existing.limiter
|
|
}
|
|
|
|
return entry.limiter
|
|
}
|