From 7e77217171fb51391cfd41acc700e9bce2d5dbd0 Mon Sep 17 00:00:00 2001 From: Kenneth Skovhede Date: Fri, 6 Sep 2024 10:50:06 +0200 Subject: [PATCH] Fix Swagger exposed API. Changed order or Json serializers to fix the parsing issue for enums. Removed support code for controllers as we are only using minimal API. --- .../WebserverCore/ApplicationPartsLogger.cs | 32 ------------------- Duplicati/WebserverCore/DuplicatiWebserver.cs | 23 +++++-------- .../Endpoints/V1/Backup/BackupPutDelete.cs | 22 ++----------- .../WebserverCore/Endpoints/V1/Backups.cs | 16 ++-------- .../Extensions/WebApplicationExtensions.cs | 12 +++---- 5 files changed, 17 insertions(+), 88 deletions(-) delete mode 100644 Duplicati/WebserverCore/ApplicationPartsLogger.cs diff --git a/Duplicati/WebserverCore/ApplicationPartsLogger.cs b/Duplicati/WebserverCore/ApplicationPartsLogger.cs deleted file mode 100644 index 7cd5f64c8..000000000 --- a/Duplicati/WebserverCore/ApplicationPartsLogger.cs +++ /dev/null @@ -1,32 +0,0 @@ -using Microsoft.AspNetCore.Mvc.ApplicationParts; -using Microsoft.AspNetCore.Mvc.Controllers; - -namespace Duplicati.WebserverCore; - -//Useful for debugging ASP.net magically loading controllers -public class ApplicationPartsLogger(ILogger logger, ApplicationPartManager partManager) - : IHostedService -{ - public Task StartAsync(CancellationToken cancellationToken) - { - // Get the names of all the application parts. This is the short assembly name for AssemblyParts - var applicationParts = partManager.ApplicationParts.Select(x => x.Name); - - // Create a controller feature, and populate it from the application parts - var controllerFeature = new ControllerFeature(); - partManager.PopulateFeature(controllerFeature); - - // Get the names of all of the controllers - var controllers = controllerFeature.Controllers.Select(x => x.Name); - - // Log the application parts and controllers - logger.LogInformation( - "Found the following application parts: '{ApplicationParts}' with the following controllers: '{Controllers}'", - string.Join(", ", applicationParts), string.Join(", ", controllers)); - - return Task.CompletedTask; - } - - // Required by the interface - public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; -} \ No newline at end of file diff --git a/Duplicati/WebserverCore/DuplicatiWebserver.cs b/Duplicati/WebserverCore/DuplicatiWebserver.cs index bd4f95871..bff6ee93d 100644 --- a/Duplicati/WebserverCore/DuplicatiWebserver.cs +++ b/Duplicati/WebserverCore/DuplicatiWebserver.cs @@ -1,4 +1,4 @@ -using System.Security.Cryptography.X509Certificates; +using System.Security.Cryptography.X509Certificates; using System.Text.Json; using System.Text.Json.Serialization; using Duplicati.Server.Database; @@ -9,6 +9,7 @@ using Duplicati.WebserverCore.Middlewares; using Duplicati.WebserverCore.Services; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Diagnostics; +using Microsoft.AspNetCore.Http.Json; using Microsoft.Extensions.Configuration.Json; using Microsoft.OpenApi.Models; @@ -70,24 +71,13 @@ public partial class DuplicatiWebserver }); }); - //builder.Host.UseRESTHandlers(); - builder.Services.ConfigureHttpJsonOptions(opt => + builder.Services.Configure(opt => { opt.SerializerOptions.PropertyNamingPolicy = null; - opt.SerializerOptions.Converters.Add(new JsonStringEnumConverter()); opt.SerializerOptions.Converters.Add(new DayOfWeekStringEnumConverter()); + opt.SerializerOptions.Converters.Add(new JsonStringEnumConverter()); }); - builder.Services.AddControllers() - // This app gets launched by a different assembly, so we need to tell it to look in this one - .AddApplicationPart(GetType().Assembly) - .AddJsonOptions(opt => - { - opt.JsonSerializerOptions.PropertyNamingPolicy = null; - opt.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()); - opt.JsonSerializerOptions.Converters.Add(new DayOfWeekStringEnumConverter()); - }); - // Generate JWTConfig with signing key if not present if (string.IsNullOrWhiteSpace(connection.ApplicationSettings.JWTConfig)) connection.ApplicationSettings.JWTConfig = JsonSerializer.Serialize(JWTConfig.Create()); @@ -96,7 +86,6 @@ public partial class DuplicatiWebserver ?? throw new Exception("Failed to deserialize JWTConfig"); builder.Services - .AddHostedService() #if DEBUG .AddEndpointsApiExplorer() .AddSwaggerGen(c => @@ -115,6 +104,7 @@ public partial class DuplicatiWebserver .AddHttpContextAccessor() .AddSingleton(new HostnameValidator(settings.AllowedHostnames)) .AddSingleton(jwtConfig) + .AddAuthorization() .AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { @@ -163,6 +153,9 @@ public partial class DuplicatiWebserver App = builder.Build(); Provider = App.Services; + App.UseAuthentication(); + App.UseAuthorization(); + #if DEBUG App.UseSwagger(); App.UseSwaggerUI(c => diff --git a/Duplicati/WebserverCore/Endpoints/V1/Backup/BackupPutDelete.cs b/Duplicati/WebserverCore/Endpoints/V1/Backup/BackupPutDelete.cs index 64149f289..5c144d6fc 100644 --- a/Duplicati/WebserverCore/Endpoints/V1/Backup/BackupPutDelete.cs +++ b/Duplicati/WebserverCore/Endpoints/V1/Backup/BackupPutDelete.cs @@ -14,24 +14,9 @@ public class BackupPutDelete : IEndpointV1 { public static void Map(RouteGroupBuilder group) { - // TODO: Figure out why the JSON deserialization is not working here - // group.MapPut("/backup/{id}", ([FromServices] Connection connection, [FromRoute] string id, [FromBody] Dto.BackupAndScheduleInputDto input) - // => ExecutePut(connection, input); - - group.MapPut("/backup/{id}", async ([FromServices] Connection connection, [FromServices] IHttpContextAccessor httpContextAccessor, [FromRoute] string id) => - { - var opts = new JsonSerializerOptions() - { - Converters = { new DayOfWeekStringEnumConverter() } - }; - - var input = (await JsonSerializer.DeserializeAsync(httpContextAccessor.HttpContext!.Request.Body, opts)) - ?? throw new BadRequestException("No data found in request body"); - - ExecutePut(GetBackup(connection, id), connection, input); - }) - .RequireAuthorization(); - + group.MapPut("/backup/{id}", ([FromServices] Connection connection, [FromRoute] string id, [FromBody] Dto.BackupAndScheduleInputDto input) + => ExecutePut(GetBackup(connection, id), connection, input)) + .RequireAuthorization(); group.MapDelete("/backup/{id}", ([FromServices] Connection connection, [FromServices] IWorkerThreadsManager workerThreadsManager, [FromServices] ICaptchaProvider captchaProvider, [FromServices] LiveControls liveControls, [FromServices] IHttpContextAccessor httpContextAccessor, [FromRoute] string id, [FromQuery(Name = "delete-remote-files")] bool? delete_remote_files, [FromQuery(Name = "delete-local-db")] bool? delete_local_db, [FromQuery(Name = "captcha-token")] string? captcha_token, [FromQuery(Name = "captcha-answer")] string? captcha_answer, [FromQuery] bool? force) => { @@ -41,7 +26,6 @@ public class BackupPutDelete : IEndpointV1 return res; }) .RequireAuthorization(); - } private static IBackup GetBackup(Connection connection, string id) diff --git a/Duplicati/WebserverCore/Endpoints/V1/Backups.cs b/Duplicati/WebserverCore/Endpoints/V1/Backups.cs index 04309f921..6eb05a5c7 100644 --- a/Duplicati/WebserverCore/Endpoints/V1/Backups.cs +++ b/Duplicati/WebserverCore/Endpoints/V1/Backups.cs @@ -16,20 +16,8 @@ public class Backups : IEndpointV1 => ExecuteGet(connection)) .RequireAuthorization(); - // TODO: Figure out why the JSON deserialization is not working here - // group.MapPost("/backups", ([FromServices] Connection connection, [FromBody] Dto.BackupAndScheduleInputDto input, [FromQuery] bool? temporary, [FromQuery] bool? existingdb) - // => ExecuteAdd(connection, input, temporary ?? false, existingdb ?? false)).RequireAuthorization(); - - group.MapPost("/backups", async ([FromServices] Connection connection, [FromQuery] bool? temporary, [FromQuery] bool? existingdb, [FromServices] IHttpContextAccessor httpContextAccessor) => - { - var opts = new JsonSerializerOptions() - { - Converters = { new DayOfWeekStringEnumConverter() } - }; - var input = await JsonSerializer.DeserializeAsync(httpContextAccessor.HttpContext!.Request.Body, opts) - ?? throw new BadRequestException("No data found in request body"); - return ExecuteAdd(connection, input, temporary ?? false, existingdb ?? false); - }) + group.MapPost("/backups", ([FromServices] Connection connection, [FromBody] Dto.BackupAndScheduleInputDto input, [FromQuery] bool? temporary, [FromQuery] bool? existingdb) + => ExecuteAdd(connection, input, temporary ?? false, existingdb ?? false)) .RequireAuthorization(); group.MapPost("/backups/import", ([FromBody] Dto.ImportBackupInputDto input, [FromServices] IJWTTokenProvider jWTTokenProvider, [FromServices] Connection connection, [FromServices] IHttpContextAccessor httpContextAccessor) => diff --git a/Duplicati/WebserverCore/Extensions/WebApplicationExtensions.cs b/Duplicati/WebserverCore/Extensions/WebApplicationExtensions.cs index 62db47088..7c56fcec9 100644 --- a/Duplicati/WebserverCore/Extensions/WebApplicationExtensions.cs +++ b/Duplicati/WebserverCore/Extensions/WebApplicationExtensions.cs @@ -18,17 +18,13 @@ public static class WebApplicationExtensions typeof(WebApplicationExtensions).Assembly.DefinedTypes .Where(t => t.ImplementedInterfaces.Contains(mapperInterfaceType)) .ToArray(); - if (endpoints.Length == 0) - { - return application; - } + + var group = application.MapGroup("/api/v1") + .AddEndpointFilter() + .AddEndpointFilter(); foreach (var endpoint in endpoints) { - var group = application.MapGroup("/api/v1") - .AddEndpointFilter() - .AddEndpointFilter(); - var methodMap = endpoint.GetMethod(nameof(IEndpointV1.Map), BindingFlags.Static | BindingFlags.Public); methodMap!.Invoke(null, [group]); }