|
@@ -0,0 +1,79 @@
|
|
|
|
|
+using System.Threading.RateLimiting;
|
|
|
|
|
+using Common.Extension;
|
|
|
|
|
+using Microsoft.AspNetCore.RateLimiting;
|
|
|
|
|
+using Microsoft.Extensions.Caching.Memory;
|
|
|
|
|
+
|
|
|
|
|
+namespace Kitty_Fall.Website.Helpers;
|
|
|
|
|
+
|
|
|
|
|
+// Gioi han so lan goi API game theo tung thue bao (msisdn trong session, khong co thi theo IP).
|
|
|
|
|
+// Chan bot ban SaveLevelResult lien tuc: request vuot nguong tra 429 ngay tai Website,
|
|
|
|
|
+// khong goi sang API. Nguong doc tu appsettings "RateLimit", khong co thi dung mac dinh.
|
|
|
|
|
+public static class GameRateLimitHelper
|
|
|
|
|
+{
|
|
|
|
|
+ public const string SavePolicy = "game-save";
|
|
|
|
|
+ public const string StartPolicy = "game-start";
|
|
|
|
|
+
|
|
|
|
|
+ private static readonly log4net.ILog log = log4net.LogManager.GetLogger(typeof(GameRateLimitHelper));
|
|
|
|
|
+
|
|
|
|
|
+ public static IServiceCollection AddGameRateLimiter(this IServiceCollection services, IConfiguration configuration)
|
|
|
|
|
+ {
|
|
|
|
|
+ // SaveLevelResult: nguoi choi that goi 1 lan moi level; cho phep toi da 10 lan lien tiep,
|
|
|
|
|
+ // sau do 10 lan moi 30 giay (~20 lan/phut).
|
|
|
|
|
+ var save = ReadOptions(configuration, "RateLimit:GameSave", tokenLimit: 10, tokensPerPeriod: 10, periodSeconds: 30);
|
|
|
|
|
+ // StartSession / ExitGame: 1 lan moi van choi.
|
|
|
|
|
+ var start = ReadOptions(configuration, "RateLimit:GameStart", tokenLimit: 5, tokensPerPeriod: 5, periodSeconds: 60);
|
|
|
|
|
+
|
|
|
|
|
+ services.AddRateLimiter(options =>
|
|
|
|
|
+ {
|
|
|
|
|
+ options.RejectionStatusCode = StatusCodes.Status429TooManyRequests;
|
|
|
|
|
+ options.AddPolicy(SavePolicy, context =>
|
|
|
|
|
+ RateLimitPartition.GetTokenBucketLimiter(GetPartitionKey(context), _ => save));
|
|
|
|
|
+ options.AddPolicy(StartPolicy, context =>
|
|
|
|
|
+ RateLimitPartition.GetTokenBucketLimiter(GetPartitionKey(context), _ => start));
|
|
|
|
|
+ options.OnRejected = async (context, cancellationToken) =>
|
|
|
|
|
+ {
|
|
|
|
|
+ LogRejected(context.HttpContext);
|
|
|
|
|
+ await context.HttpContext.Response.WriteAsJsonAsync(new
|
|
|
|
|
+ {
|
|
|
|
|
+ success = false,
|
|
|
|
|
+ reason = "RATE_LIMITED",
|
|
|
|
|
+ message = "Too many requests. Please try again later."
|
|
|
|
|
+ }, cancellationToken);
|
|
|
|
|
+ };
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ return services;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static TokenBucketRateLimiterOptions ReadOptions(
|
|
|
|
|
+ IConfiguration configuration, string section, int tokenLimit, int tokensPerPeriod, int periodSeconds)
|
|
|
|
|
+ {
|
|
|
|
|
+ return new TokenBucketRateLimiterOptions
|
|
|
|
|
+ {
|
|
|
|
|
+ TokenLimit = Math.Max(1, configuration.GetValue($"{section}:TokenLimit", tokenLimit)),
|
|
|
|
|
+ TokensPerPeriod = Math.Max(1, configuration.GetValue($"{section}:TokensPerPeriod", tokensPerPeriod)),
|
|
|
|
|
+ ReplenishmentPeriod = TimeSpan.FromSeconds(Math.Max(1, configuration.GetValue($"{section}:PeriodSeconds", periodSeconds))),
|
|
|
|
|
+ QueueLimit = 0,
|
|
|
|
|
+ AutoReplenishment = true
|
|
|
|
|
+ };
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static string GetPartitionKey(HttpContext context)
|
|
|
|
|
+ {
|
|
|
|
|
+ var msisdn = context.Session.GetComplexData<string>("msisdn");
|
|
|
|
|
+ if (!string.IsNullOrWhiteSpace(msisdn)) return "msisdn:" + msisdn;
|
|
|
|
|
+ return "ip:" + (context.Connection.RemoteIpAddress?.ToString() ?? "unknown");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Moi thue bao chi ghi 1 dong canh bao moi phut de bot khong lam day log.
|
|
|
|
|
+ private static void LogRejected(HttpContext context)
|
|
|
|
|
+ {
|
|
|
|
|
+ var key = GetPartitionKey(context);
|
|
|
|
|
+ var cache = context.RequestServices.GetService<IMemoryCache>();
|
|
|
|
|
+ var cacheKey = "rate-limit-logged:" + key;
|
|
|
|
|
+ if (cache != null && cache.TryGetValue(cacheKey, out _)) return;
|
|
|
|
|
+ cache?.Set(cacheKey, true, TimeSpan.FromMinutes(1));
|
|
|
|
|
+
|
|
|
|
|
+ log.Warn($"Game request rate limited. key={key}, path={context.Request.Path}, ip={context.Connection.RemoteIpAddress}");
|
|
|
|
|
+ }
|
|
|
|
|
+}
|