Refactor WebFinger -> WebFingerService
This commit is contained in:
parent
de34bc5271
commit
ae89337a2a
10 changed files with 93 additions and 130 deletions
|
@ -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;
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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; } = [];
|
||||
}
|
|
@ -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>();
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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") }
|
||||
}
|
||||
};
|
||||
}
|
|
@ -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>();
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
Loading…
Add table
Reference in a new issue