From ae89337a2adc5d8a0e0d9ef2bbe9d663534035fb Mon Sep 17 00:00:00 2001 From: Laura Hausmann Date: Sat, 13 Jan 2024 19:46:47 +0100 Subject: [PATCH] Refactor WebFinger -> WebFingerService --- .../ActivityStreams/Types/ASActivity.cs | 1 - .../Core/Federation/Services/UserResolver.cs | 11 +- .../Core/Federation/WebFinger/Types.cs | 11 +- .../Core/Federation/WebFinger/WebFinger.cs | 102 ------------------ .../Federation/WebFinger/WebFingerService.cs | 82 ++++++++++++++ .../Core/Helpers/HttpClientHelpers.cs | 12 --- ...{LongExtensions.cs => NumberExtensions.cs} | 0 .../Core/Helpers/ServiceExtensions.cs | 2 +- .../Core/Services/UserService.cs | 1 - Iceshrimp.Backend/Startup.cs | 1 - 10 files changed, 93 insertions(+), 130 deletions(-) delete mode 100644 Iceshrimp.Backend/Core/Federation/WebFinger/WebFinger.cs create mode 100644 Iceshrimp.Backend/Core/Federation/WebFinger/WebFingerService.cs delete mode 100644 Iceshrimp.Backend/Core/Helpers/HttpClientHelpers.cs rename Iceshrimp.Backend/Core/Helpers/{LongExtensions.cs => NumberExtensions.cs} (100%) diff --git a/Iceshrimp.Backend/Core/Federation/ActivityStreams/Types/ASActivity.cs b/Iceshrimp.Backend/Core/Federation/ActivityStreams/Types/ASActivity.cs index 4762f69a..4766ef92 100644 --- a/Iceshrimp.Backend/Core/Federation/ActivityStreams/Types/ASActivity.cs +++ b/Iceshrimp.Backend/Core/Federation/ActivityStreams/Types/ASActivity.cs @@ -2,7 +2,6 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; using J = Newtonsoft.Json.JsonPropertyAttribute; using JC = Newtonsoft.Json.JsonConverterAttribute; -using VC = Iceshrimp.Backend.Core.Federation.ActivityStreams.Types.ValueObjectConverter; namespace Iceshrimp.Backend.Core.Federation.ActivityStreams.Types; diff --git a/Iceshrimp.Backend/Core/Federation/Services/UserResolver.cs b/Iceshrimp.Backend/Core/Federation/Services/UserResolver.cs index 8d9b1a9f..290fe7e7 100644 --- a/Iceshrimp.Backend/Core/Federation/Services/UserResolver.cs +++ b/Iceshrimp.Backend/Core/Federation/Services/UserResolver.cs @@ -5,7 +5,7 @@ using Iceshrimp.Backend.Core.Services; namespace Iceshrimp.Backend.Core.Federation.Services; -public class UserResolver(ILogger logger, UserService userSvc, DatabaseContext db) { +public class UserResolver(ILogger logger, UserService userSvc, WebFingerService webFingerSvc, DatabaseContext db) { private static string AcctToDomain(string acct) => acct.StartsWith("acct:") && acct.Contains('@') ? acct[5..].Split('@')[1] @@ -25,9 +25,8 @@ public class UserResolver(ILogger logger, UserService userSvc, Dat private async Task<(string Acct, string Uri)> WebFinger(string query) { logger.LogDebug("Running WebFinger for query '{query}'", query); - var finger = new WebFinger.WebFinger(query); var responses = new Dictionary(); - var fingerRes = await finger.Resolve(); + var fingerRes = await webFingerSvc.Resolve(query); if (fingerRes == null) throw new Exception($"Failed to WebFinger '{query}'"); responses.Add(query, fingerRes); @@ -39,8 +38,7 @@ public class UserResolver(ILogger logger, UserService userSvc, Dat if (fingerRes == null) { logger.LogDebug("AP uri didn't match query, re-running WebFinger for '{apUri}'", apUri); - finger = new WebFinger.WebFinger(apUri); - fingerRes = await finger.Resolve(); + fingerRes = await webFingerSvc.Resolve(apUri); if (fingerRes == null) throw new Exception($"Failed to WebFinger '{apUri}'"); responses.Add(apUri, fingerRes); @@ -53,8 +51,7 @@ public class UserResolver(ILogger logger, UserService userSvc, Dat if (fingerRes == null) { logger.LogDebug("Acct uri didn't match query, re-running WebFinger for '{acctUri}'", acctUri); - finger = new WebFinger.WebFinger(acctUri); - fingerRes = await finger.Resolve(); + fingerRes = await webFingerSvc.Resolve(acctUri); if (fingerRes == null) throw new Exception($"Failed to WebFinger '{acctUri}'"); responses.Add(acctUri, fingerRes); diff --git a/Iceshrimp.Backend/Core/Federation/WebFinger/Types.cs b/Iceshrimp.Backend/Core/Federation/WebFinger/Types.cs index 5b1f30af..7df782a2 100644 --- a/Iceshrimp.Backend/Core/Federation/WebFinger/Types.cs +++ b/Iceshrimp.Backend/Core/Federation/WebFinger/Types.cs @@ -1,19 +1,20 @@ using System.Diagnostics.CodeAnalysis; using J = System.Text.Json.Serialization.JsonPropertyNameAttribute; using JR = System.Text.Json.Serialization.JsonRequiredAttribute; -// ReSharper disable ClassNeverInstantiated.Global namespace Iceshrimp.Backend.Core.Federation.WebFinger; -public sealed class Link { +[SuppressMessage("ReSharper", "ClassNeverInstantiated.Global")] +public sealed class WebFingerLink { [J("rel"), JR] public string Rel { get; set; } = null!; [J("type")] public string? Type { get; set; } = null!; [J("href")] public string? Href { get; set; } } [SuppressMessage("ReSharper", "CollectionNeverUpdated.Global")] +[SuppressMessage("ReSharper", "ClassNeverInstantiated.Global")] public sealed class WebFingerResponse { - [J("links"), JR] public List Links { get; set; } = null!; - [J("subject"), JR] public string Subject { get; set; } = null!; - [J("aliases")] public List Aliases { get; set; } = []; + [J("links"), JR] public List Links { get; set; } = null!; + [J("subject"), JR] public string Subject { get; set; } = null!; + [J("aliases")] public List Aliases { get; set; } = []; } \ No newline at end of file diff --git a/Iceshrimp.Backend/Core/Federation/WebFinger/WebFinger.cs b/Iceshrimp.Backend/Core/Federation/WebFinger/WebFinger.cs deleted file mode 100644 index c2a15b1a..00000000 --- a/Iceshrimp.Backend/Core/Federation/WebFinger/WebFinger.cs +++ /dev/null @@ -1,102 +0,0 @@ -using System.Net.Http.Headers; -using System.Text.Encodings.Web; -using System.Xml; -using Iceshrimp.Backend.Core.Helpers; - -namespace Iceshrimp.Backend.Core.Federation.WebFinger; - -/* - * There's two different WebFinger implementations out there - * 1. Get /.well-known/host-meta, extract the WebFinger query url template from there - * 2. Get /.well-known/webfinger?resource={uri} directly - * - * We have to check for host-meta first, and only fall back to the second implementation if it - * - doesn't exist - * - doesn't have a query url template - */ - -//FIXME: handle cursed person/group acct collisions like https://lemmy.ml/.well-known/webfinger?resource=acct:linux@lemmy.ml - -public class WebFinger { - private readonly string _query; - private readonly string _proto; - private readonly string _domain; - private string? _webFingerUrl; - - public string Domain => _domain; - - private string HostMetaUrl => $"{_proto}://{_domain}/.well-known/host-meta"; - private string DefaultWebFingerTemplate => $"{_proto}://{_domain}/.well-known/webfinger?resource={{uri}}"; - - public WebFinger(string query) { - _query = query.StartsWith("acct:") ? $"@{query[5..]}" : query; - if (_query.StartsWith("http://") || _query.StartsWith("https://")) { - var uri = new Uri(_query); - _domain = uri.Host; - _proto = _query.StartsWith("http://") ? "http" : "https"; - } - else if (_query.StartsWith('@')) { - _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]; - } - else { - throw new Exception("Invalid query"); - } - } - - private string? GetWebFingerTemplateFromHostMeta() { - var client = HttpClientHelpers.HttpClient; - var request = new HttpRequestMessage { - RequestUri = new Uri(HostMetaUrl), - Method = HttpMethod.Get, - Headers = { Accept = { MediaTypeWithQualityHeaderValue.Parse("application/xrd+xml") } } - }; - var res = client.SendAsync(request); - var xml = new XmlDocument(); - xml.Load(res.Result.Content.ReadAsStreamAsync().Result); - var section = xml["XRD"]?.GetElementsByTagName("Link"); - if (section == null) return null; - - //TODO: implement https://stackoverflow.com/a/37322614/18402176 instead - - for (var i = 0; i < section.Count; i++) { - if (section[i]?.Attributes?["rel"]?.InnerText == "lrdd") { - return section[i]?.Attributes?["template"]?.InnerText; - } - } - - return null; - } - - private string GetWebFingerUrl() { - var template = GetWebFingerTemplateFromHostMeta() ?? DefaultWebFingerTemplate; - var query = _query.StartsWith('@') ? $"acct:{_query.Substring(1)}" : _query; - var encoded = UrlEncoder.Default.Encode(query); - return template.Replace("{uri}", encoded); - } - - public async Task Resolve() { - _webFingerUrl = GetWebFingerUrl(); - - var client = HttpClientHelpers.HttpClient; - var request = new HttpRequestMessage { - RequestUri = new Uri(_webFingerUrl), - Method = HttpMethod.Get, - Headers = { - Accept = { - MediaTypeWithQualityHeaderValue.Parse("application/jrd+json"), - MediaTypeWithQualityHeaderValue.Parse("application/json") - } - } - }; - var res = await client.SendAsync(request); - return await res.Content.ReadFromJsonAsync(); - } -} \ No newline at end of file diff --git a/Iceshrimp.Backend/Core/Federation/WebFinger/WebFingerService.cs b/Iceshrimp.Backend/Core/Federation/WebFinger/WebFingerService.cs new file mode 100644 index 00000000..0e699312 --- /dev/null +++ b/Iceshrimp.Backend/Core/Federation/WebFinger/WebFingerService.cs @@ -0,0 +1,82 @@ +using System.Text.Encodings.Web; +using System.Xml; +using Iceshrimp.Backend.Core.Services; + +namespace Iceshrimp.Backend.Core.Federation.WebFinger; + +/* + * There's two different WebFinger implementations out there + * 1. Get /.well-known/host-meta, extract the WebFinger query url template from there + * 2. Get /.well-known/webfinger?resource={uri} directly + * + * We have to check for host-meta first, and only fall back to the second implementation if it + * - doesn't exist + * - doesn't have a query url template + */ + +//FIXME: handle cursed person/group acct collisions like https://lemmy.ml/.well-known/webfinger?resource=acct:linux@lemmy.ml + +public class WebFingerService(HttpClient client, HttpRequestService httpRqSvc) { + public async Task Resolve(string query) { + (query, var proto, var domain) = ParseQuery(query); + var webFingerUrl = GetWebFingerUrl(query, proto, domain); + + var req = httpRqSvc.Get(webFingerUrl, ["application/jrd+json", "application/json"]); + var res = await client.SendAsync(req); + + return await res.Content.ReadFromJsonAsync(); + } + + private static (string query, string proto, string domain) ParseQuery(string query) { + string domain; + string proto; + query = query.StartsWith("acct:") ? $"@{query[5..]}" : query; + if (query.StartsWith("http://") || query.StartsWith("https://")) { + var uri = new Uri(query); + domain = uri.Host; + proto = query.StartsWith("http://") ? "http" : "https"; + } + else if (query.StartsWith('@')) { + 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]; + } + else { + throw new Exception("Invalid query"); + } + + return (query, proto, domain); + } + + private string GetWebFingerUrl(string query, string proto, string domain) { + var template = GetWebFingerTemplateFromHostMeta($"{proto}://{domain}/.well-known/host-meta") ?? + $"{proto}://{domain}/.well-known/webfinger?resource={{uri}}"; + var finalQuery = query.StartsWith('@') ? $"acct:{query[1..]}" : query; + var encoded = UrlEncoder.Default.Encode(finalQuery); + return template.Replace("{uri}", encoded); + } + + private string? GetWebFingerTemplateFromHostMeta(string hostMetaUrl) { + var res = client.SendAsync(httpRqSvc.Get(hostMetaUrl, ["application/xrd+xml"])); + var xml = new XmlDocument(); + xml.Load(res.Result.Content.ReadAsStreamAsync().Result); + var section = xml["XRD"]?.GetElementsByTagName("Link"); + if (section == null) return null; + + //TODO: implement https://stackoverflow.com/a/37322614/18402176 instead + + for (var i = 0; i < section.Count; i++) { + if (section[i]?.Attributes?["rel"]?.InnerText == "lrdd") { + return section[i]?.Attributes?["template"]?.InnerText; + } + } + + return null; + } +} \ No newline at end of file diff --git a/Iceshrimp.Backend/Core/Helpers/HttpClientHelpers.cs b/Iceshrimp.Backend/Core/Helpers/HttpClientHelpers.cs deleted file mode 100644 index 38682f83..00000000 --- a/Iceshrimp.Backend/Core/Helpers/HttpClientHelpers.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System.Net.Http.Headers; - -namespace Iceshrimp.Backend.Core.Helpers; - -public static class HttpClientHelpers { - //TODO: replace with HttpClient service - public static readonly HttpClient HttpClient = new() { - DefaultRequestHeaders = { - UserAgent = { ProductInfoHeaderValue.Parse("Iceshrimp.NET/0.0.1") } - } - }; -} \ No newline at end of file diff --git a/Iceshrimp.Backend/Core/Helpers/LongExtensions.cs b/Iceshrimp.Backend/Core/Helpers/NumberExtensions.cs similarity index 100% rename from Iceshrimp.Backend/Core/Helpers/LongExtensions.cs rename to Iceshrimp.Backend/Core/Helpers/NumberExtensions.cs diff --git a/Iceshrimp.Backend/Core/Helpers/ServiceExtensions.cs b/Iceshrimp.Backend/Core/Helpers/ServiceExtensions.cs index e9b1794a..a6181cb8 100644 --- a/Iceshrimp.Backend/Core/Helpers/ServiceExtensions.cs +++ b/Iceshrimp.Backend/Core/Helpers/ServiceExtensions.cs @@ -1,6 +1,5 @@ using Iceshrimp.Backend.Controllers.Renderers.ActivityPub; using Iceshrimp.Backend.Core.Configuration; -using Iceshrimp.Backend.Core.Federation; using Iceshrimp.Backend.Core.Federation.Services; using Iceshrimp.Backend.Core.Federation.WebFinger; using Iceshrimp.Backend.Core.Services; @@ -17,6 +16,7 @@ public static class ServiceExtensions { services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); // Singleton = instantiated once across application lifetime services.AddSingleton(); diff --git a/Iceshrimp.Backend/Core/Services/UserService.cs b/Iceshrimp.Backend/Core/Services/UserService.cs index 70d6f6c9..0148f84a 100644 --- a/Iceshrimp.Backend/Core/Services/UserService.cs +++ b/Iceshrimp.Backend/Core/Services/UserService.cs @@ -1,6 +1,5 @@ using Iceshrimp.Backend.Core.Database; using Iceshrimp.Backend.Core.Database.Tables; -using Iceshrimp.Backend.Core.Federation; using Iceshrimp.Backend.Core.Federation.Services; using Microsoft.EntityFrameworkCore; diff --git a/Iceshrimp.Backend/Startup.cs b/Iceshrimp.Backend/Startup.cs index 0f7594af..911309ed 100644 --- a/Iceshrimp.Backend/Startup.cs +++ b/Iceshrimp.Backend/Startup.cs @@ -2,7 +2,6 @@ using Asp.Versioning; using Iceshrimp.Backend.Core.Configuration; using Iceshrimp.Backend.Core.Database; using Iceshrimp.Backend.Core.Helpers; -using Iceshrimp.Backend.Core.Services; using Vite.AspNetCore.Extensions; var builder = WebApplication.CreateBuilder(args);