diff --git a/Iceshrimp.Backend/Core/Configuration/Config.cs b/Iceshrimp.Backend/Core/Configuration/Config.cs index 8b92f4a4..75c1bfe6 100644 --- a/Iceshrimp.Backend/Core/Configuration/Config.cs +++ b/Iceshrimp.Backend/Core/Configuration/Config.cs @@ -2,10 +2,16 @@ namespace Iceshrimp.Backend.Core.Configuration; public sealed class Config { // FIXME: This doesn't reflect config updates. - public static Config StartupConfig { get; set; } = null!; - - public required InstanceSection Instance { get; set; } - public required DatabaseSection Database { get; set; } + public static Config StartupConfig { get; set; } = null!; + + public required InstanceSection Instance { get; set; } + public required DatabaseSection Database { get; set; } + public StaticSection Static = new(); + + public sealed class StaticSection { + public string Version = "0.0.1"; + public string UserAgent => $"Iceshrimp.NET/{Version} (https://{StartupConfig.Instance.WebDomain})"; + } public sealed class InstanceSection { public required int ListenPort { get; set; } = 3000; diff --git a/Iceshrimp.Backend/Core/Federation/WebFinger/Types.cs b/Iceshrimp.Backend/Core/Federation/WebFinger/Types.cs new file mode 100644 index 00000000..cd323252 --- /dev/null +++ b/Iceshrimp.Backend/Core/Federation/WebFinger/Types.cs @@ -0,0 +1,14 @@ +using J = System.Text.Json.Serialization.JsonPropertyNameAttribute; +using JR = System.Text.Json.Serialization.JsonRequiredAttribute; + +namespace Iceshrimp.Backend.Core.Federation.WebFinger; + +public sealed class Link { + [J("href"), JR] public string Href { get; set; } = null!; + [J("rel")] public string? Rel { get; set; } +} + +public sealed class WebFingerResponse { + [J("links"), JR] public List Links { get; set; } = null!; + [J("subject"), JR] public string Subject { get; set; } = null!; +} \ No newline at end of file diff --git a/Iceshrimp.Backend/Core/Federation/WebFinger/WebFinger.cs b/Iceshrimp.Backend/Core/Federation/WebFinger/WebFinger.cs new file mode 100644 index 00000000..09051993 --- /dev/null +++ b/Iceshrimp.Backend/Core/Federation/WebFinger/WebFinger.cs @@ -0,0 +1,105 @@ +using System.Net.Http.Headers; +using System.Text.Encodings.Web; +using System.Xml; +using Iceshrimp.Backend.Core.Configuration; +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 + */ + +public class WebFinger { + private readonly string _query; + private readonly string _proto; + private readonly string _domain; + private readonly string? _username; + private string? _webFingerUrl; + + 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; + 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 == 2) { + throw new Exception("Can't run WebFinger for local user"); + } + + if (split.Length == 3) { + _username = split[1]; + _domain = split[2]; + } + else { + throw new Exception("Invalid query"); + } + } + 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/Helpers/HttpClientHelpers.cs b/Iceshrimp.Backend/Core/Helpers/HttpClientHelpers.cs new file mode 100644 index 00000000..617d6757 --- /dev/null +++ b/Iceshrimp.Backend/Core/Helpers/HttpClientHelpers.cs @@ -0,0 +1,11 @@ +using System.Net.Http.Headers; + +namespace Iceshrimp.Backend.Core.Helpers; + +public static class HttpClientHelpers { + public static readonly HttpClient HttpClient = new() { + DefaultRequestHeaders = { + UserAgent = { ProductInfoHeaderValue.Parse("Iceshrimp.NET/0.0.1") } + } //FIXME (instance domain comment in parentheses doesn't work?) + }; +} \ No newline at end of file diff --git a/Iceshrimp.Backend/Core/Services/UserService.cs b/Iceshrimp.Backend/Core/Services/UserService.cs new file mode 100644 index 00000000..8c27564f --- /dev/null +++ b/Iceshrimp.Backend/Core/Services/UserService.cs @@ -0,0 +1,7 @@ +namespace Iceshrimp.Backend.Core.Services; + +public static class UserService { + public static async Task CreateUser() { + + } +} \ No newline at end of file