#7582867 add UserConnectionsLimitMiddlware.

This commit is contained in:
ngfrolov 2022-11-01 17:14:19 +05:00
parent 3b64968e77
commit 0d9f8b1819
7 changed files with 191 additions and 18 deletions

View File

@ -0,0 +1,97 @@
using AsbCloudApp.Data;
using DocumentFormat.OpenXml.Drawing.Charts;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Hosting;
using Org.BouncyCastle.Asn1.Pkcs;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using Xunit;
namespace AsbCloudWebApi.Tests.Middlware
{
public class UserConnectionsLimitMiddlwareTest
{
private readonly HttpClient httpClient;
public UserConnectionsLimitMiddlwareTest()
{
var host = Host.CreateDefaultBuilder(Array.Empty<string>())
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
})
.Build();
host.Start();
httpClient = new ();
httpClient.DefaultRequestHeaders.Authorization = new("Bearer", "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjEiLCJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1lIjoiZGV2IiwiaWRDb21wYW55IjoiMSIsImh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vd3MvMjAwOC8wNi9pZGVudGl0eS9jbGFpbXMvcm9sZSI6InJvb3QiLCJuYmYiOjE2NjY1ODY2MjAsImV4cCI6MTY5ODE0NDIyMCwiaXNzIjoiYSIsImF1ZCI6ImEifQ.zqBdR4nYB87-Xyzv025waasN47i43c9FJ23RfzIvUsM");
}
[Fact]
public async Task Send_n_requests_and_get_blocked()
{
//Данные в тестовой БД
//select
// tw.id,
// t_stat.minDate,
// t_stat.maxDate
//from(
// select
// id_telemetry,
// count(1) as count,
// min("date") as minDate,
// max("date") as maxDate
// from t_telemetry_data_saub
// group by id_telemetry
//) as t_stat
//join t_well tw on tw.id_telemetry = t_stat.id_telemetry
//where tw is not null
//order by t_stat.count
//limit 10;
var wells = new []
{
(191, new DateTime(2022, 09, 01, 21, 43, 00, DateTimeKind.Utc), new DateTime(2022, 09, 04, 07, 37, 31, DateTimeKind.Utc)),
(3 , new DateTime(2021, 09, 16, 06, 13, 33, DateTimeKind.Utc), new DateTime(2021, 09, 20, 00, 29, 28, DateTimeKind.Utc)),
(199, new DateTime(2022, 09, 15, 11, 27, 18, DateTimeKind.Utc), new DateTime(2022, 09, 20, 14, 00, 23, DateTimeKind.Utc)),
(6 , new DateTime(2021, 09, 20, 00, 35, 03, DateTimeKind.Utc), new DateTime(2021, 09, 25, 06, 46, 17, DateTimeKind.Utc)),
(41 , new DateTime(2021, 12, 10, 00, 59, 52, DateTimeKind.Utc), new DateTime(2022, 10, 31, 15, 29, 24, DateTimeKind.Utc)),
(100, new DateTime(2022, 04, 24, 03, 04, 05, DateTimeKind.Utc), new DateTime(2022, 04, 29, 11, 38, 36, DateTimeKind.Utc)),
(154, new DateTime(2022, 03, 28, 10, 09, 14, DateTimeKind.Utc), new DateTime(2022, 06, 14, 15, 01, 12, DateTimeKind.Utc)),
(5 , new DateTime(2021, 09, 25, 08, 09, 37, DateTimeKind.Utc), new DateTime(2021, 10, 01, 14, 39, 51, DateTimeKind.Utc)),
(1 , new DateTime(2021, 09, 10, 01, 32, 42, DateTimeKind.Utc), new DateTime(2021, 09, 18, 00, 35, 22, DateTimeKind.Utc)),
(112, new DateTime(2022, 04, 20, 16, 47, 51, DateTimeKind.Utc), new DateTime(2022, 04, 28, 15, 04, 33, DateTimeKind.Utc)),
};
var i = 0;
for (; i < 5; i++)
_ = Task.Run(async () =>
{
var well = wells[i];
var url = MakeUrl(well.Item1, well.Item2, well.Item3);
var response = await httpClient.GetAsync(url);
//await response.Content.ReadAsStringAsync();
await Task.Delay(1000);
});
var well = wells[i];
var url = MakeUrl(well.Item1, well.Item2, well.Item3);
var response = await httpClient.GetAsync(url);
Assert.Equal(System.Net.HttpStatusCode.TooManyRequests, response.StatusCode);
}
private static string MakeUrl(int idWell, DateTime dateBegin, DateTime dateEnd)
{
var interval = (dateEnd - dateBegin).TotalSeconds;
var dateBeginString = dateBegin.ToString("yyyy-MM-ddZ");
var url = $"http://127.0.0.1:5000/api/TelemetryDataSaub/{idWell}?begin={dateBeginString}&intervalSec={interval}&approxPointsCount={interval}";
return url;
}
}
}

