diff --git a/Iceshrimp.Backend/Controllers/FallbackController.cs b/Iceshrimp.Backend/Controllers/FallbackController.cs index 8563b230..fb19cd7a 100644 --- a/Iceshrimp.Backend/Controllers/FallbackController.cs +++ b/Iceshrimp.Backend/Controllers/FallbackController.cs @@ -1,16 +1,14 @@ +using System.Net; using Iceshrimp.Backend.Controllers.Schemas; +using Iceshrimp.Backend.Core.Middleware; using Microsoft.AspNetCore.Mvc; namespace Iceshrimp.Backend.Controllers; [Produces("application/json")] -public class FallbackController : Controller { +public class FallbackController(ILogger logger) : Controller { [ProducesResponseType(StatusCodes.Status501NotImplemented, Type = typeof(ErrorResponse))] public IActionResult FallbackAction() { - return StatusCode(501, new ErrorResponse { - StatusCode = 501, - Error = "Not implemented", - Message = "This API method has not been implemented" - }); + throw new CustomException(HttpStatusCode.NotImplemented, "This API method has not been implemented", logger); } } \ 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 82c570c1..62b7facb 100644 --- a/Iceshrimp.Backend/Controllers/Renderers/ActivityPub/UserRenderer.cs +++ b/Iceshrimp.Backend/Controllers/Renderers/ActivityPub/UserRenderer.cs @@ -3,19 +3,21 @@ using Iceshrimp.Backend.Core.Configuration; using Iceshrimp.Backend.Core.Database; using Iceshrimp.Backend.Core.Database.Tables; using Iceshrimp.Backend.Core.Federation.ActivityStreams.Types; +using Iceshrimp.Backend.Core.Middleware; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Options; namespace Iceshrimp.Backend.Controllers.Renderers.ActivityPub; -public class UserRenderer(IOptions config, DatabaseContext db) { +public class UserRenderer(IOptions config, DatabaseContext db, ILogger logger) { public async Task Render(User user) { - if (user.Host != null) throw new Exception("Refusing to render remote user"); + if (user.Host != null) + throw new CustomException("Refusing to render remote user", logger); 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 Exception("User has no keypair"); + if (keypair == null) throw new CustomException("User has no keypair", logger); var id = $"https://{config.Value.WebDomain}/users/{user.Id}"; var type = Constants.SystemUsers.Contains(user.UsernameLower) diff --git a/Iceshrimp.Backend/Controllers/Schemas/ErrorResponse.cs b/Iceshrimp.Backend/Controllers/Schemas/ErrorResponse.cs index bb7eb62a..3f4f7e60 100644 --- a/Iceshrimp.Backend/Controllers/Schemas/ErrorResponse.cs +++ b/Iceshrimp.Backend/Controllers/Schemas/ErrorResponse.cs @@ -6,4 +6,5 @@ public class ErrorResponse { [J("statusCode")] public required int StatusCode { get; set; } [J("error")] public required string Error { get; set; } [J("message")] public required string Message { get; set; } + [J("requestId")] public required string RequestId { get; set; } } \ No newline at end of file diff --git a/Iceshrimp.Backend/Core/Extensions/WebApplicationExtensions.cs b/Iceshrimp.Backend/Core/Extensions/WebApplicationExtensions.cs index f2679ba6..5a3c0e2d 100644 --- a/Iceshrimp.Backend/Core/Extensions/WebApplicationExtensions.cs +++ b/Iceshrimp.Backend/Core/Extensions/WebApplicationExtensions.cs @@ -7,7 +7,9 @@ namespace Iceshrimp.Backend.Core.Extensions; public static class WebApplicationExtensions { public static IApplicationBuilder UseCustomMiddleware(this IApplicationBuilder app) { - return app.UseMiddleware() + // Caution: make sure these are in the correct order + return app.UseMiddleware() + .UseMiddleware() .UseMiddleware(); } diff --git a/Iceshrimp.Backend/Core/Federation/ActivityPub/ActivityPubService.cs b/Iceshrimp.Backend/Core/Federation/ActivityPub/ActivityPubService.cs index 1855f5be..de521bbf 100644 --- a/Iceshrimp.Backend/Core/Federation/ActivityPub/ActivityPubService.cs +++ b/Iceshrimp.Backend/Core/Federation/ActivityPub/ActivityPubService.cs @@ -1,6 +1,7 @@ using Iceshrimp.Backend.Core.Database.Tables; using Iceshrimp.Backend.Core.Federation.ActivityStreams; using Iceshrimp.Backend.Core.Federation.ActivityStreams.Types; +using Iceshrimp.Backend.Core.Middleware; using Iceshrimp.Backend.Core.Services; using Newtonsoft.Json; using Newtonsoft.Json.Linq; @@ -10,7 +11,7 @@ namespace Iceshrimp.Backend.Core.Federation.ActivityPub; //TODO: required attribute doesn't work with Newtonsoft.Json it appears //TODO: enforce @type values -public class ActivityPubService(HttpClient client, HttpRequestService httpRqSvc) { +public class ActivityPubService(HttpClient client, HttpRequestService httpRqSvc, ILogger logger) { private static readonly JsonSerializerSettings JsonSerializerSettings = new() { DateParseHandling = DateParseHandling.None }; @@ -20,18 +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 Exception("Failed to expand JSON-LD object"); + var res = LdHelpers.Expand(json) ?? throw new CustomException("Failed to expand JSON-LD object", logger); return res.Select(p => p.ToObject(new JsonSerializer { Converters = { new ASObjectConverter() } }) ?? - throw new Exception("Failed to deserialize activity")); + throw new CustomException("Failed to deserialize activity", logger)); } public async Task FetchActor(string uri, User actor, UserKeypair keypair) { var activity = await FetchActivity(uri, actor, keypair); - return activity.OfType().FirstOrDefault() ?? throw new Exception("Failed to fetch actor"); + return activity.OfType().FirstOrDefault() ?? + throw new CustomException("Failed to fetch actor", logger); } public async Task FetchNote(string uri, User actor, UserKeypair keypair) { var activity = await FetchActivity(uri, actor, keypair); - return activity.OfType().FirstOrDefault() ?? throw new Exception("Failed to fetch note"); + return activity.OfType().FirstOrDefault() ?? throw new CustomException("Failed to fetch note", logger); } } \ 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 903e920c..0ee924e0 100644 --- a/Iceshrimp.Backend/Core/Federation/ActivityPub/UserResolver.cs +++ b/Iceshrimp.Backend/Core/Federation/ActivityPub/UserResolver.cs @@ -1,5 +1,6 @@ using Iceshrimp.Backend.Core.Database.Tables; using Iceshrimp.Backend.Core.Federation.WebFinger; +using Iceshrimp.Backend.Core.Middleware; using Iceshrimp.Backend.Core.Services; namespace Iceshrimp.Backend.Core.Federation.ActivityPub; @@ -21,12 +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 Exception($"Failed to WebFinger '{query}'"); + if (fingerRes == null) throw new CustomException($"Failed to WebFinger '{query}'", logger); responses.Add(query, fingerRes); var apUri = fingerRes.Links.FirstOrDefault(p => p.Rel == "self" && p.Type == "application/activity+json") ?.Href; - if (apUri == null) throw new Exception($"WebFinger response for '{query}' didn't contain a candidate link"); + if (apUri == null) + throw new CustomException($"WebFinger response for '{query}' didn't contain a candidate link", logger); fingerRes = responses.GetValueOrDefault(apUri); if (fingerRes == null) { @@ -34,12 +36,13 @@ public class UserResolver(ILogger logger, UserService userSvc, Web fingerRes = await webFingerSvc.Resolve(apUri); - if (fingerRes == null) throw new Exception($"Failed to WebFinger '{apUri}'"); + if (fingerRes == null) throw new CustomException($"Failed to WebFinger '{apUri}'", logger); responses.Add(apUri, fingerRes); } var acctUri = (fingerRes.Aliases ?? []).Prepend(fingerRes.Subject).FirstOrDefault(p => p.StartsWith("acct:")); - if (acctUri == null) throw new Exception($"WebFinger response for '{apUri}' didn't contain any acct uris"); + if (acctUri == null) + throw new CustomException($"WebFinger response for '{apUri}' didn't contain any acct uris", logger); fingerRes = responses.GetValueOrDefault(acctUri); if (fingerRes == null) { @@ -47,13 +50,13 @@ public class UserResolver(ILogger logger, UserService userSvc, Web fingerRes = await webFingerSvc.Resolve(acctUri); - if (fingerRes == null) throw new Exception($"Failed to WebFinger '{acctUri}'"); + if (fingerRes == null) throw new CustomException($"Failed to WebFinger '{acctUri}'", logger); 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 Exception("Final AP URI was null"); + ?.Href ?? throw new CustomException("Final AP URI was null", logger); return (finalAcct, finalUri); } diff --git a/Iceshrimp.Backend/Core/Federation/Cryptography/HttpSignature.cs b/Iceshrimp.Backend/Core/Federation/Cryptography/HttpSignature.cs index 47dbf79e..857762e6 100644 --- a/Iceshrimp.Backend/Core/Federation/Cryptography/HttpSignature.cs +++ b/Iceshrimp.Backend/Core/Federation/Cryptography/HttpSignature.cs @@ -1,7 +1,8 @@ -using System.Data; +using System.Net; using System.Net.Http.Headers; using System.Security.Cryptography; using System.Text; +using Iceshrimp.Backend.Core.Middleware; using Microsoft.Extensions.Primitives; namespace Iceshrimp.Backend.Core.Federation.Cryptography; @@ -10,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 ConstraintException("Request is missing required headers"); + throw new CustomException(HttpStatusCode.Forbidden, "Request is missing required headers"); var signingString = GenerateSigningString(signature.Headers, request.Method, request.Path, @@ -36,8 +37,10 @@ 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 Exception("Date header is missing"); - if (DateTime.Now - DateTime.Parse(date!) > TimeSpan.FromHours(12)) throw new Exception("Signature too old"); + if (!headers.TryGetValue("date", out var date)) + throw new CustomException(HttpStatusCode.Forbidden, "Date header is missing"); + if (DateTime.Now - DateTime.Parse(date!) > TimeSpan.FromHours(12)) + throw new CustomException(HttpStatusCode.Forbidden, "Request signature too old"); if (body is { Length: > 0 }) { if (body.Position != 0) @@ -104,23 +107,25 @@ public static class HttpSignature { } public static HttpSignatureHeader Parse(string header) { - //if (!request.Headers.TryGetValue("signature", out var sigHeader)) - // throw new ConstraintException("Signature string is missing the signature header"); - var sig = header.Split(",") .Select(s => s.Split('=')) .ToDictionary(p => p[0], p => (p[1] + new string('=', p.Length - 2)).Trim('"')); //TODO: these fail if the dictionary doesn't contain the key, use TryGetValue instead var signatureBase64 = sig["signature"] ?? - throw new ConstraintException("Signature string is missing the signature field"); + throw new CustomException(HttpStatusCode.Forbidden, + "Signature string is missing the signature field"); var headers = sig["headers"].Split(" ") ?? - throw new ConstraintException("Signature data is missing the headers field"); + throw new CustomException(HttpStatusCode.Forbidden, + "Signature data is missing the headers field"); - var keyId = sig["keyId"] ?? throw new ConstraintException("Signature string is missing the keyId field"); + var keyId = sig["keyId"] ?? + throw new CustomException(HttpStatusCode.Forbidden, "Signature string is missing the keyId field"); //TODO: this should fallback to sha256 - var algo = sig["algorithm"] ?? throw new ConstraintException("Signature string is missing the algorithm field"); + var algo = sig["algorithm"] ?? + throw new CustomException(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 3fe8e0dd..0c7a9e57 100644 --- a/Iceshrimp.Backend/Core/Federation/Cryptography/LdSignature.cs +++ b/Iceshrimp.Backend/Core/Federation/Cryptography/LdSignature.cs @@ -1,7 +1,9 @@ +using System.Net; using System.Security.Cryptography; using System.Text; using Iceshrimp.Backend.Core.Federation.ActivityStreams; using Iceshrimp.Backend.Core.Helpers; +using Iceshrimp.Backend.Core.Middleware; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using J = Newtonsoft.Json.JsonPropertyAttribute; @@ -12,7 +14,8 @@ 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 Exception("Invalid activity"); + if (activity.ToArray() is not [JObject obj]) + throw new CustomException(HttpStatusCode.UnprocessableEntity, "Invalid activity"); return Verify(obj, key); } @@ -33,7 +36,8 @@ public static class LdSignature { } public static Task Sign(JArray activity, string key, string? creator) { - if (activity.ToArray() is not [JObject obj]) throw new Exception("Invalid activity"); + if (activity.ToArray() is not [JObject obj]) + throw new CustomException(HttpStatusCode.UnprocessableEntity, "Invalid activity"); return Sign(obj, key, creator); } @@ -43,11 +47,12 @@ public static class LdSignature { Creator = creator, Nonce = CryptographyHelpers.GenerateRandomHexString(16), Type = ["_:RsaSignature2017"], - Domain = null, + Domain = null }; var signatureData = await GetSignatureData(activity, options); - if (signatureData == null) throw new NullReferenceException("Signature data must not be null"); + if (signatureData == null) + throw new CustomException(HttpStatusCode.Forbidden, "Signature data must not be null"); var rsa = RSA.Create(); rsa.ImportFromPem(key); @@ -58,11 +63,13 @@ public static class LdSignature { activity.Add("https://w3id.org/security#signature", JToken.FromObject(options)); - return LdHelpers.Expand(activity)?[0] as JObject ?? throw new Exception("Failed to expand signed activity"); + return LdHelpers.Expand(activity)?[0] as JObject ?? + throw new CustomException(HttpStatusCode.UnprocessableEntity, "Failed to expand signed activity"); } - private static Task GetSignatureData(JToken data, SignatureOptions options) => - GetSignatureData(data, LdHelpers.Expand(JObject.FromObject(options))!); + private static Task GetSignatureData(JToken data, SignatureOptions options) { + return GetSignatureData(data, LdHelpers.Expand(JObject.FromObject(options))!); + } private static async Task GetSignatureData(JToken data, JToken options) { if (data is not JObject inputData) return null; diff --git a/Iceshrimp.Backend/Core/Federation/WebFinger/WebFingerService.cs b/Iceshrimp.Backend/Core/Federation/WebFinger/WebFingerService.cs index 0e699312..d7b98a10 100644 --- a/Iceshrimp.Backend/Core/Federation/WebFinger/WebFingerService.cs +++ b/Iceshrimp.Backend/Core/Federation/WebFinger/WebFingerService.cs @@ -1,5 +1,7 @@ +using System.Net; using System.Text.Encodings.Web; using System.Xml; +using Iceshrimp.Backend.Core.Middleware; using Iceshrimp.Backend.Core.Services; namespace Iceshrimp.Backend.Core.Federation.WebFinger; @@ -15,8 +17,9 @@ namespace Iceshrimp.Backend.Core.Federation.WebFinger; */ //FIXME: handle cursed person/group acct collisions like https://lemmy.ml/.well-known/webfinger?resource=acct:linux@lemmy.ml +//FIXME: also check if the query references the local instance in other ways (e.g. @user@{WebDomain}, @user@{AccountDomain}, https://{WebDomain}/..., etc) -public class WebFingerService(HttpClient client, HttpRequestService httpRqSvc) { +public class WebFingerService(HttpClient client, HttpRequestService httpRqSvc, ILogger logger) { public async Task Resolve(string query) { (query, var proto, var domain) = ParseQuery(query); var webFingerUrl = GetWebFingerUrl(query, proto, domain); @@ -27,7 +30,7 @@ public class WebFingerService(HttpClient client, HttpRequestService httpRqSvc) { return await res.Content.ReadFromJsonAsync(); } - private static (string query, string proto, string domain) ParseQuery(string query) { + private (string query, string proto, string domain) ParseQuery(string query) { string domain; string proto; query = query.StartsWith("acct:") ? $"@{query[5..]}" : query; @@ -40,15 +43,14 @@ public class WebFingerService(HttpClient client, HttpRequestService httpRqSvc) { proto = "https"; var split = query.Split('@'); - if (split.Length is < 2 or > 3) - throw new Exception("Invalid query"); - if (split.Length is 2) - throw new Exception("Can't run WebFinger for local user"); - - domain = split[2]; + domain = split.Length switch { + < 2 or > 3 => throw new CustomException(HttpStatusCode.BadRequest, "Invalid query", logger), + 2 => throw new CustomException(HttpStatusCode.BadRequest, "Can't run WebFinger for local user", logger), + _ => split[2] + }; } else { - throw new Exception("Invalid query"); + throw new CustomException(HttpStatusCode.BadRequest, "Invalid query", logger); } return (query, proto, domain); @@ -71,11 +73,9 @@ public class WebFingerService(HttpClient client, HttpRequestService httpRqSvc) { //TODO: implement https://stackoverflow.com/a/37322614/18402176 instead - for (var i = 0; i < section.Count; i++) { - if (section[i]?.Attributes?["rel"]?.InnerText == "lrdd") { + for (var i = 0; i < section.Count; i++) + if (section[i]?.Attributes?["rel"]?.InnerText == "lrdd") return section[i]?.Attributes?["template"]?.InnerText; - } - } return null; } diff --git a/Iceshrimp.Backend/Core/Middleware/AuthorizedFetchMiddleware.cs b/Iceshrimp.Backend/Core/Middleware/AuthorizedFetchMiddleware.cs index aa819e35..6a4a6924 100644 --- a/Iceshrimp.Backend/Core/Middleware/AuthorizedFetchMiddleware.cs +++ b/Iceshrimp.Backend/Core/Middleware/AuthorizedFetchMiddleware.cs @@ -1,4 +1,4 @@ -using Iceshrimp.Backend.Controllers.Schemas; +using System.Net; using Iceshrimp.Backend.Core.Configuration; using Iceshrimp.Backend.Core.Database; using Iceshrimp.Backend.Core.Federation.ActivityPub; @@ -19,7 +19,8 @@ public class AuthorizedFetchMiddleware(RequestDelegate next) { if (attribute != null && config.Value.AuthorizedFetch) { var request = context.Request; if (!request.Headers.TryGetValue("signature", out var sigHeader)) - throw new Exception("Request is missing the signature header"); + throw new CustomException(HttpStatusCode.Unauthorized, "Request is missing the signature header", + logger); var sig = HttpSignature.Parse(sigHeader.ToString()); @@ -33,7 +34,9 @@ public class AuthorizedFetchMiddleware(RequestDelegate next) { } // 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"); + if (key == null) throw new CustomException("Failed to fetch key of signature user", logger); + + //TODO: re-fetch key once if signature validation fails, to properly support key rotation List headers = request.Body.Length > 0 || attribute.ForceBody ? ["(request-target)", "digest", "host", "date"] @@ -41,16 +44,8 @@ public class AuthorizedFetchMiddleware(RequestDelegate next) { var verified = await HttpSignature.Verify(context.Request, sig, headers, key.KeyPem); logger.LogDebug("HttpSignature.Verify returned {result} for key {keyId}", verified, sig.KeyId); - if (!verified) { - context.Response.StatusCode = StatusCodes.Status403Forbidden; - context.Response.ContentType = "application/json"; - await context.Response.WriteAsJsonAsync(new ErrorResponse { - StatusCode = StatusCodes.Status403Forbidden, - Error = "Unauthorized", - Message = "Request signature validation failed" - }); - return; - } + if (!verified) + throw new CustomException(HttpStatusCode.Forbidden, "Request signature validation failed", logger); } await next(context); diff --git a/Iceshrimp.Backend/Core/Middleware/ErrorHandlerMiddleware.cs b/Iceshrimp.Backend/Core/Middleware/ErrorHandlerMiddleware.cs new file mode 100644 index 00000000..a90fc5fa --- /dev/null +++ b/Iceshrimp.Backend/Core/Middleware/ErrorHandlerMiddleware.cs @@ -0,0 +1,57 @@ +using System.Net; +using Iceshrimp.Backend.Controllers.Schemas; + +namespace Iceshrimp.Backend.Core.Middleware; + +public class ErrorHandlerMiddleware(RequestDelegate next) { + public async Task InvokeAsync(HttpContext ctx, ILogger logger) { + try { + await next(ctx); + } + catch (Exception e) { + ctx.Response.ContentType = "application/json"; + + if (e is CustomException ce) { + ctx.Response.StatusCode = (int)ce.StatusCode; + await ctx.Response.WriteAsJsonAsync(new ErrorResponse { + StatusCode = ctx.Response.StatusCode, + Error = ce.Error, + Message = ce.Message, + RequestId = ctx.TraceIdentifier + }); + (ce.Logger ?? logger).LogDebug("Request {id} was rejected with {statusCode} {error} due to: {message}", + ctx.TraceIdentifier, (int)ce.StatusCode, ce.Error, ce.Message); + } + else { + ctx.Response.StatusCode = 500; + await ctx.Response.WriteAsJsonAsync(new ErrorResponse { + StatusCode = 500, + Error = "Internal Server Error", + Message = e.Message, + RequestId = ctx.TraceIdentifier + }); + logger.LogError("Request {id} encountered an unexpected error: {exception}", ctx.TraceIdentifier, + e.ToString()); + } + } + } +} + +//TODO: Find a better name for this class +public class CustomException(HttpStatusCode statusCode, string error, string message, ILogger? logger) + : Exception(message) { + public readonly string Error = error; + public readonly ILogger? Logger = logger; + + public readonly HttpStatusCode StatusCode = statusCode; + + public CustomException(HttpStatusCode statusCode, string message, ILogger logger) : + this(statusCode, statusCode.ToString(), message, logger) { } + + public CustomException(string message, ILogger logger) : + this(HttpStatusCode.InternalServerError, HttpStatusCode.InternalServerError.ToString(), message, logger) { } + + [Obsolete("Please refactor this usage and specify the ILogger constructor argument")] + public CustomException(HttpStatusCode statusCode, string message) : + this(statusCode, statusCode.ToString(), message, null) { } +} \ No newline at end of file diff --git a/Iceshrimp.Backend/Core/Services/UserService.cs b/Iceshrimp.Backend/Core/Services/UserService.cs index 64b43477..e65b58b2 100644 --- a/Iceshrimp.Backend/Core/Services/UserService.cs +++ b/Iceshrimp.Backend/Core/Services/UserService.cs @@ -1,18 +1,20 @@ +using System.Net; using System.Security.Cryptography; using Iceshrimp.Backend.Core.Database; using Iceshrimp.Backend.Core.Database.Tables; using Iceshrimp.Backend.Core.Federation.ActivityPub; using Iceshrimp.Backend.Core.Helpers; +using Iceshrimp.Backend.Core.Middleware; using Microsoft.EntityFrameworkCore; namespace Iceshrimp.Backend.Core.Services; public class UserService(ILogger logger, DatabaseContext db, ActivityPubService apSvc) { - private static (string Username, string Host) AcctToTuple(string acct) { - if (!acct.StartsWith("acct:")) throw new Exception("Invalid query"); + private (string Username, string Host) AcctToTuple(string acct) { + if (!acct.StartsWith("acct:")) throw new CustomException(HttpStatusCode.BadRequest, "Invalid query", logger); var split = acct[5..].Split('@'); - if (split.Length != 2) throw new Exception("Invalid query"); + if (split.Length != 2) throw new CustomException(HttpStatusCode.BadRequest, "Invalid query", logger); return (split[0], split[1]); } @@ -71,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 Exception("User already exists"); + throw new CustomException(HttpStatusCode.BadRequest, "User already exists", logger); if (await db.UsedUsernames.AnyAsync(p => p.Username.ToLower() == username.ToLowerInvariant())) - throw new Exception("Username was already used"); + throw new CustomException(HttpStatusCode.BadRequest, "Username was already used", logger); var keypair = RSA.Create(4096); var user = new User { @@ -124,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 Exception("User already exists"); + throw new CustomException($"System user {username} already exists", logger); var keypair = RSA.Create(4096); var user = new User {