From 4c0b21b10383de33e02631497c1a1d8f00ce0595 Mon Sep 17 00:00:00 2001 From: Sreelal TS Date: Sun, 15 Sep 2024 14:58:06 +0530 Subject: [PATCH 1/7] =?UTF-8?q?=F0=9F=94=A5=20Set=20fire=20to=20my=20rain?= =?UTF-8?q?=20Changed=20everything=20to=20cope=20with=20the=20new=20design?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../shelf_limiter_by_endpoint_example.dart | 42 +++++++++ example/shelf_limiter_example.dart | 19 ++-- lib/shelf_limiter.dart | 9 +- lib/src/classes/options.dart | 23 +++++ lib/src/{ => classes}/rate_limiter.dart | 2 +- lib/src/methods/handle_limiting.dart | 62 +++++++++++++ lib/src/methods/response_crafter.dart | 23 +++++ lib/src/options.dart | 63 ------------- lib/src/shelf_limiter_by_endpoint.dart | 45 ++++++++++ ...r_base.dart => shelf_limiter_default.dart} | 89 ++----------------- test/shelf_limiter_test.dart | 22 ++--- 11 files changed, 227 insertions(+), 172 deletions(-) create mode 100644 example/shelf_limiter_by_endpoint_example.dart create mode 100644 lib/src/classes/options.dart rename lib/src/{ => classes}/rate_limiter.dart (98%) create mode 100644 lib/src/methods/handle_limiting.dart create mode 100644 lib/src/methods/response_crafter.dart delete mode 100644 lib/src/options.dart create mode 100644 lib/src/shelf_limiter_by_endpoint.dart rename lib/src/{shelf_limiter_base.dart => shelf_limiter_default.dart} (53%) diff --git a/example/shelf_limiter_by_endpoint_example.dart b/example/shelf_limiter_by_endpoint_example.dart new file mode 100644 index 0000000..4e73b00 --- /dev/null +++ b/example/shelf_limiter_by_endpoint_example.dart @@ -0,0 +1,42 @@ +import 'dart:convert'; + +import 'package:shelf/shelf.dart'; +import 'package:shelf/shelf_io.dart' as io; +import 'package:shelf_limiter/shelf_limiter.dart'; + +void main() async { + // Define rate limiter options for specific endpoints + final endpointLimits = { + '/limited': RateLimiterOptions( + maxRequests: 5, + windowSize: const Duration(minutes: 1), + headers: {'X-Custom-Header': 'Rate limited'}, + onRateLimitExceeded: (request) async { + return Response( + 429, + body: jsonEncode({ + 'status': false, + 'message': 'Rate limit exceeded for /api/v1/limited', + }), + headers: {'Content-Type': 'application/json'}, + ); + }, + ), + }; + + // Create the rate limiter middleware with endpoint-specific options + final limiter = shelfLimiterByEndpoint( + endpointLimits: endpointLimits, + ); + + // Add the rate limiter to the pipeline and define a handler for incoming requests + final handler = + const Pipeline().addMiddleware(limiter).addHandler(_echoRequest); + + // Start the server on localhost and listen for incoming requests on port 8080 + var server = await io.serve(handler, 'localhost', 8080); + print('Server listening on port ${server.port}'); +} + +// Basic request handler that responds with 'Request received' +Response _echoRequest(Request request) => Response.ok('Request received'); diff --git a/example/shelf_limiter_example.dart b/example/shelf_limiter_example.dart index c2e4e41..ef187f6 100644 --- a/example/shelf_limiter_example.dart +++ b/example/shelf_limiter_example.dart @@ -5,34 +5,31 @@ import 'package:shelf/shelf_io.dart' as io; import 'package:shelf_limiter/shelf_limiter.dart'; void main() async { - // Additional customization (optional) - // Here we define custom headers and a custom response message for when the rate limit is exceeded + // Define custom rate limiter options final options = RateLimiterOptions( + maxRequests: 5, // Maximum number of requests allowed + windowSize: const Duration(minutes: 1), // Duration of the rate limit window headers: { 'X-Custom-Header': 'Rate limited', // Custom header to add to responses }, onRateLimitExceeded: (request) async { - // Custom message to return when the client exceeds the rate limit - // Customize it as much as you want :) + // Custom response when the rate limit is exceeded return Response( 429, body: jsonEncode({ 'status': false, - 'message': "Uh, hm! Wait a minute, that's a lot of request.", + 'message': "Uh, hm! Wait a minute, that's a lot of requests.", }), headers: { 'Content-Type': 'application/json', + 'X-Custom-Response-Header': 'CustomValue', // Additional custom header }, ); }, ); - // Create the rate limiter middleware with a max of 5 requests per 1 minute window - final limiter = shelfLimiter( - maxRequests: 5, - windowSize: const Duration(minutes: 1), - options: options, // Apply custom options - ); + // Create the rate limiter middleware with the custom options + final limiter = shelfLimiter(options); // Add the rate limiter to the pipeline and define a handler for incoming requests final handler = diff --git a/lib/shelf_limiter.dart b/lib/shelf_limiter.dart index 83bc8b2..4d3ad33 100644 --- a/lib/shelf_limiter.dart +++ b/lib/shelf_limiter.dart @@ -58,6 +58,9 @@ import 'dart:async'; import 'dart:collection'; import 'package:shelf/shelf.dart'; -part 'src/shelf_limiter_base.dart'; -part 'src/rate_limiter.dart'; -part 'src/options.dart'; +part 'src/shelf_limiter_default.dart'; +part 'src/shelf_limiter_by_endpoint.dart'; +part 'src/classes/rate_limiter.dart'; +part 'src/classes/options.dart'; +part 'src/methods/response_crafter.dart'; +part 'src/methods/handle_limiting.dart'; diff --git a/lib/src/classes/options.dart b/lib/src/classes/options.dart new file mode 100644 index 0000000..00affc1 --- /dev/null +++ b/lib/src/classes/options.dart @@ -0,0 +1,23 @@ +// ignore_for_file: public_member_api_docs, sort_constructors_first +part of '../../shelf_limiter.dart'; + +class RateLimiterOptions { + final int maxRequests; + final Duration windowSize; + final String Function(Request request)? clientIdentifierExtractor; + final FutureOr Function(Request request)? onRateLimitExceeded; + final Map headers; + + const RateLimiterOptions({ + required this.maxRequests, + required this.windowSize, + this.clientIdentifierExtractor, + this.onRateLimitExceeded, + this.headers = const {}, + }); + + @override + String toString() { + return 'RateLimiterOptions(maxRequests: $maxRequests, windowSize: $windowSize, clientIdentifierExtractor: $clientIdentifierExtractor, onRateLimitExceeded: $onRateLimitExceeded, headers: $headers)'; + } +} diff --git a/lib/src/rate_limiter.dart b/lib/src/classes/rate_limiter.dart similarity index 98% rename from lib/src/rate_limiter.dart rename to lib/src/classes/rate_limiter.dart index 3bd693c..175980e 100644 --- a/lib/src/rate_limiter.dart +++ b/lib/src/classes/rate_limiter.dart @@ -1,4 +1,4 @@ -part of '../shelf_limiter.dart'; +part of '../../shelf_limiter.dart'; /// A simple rate limiter that tracks and enforces request limits for clients. /// diff --git a/lib/src/methods/handle_limiting.dart b/lib/src/methods/handle_limiting.dart new file mode 100644 index 0000000..cbaf557 --- /dev/null +++ b/lib/src/methods/handle_limiting.dart @@ -0,0 +1,62 @@ +part of '../../shelf_limiter.dart'; + +Future _handleLimiting({ + required _RateLimiter rateLimiter, + required RateLimiterOptions options, + required Request request, + required Handler innerHandler, +}) async { + // Extract client identifier (IP by default) + final clientIdentifier = options.clientIdentifierExtractor != null + ? options.clientIdentifierExtractor!(request) + : ((request.context['shelf.io.connection_info']) as dynamic) + ?.remoteAddress + .address; + + // Check if the client has exceeded the rate limit + if (!rateLimiter.isAllowed(clientIdentifier)) { + // Retry after the window resets + final retryAfter = options.windowSize.inSeconds; + + // If a custom response is provided, apply rate limit headers to it + if (options.onRateLimitExceeded != null) { + final customResponse = await options.onRateLimitExceeded!(request); + + return _craftResponse( + customResponse.change( + headers: options.headers, + ), + options.maxRequests, + retryAfter, + ); + } + + // Default 429 response with custom rate limit headers + return _craftResponse( + Response( + 429, + body: 'Too many requests, please try again later.', + headers: options.headers, + ), + options.maxRequests, + retryAfter, + ); + } + + // Calculate remaining requests and reset time for rate limit headers + final now = DateTime.now(); + final requestTimes = rateLimiter._clientRequestTimes[clientIdentifier]!; + final resetTime = options.windowSize.inSeconds - + now.difference(requestTimes.first).inSeconds; + final remainingRequests = options.maxRequests - requestTimes.length; + + // Proceed with the request and attach rate limiting headers to the response + final response = await innerHandler(request); + + return _craftResponse( + response, + options.maxRequests, + resetTime, + remainingRequests: remainingRequests, + ); +} diff --git a/lib/src/methods/response_crafter.dart b/lib/src/methods/response_crafter.dart new file mode 100644 index 0000000..7df63f4 --- /dev/null +++ b/lib/src/methods/response_crafter.dart @@ -0,0 +1,23 @@ +part of '../../shelf_limiter.dart'; + +/// Adds rate limit related headers to the response. +/// +/// [response] is the original response object. +/// [maxRequests] is the maximum number of requests allowed in the window. +/// [retryAfter] is the number of seconds after which the client can retry. +/// [remainingRequests] is the number of requests remaining in the current window. +Response _craftResponse( + Response response, + int maxRequests, + int retryAfter, { + int? remainingRequests, +}) { + final responseHeaders = { + ...response.headers, + 'Retry-After': retryAfter.toString(), + 'X-RateLimit-Limit': maxRequests.toString(), + 'X-RateLimit-Remaining': remainingRequests?.toString() ?? '0', + 'X-RateLimit-Reset': retryAfter.toString(), + }; + return response.change(headers: responseHeaders); +} diff --git a/lib/src/options.dart b/lib/src/options.dart deleted file mode 100644 index ffa5111..0000000 --- a/lib/src/options.dart +++ /dev/null @@ -1,63 +0,0 @@ -part of '../shelf_limiter.dart'; - -/// A class to provide additional configuration options for the rate limiter middleware. -/// -/// This allows the you to customize the behavior of the rate limiter by providing -/// custom functions and headers, making it flexible and adaptable to different use cases. -class RateLimiterOptions { - /// A function to extract a unique client identifier from the [Request]. - /// - /// By default, the middleware will use the client's IP address as the identifier. - /// However, you can provide your own logic to extract a custom identifier, such as - /// a token, API key, or user ID from the request headers or body. - /// - /// Example: - /// ```dart - /// clientIdentifierExtractor: (Request request) => request.headers['X-Client-ID'] ?? 'unknown', - /// ``` - final String Function(Request request)? clientIdentifierExtractor; - - /// A function that handles the response when the client exceeds the rate limit. - /// - /// This allows you to provide a custom response when a rate limit violation occurs. - /// You can return a different status code, message, or even a custom JSON response. - /// - /// If not provided, the middleware will return a default 429 (Too Many Requests) - /// response with a simple text message. - /// - /// Example: - /// ```dart - /// onRateLimitExceeded: (Request request) async { - /// return Response(429, body: 'Custom rate limit message.'); - /// }, - /// ``` - final FutureOr Function(Request request)? onRateLimitExceeded; - - /// Custom headers to be added to the response when the rate limit is exceeded. - /// - /// You can use this to add additional headers to the rate limit exceeded response, - /// such as specific metadata or information about the rate limiting policy. - /// - /// These headers will be merged with the default rate limit headers, such as - /// `X-RateLimit-Limit`, `X-RateLimit-Remaining`, and `Retry-After`. - /// - /// Example: - /// ```dart - /// headers: { - /// 'X-Custom-Header': 'SomeValue', - /// }, - /// ``` - final Map headers; - - /// Creates an instance of [RateLimiterOptions]. - /// - /// All parameters are optional. If not provided, default behavior is used: - /// - The client identifier is extracted from the IP address. - /// - A default 429 response is returned when the rate limit is exceeded. - /// - No additional custom headers are added. - RateLimiterOptions({ - this.clientIdentifierExtractor, - this.onRateLimitExceeded, - this.headers = const {}, - }); -} diff --git a/lib/src/shelf_limiter_by_endpoint.dart b/lib/src/shelf_limiter_by_endpoint.dart new file mode 100644 index 0000000..12bd45d --- /dev/null +++ b/lib/src/shelf_limiter_by_endpoint.dart @@ -0,0 +1,45 @@ +part of '../shelf_limiter.dart'; + +Middleware shelfLimiterByEndpoint({ + required Map endpointLimits, + RateLimiterOptions? defaultOptions, +}) { + // Create and store rate limiters for each endpoint when the middleware is initialized + final rateLimiters = {}; + + endpointLimits.forEach((path, options) { + rateLimiters[path] = _RateLimiter( + maxRequests: options.maxRequests, + rateLimitDuration: options.windowSize, + ); + }); + + final defaultRateLimiter = defaultOptions != null + ? _RateLimiter( + maxRequests: defaultOptions.maxRequests, + rateLimitDuration: defaultOptions.windowSize, + ) + : null; + + return (Handler innerHandler) { + return (Request request) async { + final path = '/${request.url.path}'; + final rateLimiter = rateLimiters[path] ?? defaultRateLimiter; + + if (rateLimiter == null) { + // No rate limiter options available; proceed without rate limiting + return await innerHandler(request); + } + + // Get the appropriate RateLimiterOptions (used for client identifier and headers) + final options = endpointLimits[path] ?? defaultOptions!; + + return _handleLimiting( + rateLimiter: rateLimiter, + options: options, + request: request, + innerHandler: innerHandler, + ); + }; + }; +} diff --git a/lib/src/shelf_limiter_base.dart b/lib/src/shelf_limiter_default.dart similarity index 53% rename from lib/src/shelf_limiter_base.dart rename to lib/src/shelf_limiter_default.dart index 74c3405..247933f 100644 --- a/lib/src/shelf_limiter_base.dart +++ b/lib/src/shelf_limiter_default.dart @@ -74,93 +74,20 @@ part of '../shelf_limiter.dart'; /// - A custom 429 message is returned when the limit is exceeded. /// - Custom headers (like `X-Custom-Header`) are added to the response. /// Middleware to limit the rate of requests from clients. -Middleware shelfLimiter({ - required int maxRequests, - required Duration windowSize, - RateLimiterOptions? options, -}) { +Middleware shelfLimiter(RateLimiterOptions options) { final rateLimiter = _RateLimiter( - maxRequests: maxRequests, - rateLimitDuration: windowSize, + maxRequests: options.maxRequests, + rateLimitDuration: options.windowSize, ); return (Handler innerHandler) { return (Request request) async { - // Extract client identifier (IP by default) - final clientIdentifier = options?.clientIdentifierExtractor != null - ? options!.clientIdentifierExtractor!(request) - : ((request.context['shelf.io.connection_info']) as dynamic) - ?.remoteAddress - .address; - - // Check if the client has exceeded the rate limit - if (!rateLimiter.isAllowed(clientIdentifier)) { - // Retry after the window resets - final retryAfter = windowSize.inSeconds; - - // If a custom response is provided, apply rate limit headers to it - if (options?.onRateLimitExceeded != null) { - final customResponse = await options!.onRateLimitExceeded!(request); - - return _craftResponse( - customResponse.change( - headers: options.headers, - ), - maxRequests, - retryAfter, - ); - } - - // Default 429 response with custom rate limit headers - return _craftResponse( - Response( - 429, - body: 'Too many requests, please try again later.', - headers: options?.headers ?? {}, - ), - maxRequests, - retryAfter, - ); - } - - // Calculate remaining requests and reset time for rate limit headers - final now = DateTime.now(); - final requestTimes = rateLimiter._clientRequestTimes[clientIdentifier]!; - final resetTime = - windowSize.inSeconds - now.difference(requestTimes.first).inSeconds; - final remainingRequests = maxRequests - requestTimes.length; - - // Proceed with the request and attach rate limiting headers to the response - final response = await innerHandler(request); - - return _craftResponse( - response, - maxRequests, - resetTime, - remainingRequests: remainingRequests, + return _handleLimiting( + rateLimiter: rateLimiter, + options: options, + request: request, + innerHandler: innerHandler, ); }; }; } - -/// Adds rate limit related headers to the response. -/// -/// [response] is the original response object. -/// [maxRequests] is the maximum number of requests allowed in the window. -/// [retryAfter] is the number of seconds after which the client can retry. -/// [remainingRequests] is the number of requests remaining in the current window. -Response _craftResponse( - Response response, - int maxRequests, - int retryAfter, { - int? remainingRequests, -}) { - final responseHeaders = { - ...response.headers, - 'Retry-After': retryAfter.toString(), - 'X-RateLimit-Limit': maxRequests.toString(), - 'X-RateLimit-Remaining': remainingRequests?.toString() ?? '0', - 'X-RateLimit-Reset': retryAfter.toString(), - }; - return response.change(headers: responseHeaders); -} diff --git a/test/shelf_limiter_test.dart b/test/shelf_limiter_test.dart index 5549b88..0c22de6 100644 --- a/test/shelf_limiter_test.dart +++ b/test/shelf_limiter_test.dart @@ -13,13 +13,13 @@ void main() { setUp(() async { // Create a simple handler that just responds with 'OK' + final options = const RateLimiterOptions( + maxRequests: 2, + windowSize: Duration(seconds: 10), + ); + handler = const Pipeline() - .addMiddleware( - shelfLimiter( - maxRequests: 2, - windowSize: const Duration(seconds: 10), - ), - ) + .addMiddleware(shelfLimiter(options)) .addHandler((request) => Response.ok('OK')); // Start the server and setup the HTTP client @@ -71,6 +71,8 @@ void main() { test('Includes custom headers and responses', () async { // Set up custom options final options = RateLimiterOptions( + maxRequests: 2, + windowSize: const Duration(seconds: 10), headers: {'X-Custom-Header': 'Rate Limited'}, onRateLimitExceeded: (request) async { return Response( @@ -82,13 +84,7 @@ void main() { ); handler = const Pipeline() - .addMiddleware( - shelfLimiter( - maxRequests: 2, - windowSize: const Duration(seconds: 10), - options: options, - ), - ) + .addMiddleware(shelfLimiter(options)) .addHandler((request) => Response.ok('OK')); // Restart the server with new handler From 177863750d83ecad52f271bdbbf973519958a72b Mon Sep 17 00:00:00 2001 From: Sreelal TS Date: Sun, 15 Sep 2024 15:19:01 +0530 Subject: [PATCH 2/7] =?UTF-8?q?=F0=9F=A9=BA=20Detailed=20documentation=20f?= =?UTF-8?q?or=20all=20elements?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/shelf_limiter.dart | 130 +++++++++++++++--------- lib/src/classes/options.dart | 41 +++++++- lib/src/methods/handle_limiting.dart | 20 ++-- lib/src/shelf_limiter_by_endpoint.dart | 78 +++++++++++++++ lib/src/shelf_limiter_default.dart | 133 +++++++++++++------------ 5 files changed, 286 insertions(+), 116 deletions(-) diff --git a/lib/shelf_limiter.dart b/lib/shelf_limiter.dart index 4d3ad33..06c9f65 100644 --- a/lib/shelf_limiter.dart +++ b/lib/shelf_limiter.dart @@ -1,57 +1,97 @@ -/// # Shelf Limiter +/// # Shelf Limiter /// -/// A powerful and highly customizable rate limiter for shelf library, allowing you to easily manage and control request rates in your Dart server. +/// A powerful and highly customizable rate limiter for the [Shelf](https://pub.dev/packages/shelf) library, designed to help you control request rates and prevent abuse on your Dart server. /// -/// `shelf_limiter` is a middleware package for the [Shelf](https://pub.dev/packages/shelf) -/// library in Dart that provides rate limiting capabilities. It allows you to restrict -/// the number of requests a client can make to your server within a specified time window, -/// making it useful for preventing abuse and ensuring fair usage of your API. +/// `shelf_limiter` offers middleware to manage how frequently clients can make requests, allowing you to define and enforce rate limits across your API. Whether you're building a public API, protecting resources from overuse, or ensuring fair distribution of server resources, `shelf_limiter` is built to handle it seamlessly. /// -/// This package offers several customization options to suit your needs, including custom -/// headers and responses. It integrates seamlessly into your existing Shelf pipeline. +/// ## Features: +/// - Set a rate limit for your entire API or different limits for specific endpoints. +/// - Customize how clients are identified (by IP address, API token, or custom logic). +/// - Add custom responses when clients exceed the rate limit. +/// - Easily attach headers to responses indicating rate limit status (remaining requests, reset time). /// -/// ## Example +/// ## Installation: +/// Add `shelf_limiter` to your `pubspec.yaml`: /// +/// ```yaml +/// dependencies: +/// shelf_limiter: ^1.0.0 +/// ``` +/// +/// Import the package: /// ```dart -/// import 'dart:convert'; -/// import 'package:shelf/shelf.dart'; -/// import 'package:shelf/shelf_io.dart' as io; /// import 'package:shelf_limiter/shelf_limiter.dart'; +/// ``` +/// +/// ## Middleware Options: +/// +/// ### 1. `shelfLimiter` +/// +/// `shelfLimiter` applies a global rate limit across all incoming requests. It's a simple middleware for enforcing a single rate limit across your API. /// -/// void main() async { -/// final options = RateLimiterOptions( -/// headers: { -/// 'X-Custom-Header': 'Rate limited', -/// }, -/// onRateLimitExceeded: (request) async { -/// return Response( -/// 429, -/// body: jsonEncode({ -/// 'status': false, -/// 'message': "Uh, hm! Wait a minute, that's a lot of request.", -/// }), -/// headers: { -/// 'Content-Type': 'application/json', -/// }, -/// ); -/// }, -/// ); -/// -/// final limiter = shelfLimiter( -/// maxRequests: 5, -/// windowSize: const Duration(minutes: 1), -/// options: options, -/// ); -/// -/// final handler = -/// const Pipeline().addMiddleware(limiter).addHandler(_echoRequest); -/// -/// var server = await io.serve(handler, 'localhost', 8080); -/// print('Server listening on port ${server.port}'); -/// } -/// -/// Response _echoRequest(Request request) => Response.ok('Request received'); +/// #### Example Usage: +/// +/// ```dart +/// final limiterMiddleware = shelfLimiter( +/// RateLimiterOptions( +/// maxRequests: 10, +/// windowSize: Duration(minutes: 1), +/// ), +/// ); +/// +/// final handler = const Pipeline() +/// .addMiddleware(limiterMiddleware) +/// .addHandler(yourHandler); /// ``` +/// +/// In this example, any client is limited to 10 requests per minute. If a client exceeds this limit, they will receive a `429 Too Many Requests` response. +/// +/// ### 2. `shelfLimiterByEndpoint` +/// +/// `shelfLimiterByEndpoint` allows you to define different rate limits for different endpoints. This is useful if you want tighter restrictions on certain routes, like authentication or resource-heavy operations, while keeping more lenient limits on less critical parts of your API. +/// +/// #### Example Usage: +/// +/// ```dart +/// final limiterMiddleware = shelfLimiterByEndpoint( +/// endpointLimits: { +/// '/auth': RateLimiterOptions( +/// maxRequests: 5, +/// windowSize: Duration(minutes: 1), +/// ), +/// '/data': RateLimiterOptions( +/// maxRequests: 20, +/// windowSize: Duration(minutes: 1), +/// ), +/// }, +/// defaultOptions: RateLimiterOptions( +/// maxRequests: 100, +/// windowSize: Duration(minutes: 1), +/// ), +/// ); +/// +/// final handler = const Pipeline() +/// .addMiddleware(limiterMiddleware) +/// .addHandler(yourHandler); +/// ``` +/// +/// In this example, the `/auth` route has a rate limit of 5 requests per minute, while the `/data` route allows up to 20 requests. Any other endpoints follow the default limit of 100 requests per minute. +/// +/// ## Customization: +/// +/// Both middlewares are highly customizable: +/// - **Client Identification**: By default, clients are identified by their IP address, but you can customize this with your own logic (e.g., based on API tokens). +/// - **Custom Response**: You can define a custom response that is returned when a client exceeds the rate limit, including headers or a more user-friendly message. +/// - **Rate Limit Headers**: The middlewares automatically add headers to indicate the remaining requests and the reset time, giving clients clear feedback about their rate limit status. +/// +/// ## Source Code and Contributions: +/// If you want to dive deeper into the code or contribute, check out the project on GitHub: +/// +/// +/// +/// +/// +/// Contributions are welcome! Feel free to open issues or submit pull requests. library; import 'dart:async'; diff --git a/lib/src/classes/options.dart b/lib/src/classes/options.dart index 00affc1..a94fd3b 100644 --- a/lib/src/classes/options.dart +++ b/lib/src/classes/options.dart @@ -1,13 +1,49 @@ -// ignore_for_file: public_member_api_docs, sort_constructors_first part of '../../shelf_limiter.dart'; +/// Configuration options for the rate limiter middleware. +/// +/// This class allows you to customize the rate limiting behavior, including +/// the maximum number of requests allowed, the time window for rate limiting, +/// and how to handle exceeded rate limits. class RateLimiterOptions { + /// The maximum number of requests a client can make within the [windowSize]. + /// + /// If the client exceeds this limit, further requests will be blocked + /// until the window resets. final int maxRequests; + + /// The duration in which the client can make [maxRequests] requests. + /// + /// Once this window expires, the request count for the client will reset. final Duration windowSize; + + /// A function that extracts a unique identifier for the client, typically + /// based on the request. By default, it uses the client's IP address. + /// + /// You can provide a custom function to extract a different identifier, + /// such as an API key or a user ID from the request. final String Function(Request request)? clientIdentifierExtractor; + + /// A callback function that is triggered when a client exceeds the rate limit. + /// + /// This function allows you to customize the response sent when a client + /// exceeds the rate limit. If this is not provided, a default 429 "Too many + /// requests" response will be used. final FutureOr Function(Request request)? onRateLimitExceeded; + + /// Custom headers to include in the response when a rate limit is exceeded. + /// + /// These headers can be used to provide additional information, such as + /// rate limit reset times or remaining requests. Defaults to an empty map. final Map headers; + /// Constructs a set of options for the rate limiter middleware. + /// + /// - [maxRequests]: The maximum number of requests allowed in the [windowSize]. + /// - [windowSize]: The time duration for the rate limit window. + /// - [clientIdentifierExtractor]: Optional function to extract a client identifier from the request. + /// - [onRateLimitExceeded]: Optional function to provide a custom response when the rate limit is exceeded. + /// - [headers]: Optional custom headers to include in the rate limit response. const RateLimiterOptions({ required this.maxRequests, required this.windowSize, @@ -16,6 +52,9 @@ class RateLimiterOptions { this.headers = const {}, }); + /// Returns a string representation of the rate limiter options. + /// + /// Useful for logging and debugging purposes. @override String toString() { return 'RateLimiterOptions(maxRequests: $maxRequests, windowSize: $windowSize, clientIdentifierExtractor: $clientIdentifierExtractor, onRateLimitExceeded: $onRateLimitExceeded, headers: $headers)'; diff --git a/lib/src/methods/handle_limiting.dart b/lib/src/methods/handle_limiting.dart index cbaf557..d32fc25 100644 --- a/lib/src/methods/handle_limiting.dart +++ b/lib/src/methods/handle_limiting.dart @@ -1,27 +1,33 @@ part of '../../shelf_limiter.dart'; +/// Handles rate limiting for a specific client based on the provided options. +/// +/// If the client exceeds the allowed requests within the time window, it responds +/// with a 429 status code, or a custom response if provided. Otherwise, it proceeds +/// with the request and attaches rate limit headers to the response. Future _handleLimiting({ required _RateLimiter rateLimiter, required RateLimiterOptions options, required Request request, required Handler innerHandler, }) async { - // Extract client identifier (IP by default) + // Extract the client's identifier (usually their IP address by default). final clientIdentifier = options.clientIdentifierExtractor != null ? options.clientIdentifierExtractor!(request) : ((request.context['shelf.io.connection_info']) as dynamic) ?.remoteAddress .address; - // Check if the client has exceeded the rate limit + // Check if the client has exceeded their request limit. if (!rateLimiter.isAllowed(clientIdentifier)) { - // Retry after the window resets + // Time in seconds until the rate limit resets. final retryAfter = options.windowSize.inSeconds; - // If a custom response is provided, apply rate limit headers to it + // If a custom response is set when the limit is exceeded, use it. if (options.onRateLimitExceeded != null) { final customResponse = await options.onRateLimitExceeded!(request); + // Add rate limit headers to the custom response. return _craftResponse( customResponse.change( headers: options.headers, @@ -31,7 +37,7 @@ Future _handleLimiting({ ); } - // Default 429 response with custom rate limit headers + // Default response: 429 Too Many Requests, with custom headers if provided. return _craftResponse( Response( 429, @@ -43,14 +49,14 @@ Future _handleLimiting({ ); } - // Calculate remaining requests and reset time for rate limit headers + // Calculate how many requests the client has left and the time until the limit resets. final now = DateTime.now(); final requestTimes = rateLimiter._clientRequestTimes[clientIdentifier]!; final resetTime = options.windowSize.inSeconds - now.difference(requestTimes.first).inSeconds; final remainingRequests = options.maxRequests - requestTimes.length; - // Proceed with the request and attach rate limiting headers to the response + // Continue processing the request and attach rate limiting headers to the response. final response = await innerHandler(request); return _craftResponse( diff --git a/lib/src/shelf_limiter_by_endpoint.dart b/lib/src/shelf_limiter_by_endpoint.dart index 12bd45d..9286ee7 100644 --- a/lib/src/shelf_limiter_by_endpoint.dart +++ b/lib/src/shelf_limiter_by_endpoint.dart @@ -1,5 +1,83 @@ part of '../shelf_limiter.dart'; +/// A middleware that applies rate limiting based on specific endpoint limits. +/// +/// This middleware allows developers to set different rate limits for different +/// API endpoints. You can also provide a default rate limiter if some endpoints +/// don't have custom limits. +/// +/// If the client exceeds the allowed requests for an endpoint, the middleware +/// responds with a `429 Too Many Requests` status or a custom response if specified. +/// +/// ## Parameters: +/// - `endpointLimits`: A map where the keys are endpoint paths (as strings) and +/// the values are `RateLimiterOptions` specifying rate limits for each endpoint. +/// - `defaultOptions`: Optional. If provided, this applies rate limiting for any +/// endpoint that isn't explicitly listed in `endpointLimits`. +/// +/// ## Returns: +/// - A `Middleware` that can be applied to your API handler. +/// +/// ## Example - Basic Usage: +/// ```dart +/// // Apply a rate limit of 5 requests per minute for the `/api/resource` endpoint +/// // and 10 requests per minute for `/api/data`. +/// final limiterMiddleware = shelfLimiterByEndpoint( +/// endpointLimits: { +/// '/api/resource': RateLimiterOptions( +/// maxRequests: 5, +/// windowSize: Duration(minutes: 1), +/// ), +/// '/api/data': RateLimiterOptions( +/// maxRequests: 10, +/// windowSize: Duration(minutes: 1), +/// ), +/// }, +/// ); +/// +/// // Apply the middleware to your handler +/// final handler = const Pipeline() +/// .addMiddleware(limiterMiddleware) +/// .addHandler(yourHandler); +/// ``` +/// +/// ## Example - Advanced Usage with Client Identifier and Custom Response: +/// ```dart +/// // Apply rate limits to multiple endpoints with custom client identification (by token) +/// // and a custom response when the rate limit is exceeded. +/// final limiterMiddleware = shelfLimiterByEndpoint( +/// endpointLimits: { +/// '/api/resource': RateLimiterOptions( +/// maxRequests: 5, +/// windowSize: Duration(minutes: 1), +/// clientIdentifierExtractor: (request) => request.headers['X-Client-Token']!, +/// onRateLimitExceeded: (request) async => Response.forbidden('Rate limit exceeded'), +/// ), +/// '/api/data': RateLimiterOptions( +/// maxRequests: 10, +/// windowSize: Duration(minutes: 1), +/// ), +/// }, +/// defaultOptions: RateLimiterOptions( +/// maxRequests: 15, +/// windowSize: Duration(minutes: 5), +/// ), +/// ); +/// +/// // Apply the middleware to your handler +/// final handler = const Pipeline() +/// .addMiddleware(limiterMiddleware) +/// .addHandler(yourHandler); +/// ``` +/// +/// In this advanced example, a custom client identifier is extracted from the +/// `X-Client-Token` header, and a custom error message is returned when the +/// rate limit is exceeded for the `/api/resource` endpoint. +/// +/// ## Notes: +/// - The `endpointLimits` map allows you to configure different rate limits for each endpoint. +/// - The `defaultOptions` is optional but useful for covering any endpoints that aren't listed in `endpointLimits`. +/// - If the `clientIdentifierExtractor` is not specified, the client's IP address is used by default. Middleware shelfLimiterByEndpoint({ required Map endpointLimits, RateLimiterOptions? defaultOptions, diff --git a/lib/src/shelf_limiter_default.dart b/lib/src/shelf_limiter_default.dart index 247933f..35344a8 100644 --- a/lib/src/shelf_limiter_default.dart +++ b/lib/src/shelf_limiter_default.dart @@ -1,79 +1,86 @@ part of '../shelf_limiter.dart'; -/// A middleware function that applies rate limiting to incoming HTTP requests. +/// A middleware that applies rate limiting to all incoming requests using the provided options. /// -/// This middleware tracks the number of requests a client makes within a specified -/// time window and limits how many requests are allowed. It can be customized with -/// options to handle specific client identification and responses when the rate limit -/// is exceeded. +/// This middleware enforces a global rate limit on all requests. If the number of +/// requests from a client exceeds the specified `maxRequests` within the `windowSize` +/// time frame, the middleware will block further requests until the time window resets. /// /// ## Parameters: -/// -/// - `maxRequests`: The maximum number of requests a client can make within the time window. -/// - `windowSize`: The duration for which requests are counted. After this window, -/// the count resets. -/// - `options` (optional): An instance of [RateLimiterOptions] to customize client -/// identification, exceeded limit responses, and headers. +/// - `options`: An instance of `RateLimiterOptions` that defines the rate limiting behavior. +/// This includes the maximum number of requests, the time window for the rate limit, +/// and optional custom behavior for identifying clients and handling rate limit violations. /// /// ## Returns: +/// - A `Middleware` that can be applied to your API handler. /// -/// A `Middleware` that can be applied to a `Handler` to enforce rate limits. -/// -/// ## Behavior: -/// -/// The middleware extracts a unique identifier for each client (IP address by default), -/// tracks the number of requests they make within the specified `windowSize`, and rejects -/// further requests if the limit (`maxRequests`) is exceeded. -/// -/// When the rate limit is exceeded, it can return a default 429 (Too Many Requests) response -/// with rate limiting headers, or a custom response if provided in the `RateLimiterOptions`. -/// -/// ## Headers: -/// -/// The middleware adds rate limiting headers to both the exceeded responses and normal -/// responses: -/// -/// - `X-RateLimit-Limit`: The maximum number of allowed requests. -/// - `X-RateLimit-Remaining`: The number of remaining requests for the current window. -/// - `X-RateLimit-Reset`: Time (in seconds) until the rate limit count resets. -/// - `Retry-After`: The time (in seconds) after which the client can retry (only when -/// rate limit is exceeded). -/// -/// ## Example: -/// +/// ## Example - Basic Usage: /// ```dart -/// import 'package:shelf/shelf.dart'; -/// import 'package:shelf/shelf_io.dart' as io; -/// -/// void main() async { -/// final handler = const Pipeline() -/// .addMiddleware(shelfLimiter( -/// maxRequests: 5, -/// windowSize: Duration(minutes: 1), -/// options: RateLimiterOptions( -/// headers: { -/// 'X-Custom-Header': 'Rate limited', -/// }, -/// onRateLimitExceeded: (Request request) async { -/// return Response(429, body: 'Custom message: Too many requests!'); -/// }, -/// ), -/// )) -/// .addHandler(_echoRequest); +/// // Apply a rate limit of 10 requests per minute for all incoming requests. +/// final limiterMiddleware = shelfLimiter( +/// RateLimiterOptions( +/// maxRequests: 10, +/// windowSize: Duration(minutes: 1), +/// ), +/// ); +/// +/// // Use the middleware in your request pipeline. +/// final handler = const Pipeline() +/// .addMiddleware(limiterMiddleware) +/// .addHandler(yourHandler); +/// ``` /// -/// var server = await io.serve(handler, 'localhost', 8080); -/// print('Server listening on port ${server.port}'); -/// } +/// In this basic example, the middleware allows up to 10 requests per minute +/// from each client. Any requests beyond that within the same minute will be blocked, +/// and the client will receive a `429 Too Many Requests` response. /// -/// Response _echoRequest(Request request) => Response.ok('Request received'); +/// ## Example - Advanced Usage with Custom Response and Headers: +/// ```dart +/// final options = RateLimiterOptions( +/// maxRequests: 5, // Maximum number of requests allowed +/// windowSize: const Duration(minutes: 1), // Duration of the rate limit window +/// headers: { +/// 'X-Custom-Header': 'Rate limited', // Custom header to add to all responses +/// }, +/// onRateLimitExceeded: (request) async { +/// // Custom response when the rate limit is exceeded +/// return Response( +/// 429, +/// body: jsonEncode({ +/// 'status': false, +/// 'message': "Uh, hm! Wait a minute, that's a lot of requests.", +/// }), +/// headers: { +/// 'Content-Type': 'application/json', +/// 'X-Custom-Response-Header': 'CustomValue', // Additional custom header +/// }, +/// ); +/// }, +/// ); +/// +/// // Apply the rate limiter middleware with custom options. +/// final limiterMiddleware = shelfLimiter(options); +/// +/// // Use the middleware in your request pipeline. +/// final handler = const Pipeline() +/// .addMiddleware(limiterMiddleware) +/// .addHandler(yourHandler); /// ``` /// -/// In this example: -/// - The client is allowed 5 requests every 1 minute. -/// - The `clientIdentifierExtractor` extracts a custom client ID from the request header `X-Client-ID`. -/// - A custom 429 message is returned when the limit is exceeded. -/// - Custom headers (like `X-Custom-Header`) are added to the response. -/// Middleware to limit the rate of requests from clients. +/// In the advanced example, the rate limit allows up to 5 requests per minute. +/// If the limit is exceeded, a custom response is returned with a custom message and headers. +/// Additionally, all responses (whether rate-limited or not) will include a custom header +/// (`X-Custom-Header: Rate limited`). +/// +/// ## Notes: +/// - **Client identification**: By default, the client is identified by their IP address. +/// You can customize this behavior by providing a `clientIdentifierExtractor` in the options +/// to identify clients based on other criteria, like API tokens or session IDs. +/// - **Custom response on limit exceed**: You can provide a custom response to be returned when +/// the rate limit is exceeded using the `onRateLimitExceeded` option. This is useful when you +/// want to return more user-friendly or detailed error messages. +/// - **Rate limit headers**: The middleware automatically attaches rate limiting headers, such as +/// how many requests remain and how long until the rate limit resets. Middleware shelfLimiter(RateLimiterOptions options) { final rateLimiter = _RateLimiter( maxRequests: options.maxRequests, From 4827c52883caadda55e4923777deca65460cbe90 Mon Sep 17 00:00:00 2001 From: Sreelal TS Date: Sun, 15 Sep 2024 15:31:01 +0530 Subject: [PATCH 3/7] =?UTF-8?q?=F0=9F=A6=8B=20Updated=20README?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 79 ++++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 66 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index c1684fe..620602f 100644 --- a/README.md +++ b/README.md @@ -12,9 +12,9 @@ ---- +--- -`shelf_limiter` is a powerful and highly customizable middleware package for the [shelf](https://pub.dev/packages/shelf) library in Dart that enables efficient rate limiting. Protect your API from abuse and ensure fair usage with ease. +`shelf_limiter` is a powerful and highly customizable middleware package for the [Shelf](https://pub.dev/packages/shelf) library in Dart that enables efficient rate limiting. Protect your API from abuse and ensure fair usage with ease. ## 🌟 Features @@ -48,7 +48,7 @@ dart pub add shelf_limiter ### 🔧 Basic Usage -Implement rate limiting in your Shelf application quickly and effectively. Here’s a straightforward example: +Implement rate limiting in your Shelf application quickly and effectively. Here’s a straightforward example using the `shelfLimiter` middleware: ```dart import 'package:shelf/shelf.dart'; @@ -57,8 +57,10 @@ import 'package:shelf_limiter/shelf_limiter.dart'; void main() async { final limiter = shelfLimiter( - maxRequests: 5, - windowSize: const Duration(minutes: 1), + RateLimiterOptions( + maxRequests: 5, + windowSize: const Duration(minutes: 1), + ), ); final handler = @@ -71,9 +73,11 @@ void main() async { Response _echoRequest(Request request) => Response.ok('Request received'); ``` +In this example, any client is limited to 5 requests per minute. If a client exceeds this limit, they will receive a `429 Too Many Requests` response. + ### 🛠️ Enhance Your API with Custom Headers -Add extra details to your responses with custom headers. Here’s how: +Add extra details to your responses with custom headers using `shelfLimiter`: ```dart import 'dart:convert'; @@ -89,9 +93,11 @@ void main() async { ); final limiter = shelfLimiter( - maxRequests: 5, - windowSize: const Duration(minutes: 1), - options: options, + RateLimiterOptions( + maxRequests: 5, + windowSize: const Duration(minutes: 1), + headers: options.headers, + ), ); final handler = @@ -124,7 +130,7 @@ void main() async { 429, body: jsonEncode({ 'status': false, - 'message': "Uh, hm! Wait a minute, that's a lot of request.", + 'message': "Uh, hm! Wait a minute, that's a lot of requests.", }), headers: { 'Content-Type': 'application/json', @@ -134,9 +140,54 @@ void main() async { ); final limiter = shelfLimiter( - maxRequests: 5, - windowSize: const Duration(minutes: 1), - options: options, + RateLimiterOptions( + maxRequests: 5, + windowSize: const Duration(minutes: 1), + headers: options.headers, + onRateLimitExceeded: options.onRateLimitExceeded, + ), + ); + + final handler = + const Pipeline().addMiddleware(limiter).addHandler(_echoRequest); + + var server = await io.serve(handler, 'localhost', 8080); + print('Server listening on port ${server.port}'); +} + +Response _echoRequest(Request request) => Response.ok('Request received'); +``` + +## 📌 Advanced Usage with Endpoint-Specific Limits + +When you want to fine-tune your rate limiting strategy and avoid a one-size-fits-all approach, `shelfLimiterByEndpoint` is your best friend. This middleware allows you to set unique rate limits for different endpoints, giving you the power to tailor restrictions based on the needs of each route. Think of it as customizing speed limits for different roads in your neighborhood—some streets are just busier than others! + + +### Example - Custom Limits for Different Routes: + +Here's how you can make your API as efficient as a well-oiled machine with `shelfLimiterByEndpoint`: + +```dart +import 'package:shelf/shelf.dart'; +import 'package:shelf/shelf_io.dart' as io; +import 'package:shelf_limiter/shelf_limiter.dart'; + +void main() async { + final limiter = shelfLimiterByEndpoint( + endpointLimits: { + '/auth': RateLimiterOptions( + maxRequests: 5, + windowSize: const Duration(minutes: 1), + ), + '/data': RateLimiterOptions( + maxRequests: 20, + windowSize: const Duration(minutes: 1), + ), + }, + defaultOptions: RateLimiterOptions( + maxRequests: 100, + windowSize: const Duration(minutes: 1), + ), ); final handler = @@ -149,6 +200,8 @@ void main() async { Response _echoRequest(Request request) => Response.ok('Request received'); ``` +In this advanced example, the `/auth` endpoint has a rate limit of 5 requests per minute, the `/data` endpoint allows up to 20 requests, and all other endpoints follow the default limit of 100 requests per minute. + ## ⚙️ Configuration ### Rate Limiter Options From 68c5dabf476fef87a3a00cb0f0ce2dd953b2c53d Mon Sep 17 00:00:00 2001 From: Sreelal TS Date: Sun, 15 Sep 2024 15:33:47 +0530 Subject: [PATCH 4/7] =?UTF-8?q?=E2=9C=A8=20Version=20logs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 5 +++++ pubspec.yaml | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a11568..2d328d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +# 2.0.0 + +- Added `shelfLimiterByEndpoint` for implementing different rate limits for different endpoints. +- Breaking: Updated `shelfLimiter` definition. + # 1.0.2 - Added more documentations diff --git a/pubspec.yaml b/pubspec.yaml index c43433c..0f1fbd9 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: shelf_limiter description: A powerful and highly customizable rate limiter for shelf library, allowing you to easily manage and control request rates in your Dart server. -version: 1.0.2 +version: 2.0.0 repository: https://github.com/xooniverse/shelf_limiter issue_tracker: https://github.com/xooniverse/shelf_limiter/issues documentation: https://pub.dev/documentation/shelf_limiter/latest From b1c4998058cbd085ef18e2bdfa97a236ea66bd32 Mon Sep 17 00:00:00 2001 From: Sreelal TS Date: Sun, 15 Sep 2024 15:42:33 +0530 Subject: [PATCH 5/7] =?UTF-8?q?=F0=9F=9A=80=20Updated=20docs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 620602f..53554c8 100644 --- a/README.md +++ b/README.md @@ -18,11 +18,17 @@ ## 🌟 Features -- **🔧 Customizable Rate Limits**: Effortlessly set the maximum number of requests and time window to suit your needs. -- **📜 Custom Headers**: Add and manage custom headers in your responses to enhance control and transparency. -- **🚀 Custom Responses**: Craft personalized responses when the rate limit is exceeded, improving user feedback. -- **🔗 Easy Integration**: Integrate seamlessly into your existing Shelf pipeline with minimal setup, so you can focus on building features. +| **Feature** | **Description** | +|---------------------------------|------------------| +| **🔧 Customizable Rate Limits** | Effortlessly set the maximum number of requests and time window to suit your needs. Define global limits or different limits for specific endpoints to control how often clients can access your API. | +| **📜 Custom Headers** | Add and manage custom headers in your responses to enhance control and transparency. | +| **🚀 Custom Responses** | Looking for more control? You’ve got it! Customize and send your own response when the API limit is exceeded. | +| **🔗 Easy Integration** | Integrate seamlessly into your existing Shelf pipeline with minimal setup. Quickly apply rate limiting and focus on building the features that matter most without worrying about complex configurations. | +| **🌐 Endpoint-Specific Limits** | Set different rate limits for different endpoints. Protect high-traffic routes with stricter limits while allowing more leniency on less critical parts of your API. | +--- + +This format provides a clear and organized way to present the features, making it easy for readers to understand the capabilities of `shelf_limiter` at a glance. ## Installation Add `shelf_limiter` to your `pubspec.yaml` file: From 54629cf619824aeff8d9ad65b7f2f5a39b6ba40e Mon Sep 17 00:00:00 2001 From: Sreelal TS Date: Sun, 15 Sep 2024 15:54:32 +0530 Subject: [PATCH 6/7] =?UTF-8?q?=F0=9F=83=8F=20Joker=20&=20the=20Queen=20Wi?= =?UTF-8?q?ldcard=20pattern=20matching=20support?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- example/endpoint_wildcard_example.dart | 74 ++++++++++++++++++++++++++ lib/shelf_limiter.dart | 1 + lib/src/methods/path_matcher.dart | 18 +++++++ lib/src/shelf_limiter_by_endpoint.dart | 48 ++++++++++++----- 4 files changed, 129 insertions(+), 12 deletions(-) create mode 100644 example/endpoint_wildcard_example.dart create mode 100644 lib/src/methods/path_matcher.dart diff --git a/example/endpoint_wildcard_example.dart b/example/endpoint_wildcard_example.dart new file mode 100644 index 0000000..d5231ae --- /dev/null +++ b/example/endpoint_wildcard_example.dart @@ -0,0 +1,74 @@ +import 'dart:convert'; + +import 'package:shelf/shelf.dart'; +import 'package:shelf/shelf_io.dart' as io; +import 'package:shelf_limiter/shelf_limiter.dart'; + +void main() async { + // Define rate limiter options for specific endpoints with wildcard support + final endpointLimits = { + '/auth/*': RateLimiterOptions( + maxRequests: 3, + windowSize: const Duration(minutes: 1), + headers: {'X-Custom-Header': 'Auth Rate Limited'}, + onRateLimitExceeded: (request) async { + return Response( + 429, + body: jsonEncode({ + 'status': false, + 'message': 'Rate limit exceeded for authentication endpoint', + }), + headers: {'Content-Type': 'application/json'}, + ); + }, + ), + '/data/*/shit': RateLimiterOptions( + maxRequests: 10, + windowSize: const Duration(minutes: 1), + headers: {'X-Custom-Header': 'Data Rate Limited'}, + onRateLimitExceeded: (request) async { + return Response( + 429, + body: jsonEncode({ + 'status': false, + 'message': 'Rate limit exceeded for data endpoint', + }), + headers: {'Content-Type': 'application/json'}, + ); + }, + ), + '/': RateLimiterOptions( + maxRequests: 20, + windowSize: const Duration(minutes: 1), + headers: {'X-Custom-Header': 'Global Rate Limited'}, + onRateLimitExceeded: (request) async { + return Response( + 429, + body: jsonEncode({ + 'status': false, + 'message': 'Rate limit exceeded for global endpoint', + }), + headers: {'Content-Type': 'application/json'}, + ); + }, + ), + }; + + // Create the rate limiter middleware with endpoint-specific options + final limiter = shelfLimiterByEndpoint( + endpointLimits: endpointLimits, + ); + + // Add the rate limiter to the pipeline and define a handler for incoming requests + final handler = + const Pipeline().addMiddleware(limiter).addHandler(_echoRequest); + + // Start the server on localhost and listen for incoming requests on port 8080 + var server = await io.serve(handler, 'localhost', 8080); + print('Server listening on port ${server.port}'); +} + +// Basic request handler that responds with 'Request received' +Response _echoRequest(Request request) { + return Response.ok('Request received for ${request.url.path}'); +} diff --git a/lib/shelf_limiter.dart b/lib/shelf_limiter.dart index 06c9f65..7097141 100644 --- a/lib/shelf_limiter.dart +++ b/lib/shelf_limiter.dart @@ -104,3 +104,4 @@ part 'src/classes/rate_limiter.dart'; part 'src/classes/options.dart'; part 'src/methods/response_crafter.dart'; part 'src/methods/handle_limiting.dart'; +part 'src/methods/path_matcher.dart'; diff --git a/lib/src/methods/path_matcher.dart b/lib/src/methods/path_matcher.dart new file mode 100644 index 0000000..2562f99 --- /dev/null +++ b/lib/src/methods/path_matcher.dart @@ -0,0 +1,18 @@ +part of '../../shelf_limiter.dart'; + +bool _pathMatchesPattern(String path, String pattern) { + final patternSegments = pattern.split('/'); + final pathSegments = path.split('/'); + + if (patternSegments.length != pathSegments.length) { + return false; + } + + for (var i = 0; i < patternSegments.length; i++) { + if (patternSegments[i] != '*' && patternSegments[i] != pathSegments[i]) { + return false; + } + } + + return true; +} diff --git a/lib/src/shelf_limiter_by_endpoint.dart b/lib/src/shelf_limiter_by_endpoint.dart index 9286ee7..9479fb9 100644 --- a/lib/src/shelf_limiter_by_endpoint.dart +++ b/lib/src/shelf_limiter_by_endpoint.dart @@ -3,15 +3,16 @@ part of '../shelf_limiter.dart'; /// A middleware that applies rate limiting based on specific endpoint limits. /// /// This middleware allows developers to set different rate limits for different -/// API endpoints. You can also provide a default rate limiter if some endpoints -/// don't have custom limits. +/// API endpoints and supports wildcard path matching for more flexible rate limiting. +/// You can also provide a default rate limiter if some endpoints don't have custom limits. /// /// If the client exceeds the allowed requests for an endpoint, the middleware /// responds with a `429 Too Many Requests` status or a custom response if specified. /// /// ## Parameters: -/// - `endpointLimits`: A map where the keys are endpoint paths (as strings) and -/// the values are `RateLimiterOptions` specifying rate limits for each endpoint. +/// - `endpointLimits`: A map where the keys are endpoint paths (as strings), including +/// patterns with wildcards (e.g., `/api/v1/*`), and the values are `RateLimiterOptions` +/// specifying rate limits for each endpoint or pattern. /// - `defaultOptions`: Optional. If provided, this applies rate limiting for any /// endpoint that isn't explicitly listed in `endpointLimits`. /// @@ -32,6 +33,10 @@ part of '../shelf_limiter.dart'; /// maxRequests: 10, /// windowSize: Duration(minutes: 1), /// ), +/// '/api/v1/*': RateLimiterOptions( // Wildcard path matching +/// maxRequests: 15, +/// windowSize: Duration(minutes: 2), +/// ), /// }, /// ); /// @@ -57,9 +62,13 @@ part of '../shelf_limiter.dart'; /// maxRequests: 10, /// windowSize: Duration(minutes: 1), /// ), +/// '/api/v1/*': RateLimiterOptions( // Wildcard path matching +/// maxRequests: 15, +/// windowSize: Duration(minutes: 2), +/// ), /// }, /// defaultOptions: RateLimiterOptions( -/// maxRequests: 15, +/// maxRequests: 20, /// windowSize: Duration(minutes: 5), /// ), /// ); @@ -75,18 +84,20 @@ part of '../shelf_limiter.dart'; /// rate limit is exceeded for the `/api/resource` endpoint. /// /// ## Notes: -/// - The `endpointLimits` map allows you to configure different rate limits for each endpoint. -/// - The `defaultOptions` is optional but useful for covering any endpoints that aren't listed in `endpointLimits`. +/// - The `endpointLimits` map supports wildcard path matching to apply rate limits to +/// multiple endpoints with similar patterns (e.g., `/api/v1/*`). +/// - The `defaultOptions` is optional but useful for covering any endpoints that aren't +/// listed in `endpointLimits`. /// - If the `clientIdentifierExtractor` is not specified, the client's IP address is used by default. Middleware shelfLimiterByEndpoint({ required Map endpointLimits, RateLimiterOptions? defaultOptions, }) { - // Create and store rate limiters for each endpoint when the middleware is initialized + // Create and store rate limiters for each endpoint pattern when the middleware is initialized final rateLimiters = {}; - endpointLimits.forEach((path, options) { - rateLimiters[path] = _RateLimiter( + endpointLimits.forEach((pattern, options) { + rateLimiters[pattern] = _RateLimiter( maxRequests: options.maxRequests, rateLimitDuration: options.windowSize, ); @@ -102,7 +113,18 @@ Middleware shelfLimiterByEndpoint({ return (Handler innerHandler) { return (Request request) async { final path = '/${request.url.path}'; - final rateLimiter = rateLimiters[path] ?? defaultRateLimiter; + _RateLimiter? rateLimiter; + + // Find the best matching rate limiter based on path patterns + for (final pattern in rateLimiters.keys) { + if (_pathMatchesPattern(path, pattern)) { + rateLimiter = rateLimiters[pattern]; + break; + } + } + + // Use the default rate limiter if no specific match is found + rateLimiter ??= defaultRateLimiter; if (rateLimiter == null) { // No rate limiter options available; proceed without rate limiting @@ -110,7 +132,9 @@ Middleware shelfLimiterByEndpoint({ } // Get the appropriate RateLimiterOptions (used for client identifier and headers) - final options = endpointLimits[path] ?? defaultOptions!; + final options = endpointLimits[rateLimiters.keys + .firstWhere((p) => _pathMatchesPattern(path, p))] ?? + defaultOptions!; return _handleLimiting( rateLimiter: rateLimiter, From d656c446488f3d8de523ee03acd9bc2889ff8f53 Mon Sep 17 00:00:00 2001 From: Sreelal TS Date: Sun, 15 Sep 2024 15:57:43 +0530 Subject: [PATCH 7/7] =?UTF-8?q?=F0=9F=A4=99=20How=20was=20I=20to=20you=3F?= =?UTF-8?q?=20It's=20a=20crazy=20thing!=20I=20updated=20README=20again?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 53554c8..6d2b87d 100644 --- a/README.md +++ b/README.md @@ -24,18 +24,15 @@ | **📜 Custom Headers** | Add and manage custom headers in your responses to enhance control and transparency. | | **🚀 Custom Responses** | Looking for more control? You’ve got it! Customize and send your own response when the API limit is exceeded. | | **🔗 Easy Integration** | Integrate seamlessly into your existing Shelf pipeline with minimal setup. Quickly apply rate limiting and focus on building the features that matter most without worrying about complex configurations. | -| **🌐 Endpoint-Specific Limits** | Set different rate limits for different endpoints. Protect high-traffic routes with stricter limits while allowing more leniency on less critical parts of your API. | +| **🌐 Endpoint-Specific Limits** | Set different rate limits for different endpoints. Use wildcard patterns (e.g., `/api/v1/*`) to apply rate limits to multiple routes, allowing you to protect high-traffic routes with stricter limits while allowing more leniency on less critical parts of your API. | ---- - -This format provides a clear and organized way to present the features, making it easy for readers to understand the capabilities of `shelf_limiter` at a glance. ## Installation Add `shelf_limiter` to your `pubspec.yaml` file: ```yaml dependencies: - shelf_limiter: ^1.0.0 + shelf_limiter: ``` Then run: @@ -168,7 +165,6 @@ Response _echoRequest(Request request) => Response.ok('Request received'); When you want to fine-tune your rate limiting strategy and avoid a one-size-fits-all approach, `shelfLimiterByEndpoint` is your best friend. This middleware allows you to set unique rate limits for different endpoints, giving you the power to tailor restrictions based on the needs of each route. Think of it as customizing speed limits for different roads in your neighborhood—some streets are just busier than others! - ### Example - Custom Limits for Different Routes: Here's how you can make your API as efficient as a well-oiled machine with `shelfLimiterByEndpoint`: @@ -189,6 +185,10 @@ void main() async { maxRequests: 20, windowSize: const Duration(minutes: 1), ), + '/api/v1/*': RateLimiterOptions( // Wildcard path matching + maxRequests: 15, + windowSize: const Duration(minutes: 2), + ), }, defaultOptions: RateLimiterOptions( maxRequests: 100, @@ -206,7 +206,7 @@ void main() async { Response _echoRequest(Request request) => Response.ok('Request received'); ``` -In this advanced example, the `/auth` endpoint has a rate limit of 5 requests per minute, the `/data` endpoint allows up to 20 requests, and all other endpoints follow the default limit of 100 requests per minute. +In this advanced example, the `/auth` endpoint has a rate limit of 5 requests per minute, the `/data` endpoint allows up to 20 requests, and all other endpoints following the `/api/v1/*` pattern are limited to 15 requests per 2 minutes. Any endpoint not explicitly listed follows the default limit of 100 requests per minute. ## ⚙️ Configuration