diff --git a/Iceshrimp.Backend/Controllers/AuthController.cs b/Iceshrimp.Backend/Controllers/AuthController.cs index a23612cf..53caa1f7 100644 --- a/Iceshrimp.Backend/Controllers/AuthController.cs +++ b/Iceshrimp.Backend/Controllers/AuthController.cs @@ -42,6 +42,7 @@ public class AuthController(DatabaseContext db, UserService userSvc) : Controlle } [HttpPost] + [HideRequestDuration] [EnableRateLimiting("strict")] [Consumes(MediaTypeNames.Application.Json)] [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(AuthResponse))] diff --git a/Iceshrimp.Backend/Core/Extensions/ModelBinderProviderExtensions.cs b/Iceshrimp.Backend/Core/Extensions/ModelBinderProviderExtensions.cs index 802e9fec..d557b145 100644 --- a/Iceshrimp.Backend/Core/Extensions/ModelBinderProviderExtensions.cs +++ b/Iceshrimp.Backend/Core/Extensions/ModelBinderProviderExtensions.cs @@ -45,6 +45,7 @@ public class QueryCollectionModelBinderProvider(IModelBinderProvider provider) : public IModelBinder? GetBinder(ModelBinderProviderContext context) { if (context.BindingInfo.BindingSource == null) return null; if (!context.BindingInfo.BindingSource.CanAcceptDataFrom(BindingSource.Query)) return null; + if (!context.Metadata.IsCollectionType) return null; var binder = provider.GetBinder(context); return new QueryCollectionModelBinder(binder); diff --git a/Iceshrimp.Backend/Core/Extensions/ServiceExtensions.cs b/Iceshrimp.Backend/Core/Extensions/ServiceExtensions.cs index f0b2165b..29ae63cb 100644 --- a/Iceshrimp.Backend/Core/Extensions/ServiceExtensions.cs +++ b/Iceshrimp.Backend/Core/Extensions/ServiceExtensions.cs @@ -46,6 +46,7 @@ public static class ServiceExtensions { services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); // Hosted services = long running background tasks // Note: These need to be added as a singleton as well to ensure data consistency diff --git a/Iceshrimp.Backend/Core/Extensions/WebApplicationExtensions.cs b/Iceshrimp.Backend/Core/Extensions/WebApplicationExtensions.cs index 08352a48..658fcda5 100644 --- a/Iceshrimp.Backend/Core/Extensions/WebApplicationExtensions.cs +++ b/Iceshrimp.Backend/Core/Extensions/WebApplicationExtensions.cs @@ -9,7 +9,8 @@ namespace Iceshrimp.Backend.Core.Extensions; public static class WebApplicationExtensions { public static IApplicationBuilder UseCustomMiddleware(this IApplicationBuilder app) { // Caution: make sure these are in the correct order - return app.UseMiddleware() + return app.UseMiddleware() + .UseMiddleware() .UseMiddleware() .UseMiddleware() .UseMiddleware() diff --git a/Iceshrimp.Backend/Core/Middleware/ErrorHandlerMiddleware.cs b/Iceshrimp.Backend/Core/Middleware/ErrorHandlerMiddleware.cs index fcee21d4..1e38f3e2 100644 --- a/Iceshrimp.Backend/Core/Middleware/ErrorHandlerMiddleware.cs +++ b/Iceshrimp.Backend/Core/Middleware/ErrorHandlerMiddleware.cs @@ -1,4 +1,5 @@ using System.Diagnostics.CodeAnalysis; +using System.Globalization; using System.Net; using Iceshrimp.Backend.Controllers.Mastodon.Attributes; using Iceshrimp.Backend.Controllers.Mastodon.Schemas; @@ -66,7 +67,8 @@ public class ErrorHandlerMiddleware(IOptions options, IL } } else { - ctx.Response.StatusCode = 500; + ctx.Response.StatusCode = 500; + ctx.Response.Headers.RequestId = ctx.TraceIdentifier; await ctx.Response.WriteAsJsonAsync(new ErrorResponse { StatusCode = 500, Error = "Internal Server Error", diff --git a/Iceshrimp.Backend/Core/Middleware/RequestDurationMiddleware.cs b/Iceshrimp.Backend/Core/Middleware/RequestDurationMiddleware.cs new file mode 100644 index 00000000..88113c62 --- /dev/null +++ b/Iceshrimp.Backend/Core/Middleware/RequestDurationMiddleware.cs @@ -0,0 +1,28 @@ +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Net; +using Iceshrimp.Backend.Controllers.Mastodon.Attributes; +using Iceshrimp.Backend.Controllers.Mastodon.Schemas; +using Iceshrimp.Backend.Controllers.Schemas; +using Iceshrimp.Backend.Core.Configuration; +using Microsoft.Extensions.Options; + +namespace Iceshrimp.Backend.Core.Middleware; + +public class RequestDurationMiddleware : IMiddleware { + public async Task InvokeAsync(HttpContext ctx, RequestDelegate next) { + if (ctx.GetEndpoint()?.Metadata.GetMetadata() == null) { + var pre = DateTime.Now; + ctx.Response.OnStarting(() => { + var duration = (int)(DateTime.Now - pre).TotalMilliseconds; + ctx.Response.Headers.Append("X-Request-Duration", + duration.ToString(CultureInfo.InvariantCulture) + " ms"); + return Task.CompletedTask; + }); + } + + await next(ctx); + } +} + +public class HideRequestDuration : Attribute; \ No newline at end of file diff --git a/Iceshrimp.Backend/Pages/OAuth/Authorize.cshtml.cs b/Iceshrimp.Backend/Pages/OAuth/Authorize.cshtml.cs index 9e1ed4fc..bdc03826 100644 --- a/Iceshrimp.Backend/Pages/OAuth/Authorize.cshtml.cs +++ b/Iceshrimp.Backend/Pages/OAuth/Authorize.cshtml.cs @@ -9,12 +9,13 @@ using Microsoft.EntityFrameworkCore; namespace Iceshrimp.Backend.Pages.OAuth; +[HideRequestDuration] public class AuthorizeModel(DatabaseContext db) : PageModel { - [FromQuery(Name = "response_type")] public required string ResponseType { get; set; } - [FromQuery(Name = "client_id")] public required string ClientId { get; set; } - [FromQuery(Name = "redirect_uri")] public required string RedirectUri { get; set; } - [FromQuery(Name = "force_login")] public bool ForceLogin { get; set; } = false; - [FromQuery(Name = "lang")] public string? Language { get; set; } + [FromQuery(Name = "response_type")] public string ResponseType { get; set; } = null!; + [FromQuery(Name = "client_id")] public string ClientId { get; set; } = null!; + [FromQuery(Name = "redirect_uri")] public string RedirectUri { get; set; } = null!; + [FromQuery(Name = "force_login")] public bool ForceLogin { get; set; } = false; + [FromQuery(Name = "lang")] public string? Language { get; set; } [FromQuery(Name = "scope")] public string Scope { @@ -27,6 +28,8 @@ public class AuthorizeModel(DatabaseContext db) : PageModel { public OauthToken? Token = null; public async Task OnGet() { + if (ResponseType == null || ClientId == null || RedirectUri == null) + throw GracefulException.BadRequest("Required parameters are missing or invalid"); App = await db.OauthApps.FirstOrDefaultAsync(p => p.ClientId == ClientId.Replace(' ', '+')) ?? throw GracefulException.BadRequest("Invalid client_id"); if (MastodonOauthHelpers.ExpandScopes(Scopes).Except(MastodonOauthHelpers.ExpandScopes(App.Scopes)).Any())