mirror of
https://github.com/andwati/telecom-platform.git
synced 2026-11-12 06:55:15 +00:00
feat: Add comprehensive rate limiting middleware with IP-based, user-based, and endpoint-specific throttling strategies
- Add SimpleRateLimiter struct with requests map, mutex, limit, window fields for in-memory rate limit tracking - Add NewSimpleRateLimiter constructor and Allow method with time-based request cleanup and limit validation - Add global rate limiters: authRateLimiter (10/min), apiRateLimiter (100/min), readRateLimiter (200/min), adminRateLimiter (50/min) - Add RateLimit middleware
This commit is contained in:
@@ -0,0 +1,190 @@
|
|||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Simple rate limiter implementation using in-memory storage
|
||||||
|
type SimpleRateLimiter struct {
|
||||||
|
requests map[string][]time.Time
|
||||||
|
mutex sync.RWMutex
|
||||||
|
limit int
|
||||||
|
window time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewSimpleRateLimiter creates a new simple rate limiter
|
||||||
|
func NewSimpleRateLimiter(limit int, window time.Duration) *SimpleRateLimiter {
|
||||||
|
return &SimpleRateLimiter{
|
||||||
|
requests: make(map[string][]time.Time),
|
||||||
|
limit: limit,
|
||||||
|
window: window,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allow checks if a request is allowed based on the key
|
||||||
|
func (r *SimpleRateLimiter) Allow(key string) bool {
|
||||||
|
r.mutex.Lock()
|
||||||
|
defer r.mutex.Unlock()
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
|
||||||
|
// Clean old requests
|
||||||
|
if requests, exists := r.requests[key]; exists {
|
||||||
|
var validRequests []time.Time
|
||||||
|
for _, reqTime := range requests {
|
||||||
|
if now.Sub(reqTime) < r.window {
|
||||||
|
validRequests = append(validRequests, reqTime)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
r.requests[key] = validRequests
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if under limit
|
||||||
|
if len(r.requests[key]) < r.limit {
|
||||||
|
r.requests[key] = append(r.requests[key], now)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Global rate limiter instance
|
||||||
|
var (
|
||||||
|
authRateLimiter = NewSimpleRateLimiter(10, time.Minute) // 10 requests per minute for auth
|
||||||
|
apiRateLimiter = NewSimpleRateLimiter(100, time.Minute) // 100 requests per minute for general API
|
||||||
|
readRateLimiter = NewSimpleRateLimiter(200, time.Minute) // 200 requests per minute for read endpoints
|
||||||
|
adminRateLimiter = NewSimpleRateLimiter(50, time.Minute) // 50 requests per minute for admin endpoints
|
||||||
|
)
|
||||||
|
|
||||||
|
// RateLimit creates a rate limiting middleware
|
||||||
|
func RateLimit(requestsPerMinute int) gin.HandlerFunc {
|
||||||
|
limiter := NewSimpleRateLimiter(requestsPerMinute, time.Minute)
|
||||||
|
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
key := c.ClientIP()
|
||||||
|
|
||||||
|
if !limiter.Allow(key) {
|
||||||
|
c.JSON(http.StatusTooManyRequests, gin.H{
|
||||||
|
"error": "Rate limit exceeded",
|
||||||
|
"message": "Too many requests, please try again later",
|
||||||
|
})
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RateLimitByEndpoint creates different rate limits for different endpoint types
|
||||||
|
func RateLimitByEndpoint() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
path := c.Request.URL.Path
|
||||||
|
method := c.Request.Method
|
||||||
|
key := c.ClientIP()
|
||||||
|
|
||||||
|
var allowed bool
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case path == "/v1/auth/login" || path == "/v1/auth/register":
|
||||||
|
// Strict rate limiting for auth endpoints
|
||||||
|
allowed = authRateLimiter.Allow(key)
|
||||||
|
case method == "GET" && (path == "/v1/services" || path == "/v1/monitoring/metrics"):
|
||||||
|
// Lenient rate limiting for read-heavy endpoints
|
||||||
|
allowed = readRateLimiter.Allow(key)
|
||||||
|
case path == "/v1/users" || path == "/v1/config":
|
||||||
|
// Admin rate limiting for sensitive endpoints
|
||||||
|
allowed = adminRateLimiter.Allow(key)
|
||||||
|
default:
|
||||||
|
// Default rate limiting for other endpoints
|
||||||
|
allowed = apiRateLimiter.Allow(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !allowed {
|
||||||
|
c.JSON(http.StatusTooManyRequests, gin.H{
|
||||||
|
"error": "Rate limit exceeded",
|
||||||
|
"message": "Too many requests, please try again later",
|
||||||
|
})
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RateLimitByUser creates rate limiting based on authenticated user
|
||||||
|
func RateLimitByUser(requestsPerMinute int) gin.HandlerFunc {
|
||||||
|
limiter := NewSimpleRateLimiter(requestsPerMinute, time.Minute)
|
||||||
|
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
var key string
|
||||||
|
|
||||||
|
// Try to get user ID from context
|
||||||
|
if userID, exists := c.Get("user_id"); exists {
|
||||||
|
key = "user:" + userID.(string)
|
||||||
|
} else {
|
||||||
|
// Fallback to IP if not authenticated
|
||||||
|
key = "anon:" + c.ClientIP()
|
||||||
|
}
|
||||||
|
|
||||||
|
if !limiter.Allow(key) {
|
||||||
|
c.JSON(http.StatusTooManyRequests, gin.H{
|
||||||
|
"error": "Rate limit exceeded",
|
||||||
|
"message": "Too many requests, please try again later",
|
||||||
|
})
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RateLimitByIP creates rate limiting based on IP address only
|
||||||
|
func RateLimitByIP(requestsPerMinute int) gin.HandlerFunc {
|
||||||
|
limiter := NewSimpleRateLimiter(requestsPerMinute, time.Minute)
|
||||||
|
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
key := c.ClientIP()
|
||||||
|
|
||||||
|
if !limiter.Allow(key) {
|
||||||
|
c.JSON(http.StatusTooManyRequests, gin.H{
|
||||||
|
"error": "Rate limit exceeded",
|
||||||
|
"message": "Too many requests, please try again later",
|
||||||
|
})
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RateLimitWithHeaders adds rate limit headers to responses
|
||||||
|
func RateLimitWithHeaders(requestsPerMinute int) gin.HandlerFunc {
|
||||||
|
limiter := NewSimpleRateLimiter(requestsPerMinute, time.Minute)
|
||||||
|
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
key := c.ClientIP()
|
||||||
|
|
||||||
|
// Add rate limit headers
|
||||||
|
c.Header("X-RateLimit-Limit", string(rune(requestsPerMinute)))
|
||||||
|
c.Header("X-RateLimit-Window", "60")
|
||||||
|
|
||||||
|
if !limiter.Allow(key) {
|
||||||
|
c.JSON(http.StatusTooManyRequests, gin.H{
|
||||||
|
"error": "Rate limit exceeded",
|
||||||
|
"message": "Too many requests, please try again later",
|
||||||
|
})
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user