View File

@ -23,7 +23,7 @@ public class EventServiceTest
cacheDb = new CacheDb(); cacheDb = new CacheDb();
var telemetryTracker = new Mock<ITelemetryTracker>(); var telemetryTracker = new Mock<ITelemetryTracker>();
var imezoneServiceMock = new Mock<ITimezoneService>(); var imezoneServiceMock = new Mock<ITimezoneService>();
var telemetryService = new TelemetryService(context, telemetryTracker.Object, imezoneServiceMock.Object, cacheDb); var telemetryService = new TelemetryService(context, telemetryTracker.Object, imezoneServiceMock.Object);
service = new EventService(context, telemetryService); service = new EventService(context, telemetryService);
} }

View File

@ -42,7 +42,7 @@ namespace AsbCloudWebApi.Tests.ServicesTests
context = TestHelpter.MakeTestContext(); context = TestHelpter.MakeTestContext();
cacheDb = new CacheDb(); cacheDb = new CacheDb();
telemetryService = new TelemetryService(context, telemetryTracker.Object, timezoneService.Object, cacheDb); telemetryService = new TelemetryService(context, telemetryTracker.Object, timezoneService.Object);
var info = new TelemetryInfoDto var info = new TelemetryInfoDto
{ {

View File

@ -2,19 +2,23 @@
using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using System; using System;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace AsbCloudWebApi.Middlewares namespace AsbCloudWebApi.Middlewares
{ {
#nullable enable
public class PermissionsMiddlware public class PermissionsMiddlware
{ {
private readonly RequestDelegate next; private readonly RequestDelegate next;
private readonly UserConnectionsLimitMiddlware userConnectionsLimitMiddlware;
public PermissionsMiddlware(RequestDelegate next) public PermissionsMiddlware(RequestDelegate next, IConfiguration configuration)
{ {
this.next = next; this.next = next;
userConnectionsLimitMiddlware = new UserConnectionsLimitMiddlware(next, configuration);
} }
public async Task InvokeAsync(HttpContext context) public async Task InvokeAsync(HttpContext context)
@ -30,53 +34,51 @@ namespace AsbCloudWebApi.Middlewares
var idUser = context.User.GetUserId(); var idUser = context.User.GetUserId();
if (idUser is null) if (idUser is null)
{ {
context.User = null;
await context.ForbidAsync(); await context.ForbidAsync();
return; return;
} }
var controllerName = endpoint!.Metadata
.GetMetadata<Microsoft.AspNetCore.Mvc.Controllers.ControllerActionDescriptor>()
?.ControllerName;
bool isAuthorized; bool isAuthorized;
if (idUser == 1) if (idUser == 1)
isAuthorized = true; isAuthorized = true;
else else
{ {
var permissionName = permission.Name; var permissionName = permission.Name;
if (string.IsNullOrEmpty(permissionName)) if (string.IsNullOrEmpty(permissionName))
{ {
var controller = endpoint.Metadata
.GetMetadata<Microsoft.AspNetCore.Mvc.Controllers.ControllerActionDescriptor>()
?.ControllerName;
var httpMethod = endpoint.Metadata var httpMethod = endpoint.Metadata
.GetMetadata<Microsoft.AspNetCore.Routing.HttpMethodMetadata>() .GetMetadata<Microsoft.AspNetCore.Routing.HttpMethodMetadata>()
.HttpMethods[0] ?.HttpMethods[0]
.ToLower(); .ToLower();
permissionName = httpMethod switch permissionName = httpMethod switch
{ {
"get" or "delete" => $"{controller}.{httpMethod}", "get" or "delete" => $"{controllerName}.{httpMethod}",
"post" or "put" or "patch" => $"{controller}.edit", "post" or "put" or "patch" => $"{controllerName}.edit",
_ => throw new NotImplementedException(), _ => throw new NotImplementedException(),
}; };
PermissionAttribute.Registered.Add(permissionName); PermissionAttribute.Registered.Add(permissionName);
} }
else if (permissionName.Contains("[controller]")) else if (permissionName.Contains("[controller]"))
{ {
var controller = endpoint.Metadata permissionName = permissionName.Replace("[controller]", controllerName);
.GetMetadata<Microsoft.AspNetCore.Mvc.Controllers.ControllerActionDescriptor>()
?.ControllerName;
permissionName = permissionName.Replace("[controller]", controller);
PermissionAttribute.Registered.Add(permissionName); PermissionAttribute.Registered.Add(permissionName);
} }
var userService = context.RequestServices.GetRequiredService<IUserService>(); var userService = context.RequestServices.GetRequiredService<IUserService>();
isAuthorized = userService.HasPermission((int)idUser, permissionName); isAuthorized = userService.HasPermission(idUser!.Value, permissionName);
} }
if (isAuthorized) if (isAuthorized)
await next?.Invoke(context); await userConnectionsLimitMiddlware.InvokeAsync(context, idUser!.Value, controllerName!);
else else
await context.ForbidAsync(); await context.ForbidAsync();
} }
} }
#nullable disable
} }

