diff --git a/Iceshrimp.Backend/Controllers/FallbackController.cs b/Iceshrimp.Backend/Controllers/FallbackController.cs index 58528819..b53664fd 100644 --- a/Iceshrimp.Backend/Controllers/FallbackController.cs +++ b/Iceshrimp.Backend/Controllers/FallbackController.cs @@ -9,6 +9,6 @@ namespace Iceshrimp.Backend.Controllers; public class FallbackController(ILogger logger) : Controller { [ProducesResponseType(StatusCodes.Status501NotImplemented, Type = typeof(ErrorResponse))] public IActionResult FallbackAction() { - throw new CustomException(HttpStatusCode.NotImplemented, "This API method has not been implemented"); + throw new GracefulException(HttpStatusCode.NotImplemented, "This API method has not been implemented"); } } \ No newline at end of file diff --git a/Iceshrimp.Backend/Controllers/Renderers/ActivityPub/UserRenderer.cs b/Iceshrimp.Backend/Controllers/Renderers/ActivityPub/UserRenderer.cs index f51102a6..d7fcff45 100644 --- a/Iceshrimp.Backend/Controllers/Renderers/ActivityPub/UserRenderer.cs +++ b/Iceshrimp.Backend/Controllers/Renderers/ActivityPub/UserRenderer.cs @@ -12,12 +12,12 @@ namespace Iceshrimp.Backend.Controllers.Renderers.ActivityPub; public class UserRenderer(IOptions config, DatabaseContext db, ILogger logger) { public async Task Render(User user) { if (user.Host != null) - throw new CustomException("Refusing to render remote user"); + throw new GracefulException("Refusing to render remote user"); var profile = await db.UserProfiles.FirstOrDefaultAsync(p => p.UserId == user.Id); var keypair = await db.UserKeypairs.FirstOrDefaultAsync(p => p.UserId == user.Id); - if (keypair == null) throw new CustomException("User has no keypair"); + if (keypair == null) throw new GracefulException("User has no keypair"); var id = $"https://{config.Value.WebDomain}/users/{user.Id}"; var type = Constants.SystemUsers.Contains(user.UsernameLower) diff --git a/Iceshrimp.Backend/Core/Federation/ActivityPub/ActivityPubService.cs b/Iceshrimp.Backend/Core/Federation/ActivityPub/ActivityPubService.cs index 5f6d0827..c9d3ba6f 100644 --- a/Iceshrimp.Backend/Core/Federation/ActivityPub/ActivityPubService.cs +++ b/Iceshrimp.Backend/Core/Federation/ActivityPub/ActivityPubService.cs @@ -21,19 +21,19 @@ public class ActivityPubService(HttpClient client, HttpRequestService httpRqSvc) var input = await response.Content.ReadAsStringAsync(); var json = JsonConvert.DeserializeObject(input, JsonSerializerSettings); - var res = LdHelpers.Expand(json) ?? throw new CustomException("Failed to expand JSON-LD object"); + var res = LdHelpers.Expand(json) ?? throw new GracefulException("Failed to expand JSON-LD object"); return res.Select(p => p.ToObject(new JsonSerializer { Converters = { new ASObjectConverter() } }) ?? - throw new CustomException("Failed to deserialize activity")); + throw new GracefulException("Failed to deserialize activity")); } public async Task FetchActor(string uri, User actor, UserKeypair keypair) { var activity = await FetchActivity(uri, actor, keypair); return activity.OfType().FirstOrDefault() ?? - throw new CustomException("Failed to fetch actor"); + throw new GracefulException("Failed to fetch actor"); } public async Task FetchNote(string uri, User actor, UserKeypair keypair) { var activity = await FetchActivity(uri, actor, keypair); - return activity.OfType().FirstOrDefault() ?? throw new CustomException("Failed to fetch note"); + return activity.OfType().FirstOrDefault() ?? throw new GracefulException("Failed to fetch note"); } } \ No newline at end of file diff --git a/Iceshrimp.Backend/Core/Federation/ActivityPub/UserResolver.cs b/Iceshrimp.Backend/Core/Federation/ActivityPub/UserResolver.cs index 4d511dbe..e94092e3 100644 --- a/Iceshrimp.Backend/Core/Federation/ActivityPub/UserResolver.cs +++ b/Iceshrimp.Backend/Core/Federation/ActivityPub/UserResolver.cs @@ -22,13 +22,13 @@ public class UserResolver(ILogger logger, UserService userSvc, Web var responses = new Dictionary(); var fingerRes = await webFingerSvc.Resolve(query); - if (fingerRes == null) throw new CustomException($"Failed to WebFinger '{query}'"); + if (fingerRes == null) throw new GracefulException($"Failed to WebFinger '{query}'"); responses.Add(query, fingerRes); var apUri = fingerRes.Links.FirstOrDefault(p => p.Rel == "self" && p.Type == "application/activity+json") ?.Href; if (apUri == null) - throw new CustomException($"WebFinger response for '{query}' didn't contain a candidate link"); + throw new GracefulException($"WebFinger response for '{query}' didn't contain a candidate link"); fingerRes = responses.GetValueOrDefault(apUri); if (fingerRes == null) { @@ -36,13 +36,13 @@ public class UserResolver(ILogger logger, UserService userSvc, Web fingerRes = await webFingerSvc.Resolve(apUri); - if (fingerRes == null) throw new CustomException($"Failed to WebFinger '{apUri}'"); + if (fingerRes == null) throw new GracefulException($"Failed to WebFinger '{apUri}'"); responses.Add(apUri, fingerRes); } var acctUri = (fingerRes.Aliases ?? []).Prepend(fingerRes.Subject).FirstOrDefault(p => p.StartsWith("acct:")); if (acctUri == null) - throw new CustomException($"WebFinger response for '{apUri}' didn't contain any acct uris"); + throw new GracefulException($"WebFinger response for '{apUri}' didn't contain any acct uris"); fingerRes = responses.GetValueOrDefault(acctUri); if (fingerRes == null) { @@ -50,13 +50,13 @@ public class UserResolver(ILogger logger, UserService userSvc, Web fingerRes = await webFingerSvc.Resolve(acctUri); - if (fingerRes == null) throw new CustomException($"Failed to WebFinger '{acctUri}'"); + if (fingerRes == null) throw new GracefulException($"Failed to WebFinger '{acctUri}'"); responses.Add(acctUri, fingerRes); } var finalAcct = fingerRes.Subject; var finalUri = fingerRes.Links.FirstOrDefault(p => p.Rel == "self" && p.Type == "application/activity+json") - ?.Href ?? throw new CustomException("Final AP URI was null"); + ?.Href ?? throw new GracefulException("Final AP URI was null"); return (finalAcct, finalUri); } diff --git a/Iceshrimp.Backend/Core/Federation/Cryptography/HttpSignature.cs b/Iceshrimp.Backend/Core/Federation/Cryptography/HttpSignature.cs index 857762e6..bcc77468 100644 --- a/Iceshrimp.Backend/Core/Federation/Cryptography/HttpSignature.cs +++ b/Iceshrimp.Backend/Core/Federation/Cryptography/HttpSignature.cs @@ -11,7 +11,7 @@ public static class HttpSignature { public static async Task Verify(HttpRequest request, HttpSignatureHeader signature, IEnumerable requiredHeaders, string key) { if (!requiredHeaders.All(signature.Headers.Contains)) - throw new CustomException(HttpStatusCode.Forbidden, "Request is missing required headers"); + throw new GracefulException(HttpStatusCode.Forbidden, "Request is missing required headers"); var signingString = GenerateSigningString(signature.Headers, request.Method, request.Path, @@ -38,9 +38,9 @@ public static class HttpSignature { private static async Task VerifySignature(string key, string signingString, HttpSignatureHeader signature, IHeaderDictionary headers, Stream? body) { if (!headers.TryGetValue("date", out var date)) - throw new CustomException(HttpStatusCode.Forbidden, "Date header is missing"); + throw new GracefulException(HttpStatusCode.Forbidden, "Date header is missing"); if (DateTime.Now - DateTime.Parse(date!) > TimeSpan.FromHours(12)) - throw new CustomException(HttpStatusCode.Forbidden, "Request signature too old"); + throw new GracefulException(HttpStatusCode.Forbidden, "Request signature too old"); if (body is { Length: > 0 }) { if (body.Position != 0) @@ -113,18 +113,18 @@ public static class HttpSignature { //TODO: these fail if the dictionary doesn't contain the key, use TryGetValue instead var signatureBase64 = sig["signature"] ?? - throw new CustomException(HttpStatusCode.Forbidden, + throw new GracefulException(HttpStatusCode.Forbidden, "Signature string is missing the signature field"); var headers = sig["headers"].Split(" ") ?? - throw new CustomException(HttpStatusCode.Forbidden, + throw new GracefulException(HttpStatusCode.Forbidden, "Signature data is missing the headers field"); var keyId = sig["keyId"] ?? - throw new CustomException(HttpStatusCode.Forbidden, "Signature string is missing the keyId field"); + throw new GracefulException(HttpStatusCode.Forbidden, "Signature string is missing the keyId field"); //TODO: this should fallback to sha256 var algo = sig["algorithm"] ?? - throw new CustomException(HttpStatusCode.Forbidden, + throw new GracefulException(HttpStatusCode.Forbidden, "Signature string is missing the algorithm field"); var signature = Convert.FromBase64String(signatureBase64); diff --git a/Iceshrimp.Backend/Core/Federation/Cryptography/LdSignature.cs b/Iceshrimp.Backend/Core/Federation/Cryptography/LdSignature.cs index 0c7a9e57..2f95e66a 100644 --- a/Iceshrimp.Backend/Core/Federation/Cryptography/LdSignature.cs +++ b/Iceshrimp.Backend/Core/Federation/Cryptography/LdSignature.cs @@ -15,7 +15,7 @@ namespace Iceshrimp.Backend.Core.Federation.Cryptography; public static class LdSignature { public static Task Verify(JArray activity, string key) { if (activity.ToArray() is not [JObject obj]) - throw new CustomException(HttpStatusCode.UnprocessableEntity, "Invalid activity"); + throw new GracefulException(HttpStatusCode.UnprocessableEntity, "Invalid activity"); return Verify(obj, key); } @@ -37,7 +37,7 @@ public static class LdSignature { public static Task Sign(JArray activity, string key, string? creator) { if (activity.ToArray() is not [JObject obj]) - throw new CustomException(HttpStatusCode.UnprocessableEntity, "Invalid activity"); + throw new GracefulException(HttpStatusCode.UnprocessableEntity, "Invalid activity"); return Sign(obj, key, creator); } @@ -52,7 +52,7 @@ public static class LdSignature { var signatureData = await GetSignatureData(activity, options); if (signatureData == null) - throw new CustomException(HttpStatusCode.Forbidden, "Signature data must not be null"); + throw new GracefulException(HttpStatusCode.Forbidden, "Signature data must not be null"); var rsa = RSA.Create(); rsa.ImportFromPem(key); @@ -64,7 +64,7 @@ public static class LdSignature { activity.Add("https://w3id.org/security#signature", JToken.FromObject(options)); return LdHelpers.Expand(activity)?[0] as JObject ?? - throw new CustomException(HttpStatusCode.UnprocessableEntity, "Failed to expand signed activity"); + throw new GracefulException(HttpStatusCode.UnprocessableEntity, "Failed to expand signed activity"); } private static Task GetSignatureData(JToken data, SignatureOptions options) { diff --git a/Iceshrimp.Backend/Core/Federation/WebFinger/WebFingerService.cs b/Iceshrimp.Backend/Core/Federation/WebFinger/WebFingerService.cs index 8dde25ef..50bc121b 100644 --- a/Iceshrimp.Backend/Core/Federation/WebFinger/WebFingerService.cs +++ b/Iceshrimp.Backend/Core/Federation/WebFinger/WebFingerService.cs @@ -44,13 +44,13 @@ public class WebFingerService(HttpClient client, HttpRequestService httpRqSvc, I var split = query.Split('@'); domain = split.Length switch { - < 2 or > 3 => throw new CustomException(HttpStatusCode.BadRequest, "Invalid query"), - 2 => throw new CustomException(HttpStatusCode.BadRequest, "Can't run WebFinger for local user"), + < 2 or > 3 => throw new GracefulException(HttpStatusCode.BadRequest, "Invalid query"), + 2 => throw new GracefulException(HttpStatusCode.BadRequest, "Can't run WebFinger for local user"), _ => split[2] }; } else { - throw new CustomException(HttpStatusCode.BadRequest, "Invalid query"); + throw new GracefulException(HttpStatusCode.BadRequest, "Invalid query"); } return (query, proto, domain); diff --git a/Iceshrimp.Backend/Core/Middleware/AuthorizedFetchMiddleware.cs b/Iceshrimp.Backend/Core/Middleware/AuthorizedFetchMiddleware.cs index 4555e33b..77114978 100644 --- a/Iceshrimp.Backend/Core/Middleware/AuthorizedFetchMiddleware.cs +++ b/Iceshrimp.Backend/Core/Middleware/AuthorizedFetchMiddleware.cs @@ -21,7 +21,7 @@ public class AuthorizedFetchMiddleware( if (attribute != null && config.Value.AuthorizedFetch) { var request = ctx.Request; if (!request.Headers.TryGetValue("signature", out var sigHeader)) - throw new CustomException(HttpStatusCode.Unauthorized, "Request is missing the signature header"); + throw new GracefulException(HttpStatusCode.Unauthorized, "Request is missing the signature header"); var sig = HttpSignature.Parse(sigHeader.ToString()); @@ -35,7 +35,7 @@ public class AuthorizedFetchMiddleware( } // If we still don't have the key, something went wrong and we need to throw an exception - if (key == null) throw new CustomException("Failed to fetch key of signature user"); + if (key == null) throw new GracefulException("Failed to fetch key of signature user"); //TODO: re-fetch key once if signature validation fails, to properly support key rotation @@ -46,7 +46,7 @@ public class AuthorizedFetchMiddleware( var verified = await HttpSignature.Verify(ctx.Request, sig, headers, key.KeyPem); logger.LogDebug("HttpSignature.Verify returned {result} for key {keyId}", verified, sig.KeyId); if (!verified) - throw new CustomException(HttpStatusCode.Forbidden, "Request signature validation failed"); + throw new GracefulException(HttpStatusCode.Forbidden, "Request signature validation failed"); } await next(ctx); diff --git a/Iceshrimp.Backend/Core/Middleware/ErrorHandlerMiddleware.cs b/Iceshrimp.Backend/Core/Middleware/ErrorHandlerMiddleware.cs index 4aa5fb0e..78480b74 100644 --- a/Iceshrimp.Backend/Core/Middleware/ErrorHandlerMiddleware.cs +++ b/Iceshrimp.Backend/Core/Middleware/ErrorHandlerMiddleware.cs @@ -18,7 +18,7 @@ public class ErrorHandlerMiddleware(ILoggerFactory loggerFactory) : IMiddleware var logger = loggerFactory.CreateLogger(type); - if (e is CustomException ce) { + if (e is GracefulException ce) { ctx.Response.StatusCode = (int)ce.StatusCode; await ctx.Response.WriteAsJsonAsync(new ErrorResponse { StatusCode = ctx.Response.StatusCode, @@ -45,16 +45,15 @@ public class ErrorHandlerMiddleware(ILoggerFactory loggerFactory) : IMiddleware } } -//TODO: Find a better name for this class -public class CustomException(HttpStatusCode statusCode, string error, string message) - : Exception(message) { - public readonly string Error = error; - +//TODO: Allow specifying differing messages for api response and server logs +//TODO: Make this configurable +public class GracefulException(HttpStatusCode statusCode, string error, string message) : Exception(message) { + public readonly string Error = error; public readonly HttpStatusCode StatusCode = statusCode; - public CustomException(HttpStatusCode statusCode, string message) : + public GracefulException(HttpStatusCode statusCode, string message) : this(statusCode, statusCode.ToString(), message) { } - public CustomException(string message) : + public GracefulException(string message) : this(HttpStatusCode.InternalServerError, HttpStatusCode.InternalServerError.ToString(), message) { } } \ No newline at end of file diff --git a/Iceshrimp.Backend/Core/Services/UserService.cs b/Iceshrimp.Backend/Core/Services/UserService.cs index 18a89520..e504e4f8 100644 --- a/Iceshrimp.Backend/Core/Services/UserService.cs +++ b/Iceshrimp.Backend/Core/Services/UserService.cs @@ -11,10 +11,10 @@ namespace Iceshrimp.Backend.Core.Services; public class UserService(ILogger logger, DatabaseContext db, ActivityPubService apSvc) { private (string Username, string Host) AcctToTuple(string acct) { - if (!acct.StartsWith("acct:")) throw new CustomException(HttpStatusCode.BadRequest, "Invalid query"); + if (!acct.StartsWith("acct:")) throw new GracefulException(HttpStatusCode.BadRequest, "Invalid query"); var split = acct[5..].Split('@'); - if (split.Length != 2) throw new CustomException(HttpStatusCode.BadRequest, "Invalid query"); + if (split.Length != 2) throw new GracefulException(HttpStatusCode.BadRequest, "Invalid query"); return (split[0], split[1]); } @@ -73,10 +73,10 @@ public class UserService(ILogger logger, DatabaseContext db, Activi public async Task CreateLocalUser(string username, string password) { if (await db.Users.AnyAsync(p => p.Host == null && p.UsernameLower == username.ToLowerInvariant())) - throw new CustomException(HttpStatusCode.BadRequest, "User already exists"); + throw new GracefulException(HttpStatusCode.BadRequest, "User already exists"); if (await db.UsedUsernames.AnyAsync(p => p.Username.ToLower() == username.ToLowerInvariant())) - throw new CustomException(HttpStatusCode.BadRequest, "Username was already used"); + throw new GracefulException(HttpStatusCode.BadRequest, "Username was already used"); var keypair = RSA.Create(4096); var user = new User { @@ -126,7 +126,7 @@ public class UserService(ILogger logger, DatabaseContext db, Activi private async Task CreateSystemUser(string username) { if (await db.Users.AnyAsync(p => p.UsernameLower == username.ToLowerInvariant() && p.Host == null)) - throw new CustomException($"System user {username} already exists"); + throw new GracefulException($"System user {username} already exists"); var keypair = RSA.Create(4096); var user = new User { diff --git a/Iceshrimp.Tests/Cryptography/HttpSignatureTests.cs b/Iceshrimp.Tests/Cryptography/HttpSignatureTests.cs index 0f1bbbfc..1515dd94 100644 --- a/Iceshrimp.Tests/Cryptography/HttpSignatureTests.cs +++ b/Iceshrimp.Tests/Cryptography/HttpSignatureTests.cs @@ -43,7 +43,7 @@ public class HttpSignatureTests { request.Headers.Date = DateTimeOffset.Now - TimeSpan.FromHours(13); - var e = await Assert.ThrowsExceptionAsync(async () => + var e = await Assert.ThrowsExceptionAsync(async () => await request.Verify(MockObjects.UserKeypair .PublicKey)); e.StatusCode.Should().Be(HttpStatusCode.Forbidden);