From 0d9f8b1819f136b71202c3af3cadf617c283ad37 Mon Sep 17 00:00:00 2001 From: ngfrolov Date: Tue, 1 Nov 2022 17:14:19 +0500 Subject: [PATCH] #7582867 add UserConnectionsLimitMiddlware. --- .../UserConnectionsLimitMiddlwareTest.cs | 97 +++++++++++++++++++ .../ServicesTests/EventServiceTest.cs | 2 +- .../TelemetryDataSaubServiceTest.cs | 2 +- .../Middlewares/PermissionsMiddlware.cs | 32 +++--- .../UserConnectionsLimitMiddlware.cs | 71 ++++++++++++++ AsbCloudWebApi/Startup.cs | 2 +- AsbCloudWebApi/appsettings.json | 3 + 7 files changed, 191 insertions(+), 18 deletions(-) create mode 100644 AsbCloudWebApi.Tests/Middlware/UserConnectionsLimitMiddlwareTest.cs create mode 100644 AsbCloudWebApi/Middlewares/UserConnectionsLimitMiddlware.cs diff --git a/AsbCloudWebApi.Tests/Middlware/UserConnectionsLimitMiddlwareTest.cs b/AsbCloudWebApi.Tests/Middlware/UserConnectionsLimitMiddlwareTest.cs new file mode 100644 index 00000000..15e94f97 --- /dev/null +++ b/AsbCloudWebApi.Tests/Middlware/UserConnectionsLimitMiddlwareTest.cs @@ -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()) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }) + .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; + } + } +} diff --git a/AsbCloudWebApi.Tests/ServicesTests/EventServiceTest.cs b/AsbCloudWebApi.Tests/ServicesTests/EventServiceTest.cs index 1a6ac59a..dc8b9acf 100644 --- a/AsbCloudWebApi.Tests/ServicesTests/EventServiceTest.cs +++ b/AsbCloudWebApi.Tests/ServicesTests/EventServiceTest.cs @@ -23,7 +23,7 @@ public class EventServiceTest cacheDb = new CacheDb(); var telemetryTracker = new Mock(); var imezoneServiceMock = new Mock(); - var telemetryService = new TelemetryService(context, telemetryTracker.Object, imezoneServiceMock.Object, cacheDb); + var telemetryService = new TelemetryService(context, telemetryTracker.Object, imezoneServiceMock.Object); service = new EventService(context, telemetryService); } diff --git a/AsbCloudWebApi.Tests/ServicesTests/TelemetryDataSaubServiceTest.cs b/AsbCloudWebApi.Tests/ServicesTests/TelemetryDataSaubServiceTest.cs index 5e61701d..830edb3a 100644 --- a/AsbCloudWebApi.Tests/ServicesTests/TelemetryDataSaubServiceTest.cs +++ b/AsbCloudWebApi.Tests/ServicesTests/TelemetryDataSaubServiceTest.cs @@ -42,7 +42,7 @@ namespace AsbCloudWebApi.Tests.ServicesTests context = TestHelpter.MakeTestContext(); cacheDb = new CacheDb(); - telemetryService = new TelemetryService(context, telemetryTracker.Object, timezoneService.Object, cacheDb); + telemetryService = new TelemetryService(context, telemetryTracker.Object, timezoneService.Object); var info = new TelemetryInfoDto { diff --git a/AsbCloudWebApi/Middlewares/PermissionsMiddlware.cs b/AsbCloudWebApi/Middlewares/PermissionsMiddlware.cs index 5373cf64..f131fc84 100644 --- a/AsbCloudWebApi/Middlewares/PermissionsMiddlware.cs +++ b/AsbCloudWebApi/Middlewares/PermissionsMiddlware.cs @@ -2,19 +2,23 @@ using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using System; using System.Threading.Tasks; namespace AsbCloudWebApi.Middlewares { +#nullable enable public class PermissionsMiddlware { private readonly RequestDelegate next; + private readonly UserConnectionsLimitMiddlware userConnectionsLimitMiddlware; - public PermissionsMiddlware(RequestDelegate next) + public PermissionsMiddlware(RequestDelegate next, IConfiguration configuration) { this.next = next; + userConnectionsLimitMiddlware = new UserConnectionsLimitMiddlware(next, configuration); } public async Task InvokeAsync(HttpContext context) @@ -30,53 +34,51 @@ namespace AsbCloudWebApi.Middlewares var idUser = context.User.GetUserId(); if (idUser is null) { - context.User = null; await context.ForbidAsync(); return; } + var controllerName = endpoint!.Metadata + .GetMetadata() + ?.ControllerName; + bool isAuthorized; if (idUser == 1) isAuthorized = true; + else { var permissionName = permission.Name; if (string.IsNullOrEmpty(permissionName)) { - var controller = endpoint.Metadata - .GetMetadata() - ?.ControllerName; - var httpMethod = endpoint.Metadata .GetMetadata() - .HttpMethods[0] + ?.HttpMethods[0] .ToLower(); permissionName = httpMethod switch { - "get" or "delete" => $"{controller}.{httpMethod}", - "post" or "put" or "patch" => $"{controller}.edit", + "get" or "delete" => $"{controllerName}.{httpMethod}", + "post" or "put" or "patch" => $"{controllerName}.edit", _ => throw new NotImplementedException(), }; PermissionAttribute.Registered.Add(permissionName); } else if (permissionName.Contains("[controller]")) { - var controller = endpoint.Metadata - .GetMetadata() - ?.ControllerName; - permissionName = permissionName.Replace("[controller]", controller); + permissionName = permissionName.Replace("[controller]", controllerName); PermissionAttribute.Registered.Add(permissionName); } var userService = context.RequestServices.GetRequiredService(); - isAuthorized = userService.HasPermission((int)idUser, permissionName); + isAuthorized = userService.HasPermission(idUser!.Value, permissionName); } if (isAuthorized) - await next?.Invoke(context); + await userConnectionsLimitMiddlware.InvokeAsync(context, idUser!.Value, controllerName!); else await context.ForbidAsync(); } } +#nullable disable } diff --git a/AsbCloudWebApi/Middlewares/UserConnectionsLimitMiddlware.cs b/AsbCloudWebApi/Middlewares/UserConnectionsLimitMiddlware.cs new file mode 100644 index 00000000..96b036e2 --- /dev/null +++ b/AsbCloudWebApi/Middlewares/UserConnectionsLimitMiddlware.cs @@ -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 + /// + /// This is not real middleware it`s part of PermissionsMiddlware. + /// DO NOT register it in setup.cs as middleware. + /// + class UserConnectionsLimitMiddlware + { + private readonly RequestDelegate next; + private readonly int parallelRequestsToController; + private readonly byte[] body; + private readonly ConcurrentDictionary> stat = new (); + private readonly IEnumerable? controllerNames; + + public UserConnectionsLimitMiddlware(RequestDelegate next, IConfiguration configuration) + { + this.next = next; + + var parallelRequestsToController = configuration.GetSection("userLimits")?.GetValue("parallelRequestsToController") ?? 5; + this.parallelRequestsToController = parallelRequestsToController > 0 + ? parallelRequestsToController + : 5; + + controllerNames = configuration.GetSection("userLimits")?.GetValue>("controllerNames"); + + var bodyText = $"Too Many Requests

Too Many Requests

I only allow {parallelRequestsToController} parallel requests per user. Try again soon.

"; + 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 +} diff --git a/AsbCloudWebApi/Startup.cs b/AsbCloudWebApi/Startup.cs index 73410e21..8046d92c 100644 --- a/AsbCloudWebApi/Startup.cs +++ b/AsbCloudWebApi/Startup.cs @@ -117,7 +117,7 @@ namespace AsbCloudWebApi app.UseAuthentication(); app.UseAuthorization(); - app.UseMiddleware(); + app.UseMiddleware(Configuration); app.UseMiddleware(); app.UseMiddleware(); diff --git a/AsbCloudWebApi/appsettings.json b/AsbCloudWebApi/appsettings.json index 372f92e6..2a1fbd3b 100644 --- a/AsbCloudWebApi/appsettings.json +++ b/AsbCloudWebApi/appsettings.json @@ -13,6 +13,9 @@ "LocalConnection": "Host=localhost;Database=postgres;Username=postgres;Password=q;Persist Security Info=True" }, "AllowedHosts": "*", + "userLimits": { + "parallelRequestsToController": 5 + }, "email": { "smtpServer": "smtp.timeweb.ru", "sender": "bot@autodrilling.ru",