diff --git a/server/internal/service/service.go b/server/internal/service/service.go index eb3d6d6..3fca9a1 100644 --- a/server/internal/service/service.go +++ b/server/internal/service/service.go @@ -14,7 +14,10 @@ import ( "github.com/rs/zerolog/log" ) -const authRateLimit = 5 // req/min +const ( + authRateLimit = 5 // req/min + postAuthRateLimit = 100 // req/min +) type SvcConfig struct { JWTSecret string @@ -40,7 +43,7 @@ func Run(conn *pgx.Conn, q *data.Queries, config SvcConfig) error { authRouter := authResource{ Config: config, - RateLimiter: httprate.NewRateLimiter(authRateLimit, time.Minute), + RateLimiter: getRateLimiter(authRateLimit), Users: q, Tokens: q, } @@ -55,7 +58,7 @@ func Run(conn *pgx.Conn, q *data.Queries, config SvcConfig) error { r.Use(middleware.RequestID) r.Use(middleware.RealIP) r.Use(loggerMiddleware(&log.Logger)) - r.Use(httprate.LimitByIP(100, time.Minute)) // Base limit for all routes + r.Use(getRateLimiter(postAuthRateLimit).Handler) // Base limit applied globally r.Use(cors.Handler(cors.Options{ AllowedOrigins: config.allowedOrigins(), AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}, @@ -86,6 +89,14 @@ func Run(conn *pgx.Conn, q *data.Queries, config SvcConfig) error { return http.ListenAndServe(":8080", r) } +// Generate a new rate limiter that responds to surpassing clients with HTTP 429 + JSON error message. +func getRateLimiter(requestLimit int) *httprate.RateLimiter { + return httprate.NewRateLimiter(requestLimit, time.Minute, httprate.WithLimitHandler(func(w http.ResponseWriter, r *http.Request) { + // NOTE: Error message mustn't contain a dot due to one being added client-side anyway + respondError(w, http.StatusTooManyRequests, "You're rate-limited, please slow down") + })) +} + // Start worker that automatically cleans up the `notes` (cascading to `note_versions`) and // `refresh_tokens` tables from expired (or revoked) entries. The tasks run once during // initialization and then once an hour until the backend is shutdown.