Rename CustomException to GracefulException
This commit is contained in:
parent
2e4a1137ed
commit
5569fe061f
11 changed files with 43 additions and 44 deletions
|
@ -9,6 +9,6 @@ namespace Iceshrimp.Backend.Controllers;
|
|||
public class FallbackController(ILogger<FallbackController> 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");
|
||||
}
|
||||
}
|
|
@ -12,12 +12,12 @@ namespace Iceshrimp.Backend.Controllers.Renderers.ActivityPub;
|
|||
public class UserRenderer(IOptions<Config.InstanceSection> config, DatabaseContext db, ILogger<UserRenderer> logger) {
|
||||
public async Task<ASActor> 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)
|
||||
|
|
|
@ -21,19 +21,19 @@ public class ActivityPubService(HttpClient client, HttpRequestService httpRqSvc)
|
|||
var input = await response.Content.ReadAsStringAsync();
|
||||
var json = JsonConvert.DeserializeObject<JObject?>(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<ASObject>(new JsonSerializer { Converters = { new ASObjectConverter() } }) ??
|
||||
throw new CustomException("Failed to deserialize activity"));
|
||||
throw new GracefulException("Failed to deserialize activity"));
|
||||
}
|
||||
|
||||
public async Task<ASActor> FetchActor(string uri, User actor, UserKeypair keypair) {
|
||||
var activity = await FetchActivity(uri, actor, keypair);
|
||||
return activity.OfType<ASActor>().FirstOrDefault() ??
|
||||
throw new CustomException("Failed to fetch actor");
|
||||
throw new GracefulException("Failed to fetch actor");
|
||||
}
|
||||
|
||||
public async Task<ASNote> FetchNote(string uri, User actor, UserKeypair keypair) {
|
||||
var activity = await FetchActivity(uri, actor, keypair);
|
||||
return activity.OfType<ASNote>().FirstOrDefault() ?? throw new CustomException("Failed to fetch note");
|
||||
return activity.OfType<ASNote>().FirstOrDefault() ?? throw new GracefulException("Failed to fetch note");
|
||||
}
|
||||
}
|
|
@ -22,13 +22,13 @@ public class UserResolver(ILogger<UserResolver> logger, UserService userSvc, Web
|
|||
|
||||
var responses = new Dictionary<string, WebFingerResponse>();
|
||||
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<UserResolver> 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<UserResolver> 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);
|
||||
}
|
||||
|
|
|
@ -11,7 +11,7 @@ public static class HttpSignature {
|
|||
public static async Task<bool> Verify(HttpRequest request, HttpSignatureHeader signature,
|
||||
IEnumerable<string> 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<bool> 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);
|
||||
|
|
|
@ -15,7 +15,7 @@ namespace Iceshrimp.Backend.Core.Federation.Cryptography;
|
|||
public static class LdSignature {
|
||||
public static Task<bool> 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<JObject> 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<byte[]?> GetSignatureData(JToken data, SignatureOptions options) {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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) { }
|
||||
}
|
|
@ -11,10 +11,10 @@ namespace Iceshrimp.Backend.Core.Services;
|
|||
|
||||
public class UserService(ILogger<UserService> 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<UserService> logger, DatabaseContext db, Activi
|
|||
|
||||
public async Task<User> 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<UserService> logger, DatabaseContext db, Activi
|
|||
|
||||
private async Task<User> 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 {
|
||||
|
|
|
@ -43,7 +43,7 @@ public class HttpSignatureTests {
|
|||
|
||||
request.Headers.Date = DateTimeOffset.Now - TimeSpan.FromHours(13);
|
||||
|
||||
var e = await Assert.ThrowsExceptionAsync<CustomException>(async () =>
|
||||
var e = await Assert.ThrowsExceptionAsync<GracefulException>(async () =>
|
||||
await request.Verify(MockObjects.UserKeypair
|
||||
.PublicKey));
|
||||
e.StatusCode.Should().Be(HttpStatusCode.Forbidden);
|
||||
|
|
Loading…
Add table
Reference in a new issue