Iceshrimp.NET/Iceshrimp.Backend/Core/Middleware/AuthorizedFetchMiddleware.cs
Laura Hausmann 705e061f74
[backend/asp] Refactor middleware stack
This commit splits the request pipeline conditionally instead of invoking every middleware in the stack.

It also simplifies middleware instantiation by using runtime discovery, allowing for Plugins to add Middleware.
2024-11-18 19:02:44 +01:00

150 lines
No EOL
5 KiB
C#

using System.Diagnostics.CodeAnalysis;
using System.Net;
using Iceshrimp.Backend.Core.Configuration;
using Iceshrimp.Backend.Core.Database;
using Iceshrimp.Backend.Core.Database.Tables;
using Iceshrimp.Backend.Core.Extensions;
using Iceshrimp.Backend.Core.Federation.Cryptography;
using Iceshrimp.Backend.Core.Services;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;
using static Iceshrimp.Backend.Core.Federation.ActivityPub.UserResolver;
namespace Iceshrimp.Backend.Core.Middleware;
public class AuthorizedFetchMiddleware(
[SuppressMessage("ReSharper", "SuggestBaseTypeForParameterInConstructor")]
IOptionsSnapshot<Config.SecuritySection> config,
DatabaseContext db,
ActivityPub.UserResolver userResolver,
UserService userSvc,
SystemUserService systemUserSvc,
ActivityPub.FederationControlService fedCtrlSvc,
ILogger<AuthorizedFetchMiddleware> logger,
IHostApplicationLifetime appLifetime
) : ConditionalMiddleware<AuthorizedFetchAttribute>, IMiddlewareService
{
public static ServiceLifetime Lifetime => ServiceLifetime.Scoped;
public async Task InvokeAsync(HttpContext ctx, RequestDelegate next)
{
if (!config.Value.AuthorizedFetch)
{
await next(ctx);
return;
}
ctx.Response.Headers.CacheControl = "private, no-store";
var request = ctx.Request;
var ct = appLifetime.ApplicationStopping;
//TODO: cache this somewhere
var instanceActorUri = $"/users/{(await systemUserSvc.GetInstanceActorAsync()).Id}";
if (request.Path.Value == instanceActorUri)
{
await next(ctx);
return;
}
UserPublickey? key = null;
var verified = false;
logger.LogTrace("Processing authorized fetch request for {path}", request.Path);
try
{
if (!request.Headers.TryGetValue("signature", out var sigHeader))
throw new GracefulException(HttpStatusCode.Unauthorized, "Request is missing the signature header");
var sig = HttpSignature.Parse(sigHeader.ToString());
if (await fedCtrlSvc.ShouldBlockAsync(sig.KeyId))
throw new GracefulException(HttpStatusCode.Forbidden, "Forbidden", "Instance is blocked",
suppressLog: true);
// First, we check if we already have the key
key = await db.UserPublickeys.Include(p => p.User)
.FirstOrDefaultAsync(p => p.KeyId == sig.KeyId, ct);
// If we don't, we need to try to fetch it
if (key == null)
{
try
{
var user = await userResolver.ResolveAsync(sig.KeyId, ResolveFlags.Uri).WaitAsync(ct);
key = await db.UserPublickeys.Include(p => p.User)
.FirstOrDefaultAsync(p => p.User == user, ct);
// If the key is still null here, we have a data consistency issue and need to update the key manually
key ??= await userSvc.UpdateUserPublicKeyAsync(user).WaitAsync(ct);
}
catch (Exception e)
{
if (e is GracefulException) throw;
throw new Exception($"Failed to fetch key of signature user ({sig.KeyId}) - {e.Message}");
}
}
// If we still don't have the key, something went wrong and we need to throw an exception
if (key == null) throw new Exception($"Failed to fetch key of signature user ({sig.KeyId})");
if (key.User.IsLocalUser)
throw new Exception("Remote user must have a host");
// We want to check both the user host & the keyId host (as account & web domain might be different)
if (await fedCtrlSvc.ShouldBlockAsync(key.User.Host, key.KeyId))
throw new GracefulException(HttpStatusCode.Forbidden, "Forbidden", "Instance is blocked",
suppressLog: true);
List<string> headers = ["(request-target)", "host"];
if (sig.Created != null && !sig.Headers.Contains("date"))
headers.Add("(created)");
else
headers.Add("date");
verified = await HttpSignature.VerifyAsync(request, sig, headers, key.KeyPem);
logger.LogDebug("HttpSignature.Verify returned {result} for key {keyId}", verified, sig.KeyId);
if (!verified)
{
logger.LogDebug("Refetching user key...");
key = await userSvc.UpdateUserPublicKeyAsync(key);
verified = await HttpSignature.VerifyAsync(request, sig, headers, key.KeyPem);
logger.LogDebug("HttpSignature.Verify returned {result} for key {keyId}", verified, sig.KeyId);
}
}
catch (Exception e)
{
if (e is AuthFetchException afe) throw GracefulException.Accepted(afe.Message);
if (e is GracefulException { SuppressLog: true }) throw;
logger.LogDebug("Error validating HTTP signature: {error}", e.Message);
}
if (!verified || key == null)
throw new GracefulException(HttpStatusCode.Forbidden, "Request signature validation failed");
ctx.SetActor(key.User);
await next(ctx);
}
}
public class AuthorizedFetchAttribute : Attribute;
public static partial class HttpContextExtensions
{
private const string ActorKey = "auth-fetch-user";
internal static void SetActor(this HttpContext ctx, User actor)
{
ctx.Items.Add(ActorKey, actor);
}
public static User? GetActor(this HttpContext ctx)
{
ctx.Items.TryGetValue(ActorKey, out var actor);
return actor as User;
}
}