2026-10-01 17:49:22 +02:00
|
|
|
using MongoDB.Entities;
|
|
|
|
|
|
|
|
|
|
using PrivaPub.Infrastructure.Http;
|
|
|
|
|
using PrivaPub.Infrastructure.Jobs;
|
|
|
|
|
using PrivaPub.Models.Jobs;
|
|
|
|
|
|
|
|
|
|
using System.Text.Json;
|
|
|
|
|
|
|
|
|
|
namespace PrivaPub.Federation.Objects
|
|
|
|
|
{
|
|
|
|
|
public class InstanceDescriber : IJobHandler
|
|
|
|
|
{
|
|
|
|
|
const int MaxNodeInfoChars = 64 * 1024;
|
|
|
|
|
static readonly string[] Schemas =
|
|
|
|
|
{
|
|
|
|
|
"http://nodeinfo.diaspora.software/ns/schema/2.1",
|
|
|
|
|
"http://nodeinfo.diaspora.software/ns/schema/2.0"
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
readonly IFederationHttp _http;
|
|
|
|
|
|
|
|
|
|
public InstanceDescriber(IFederationHttp http) => _http = http;
|
|
|
|
|
|
|
|
|
|
public JobKind Kind => JobKind.DescribeInstance;
|
|
|
|
|
public int Concurrency => 1;
|
|
|
|
|
public int MaxAttempts => 2;
|
|
|
|
|
public int PerHostLimit => 1;
|
|
|
|
|
|
|
|
|
|
public async Task<JobOutcome> Handle(Job job, CancellationToken token)
|
|
|
|
|
{
|
|
|
|
|
var host = job.Payload;
|
2026-10-03 11:10:42 +02:00
|
|
|
using var scope = HttpScope.For("nodeinfo");
|
2026-10-03 11:51:19 +02:00
|
|
|
using var links = await _http.GetJson(Address($"https://{host}/.well-known/nodeinfo"), "application/json", sign: default, token);
|
2026-10-01 17:49:22 +02:00
|
|
|
var href = links == default ? default : Schemas.Select(schema => LinkFor(links.Root, schema)).FirstOrDefault(link => link != default);
|
|
|
|
|
if (href == default)
|
|
|
|
|
return await Described(host, default, "no NodeInfo", token);
|
|
|
|
|
|
2026-10-03 11:51:19 +02:00
|
|
|
using var nodeInfo = await _http.GetJson(Address(href), "application/json", sign: default, token);
|
2026-10-01 17:49:22 +02:00
|
|
|
return nodeInfo == default ? await Described(host, default, "NodeInfo unreadable", token) : await Described(host, nodeInfo.Root, default, token);
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-03 11:51:19 +02:00
|
|
|
//where an https address is fetched from: itself, except in tests, which point it at a plain-http peer
|
|
|
|
|
protected virtual string Address(string url) => url;
|
|
|
|
|
|
2026-10-01 17:49:22 +02:00
|
|
|
static async Task<JobOutcome> Described(string host, JsonElement? nodeInfo, string error, CancellationToken token)
|
|
|
|
|
{
|
|
|
|
|
var update = DB.Default.Update<RemoteInstance>()
|
|
|
|
|
.Match(i => i.Host == host)
|
|
|
|
|
.Modify(i => i.DescribedAt, DateTime.UtcNow)
|
|
|
|
|
.Modify(i => i.DescriptionError, error)
|
|
|
|
|
.Option(o => o.IsUpsert = true);
|
|
|
|
|
if (nodeInfo is { } root)
|
|
|
|
|
{
|
|
|
|
|
var raw = root.GetRawText();
|
|
|
|
|
update = update
|
|
|
|
|
.Modify(i => i.Software, Text(root, "software", "name"))
|
|
|
|
|
.Modify(i => i.SoftwareVersion, Text(root, "software", "version"))
|
|
|
|
|
.Modify(i => i.NodeName, Text(root, "metadata", "nodeName"))
|
|
|
|
|
.Modify(i => i.Protocols, root.TryGetProperty("protocols", out var protocols) && protocols.ValueKind == JsonValueKind.Array
|
|
|
|
|
? protocols.EnumerateArray().Where(p => p.ValueKind == JsonValueKind.String).Select(p => p.GetString()).Take(16).ToList()
|
|
|
|
|
: new List<string>())
|
|
|
|
|
.Modify(i => i.OpenRegistrations, root.TryGetProperty("openRegistrations", out var open) && open.ValueKind is JsonValueKind.True or JsonValueKind.False
|
|
|
|
|
? open.GetBoolean()
|
|
|
|
|
: default(bool?))
|
|
|
|
|
.Modify(i => i.NodeInfo, raw.Length <= MaxNodeInfoChars ? raw : default);
|
|
|
|
|
}
|
|
|
|
|
await update.ExecuteAsync(token);
|
|
|
|
|
return JobOutcome.Done;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static string LinkFor(JsonElement root, string schema) =>
|
|
|
|
|
root.TryGetProperty("links", out var links) && links.ValueKind == JsonValueKind.Array
|
|
|
|
|
? links.EnumerateArray()
|
|
|
|
|
.Where(l => l.ValueKind == JsonValueKind.Object && l.TryGetProperty("rel", out var rel) && rel.ValueKind == JsonValueKind.String && rel.GetString() == schema)
|
|
|
|
|
.Select(l => l.TryGetProperty("href", out var href) && href.ValueKind == JsonValueKind.String ? href.GetString() : default)
|
|
|
|
|
.FirstOrDefault(h => h != default && h.StartsWith("https://", StringComparison.OrdinalIgnoreCase))
|
|
|
|
|
: default;
|
|
|
|
|
|
|
|
|
|
static string Text(JsonElement root, string parent, string name)
|
|
|
|
|
{
|
|
|
|
|
if (!root.TryGetProperty(parent, out var section) || section.ValueKind != JsonValueKind.Object
|
|
|
|
|
|| !section.TryGetProperty(name, out var value) || value.ValueKind != JsonValueKind.String)
|
|
|
|
|
return default;
|
|
|
|
|
var text = value.GetString();
|
|
|
|
|
return text.Length <= 200 ? text : text[..200];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|