Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Solution by middleware #181

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions RateLimiter.sln
Original file line number Diff line number Diff line change
Expand Up @@ -3,29 +3,29 @@ Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
VisualStudioVersion = 15.0.26730.15
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RateLimiter", "RateLimiter\RateLimiter.csproj", "{36F4BDC6-D3DA-403A-8DB7-0C79F94B938F}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RateLimiter.Tests", "RateLimiter.Tests\RateLimiter.Tests.csproj", "{C4F9249B-010E-46BE-94B8-DD20D82F1E60}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{9B206889-9841-4B5E-B79B-D5B2610CCCFF}"
ProjectSection(SolutionItems) = preProject
README.md = README.md
EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RateLimiter", "RateLimiter\RateLimiter.csproj", "{09E0D494-161F-43A1-AE75-099CFD63E376}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{36F4BDC6-D3DA-403A-8DB7-0C79F94B938F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{36F4BDC6-D3DA-403A-8DB7-0C79F94B938F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{36F4BDC6-D3DA-403A-8DB7-0C79F94B938F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{36F4BDC6-D3DA-403A-8DB7-0C79F94B938F}.Release|Any CPU.Build.0 = Release|Any CPU
{C4F9249B-010E-46BE-94B8-DD20D82F1E60}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C4F9249B-010E-46BE-94B8-DD20D82F1E60}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C4F9249B-010E-46BE-94B8-DD20D82F1E60}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C4F9249B-010E-46BE-94B8-DD20D82F1E60}.Release|Any CPU.Build.0 = Release|Any CPU
{09E0D494-161F-43A1-AE75-099CFD63E376}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{09E0D494-161F-43A1-AE75-099CFD63E376}.Debug|Any CPU.Build.0 = Debug|Any CPU
{09E0D494-161F-43A1-AE75-099CFD63E376}.Release|Any CPU.ActiveCfg = Release|Any CPU
{09E0D494-161F-43A1-AE75-099CFD63E376}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down
7 changes: 7 additions & 0 deletions RateLimiter/DTOs/BaseDTO.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace RateLimiter.DTOs;

public class BaseDTO
{
public String? Country { get; set; }
public String? Token { get; set; }
}
30 changes: 30 additions & 0 deletions RateLimiter/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using RateLimiter.Utilities;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.Configure<RateLimits>(builder.Configuration.GetSection("Limits"));

builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var app = builder.Build();

app.UseMiddleware<LimitsMiddleware>();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}

app.UseHttpsRedirection();

app.UseAuthorization();

app.MapControllers();

