package acl //token buket concept: // every time the ip makes a request we take a token out of the bucket // if bucket empty-> request rejected // the bucket constantly refills at requestsPerSecond rate // so to be clear the buket contain at max burst (20) tokens, and fills requestsPerSecond (5) token/sec // RateLimiterConfig contains a dictionary with a client an his related ip // The mutext is used to make that only one request at a time can write client map import ( "clapclap/internal/apperr" "clapclap/internal/env" "sync" "time" "github.com/gin-gonic/gin" "golang.org/x/time/rate" ) type client struct { limiter *rate.Limiter lastSeen time.Time } type RateLimiterConfig struct { clients map[string]*client mu sync.Mutex rate rate.Limit burst int } // the bucket is filled automatically when the request is done without using cpu all the time func NewRateLimiter(requestsPerSecond float64, burst int) gin.HandlerFunc { cfg := &RateLimiterConfig{ clients: make(map[string]*client), rate: rate.Limit(requestsPerSecond), burst: burst, } // we want to clean unused ip adress to prevent oom go cfg.cleanupClients() return func(c *gin.Context) { identifier := c.ClientIP() // if a ip is not in the map we add it cfg.mu.Lock() clientRecord, found := cfg.clients[identifier] if !found { clientRecord = &client{ limiter: rate.NewLimiter(cfg.rate, cfg.burst), } cfg.clients[identifier] = clientRecord } // we add the time of the last request to the client clientRecord.lastSeen = time.Now() // Check if the bucket of the client is empty or not if !clientRecord.limiter.Allow() { cfg.mu.Unlock() apperr.AbortWithError(c, apperr.NewTooManyRequestsError(nil)) return } cfg.mu.Unlock() c.Next() } } // cleanupClients runs periodically to remove inactive IPs func (cfg *RateLimiterConfig) cleanupClients() { cleanupThreshold := env.C.RATE_LIMIT_CLEANUP_MINUTES for { time.Sleep(time.Minute) // Run cleanup every minute cfg.mu.Lock() for ip, c := range cfg.clients { // If a client hasn't been seen in 3 minutes, delete their limiter if time.Since(c.lastSeen) > cleanupThreshold { delete(cfg.clients, ip) } } cfg.mu.Unlock() } }