[backend] Add request duration header for non-timing-sensitive (cryptography-related) endpoints

This commit is contained in:
Laura Hausmann 2024-02-06 02:42:35 +01:00
parent 2984abbe2b
commit 94820c2b71
No known key found for this signature in database
GPG key ID: D044E84C5BE01605
7 changed files with 44 additions and 7 deletions

View file

@ -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))]

View file

@ -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);

View file

@ -46,6 +46,7 @@ public static class ServiceExtensions {
services.AddSingleton<RequestBufferingMiddleware>();
services.AddSingleton<AuthorizationMiddleware>();
services.AddSingleton<RequestVerificationMiddleware>();
services.AddSingleton<RequestDurationMiddleware>();
// Hosted services = long running background tasks
// Note: These need to be added as a singleton as well to ensure data consistency

View file

@ -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<ErrorHandlerMiddleware>()
return app.UseMiddleware<RequestDurationMiddleware>()
.UseMiddleware<ErrorHandlerMiddleware>()
.UseMiddleware<RequestVerificationMiddleware>()
.UseMiddleware<RequestBufferingMiddleware>()
.UseMiddleware<AuthenticationMiddleware>()

View file

@ -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<Config.SecuritySection> 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",

View file

@ -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<HideRequestDuration>() == 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;

View file

@ -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())