Implement exception handler
This commit is contained in:
parent
28066784f2
commit
f15fd9cc79
12 changed files with 145 additions and 71 deletions
|
@ -1,16 +1,14 @@
|
||||||
|
using System.Net;
|
||||||
using Iceshrimp.Backend.Controllers.Schemas;
|
using Iceshrimp.Backend.Controllers.Schemas;
|
||||||
|
using Iceshrimp.Backend.Core.Middleware;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
namespace Iceshrimp.Backend.Controllers;
|
namespace Iceshrimp.Backend.Controllers;
|
||||||
|
|
||||||
[Produces("application/json")]
|
[Produces("application/json")]
|
||||||
public class FallbackController : Controller {
|
public class FallbackController(ILogger<FallbackController> logger) : Controller {
|
||||||
[ProducesResponseType(StatusCodes.Status501NotImplemented, Type = typeof(ErrorResponse))]
|
[ProducesResponseType(StatusCodes.Status501NotImplemented, Type = typeof(ErrorResponse))]
|
||||||
public IActionResult FallbackAction() {
|
public IActionResult FallbackAction() {
|
||||||
return StatusCode(501, new ErrorResponse {
|
throw new CustomException(HttpStatusCode.NotImplemented, "This API method has not been implemented", logger);
|
||||||
StatusCode = 501,
|
|
||||||
Error = "Not implemented",
|
|
||||||
Message = "This API method has not been implemented"
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -3,19 +3,21 @@ using Iceshrimp.Backend.Core.Configuration;
|
||||||
using Iceshrimp.Backend.Core.Database;
|
using Iceshrimp.Backend.Core.Database;
|
||||||
using Iceshrimp.Backend.Core.Database.Tables;
|
using Iceshrimp.Backend.Core.Database.Tables;
|
||||||
using Iceshrimp.Backend.Core.Federation.ActivityStreams.Types;
|
using Iceshrimp.Backend.Core.Federation.ActivityStreams.Types;
|
||||||
|
using Iceshrimp.Backend.Core.Middleware;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
namespace Iceshrimp.Backend.Controllers.Renderers.ActivityPub;
|
namespace Iceshrimp.Backend.Controllers.Renderers.ActivityPub;
|
||||||
|
|
||||||
public class UserRenderer(IOptions<Config.InstanceSection> config, DatabaseContext db) {
|
public class UserRenderer(IOptions<Config.InstanceSection> config, DatabaseContext db, ILogger<UserRenderer> logger) {
|
||||||
public async Task<ASActor> Render(User user) {
|
public async Task<ASActor> 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 profile = await db.UserProfiles.FirstOrDefaultAsync(p => p.UserId == user.Id);
|
||||||
var keypair = await db.UserKeypairs.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 id = $"https://{config.Value.WebDomain}/users/{user.Id}";
|
||||||
var type = Constants.SystemUsers.Contains(user.UsernameLower)
|
var type = Constants.SystemUsers.Contains(user.UsernameLower)
|
||||||
|
|
|
@ -6,4 +6,5 @@ public class ErrorResponse {
|
||||||
[J("statusCode")] public required int StatusCode { get; set; }
|
[J("statusCode")] public required int StatusCode { get; set; }
|
||||||
[J("error")] public required string Error { get; set; }
|
[J("error")] public required string Error { get; set; }
|
||||||
[J("message")] public required string Message { get; set; }
|
[J("message")] public required string Message { get; set; }
|
||||||
|
[J("requestId")] public required string RequestId { get; set; }
|
||||||
}
|
}
|
|
@ -7,7 +7,9 @@ namespace Iceshrimp.Backend.Core.Extensions;
|
||||||
|
|
||||||
public static class WebApplicationExtensions {
|
public static class WebApplicationExtensions {
|
||||||
public static IApplicationBuilder UseCustomMiddleware(this IApplicationBuilder app) {
|
public static IApplicationBuilder UseCustomMiddleware(this IApplicationBuilder app) {
|
||||||
return app.UseMiddleware<RequestBufferingMiddleware>()
|
// Caution: make sure these are in the correct order
|
||||||
|
return app.UseMiddleware<ErrorHandlerMiddleware>()
|
||||||
|
.UseMiddleware<RequestBufferingMiddleware>()
|
||||||
.UseMiddleware<AuthorizedFetchMiddleware>();
|
.UseMiddleware<AuthorizedFetchMiddleware>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
using Iceshrimp.Backend.Core.Database.Tables;
|
using Iceshrimp.Backend.Core.Database.Tables;
|
||||||
using Iceshrimp.Backend.Core.Federation.ActivityStreams;
|
using Iceshrimp.Backend.Core.Federation.ActivityStreams;
|
||||||
using Iceshrimp.Backend.Core.Federation.ActivityStreams.Types;
|
using Iceshrimp.Backend.Core.Federation.ActivityStreams.Types;
|
||||||
|
using Iceshrimp.Backend.Core.Middleware;
|
||||||
using Iceshrimp.Backend.Core.Services;
|
using Iceshrimp.Backend.Core.Services;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using Newtonsoft.Json.Linq;
|
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: required attribute doesn't work with Newtonsoft.Json it appears
|
||||||
//TODO: enforce @type values
|
//TODO: enforce @type values
|
||||||
|
|
||||||
public class ActivityPubService(HttpClient client, HttpRequestService httpRqSvc) {
|
public class ActivityPubService(HttpClient client, HttpRequestService httpRqSvc, ILogger<ActivityPubService> logger) {
|
||||||
private static readonly JsonSerializerSettings JsonSerializerSettings =
|
private static readonly JsonSerializerSettings JsonSerializerSettings =
|
||||||
new() { DateParseHandling = DateParseHandling.None };
|
new() { DateParseHandling = DateParseHandling.None };
|
||||||
|
|
||||||
|
@ -20,18 +21,19 @@ public class ActivityPubService(HttpClient client, HttpRequestService httpRqSvc)
|
||||||
var input = await response.Content.ReadAsStringAsync();
|
var input = await response.Content.ReadAsStringAsync();
|
||||||
var json = JsonConvert.DeserializeObject<JObject?>(input, JsonSerializerSettings);
|
var json = JsonConvert.DeserializeObject<JObject?>(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<ASObject>(new JsonSerializer { Converters = { new ASObjectConverter() } }) ??
|
return res.Select(p => p.ToObject<ASObject>(new JsonSerializer { Converters = { new ASObjectConverter() } }) ??
|
||||||
throw new Exception("Failed to deserialize activity"));
|
throw new CustomException("Failed to deserialize activity", logger));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<ASActor> FetchActor(string uri, User actor, UserKeypair keypair) {
|
public async Task<ASActor> FetchActor(string uri, User actor, UserKeypair keypair) {
|
||||||
var activity = await FetchActivity(uri, actor, keypair);
|
var activity = await FetchActivity(uri, actor, keypair);
|
||||||
return activity.OfType<ASActor>().FirstOrDefault() ?? throw new Exception("Failed to fetch actor");
|
return activity.OfType<ASActor>().FirstOrDefault() ??
|
||||||
|
throw new CustomException("Failed to fetch actor", logger);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<ASNote> FetchNote(string uri, User actor, UserKeypair keypair) {
|
public async Task<ASNote> FetchNote(string uri, User actor, UserKeypair keypair) {
|
||||||
var activity = await FetchActivity(uri, actor, keypair);
|
var activity = await FetchActivity(uri, actor, keypair);
|
||||||
return activity.OfType<ASNote>().FirstOrDefault() ?? throw new Exception("Failed to fetch note");
|
return activity.OfType<ASNote>().FirstOrDefault() ?? throw new CustomException("Failed to fetch note", logger);
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,5 +1,6 @@
|
||||||
using Iceshrimp.Backend.Core.Database.Tables;
|
using Iceshrimp.Backend.Core.Database.Tables;
|
||||||
using Iceshrimp.Backend.Core.Federation.WebFinger;
|
using Iceshrimp.Backend.Core.Federation.WebFinger;
|
||||||
|
using Iceshrimp.Backend.Core.Middleware;
|
||||||
using Iceshrimp.Backend.Core.Services;
|
using Iceshrimp.Backend.Core.Services;
|
||||||
|
|
||||||
namespace Iceshrimp.Backend.Core.Federation.ActivityPub;
|
namespace Iceshrimp.Backend.Core.Federation.ActivityPub;
|
||||||
|
@ -21,12 +22,13 @@ public class UserResolver(ILogger<UserResolver> logger, UserService userSvc, Web
|
||||||
|
|
||||||
var responses = new Dictionary<string, WebFingerResponse>();
|
var responses = new Dictionary<string, WebFingerResponse>();
|
||||||
var fingerRes = await webFingerSvc.Resolve(query);
|
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);
|
responses.Add(query, fingerRes);
|
||||||
|
|
||||||
var apUri = fingerRes.Links.FirstOrDefault(p => p.Rel == "self" && p.Type == "application/activity+json")
|
var apUri = fingerRes.Links.FirstOrDefault(p => p.Rel == "self" && p.Type == "application/activity+json")
|
||||||
?.Href;
|
?.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);
|
fingerRes = responses.GetValueOrDefault(apUri);
|
||||||
if (fingerRes == null) {
|
if (fingerRes == null) {
|
||||||
|
@ -34,12 +36,13 @@ public class UserResolver(ILogger<UserResolver> logger, UserService userSvc, Web
|
||||||
|
|
||||||
fingerRes = await webFingerSvc.Resolve(apUri);
|
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);
|
responses.Add(apUri, fingerRes);
|
||||||
}
|
}
|
||||||
|
|
||||||
var acctUri = (fingerRes.Aliases ?? []).Prepend(fingerRes.Subject).FirstOrDefault(p => p.StartsWith("acct:"));
|
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);
|
fingerRes = responses.GetValueOrDefault(acctUri);
|
||||||
if (fingerRes == null) {
|
if (fingerRes == null) {
|
||||||
|
@ -47,13 +50,13 @@ public class UserResolver(ILogger<UserResolver> logger, UserService userSvc, Web
|
||||||
|
|
||||||
fingerRes = await webFingerSvc.Resolve(acctUri);
|
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);
|
responses.Add(acctUri, fingerRes);
|
||||||
}
|
}
|
||||||
|
|
||||||
var finalAcct = fingerRes.Subject;
|
var finalAcct = fingerRes.Subject;
|
||||||
var finalUri = fingerRes.Links.FirstOrDefault(p => p.Rel == "self" && p.Type == "application/activity+json")
|
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);
|
return (finalAcct, finalUri);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
using System.Data;
|
using System.Net;
|
||||||
using System.Net.Http.Headers;
|
using System.Net.Http.Headers;
|
||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using Iceshrimp.Backend.Core.Middleware;
|
||||||
using Microsoft.Extensions.Primitives;
|
using Microsoft.Extensions.Primitives;
|
||||||
|
|
||||||
namespace Iceshrimp.Backend.Core.Federation.Cryptography;
|
namespace Iceshrimp.Backend.Core.Federation.Cryptography;
|
||||||
|
@ -10,7 +11,7 @@ public static class HttpSignature {
|
||||||
public static async Task<bool> Verify(HttpRequest request, HttpSignatureHeader signature,
|
public static async Task<bool> Verify(HttpRequest request, HttpSignatureHeader signature,
|
||||||
IEnumerable<string> requiredHeaders, string key) {
|
IEnumerable<string> requiredHeaders, string key) {
|
||||||
if (!requiredHeaders.All(signature.Headers.Contains))
|
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,
|
var signingString = GenerateSigningString(signature.Headers, request.Method,
|
||||||
request.Path,
|
request.Path,
|
||||||
|
@ -36,8 +37,10 @@ public static class HttpSignature {
|
||||||
|
|
||||||
private static async Task<bool> VerifySignature(string key, string signingString, HttpSignatureHeader signature,
|
private static async Task<bool> VerifySignature(string key, string signingString, HttpSignatureHeader signature,
|
||||||
IHeaderDictionary headers, Stream? body) {
|
IHeaderDictionary headers, Stream? body) {
|
||||||
if (!headers.TryGetValue("date", out var date)) throw new Exception("Date header is missing");
|
if (!headers.TryGetValue("date", out var date))
|
||||||
if (DateTime.Now - DateTime.Parse(date!) > TimeSpan.FromHours(12)) throw new Exception("Signature too old");
|
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 is { Length: > 0 }) {
|
||||||
if (body.Position != 0)
|
if (body.Position != 0)
|
||||||
|
@ -104,23 +107,25 @@ public static class HttpSignature {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static HttpSignatureHeader Parse(string header) {
|
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(",")
|
var sig = header.Split(",")
|
||||||
.Select(s => s.Split('='))
|
.Select(s => s.Split('='))
|
||||||
.ToDictionary(p => p[0], p => (p[1] + new string('=', p.Length - 2)).Trim('"'));
|
.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
|
//TODO: these fail if the dictionary doesn't contain the key, use TryGetValue instead
|
||||||
var signatureBase64 = sig["signature"] ??
|
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(" ") ??
|
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
|
//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);
|
var signature = Convert.FromBase64String(signatureBase64);
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
|
using System.Net;
|
||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using Iceshrimp.Backend.Core.Federation.ActivityStreams;
|
using Iceshrimp.Backend.Core.Federation.ActivityStreams;
|
||||||
using Iceshrimp.Backend.Core.Helpers;
|
using Iceshrimp.Backend.Core.Helpers;
|
||||||
|
using Iceshrimp.Backend.Core.Middleware;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
using J = Newtonsoft.Json.JsonPropertyAttribute;
|
using J = Newtonsoft.Json.JsonPropertyAttribute;
|
||||||
|
@ -12,7 +14,8 @@ namespace Iceshrimp.Backend.Core.Federation.Cryptography;
|
||||||
|
|
||||||
public static class LdSignature {
|
public static class LdSignature {
|
||||||
public static Task<bool> Verify(JArray activity, string key) {
|
public static Task<bool> 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);
|
return Verify(obj, key);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -33,7 +36,8 @@ public static class LdSignature {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Task<JObject> Sign(JArray activity, string key, string? creator) {
|
public static Task<JObject> 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);
|
return Sign(obj, key, creator);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -43,11 +47,12 @@ public static class LdSignature {
|
||||||
Creator = creator,
|
Creator = creator,
|
||||||
Nonce = CryptographyHelpers.GenerateRandomHexString(16),
|
Nonce = CryptographyHelpers.GenerateRandomHexString(16),
|
||||||
Type = ["_:RsaSignature2017"],
|
Type = ["_:RsaSignature2017"],
|
||||||
Domain = null,
|
Domain = null
|
||||||
};
|
};
|
||||||
|
|
||||||
var signatureData = await GetSignatureData(activity, options);
|
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();
|
var rsa = RSA.Create();
|
||||||
rsa.ImportFromPem(key);
|
rsa.ImportFromPem(key);
|
||||||
|
@ -58,11 +63,13 @@ public static class LdSignature {
|
||||||
|
|
||||||
activity.Add("https://w3id.org/security#signature", JToken.FromObject(options));
|
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<byte[]?> GetSignatureData(JToken data, SignatureOptions options) =>
|
private static Task<byte[]?> GetSignatureData(JToken data, SignatureOptions options) {
|
||||||
GetSignatureData(data, LdHelpers.Expand(JObject.FromObject(options))!);
|
return GetSignatureData(data, LdHelpers.Expand(JObject.FromObject(options))!);
|
||||||
|
}
|
||||||
|
|
||||||
private static async Task<byte[]?> GetSignatureData(JToken data, JToken options) {
|
private static async Task<byte[]?> GetSignatureData(JToken data, JToken options) {
|
||||||
if (data is not JObject inputData) return null;
|
if (data is not JObject inputData) return null;
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
|
using System.Net;
|
||||||
using System.Text.Encodings.Web;
|
using System.Text.Encodings.Web;
|
||||||
using System.Xml;
|
using System.Xml;
|
||||||
|
using Iceshrimp.Backend.Core.Middleware;
|
||||||
using Iceshrimp.Backend.Core.Services;
|
using Iceshrimp.Backend.Core.Services;
|
||||||
|
|
||||||
namespace Iceshrimp.Backend.Core.Federation.WebFinger;
|
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: 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<WebFingerService> logger) {
|
||||||
public async Task<WebFingerResponse?> Resolve(string query) {
|
public async Task<WebFingerResponse?> Resolve(string query) {
|
||||||
(query, var proto, var domain) = ParseQuery(query);
|
(query, var proto, var domain) = ParseQuery(query);
|
||||||
var webFingerUrl = GetWebFingerUrl(query, proto, domain);
|
var webFingerUrl = GetWebFingerUrl(query, proto, domain);
|
||||||
|
@ -27,7 +30,7 @@ public class WebFingerService(HttpClient client, HttpRequestService httpRqSvc) {
|
||||||
return await res.Content.ReadFromJsonAsync<WebFingerResponse>();
|
return await res.Content.ReadFromJsonAsync<WebFingerResponse>();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static (string query, string proto, string domain) ParseQuery(string query) {
|
private (string query, string proto, string domain) ParseQuery(string query) {
|
||||||
string domain;
|
string domain;
|
||||||
string proto;
|
string proto;
|
||||||
query = query.StartsWith("acct:") ? $"@{query[5..]}" : query;
|
query = query.StartsWith("acct:") ? $"@{query[5..]}" : query;
|
||||||
|
@ -40,15 +43,14 @@ public class WebFingerService(HttpClient client, HttpRequestService httpRqSvc) {
|
||||||
proto = "https";
|
proto = "https";
|
||||||
|
|
||||||
var split = query.Split('@');
|
var split = query.Split('@');
|
||||||
if (split.Length is < 2 or > 3)
|
domain = split.Length switch {
|
||||||
throw new Exception("Invalid query");
|
< 2 or > 3 => throw new CustomException(HttpStatusCode.BadRequest, "Invalid query", logger),
|
||||||
if (split.Length is 2)
|
2 => throw new CustomException(HttpStatusCode.BadRequest, "Can't run WebFinger for local user", logger),
|
||||||
throw new Exception("Can't run WebFinger for local user");
|
_ => split[2]
|
||||||
|
};
|
||||||
domain = split[2];
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
throw new Exception("Invalid query");
|
throw new CustomException(HttpStatusCode.BadRequest, "Invalid query", logger);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (query, proto, domain);
|
return (query, proto, domain);
|
||||||
|
@ -71,11 +73,9 @@ public class WebFingerService(HttpClient client, HttpRequestService httpRqSvc) {
|
||||||
|
|
||||||
//TODO: implement https://stackoverflow.com/a/37322614/18402176 instead
|
//TODO: implement https://stackoverflow.com/a/37322614/18402176 instead
|
||||||
|
|
||||||
for (var i = 0; i < section.Count; i++) {
|
for (var i = 0; i < section.Count; i++)
|
||||||
if (section[i]?.Attributes?["rel"]?.InnerText == "lrdd") {
|
if (section[i]?.Attributes?["rel"]?.InnerText == "lrdd")
|
||||||
return section[i]?.Attributes?["template"]?.InnerText;
|
return section[i]?.Attributes?["template"]?.InnerText;
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
using Iceshrimp.Backend.Controllers.Schemas;
|
using System.Net;
|
||||||
using Iceshrimp.Backend.Core.Configuration;
|
using Iceshrimp.Backend.Core.Configuration;
|
||||||
using Iceshrimp.Backend.Core.Database;
|
using Iceshrimp.Backend.Core.Database;
|
||||||
using Iceshrimp.Backend.Core.Federation.ActivityPub;
|
using Iceshrimp.Backend.Core.Federation.ActivityPub;
|
||||||
|
@ -19,7 +19,8 @@ public class AuthorizedFetchMiddleware(RequestDelegate next) {
|
||||||
if (attribute != null && config.Value.AuthorizedFetch) {
|
if (attribute != null && config.Value.AuthorizedFetch) {
|
||||||
var request = context.Request;
|
var request = context.Request;
|
||||||
if (!request.Headers.TryGetValue("signature", out var sigHeader))
|
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());
|
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 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<string> headers = request.Body.Length > 0 || attribute.ForceBody
|
List<string> headers = request.Body.Length > 0 || attribute.ForceBody
|
||||||
? ["(request-target)", "digest", "host", "date"]
|
? ["(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);
|
var verified = await HttpSignature.Verify(context.Request, sig, headers, key.KeyPem);
|
||||||
logger.LogDebug("HttpSignature.Verify returned {result} for key {keyId}", verified, sig.KeyId);
|
logger.LogDebug("HttpSignature.Verify returned {result} for key {keyId}", verified, sig.KeyId);
|
||||||
if (!verified) {
|
if (!verified)
|
||||||
context.Response.StatusCode = StatusCodes.Status403Forbidden;
|
throw new CustomException(HttpStatusCode.Forbidden, "Request signature validation failed", logger);
|
||||||
context.Response.ContentType = "application/json";
|
|
||||||
await context.Response.WriteAsJsonAsync(new ErrorResponse {
|
|
||||||
StatusCode = StatusCodes.Status403Forbidden,
|
|
||||||
Error = "Unauthorized",
|
|
||||||
Message = "Request signature validation failed"
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await next(context);
|
await next(context);
|
||||||
|
|
57
Iceshrimp.Backend/Core/Middleware/ErrorHandlerMiddleware.cs
Normal file
57
Iceshrimp.Backend/Core/Middleware/ErrorHandlerMiddleware.cs
Normal file
|
@ -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<ErrorHandlerMiddleware> 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<CallingClass> constructor argument")]
|
||||||
|
public CustomException(HttpStatusCode statusCode, string message) :
|
||||||
|
this(statusCode, statusCode.ToString(), message, null) { }
|
||||||
|
}
|
|
@ -1,18 +1,20 @@
|
||||||
|
using System.Net;
|
||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
using Iceshrimp.Backend.Core.Database;
|
using Iceshrimp.Backend.Core.Database;
|
||||||
using Iceshrimp.Backend.Core.Database.Tables;
|
using Iceshrimp.Backend.Core.Database.Tables;
|
||||||
using Iceshrimp.Backend.Core.Federation.ActivityPub;
|
using Iceshrimp.Backend.Core.Federation.ActivityPub;
|
||||||
using Iceshrimp.Backend.Core.Helpers;
|
using Iceshrimp.Backend.Core.Helpers;
|
||||||
|
using Iceshrimp.Backend.Core.Middleware;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
namespace Iceshrimp.Backend.Core.Services;
|
namespace Iceshrimp.Backend.Core.Services;
|
||||||
|
|
||||||
public class UserService(ILogger<UserService> logger, DatabaseContext db, ActivityPubService apSvc) {
|
public class UserService(ILogger<UserService> logger, DatabaseContext db, ActivityPubService apSvc) {
|
||||||
private static (string Username, string Host) AcctToTuple(string acct) {
|
private (string Username, string Host) AcctToTuple(string acct) {
|
||||||
if (!acct.StartsWith("acct:")) throw new Exception("Invalid query");
|
if (!acct.StartsWith("acct:")) throw new CustomException(HttpStatusCode.BadRequest, "Invalid query", logger);
|
||||||
|
|
||||||
var split = acct[5..].Split('@');
|
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]);
|
return (split[0], split[1]);
|
||||||
}
|
}
|
||||||
|
@ -71,10 +73,10 @@ public class UserService(ILogger<UserService> logger, DatabaseContext db, Activi
|
||||||
|
|
||||||
public async Task<User> CreateLocalUser(string username, string password) {
|
public async Task<User> CreateLocalUser(string username, string password) {
|
||||||
if (await db.Users.AnyAsync(p => p.Host == null && p.UsernameLower == username.ToLowerInvariant()))
|
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()))
|
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 keypair = RSA.Create(4096);
|
||||||
var user = new User {
|
var user = new User {
|
||||||
|
@ -124,7 +126,7 @@ public class UserService(ILogger<UserService> logger, DatabaseContext db, Activi
|
||||||
|
|
||||||
private async Task<User> CreateSystemUser(string username) {
|
private async Task<User> CreateSystemUser(string username) {
|
||||||
if (await db.Users.AnyAsync(p => p.UsernameLower == username.ToLowerInvariant() && p.Host == null))
|
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 keypair = RSA.Create(4096);
|
||||||
var user = new User {
|
var user = new User {
|
||||||
|
|
Loading…
Add table
Reference in a new issue