app.Run();
31 changes: 31 additions & 0 deletions RateLimiter/Properties/launchSettings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:25059",
"sslPort": 44373
}
},
"profiles": {
"RateLimiter": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"launchUrl": "swagger",
"applicationUrl": "https://localhost:7146;http://localhost:5106",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"launchUrl": "swagger",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
24 changes: 17 additions & 7 deletions RateLimiter/RateLimiter.csproj
Original file line number Diff line number Diff line change
@@ -1,7 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0"/>
</ItemGroup>

<ItemGroup>
<Folder Include="Controllers\" />
</ItemGroup>

</Project>
101 changes: 101 additions & 0 deletions RateLimiter/Utilities/LimitsMiddleware.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
using System.Collections.Concurrent;
using Microsoft.Extensions.Options;
using RateLimiter.DTOs;

namespace RateLimiter.Utilities;

public class LimitsMiddleware
{
private readonly RequestDelegate _next;
private readonly RateLimits _options;
private static readonly ConcurrentDictionary<string, List<DateTime>> _requests = new();

public LimitsMiddleware(RequestDelegate next, IOptions<RateLimits> options)
{
_next = next;
_options = options.Value;
}

public async Task InvokeAsync(HttpContext context)
{
//Решил, что необходимые параметры будут передаваться через Query запроса
BaseDTO baseInfo = new BaseDTO()
{
Token = context.Request.Query["tokenValue"].ToString(),
Country = context.Request.Query["countryValue"].ToString()
};
//Проверка, что токен есть
if (string.IsNullOrEmpty(baseInfo.Token))
{
context.Response.StatusCode = 401;
await context.Response.WriteAsync("Unauthorized");
return;
}
//Проверка, что страна указана
if (string.IsNullOrEmpty(baseInfo.Country))
{
context.Response.StatusCode = 403;
await context.Response.WriteAsync("Country code required");
return;
}
//Проверка ограничений
if (!IsRequestAllowed(baseInfo))
{
context.Response.StatusCode = 429;
await context.Response.WriteAsync("Too Many Requests");
return;
}

await _next(context);
}

private bool IsRequestAllowed(BaseDTO baseInfo)
{
//Тут есть вопрос, может-ли быть ограничение, что вообще для определенной страны запрещены запросы (то если запросов небыло к системе, все равно нужно проверять ограничения ).
//Если да, то проверку requestTimes != null нужно убрать. Но такого небыло в требованиях, поэтому я решил, что такого не может быть =)
var now = DateTime.UtcNow;
var key = $"{baseInfo.Country}:${baseInfo.Token}";
var requestTimes = _requests.Where(pair => pair.Key == key).FirstOrDefault().Value;
if (requestTimes != null)
{
lock (requestTimes)
{
// Поиск ограничений по указанной в запросе стране
var rule = _options.Rules.Where(rule => rule.Country.Equals(baseInfo.Country, StringComparison.OrdinalIgnoreCase)).FirstOrDefault();
if (rule != null)
{
// Проверка, что если указана пауза между запросами, она соблюдается
if (rule.TimeFromLastCall != null)
{
// TimeFromLastCall = 7200 сек
// Если последний запрос был в 12:00, а сейчас 13:00 (12:00 + 2:00 > 13:00 = true) - запрос не пропускаем
// Если последний запрос был в 12:00, а сейчас 14:00 (12:00 + 2:00 > 14:00 = true) - запрос не пропускаем
// Если последний запрос был в 12:00, а сейчас 14:01 (12:00 + 2:00 > 14:01 = false) - запрос пропускаем
if (requestTimes.Max().AddSeconds(rule.TimeFromLastCall.Value) > now)
{
return false;
}
}
// Проверка, что если не превышено количество запросов RequestCount за промежуток времени TimeSpanSeconds
if (rule.XRequestsPerTimespan != null)
{
if (requestTimes.Count(req =>
req >= now.AddSeconds(-rule.XRequestsPerTimespan.TimeSpanSeconds)) >=
rule.XRequestsPerTimespan.RequestCount)
{
return false;
}
}
}

requestTimes.Add(now);
}
}
else
{
_requests.TryAdd(key, new List<DateTime>() { now });
}

return true;
}
}
6 changes: 6 additions & 0 deletions RateLimiter/Utilities/RateLimits.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace RateLimiter.Utilities;

public class RateLimits
{
public List<Rules> Rules { get; set; }
}
8 changes: 8 additions & 0 deletions RateLimiter/Utilities/Rules.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace RateLimiter.Utilities;

public class Rules
{
public string Country { get; set; }
public int? TimeFromLastCall { get; set; }
public XRequestsPerTimespan? XRequestsPerTimespan { get; set; }
}
7 changes: 7 additions & 0 deletions RateLimiter/Utilities/XRequestsPerTimespan.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace RateLimiter.Utilities;

public class XRequestsPerTimespan
{
public int RequestCount { get; set; }
public int TimeSpanSeconds { get; set; }
}
8 changes: 8 additions & 0 deletions RateLimiter/appsettings.Development.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}
17 changes: 17 additions & 0 deletions RateLimiter/appsettings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"Limits": {
"Rules": [
{
"Country": "US",
"XRequestsPerTimespan" : {
"RequestCount" : 20,
"TimeSpanSeconds": 300
}
},
{
"Country": "EU",
"TimeFromLastCall": 2
}
]
}
}
Loading