diff --git a/lib/angel_security.dart b/lib/angel_security.dart index 2f1b6977..e9a28383 100644 --- a/lib/angel_security.dart +++ b/lib/angel_security.dart @@ -1,2 +1,3 @@ +export 'src/in_memory_rate_limiter.dart'; export 'src/rate_limiter.dart'; export 'src/rate_limiting_window.dart'; \ No newline at end of file diff --git a/lib/src/in_memory_rate_limiter.dart b/lib/src/in_memory_rate_limiter.dart new file mode 100644 index 00000000..d545b8b6 --- /dev/null +++ b/lib/src/in_memory_rate_limiter.dart @@ -0,0 +1,30 @@ +import 'dart:async'; +import 'package:angel_framework/angel_framework.dart'; +import 'rate_limiter.dart'; +import 'rate_limiting_window.dart'; + +/// A simple [RateLimiter] implementation that uses a simple in-memory map +/// to store rate limiting information. +class InMemoryRateLimiter extends RateLimiter { + /// A callback used to compute the current user. + final FutureOr Function(RequestContext, ResponseContext) getUser; + final _cache = >{}; + + InMemoryRateLimiter( + int maxPointsPerWindow, Duration windowDuration, this.getUser, + {String errorMessage}) + : super(maxPointsPerWindow, windowDuration, errorMessage: errorMessage); + + @override + FutureOr> getCurrentWindow( + RequestContext req, ResponseContext res, DateTime currentTime) async { + var user = await getUser(req, res); + return _cache[user] ??= RateLimitingWindow(user, currentTime, 0); + } + + @override + FutureOr updateCurrentWindow(RequestContext req, ResponseContext res, + RateLimitingWindow window, DateTime currentTime) { + _cache[window.user] = window; + } +} diff --git a/lib/src/rate_limiter.dart b/lib/src/rate_limiter.dart index f450aae5..7ece0fa3 100644 --- a/lib/src/rate_limiter.dart +++ b/lib/src/rate_limiter.dart @@ -34,12 +34,12 @@ abstract class RateLimiter { /// then you would return a window containing the current hour, /// and the number of requests the user has sent in the past hour. FutureOr> getCurrentWindow( - RequestContext req, ResponseContext res); + RequestContext req, ResponseContext res, DateTime currentTime); /// Updates the underlying store with information about the new /// [window] that the user is operating in. - FutureOr updateCurrentWindow( - RequestContext req, ResponseContext res, RateLimitingWindow window); + FutureOr updateCurrentWindow(RequestContext req, ResponseContext res, + RateLimitingWindow window, DateTime currentTime); /// Computes the amount of points that a given request will cost. This amount /// is then added to the amount of points that the user has already consumed @@ -96,11 +96,11 @@ abstract class RateLimiter { /// [maxPointsPerWindow]. Future handleRequest(RequestContext req, ResponseContext res) async { // Obtain information about the current window. - var currentWindow = await getCurrentWindow(req, res); + var now = DateTime.now().toUtc(); + var currentWindow = await getCurrentWindow(req, res, now); // Check if the rate limit has been exceeded. If so, reject the request. // To perform this check, we must first determine whether a new window // has begun since the previous request. - var now = DateTime.now().toUtc(); var currentWindowEnd = currentWindow.startTime.toUtc().add(windowDuration); // We must also compute the missing information about the current window, // so that we can relay that information to the client. @@ -119,7 +119,7 @@ abstract class RateLimiter { ..pointLimit = maxPointsPerWindow ..remainingPoints = remainingPoints < 0 ? 0 : remainingPoints ..resetTime = now.add(windowDuration); - await updateCurrentWindow(req, res, newWindow); + await updateCurrentWindow(req, res, newWindow, now); await sendWindowInformation(req, res, newWindow); } @@ -143,7 +143,7 @@ abstract class RateLimiter { if (currentWindow.remainingPoints < 0) { currentWindow.remainingPoints = 0; } - await updateCurrentWindow(req, res, currentWindow); + await updateCurrentWindow(req, res, currentWindow, now); await sendWindowInformation(req, res, currentWindow); }