|
@@ -17,6 +17,7 @@ import (
|
|
|
chimiddleware "github.com/go-chi/chi/v5/middleware"
|
|
chimiddleware "github.com/go-chi/chi/v5/middleware"
|
|
|
"github.com/go-chi/cors"
|
|
"github.com/go-chi/cors"
|
|
|
"github.com/redis/go-redis/v9"
|
|
"github.com/redis/go-redis/v9"
|
|
|
|
|
+ "golang.org/x/time/rate"
|
|
|
|
|
|
|
|
"gogs.fxtmmsk.ru/foxtime/photoplaces/backend/internal/config"
|
|
"gogs.fxtmmsk.ru/foxtime/photoplaces/backend/internal/config"
|
|
|
"gogs.fxtmmsk.ru/foxtime/photoplaces/backend/internal/handlers"
|
|
"gogs.fxtmmsk.ru/foxtime/photoplaces/backend/internal/handlers"
|
|
@@ -87,16 +88,48 @@ func main() {
|
|
|
redisClient := redis.NewClient(redisOpts)
|
|
redisClient := redis.NewClient(redisOpts)
|
|
|
defer redisClient.Close()
|
|
defer redisClient.Close()
|
|
|
|
|
|
|
|
|
|
+ redisAvailable := true
|
|
|
if err := redisClient.Ping(ctx).Err(); err != nil {
|
|
if err := redisClient.Ping(ctx).Err(); err != nil {
|
|
|
- logger.WarnContext(ctx, "redis connection failed, rate limiting disabled", log.WithError(err))
|
|
|
|
|
|
|
+ logger.WarnContext(ctx, "redis connection failed, falling back to in-memory rate limiting", log.WithError(err))
|
|
|
|
|
+ redisAvailable = false
|
|
|
} else {
|
|
} else {
|
|
|
logger.InfoContext(ctx, "redis connected")
|
|
logger.InfoContext(ctx, "redis connected")
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- authLimiter, _ := middleware.RateLimitAuthEndpoints(redisClient)
|
|
|
|
|
- apiReadLimiter, _ := middleware.RateLimitAPIRead(redisClient)
|
|
|
|
|
- apiWriteLimiter, _ := middleware.RateLimitAPIWrite(redisClient)
|
|
|
|
|
- adminLimiter, _ := middleware.RateLimitAdmin(redisClient)
|
|
|
|
|
|
|
+ failOpen := cfg.AppEnv != "production"
|
|
|
|
|
+
|
|
|
|
|
+ var (
|
|
|
|
|
+ authLimiter rateLimiterInterface
|
|
|
|
|
+ apiReadLimiter rateLimiterInterface
|
|
|
|
|
+ apiWriteLimiter rateLimiterInterface
|
|
|
|
|
+ adminLimiter rateLimiterInterface
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ // Create rate limiters with Redis fallback to in-memory
|
|
|
|
|
+ authLimiter = newRateLimiterWithFallback(redisClient, redisAvailable, logger, failOpen,
|
|
|
|
|
+ func(client *redis.Client, log *slog.Logger, fo bool) (rateLimiterInterface, error) {
|
|
|
|
|
+ return middleware.RateLimitAuthEndpoints(client, log, fo)
|
|
|
|
|
+ },
|
|
|
|
|
+ func() *middleware.RateLimiter { return middleware.NewRateLimiter(rate.Limit(10/60.0), 10) }, // 10 req/min
|
|
|
|
|
+ )
|
|
|
|
|
+ apiReadLimiter = newRateLimiterWithFallback(redisClient, redisAvailable, logger, failOpen,
|
|
|
|
|
+ func(client *redis.Client, log *slog.Logger, fo bool) (rateLimiterInterface, error) {
|
|
|
|
|
+ return middleware.RateLimitAPIRead(client, log, fo)
|
|
|
|
|
+ },
|
|
|
|
|
+ func() *middleware.RateLimiter { return middleware.NewRateLimiter(rate.Limit(60/60.0), 60) }, // 60 req/min
|
|
|
|
|
+ )
|
|
|
|
|
+ apiWriteLimiter = newRateLimiterWithFallback(redisClient, redisAvailable, logger, failOpen,
|
|
|
|
|
+ func(client *redis.Client, log *slog.Logger, fo bool) (rateLimiterInterface, error) {
|
|
|
|
|
+ return middleware.RateLimitAPIWrite(client, log, fo)
|
|
|
|
|
+ },
|
|
|
|
|
+ func() *middleware.RateLimiter { return middleware.NewRateLimiter(rate.Limit(10/60.0), 10) }, // 10 req/min
|
|
|
|
|
+ )
|
|
|
|
|
+ adminLimiter = newRateLimiterWithFallback(redisClient, redisAvailable, logger, failOpen,
|
|
|
|
|
+ func(client *redis.Client, log *slog.Logger, fo bool) (rateLimiterInterface, error) {
|
|
|
|
|
+ return middleware.RateLimitAdmin(client, log, fo)
|
|
|
|
|
+ },
|
|
|
|
|
+ func() *middleware.RateLimiter { return middleware.NewRateLimiter(rate.Limit(100/60.0), 100) }, // 100 req/min
|
|
|
|
|
+ )
|
|
|
|
|
|
|
|
// Роутер
|
|
// Роутер
|
|
|
r := chi.NewRouter()
|
|
r := chi.NewRouter()
|
|
@@ -262,3 +295,28 @@ func requestLoggerMiddleware(logger *slog.Logger) func(http.Handler) http.Handle
|
|
|
})
|
|
})
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+// rateLimiterInterface defines the common interface for rate limiters
|
|
|
|
|
+type rateLimiterInterface interface {
|
|
|
|
|
+ Middleware() func(http.Handler) http.Handler
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// newRateLimiterWithFallback creates a rate limiter with Redis as primary and in-memory as fallback
|
|
|
|
|
+func newRateLimiterWithFallback(
|
|
|
|
|
+ redisClient *redis.Client,
|
|
|
|
|
+ redisAvailable bool,
|
|
|
|
|
+ logger *slog.Logger,
|
|
|
|
|
+ failOpen bool,
|
|
|
|
|
+ redisLimiterFn func(*redis.Client, *slog.Logger, bool) (rateLimiterInterface, error),
|
|
|
|
|
+ inMemoryLimiterFn func() *middleware.RateLimiter,
|
|
|
|
|
+) rateLimiterInterface {
|
|
|
|
|
+ if redisAvailable {
|
|
|
|
|
+ if limiter, err := redisLimiterFn(redisClient, logger, failOpen); err == nil {
|
|
|
|
|
+ logger.InfoContext(context.Background(), "using Redis rate limiter")
|
|
|
|
|
+ return limiter
|
|
|
|
|
+ }
|
|
|
|
|
+ logger.WarnContext(context.Background(), "failed to create Redis rate limiter, falling back to in-memory")
|
|
|
|
|
+ }
|
|
|
|
|
+ logger.InfoContext(context.Background(), "using in-memory rate limiter")
|
|
|
|
|
+ return inMemoryLimiterFn()
|
|
|
|
|
+}
|