ratelimit_redis.go 3.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122
  1. package middleware
  2. import (
  3. "context"
  4. "net/http"
  5. "strconv"
  6. "time"
  7. "github.com/redis/go-redis/v9"
  8. "github.com/ulule/limiter/v3"
  9. "github.com/ulule/limiter/v3/drivers/store/redisstore"
  10. chimiddleware "github.com/go-chi/chi/v5/middleware"
  11. )
  12. type RedisRateLimiter struct {
  13. instance *limiter.Limiter
  14. keyFunc func(*http.Request) string
  15. }
  16. type RateLimitConfig struct {
  17. Rate limiter.Rate
  18. KeyFunc func(*http.Request) string
  19. }
  20. func NewRedisRateLimiter(redisClient *redis.Client, config RateLimitConfig) (*RedisRateLimiter, error) {
  21. store, err := redisstore.NewStoreWithOptions(redisClient, limiter.StoreOptions{
  22. Prefix: "ratelimit",
  23. MaxRetry: 3,
  24. })
  25. if err != nil {
  26. return nil, err
  27. }
  28. instance := limiter.New(store, config.Rate)
  29. return &RedisRateLimiter{
  30. instance: instance,
  31. keyFunc: config.KeyFunc,
  32. }, nil
  33. }
  34. func (r *RedisRateLimiter) Middleware() func(http.Handler) http.Handler {
  35. return func(next http.Handler) http.Handler {
  36. return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
  37. key := r.keyFunc(req)
  38. ctx, cancel := context.WithTimeout(req.Context(), 100*time.Millisecond)
  39. defer cancel()
  40. limit, err := r.instance.Get(ctx, key)
  41. if err != nil {
  42. // Fail open - log error but allow request through
  43. next.ServeHTTP(w, req)
  44. return
  45. }
  46. w.Header().Set("X-RateLimit-Limit", strconv.FormatInt(limit.Limit, 10))
  47. w.Header().Set("X-RateLimit-Remaining", strconv.FormatInt(limit.Remaining, 10))
  48. w.Header().Set("X-RateLimit-Reset", strconv.FormatInt(limit.Reset, 10))
  49. if limit.Reached {
  50. writeRateLimitError(w, limit.Reset)
  51. return
  52. }
  53. next.ServeHTTP(w, req)
  54. })
  55. }
  56. }
  57. func writeRateLimitError(w http.ResponseWriter, reset int64) {
  58. w.Header().Set("Content-Type", "application/json")
  59. w.Header().Set("Retry-After", strconv.FormatInt(reset, 10))
  60. w.WriteHeader(http.StatusTooManyRequests)
  61. _, _ = w.Write([]byte(`{"error":"rate limit exceeded"}`))
  62. }
  63. func GetClientIP(r *http.Request) string {
  64. return chimiddleware.GetIP(r)
  65. }
  66. func KeyByIP(prefix string) func(*http.Request) string {
  67. return func(r *http.Request) string {
  68. return prefix + ":" + GetClientIP(r)
  69. }
  70. }
  71. func KeyByUserID(prefix string) func(*http.Request) string {
  72. return func(r *http.Request) string {
  73. userID := GetUserID(r.Context())
  74. if userID != "" {
  75. return prefix + ":user:" + userID
  76. }
  77. return prefix + ":ip:" + GetClientIP(r)
  78. }
  79. }
  80. func RateLimitAuthEndpoints(redisClient *redis.Client) (*RedisRateLimiter, error) {
  81. return NewRedisRateLimiter(redisClient, RateLimitConfig{
  82. Rate: limiter.Rate{Period: time.Minute, Limit: 10},
  83. KeyFunc: KeyByIP("auth"),
  84. })
  85. }
  86. func RateLimitAPIRead(redisClient *redis.Client) (*RedisRateLimiter, error) {
  87. return NewRedisRateLimiter(redisClient, RateLimitConfig{
  88. Rate: limiter.Rate{Period: time.Minute, Limit: 60},
  89. KeyFunc: KeyByUserID("api_read"),
  90. })
  91. }
  92. func RateLimitAPIWrite(redisClient *redis.Client) (*RedisRateLimiter, error) {
  93. return NewRedisRateLimiter(redisClient, RateLimitConfig{
  94. Rate: limiter.Rate{Period: time.Minute, Limit: 10},
  95. KeyFunc: KeyByUserID("api_write"),
  96. })
  97. }
  98. func RateLimitAdmin(redisClient *redis.Client) (*RedisRateLimiter, error) {
  99. return NewRedisRateLimiter(redisClient, RateLimitConfig{
  100. Rate: limiter.Rate{Period: time.Minute, Limit: 100},
  101. KeyFunc: KeyByUserID("admin"),
  102. })
  103. }