View File

@ -0,0 +1,71 @@
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace AsbCloudWebApi.Middlewares
{
#nullable enable
/// <summary>
/// This is not real middleware it`s part of PermissionsMiddlware.
/// DO NOT register it in setup.cs as middleware.
/// </summary>
class UserConnectionsLimitMiddlware
{
private readonly RequestDelegate next;
private readonly int parallelRequestsToController;
private readonly byte[] body;
private readonly ConcurrentDictionary<int, ConcurrentDictionary<string, int>> stat = new ();
private readonly IEnumerable<string>? controllerNames;
public UserConnectionsLimitMiddlware(RequestDelegate next, IConfiguration configuration)
{
this.next = next;
var parallelRequestsToController = configuration.GetSection("userLimits")?.GetValue<int>("parallelRequestsToController") ?? 5;
this.parallelRequestsToController = parallelRequestsToController > 0
? parallelRequestsToController
: 5;
controllerNames = configuration.GetSection("userLimits")?.GetValue<IEnumerable<string>>("controllerNames");
var bodyText = $"<html><head><title>Too Many Requests</title></head><body><h1>Too Many Requests</h1><p>I only allow {parallelRequestsToController} parallel requests per user. Try again soon.</p></body></html>";
body = System.Text.Encoding.UTF8.GetBytes(bodyText);
}
public async Task InvokeAsync(HttpContext context, int idUser, string controllerName)
{
if(controllerNames?.Any(n => controllerName.StartsWith(n)) == false)
{
await next(context);
return;
}
var userStat = stat.GetOrAdd(idUser, idUser => new());
var count = userStat.AddOrUpdate(controllerName, 1, (key, value) => value + 1);
if(count < parallelRequestsToController)
{
try
{
await next(context);
}
finally
{
userStat[controllerName]--;
}
}
else
{
context.Response.Clear();
context.Response.StatusCode = (int)System.Net.HttpStatusCode.TooManyRequests;
context.Response.Headers.RetryAfter = "1000";
context.Response.Headers.ContentType = "text/html";
await context.Response.BodyWriter.WriteAsync(body);
}
}
}
#nullable disable
}

View File

@ -117,7 +117,7 @@ namespace AsbCloudWebApi
app.UseAuthentication(); app.UseAuthentication();
app.UseAuthorization(); app.UseAuthorization();
app.UseMiddleware<PermissionsMiddlware>(); app.UseMiddleware<PermissionsMiddlware>(Configuration);
app.UseMiddleware<SimplifyExceptionsMiddleware>(); app.UseMiddleware<SimplifyExceptionsMiddleware>();
app.UseMiddleware<RequerstTrackerMiddleware>(); app.UseMiddleware<RequerstTrackerMiddleware>();

View File

@ -13,6 +13,9 @@
"LocalConnection": "Host=localhost;Database=postgres;Username=postgres;Password=q;Persist Security Info=True" "LocalConnection": "Host=localhost;Database=postgres;Username=postgres;Password=q;Persist Security Info=True"
}, },
"AllowedHosts": "*", "AllowedHosts": "*",
"userLimits": {
"parallelRequestsToController": 5
},
"email": { "email": {
"smtpServer": "smtp.timeweb.ru", "smtpServer": "smtp.timeweb.ru",
"sender": "bot@autodrilling.ru", "sender": "bot@autodrilling.ru",