diff --git a/backend/internal/ratelimiter/fixed-window.go b/backend/internal/ratelimiter/fixed-window.go new file mode 100644 index 0000000..33a8abf --- /dev/null +++ b/backend/internal/ratelimiter/fixed-window.go @@ -0,0 +1,49 @@ +package ratelimiter + +// Copied from https://www.youtube.com/watch?v=m5oyY9fgZPs + +import ( + "sync" + "time" +) + +type FixedWindowRateLimiter struct { + sync.RWMutex + clients map[string]int + limit int + window time.Duration +} + +func NewFixedWindowLimiter(limit int, window time.Duration) *FixedWindowRateLimiter { + return &FixedWindowRateLimiter{ + clients: make(map[string]int), + limit: limit, + window: window, + } +} + +func (rl *FixedWindowRateLimiter) Allow(ip string) (bool, time.Duration) { + rl.RLock() + count, exists := rl.clients[ip] + rl.RUnlock() + + if !exists || count < rl.limit { + // begin the reset count for a new window + rl.Lock() + if !exists { + go rl.resetCount(ip) + } + rl.clients[ip]++ + rl.Unlock() + return true, time.Duration(0) + } + return false, rl.window + +} + +func (rl *FixedWindowRateLimiter) resetCount(ip string) { + time.Sleep(rl.window) + rl.Lock() + delete(rl.clients, ip) + rl.Unlock() +} diff --git a/backend/internal/ratelimiter/ratelimiter.go b/backend/internal/ratelimiter/ratelimiter.go new file mode 100644 index 0000000..e77d8cd --- /dev/null +++ b/backend/internal/ratelimiter/ratelimiter.go @@ -0,0 +1,13 @@ +package ratelimiter + +import "time" + +type Limiter interface { + Allow(ip string) (bool, time.Duration) +} + +type Config struct { + RequestsPerTimeFrame int + TimeFrame time.Duration + Enabled bool +} diff --git a/backend/internal/ratelimiter/sliding-window.go b/backend/internal/ratelimiter/sliding-window.go new file mode 100644 index 0000000..afdd732 --- /dev/null +++ b/backend/internal/ratelimiter/sliding-window.go @@ -0,0 +1,42 @@ +package ratelimiter + +import ( + "sync" + "time" +) + +type SlidingWindowRateLimiter struct { + sync.RWMutex + clients map[string][]time.Time + limit int + window time.Duration +} + +func NewSlidingWindowLimiter(limit int, window time.Duration) *SlidingWindowRateLimiter { + return &SlidingWindowRateLimiter{ + clients: make(map[string][]time.Time), + limit: limit, + window: window, + } +} + +func (rl *SlidingWindowRateLimiter) Allow(ip string) (bool, time.Duration) { + rl.RLock() + defer rl.Unlock() + + // add new request attempt + rl.clients[ip] = append(rl.clients[ip], time.Now()) + + // remove ones outside the window + for len(rl.clients[ip]) > 0 && time.Since(rl.clients[ip][0]) > rl.window { + rl.clients[ip] = rl.clients[ip][1:] + } + + // do actual check now + if len(rl.clients[ip]) > rl.limit { + // calc retry wait time + retryAfter := rl.window - time.Since(rl.clients[ip][0]) + return false, time.Duration(retryAfter) + } + return true, time.Duration(0) +} diff --git a/backend/internal/ratelimiter/token-bucket.go b/backend/internal/ratelimiter/token-bucket.go new file mode 100644 index 0000000..49918f7 --- /dev/null +++ b/backend/internal/ratelimiter/token-bucket.go @@ -0,0 +1,75 @@ +package ratelimiter + +import ( + "sync" + "time" + + "golang.org/x/time/rate" +) + +type TokenBucketRateLimiter struct { + sync.RWMutex + clients map[string]*Client + config Config +} + +func NewTokenBucketRateLimiter(config Config) *TokenBucketRateLimiter { + rl := &TokenBucketRateLimiter{ + clients: make(map[string]*Client), + config: config, + } + + go rl.cleanupClients() + + return rl +} + +type Client struct { + Limiter *rate.Limiter + LastSeen time.Time +} + +func (rl *TokenBucketRateLimiter) getClient(ip string) *Client { + rl.RLock() + _, exists := rl.clients[ip] + rl.RUnlock() + + if !exists { + limiter := rate.NewLimiter(rate.Every(rl.config.TimeFrame), rl.config.RequestsPerTimeFrame) + rl.Lock() + rl.clients[ip] = &Client{ + Limiter: limiter, + LastSeen: time.Now()} + + rl.Unlock() + } + + client := rl.clients[ip] + client.LastSeen = time.Now() + + return client +} + +func (rl *TokenBucketRateLimiter) Allow(ip string) (bool, float64) { + client := rl.getClient(ip) + + allowed := client.Limiter.Allow() + tokens := client.Limiter.Tokens() + + return allowed, tokens +} + +func (rl *TokenBucketRateLimiter) cleanupClients() { + for { + time.Sleep(time.Minute) + //log cleaning up clients + + rl.Lock() + for ip, client := range rl.clients { + if time.Since(client.LastSeen) > 3*time.Minute { // timeout period + delete(rl.clients, ip) + } + } + rl.Unlock() + } +}