Finish basic implementation
This commit is contained in:
parent
2e2bf7c2ed
commit
3ee00e9e0f
2 changed files with 62 additions and 11 deletions
|
@ -67,12 +67,12 @@ abstract class RateLimiter<User> {
|
|||
'x-ratelimit-limit': window.pointLimit.toString(),
|
||||
'x-ratelimit-remaining': window.remainingPoints.toString(),
|
||||
'x-ratelimit-reset':
|
||||
(window.resetsAt.millisecondsSinceEpoch ~/ 1000).toString(),
|
||||
(window.resetTime.millisecondsSinceEpoch ~/ 1000).toString(),
|
||||
});
|
||||
}
|
||||
|
||||
/// Signals to a user that they have exceeded the rate limit for the
|
||||
/// current window.
|
||||
/// current window, and terminates execution of the current [RequestContext].
|
||||
///
|
||||
/// The default implementation is throw an [AngelHttpException] with
|
||||
/// status code `429` and the given `errorMessage`, as well as sending
|
||||
|
@ -82,7 +82,7 @@ abstract class RateLimiter<User> {
|
|||
/// Whatever is returned here will be returned in [handleRequest].
|
||||
FutureOr<Object> rejectRequest(RequestContext req, ResponseContext res,
|
||||
RateLimitingWindow<User> window, DateTime currentTime) {
|
||||
var retryAfter = window.resetsAt.difference(currentTime);
|
||||
var retryAfter = window.resetTime.difference(currentTime);
|
||||
res.headers['retry-after'] = retryAfter.inSeconds.toString();
|
||||
throw AngelHttpException(null, message: errorMessage, statusCode: 429);
|
||||
}
|
||||
|
@ -98,5 +98,56 @@ abstract class RateLimiter<User> {
|
|||
// Obtain information about the current window.
|
||||
var currentWindow = await getCurrentWindow(req, res);
|
||||
// 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.
|
||||
var remainingPoints = maxPointsPerWindow - currentWindow.pointsConsumed;
|
||||
currentWindow
|
||||
..pointLimit = maxPointsPerWindow
|
||||
..remainingPoints = remainingPoints < 0 ? 0 : remainingPoints
|
||||
..resetTime = currentWindow.startTime.add(windowDuration);
|
||||
|
||||
// If the previous window ended in the past, begin a new window.
|
||||
if (now.compareTo(currentWindowEnd) >= 0) {
|
||||
// Create a new window.
|
||||
var cost = await getEndpointCost(req, res, currentWindow);
|
||||
var remainingPoints = maxPointsPerWindow - cost;
|
||||
var newWindow = RateLimitingWindow(currentWindow.user, now, cost)
|
||||
..pointLimit = maxPointsPerWindow
|
||||
..remainingPoints = remainingPoints < 0 ? 0 : remainingPoints
|
||||
..resetTime = now.add(windowDuration);
|
||||
await updateCurrentWindow(req, res, newWindow);
|
||||
await sendWindowInformation(req, res, newWindow);
|
||||
}
|
||||
|
||||
// If we are still within the previous window, check if the user has
|
||||
// exceeded the rate limit.
|
||||
//
|
||||
// Otherwise, update the current window.
|
||||
//
|
||||
// We only use `>` (not `>=`), because at this point in the computation,
|
||||
// we are still only considering whether the *previous* request took the
|
||||
// user over the rate limit.
|
||||
else if (currentWindow.pointsConsumed > maxPointsPerWindow) {
|
||||
await sendWindowInformation(req, res, currentWindow);
|
||||
return await rejectRequest(req, res, currentWindow, now);
|
||||
} else {
|
||||
// Add the cost of the current endpoint, and update the window.
|
||||
var cost = await getEndpointCost(req, res, currentWindow);
|
||||
currentWindow
|
||||
..pointsConsumed += cost
|
||||
..remainingPoints -= cost;
|
||||
if (currentWindow.remainingPoints < 0) {
|
||||
currentWindow.remainingPoints = 0;
|
||||
}
|
||||
await updateCurrentWindow(req, res, currentWindow);
|
||||
await sendWindowInformation(req, res, currentWindow);
|
||||
}
|
||||
|
||||
// Pass through, so other handlers can be executed.
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,34 +2,34 @@
|
|||
/// a [user] is accessing some API or endpoint.
|
||||
class RateLimitingWindow<User> {
|
||||
/// The user who is accessing the endpoint.
|
||||
final User user;
|
||||
User user;
|
||||
|
||||
/// The time at which the user's current window began.
|
||||
final DateTime start;
|
||||
DateTime startTime;
|
||||
|
||||
/// The number of points the user has already consumed within
|
||||
/// the current window.
|
||||
final int pointsConsumed;
|
||||
int pointsConsumed;
|
||||
|
||||
/// The maximum amount of points allowed within a single window.
|
||||
///
|
||||
/// This field is typically only set by the [RateLimiter] middleware,
|
||||
/// and is therefore optional in the constructor.
|
||||
final int pointLimit;
|
||||
int pointLimit;
|
||||
|
||||
/// The amount of points the user can consume before hitting the
|
||||
/// rate limit for the current window.
|
||||
///
|
||||
/// This field is typically only set by the [RateLimiter] middleware,
|
||||
/// and is therefore optional in the constructor.
|
||||
final int remainingPoints;
|
||||
int remainingPoints;
|
||||
|
||||
/// The time at which the window will reset.
|
||||
///
|
||||
/// This field is typically only set by the [RateLimiter] middleware,
|
||||
/// and is therefore optional in the constructor.
|
||||
final DateTime resetsAt;
|
||||
DateTime resetTime;
|
||||
|
||||
RateLimitingWindow(this.user, this.start, this.pointsConsumed,
|
||||
{this.pointLimit, this.remainingPoints, this.resetsAt});
|
||||
RateLimitingWindow(this.user, this.startTime, this.pointsConsumed,
|
||||
{this.pointLimit, this.remainingPoints, this.resetTime});
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue