Refactor WebFinger -> WebFingerService

This commit is contained in:
Laura Hausmann 2024-01-13 19:46:47 +01:00
parent de34bc5271
commit ae89337a2a
No known key found for this signature in database
GPG key ID: D044E84C5BE01605
10 changed files with 93 additions and 130 deletions

View file

@ -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;

View file

@ -5,7 +5,7 @@ using Iceshrimp.Backend.Core.Services;
namespace Iceshrimp.Backend.Core.Federation.Services;
public class UserResolver(ILogger<UserResolver> logger, UserService userSvc, DatabaseContext db) {
public class UserResolver(ILogger<UserResolver> 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<UserResolver> 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<string, WebFingerResponse>();
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<UserResolver> 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<UserResolver> 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);

View file

@ -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<Link> Links { get; set; } = null!;
[J("links"), JR] public List<WebFingerLink> Links { get; set; } = null!;
[J("subject"), JR] public string Subject { get; set; } = null!;
[J("aliases")] public List<string> Aliases { get; set; } = [];
}

View file

@ -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<WebFingerResponse?> 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<WebFingerResponse>();
}
}

View file

@ -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<WebFingerResponse?> 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<WebFingerResponse>();
}
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;
}
}

View file

@ -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") }
}
};
}

View file

@ -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<UserService>();
services.AddScoped<NoteService>();
services.AddScoped<APUserRenderer>();
services.AddScoped<WebFingerService>();
// Singleton = instantiated once across application lifetime
services.AddSingleton<HttpClient>();

View file

@ -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;

View file

@ -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);