Skip to content

Commit

Permalink
Merge pull request #178 from IPdotSetAF/rate-limiter-policy-customiza…
Browse files Browse the repository at this point in the history
…tion

feat: added Rate Limit Custom response option
  • Loading branch information
VahidN authored Apr 6, 2024
2 parents 62c1fff + 042ad53 commit 8184352
Show file tree
Hide file tree
Showing 3 changed files with 62 additions and 2 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ namespace DNTCaptcha.TestWebApp
.UseCustomFont(Path.Combine(_env.WebRootPath, "fonts", "IRANSans(FaNum)_Bold.ttf")) // This is optional.
.AbsoluteExpiration(minutes: 7)
.RateLimiterPermitLimit(10) // for .NET 7x+, Also you need to call app.UseRateLimiter() after calling app.UseRouting().
.WithRateLimiterRejectResponse("RateLimit Exceeded.") //you can instead provide an object, it will automatically converted to json result.
.ShowThousandsSeparators(false)
.WithNoise(0.015f, 0.015f, 1, 0.0f)
.WithEncryptionKey("This is my secure key!")
Expand Down
42 changes: 42 additions & 0 deletions src/DNTCaptcha.Core/DNTCaptchaOptions.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.IO;
using System.Text.Json;
using Microsoft.AspNetCore.Http;

namespace DNTCaptcha.Core;
Expand All @@ -9,6 +10,19 @@ namespace DNTCaptcha.Core;
/// </summary>
public class DNTCaptchaOptions
{
/// <summary>
/// Its default value is `TooManyRequests.`
/// '/TimeoutWindow/' will be replaced by the window time.
/// '/PermitLimit/' will be replaced by the RateLimiterPermitLimit.
/// </summary>
public string? RateLimiterRejectedResponse { set; get; } = "TooManyRequests.";

/// <summary>
/// Its default value is false.
/// set to true for Json response.
/// </summary>
public bool? RateLimiterRejectedResponseType { set; get; } = false;

/// <summary>
/// Its default value is `DNTCaptchaImage/[action]`
/// </summary>
Expand Down Expand Up @@ -117,6 +131,34 @@ public DNTCaptchaOptions RateLimiterPermitLimit(int permitLimit)
return this;
}

/// <summary>
/// The response that is sent to user when the captcha rate limit has exceeded.
/// The default response is : `TooManyRequests.`
/// '/TimeoutWindow/' will be replaced by the window time.
/// '/PermitLimit/' will be replaced by the RateLimiterPermitLimit.
/// </summary>
public DNTCaptchaOptions WithRateLimiterRejectResponse(string rejectResponse)
{
RateLimiterRejectedResponse = rejectResponse;
RateLimiterRejectedResponseType = false;

return this;
}

/// <summary>
/// The response that is sent to user when the captcha rate limit has exceeded.
/// The default response is : `TooManyRequests.`
/// '/TimeoutWindow/' will be replaced by the window time.
/// '/PermitLimit/' will be replaced by the RateLimiterPermitLimit.
/// </summary>
public DNTCaptchaOptions WithRateLimiterRejectResponse(object rejectResponse)
{
RateLimiterRejectedResponse = JsonSerializer.Serialize(rejectResponse);
RateLimiterRejectedResponseType = true;

return this;
}

/// <summary>
/// The CSS class name of the captcha's DIV.
/// Its default value is `dntCaptcha`.
Expand Down
21 changes: 19 additions & 2 deletions src/DNTCaptcha.Core/DNTCaptchaRateLimiterPolicy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@ public class DNTCaptchaRateLimiterPolicy : IRateLimiterPolicy<string>
/// </summary>
public const string Name = "DNTCaptcha-RateLimiter-Policy";

/// <summary>
/// The response that is sent to user when rate limit has exceeded.
/// </summary>
private static string? _rejectedResponse { get; set; } = string.Empty;
private static bool? _rejectedResponseType { get; set; } = false;

private readonly DNTCaptchaOptions _options;

/// <summary>
Expand All @@ -28,17 +34,28 @@ public class DNTCaptchaRateLimiterPolicy : IRateLimiterPolicy<string>
public DNTCaptchaRateLimiterPolicy(IOptions<DNTCaptchaOptions> options)
{
_options = options == null ? throw new ArgumentNullException(nameof(options)) : options.Value;
_rejectedResponse = _options.RateLimiterRejectedResponse;
if (!string.IsNullOrEmpty(_rejectedResponse))
{
_rejectedResponse = _rejectedResponse.Replace("/TimeoutWindow/", "1", StringComparison.OrdinalIgnoreCase).Replace("/PermitLimit/", _options.PermitLimit.ToString(CultureInfo.InvariantCulture), StringComparison.OrdinalIgnoreCase);
_rejectedResponseType = _options.RateLimiterRejectedResponseType;
}
}

/// <inheritdoc />
public Func<OnRejectedContext, CancellationToken, ValueTask>? OnRejected { get; } = (context, cancellationToken) =>
public Func<OnRejectedContext, CancellationToken, ValueTask>? OnRejected { get; } = async (context, cancellationToken) =>
{
if (context.Lease.TryGetMetadata(MetadataName.RetryAfter, out var retryAfter))
context.HttpContext.Response.Headers.RetryAfter =
((int)retryAfter.TotalSeconds).ToString(NumberFormatInfo.InvariantInfo);
context.HttpContext.Response.StatusCode = StatusCodes.Status429TooManyRequests;
return default;
if (!string.IsNullOrEmpty(_rejectedResponse) && _rejectedResponseType != null)
{
context.HttpContext.Response.Headers.ContentType = (bool)_rejectedResponseType ? "application/json" : "text/plain";
await context.HttpContext.Response.WriteAsync(_rejectedResponse);
}
};

/// <inheritdoc />
Expand Down

0 comments on commit 8184352

Please sign in to comment.