[backend/federation] Use content negotiation for host-meta responses
This commit is contained in:
parent
7b550fe81b
commit
92f957a536
7 changed files with 76 additions and 99 deletions
|
@ -1,27 +0,0 @@
|
|||
using J = System.Text.Json.Serialization.JsonPropertyNameAttribute;
|
||||
|
||||
namespace Iceshrimp.Backend.Controllers.Federation.Schemas;
|
||||
|
||||
public class HostMetaJsonResponse()
|
||||
{
|
||||
public HostMetaJsonResponse(string webDomain) : this()
|
||||
{
|
||||
Links = [new HostMetaJsonResponseLink(webDomain)];
|
||||
}
|
||||
|
||||
[J("links")] public List<HostMetaJsonResponseLink>? Links { get; set; }
|
||||
}
|
||||
|
||||
public class HostMetaJsonResponseLink()
|
||||
{
|
||||
public HostMetaJsonResponseLink(string webDomain) : this()
|
||||
{
|
||||
Rel = "lrdd";
|
||||
Type = "application/jrd+json";
|
||||
Template = $"https://{webDomain}/.well-known/webfinger?resource={{uri}}";
|
||||
}
|
||||
|
||||
[J("rel")] public string? Rel { get; set; }
|
||||
[J("type")] public string? Type { get; set; }
|
||||
[J("template")] public string? Template { get; set; }
|
||||
}
|
|
@ -1,24 +0,0 @@
|
|||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Xml.Serialization;
|
||||
|
||||
namespace Iceshrimp.Backend.Controllers.Federation.Schemas;
|
||||
|
||||
[XmlRoot("XRD", Namespace = "http://docs.oasis-open.org/ns/xri/xrd-1.0", IsNullable = false)]
|
||||
public class HostMetaXmlResponse()
|
||||
{
|
||||
[XmlElement("Link")] public required HostMetaXmlResponseLink Link;
|
||||
|
||||
[SetsRequiredMembers]
|
||||
public HostMetaXmlResponse(string webDomain) : this() => Link = new HostMetaXmlResponseLink(webDomain);
|
||||
}
|
||||
|
||||
public class HostMetaXmlResponseLink()
|
||||
{
|
||||
[XmlAttribute("rel")] public string Rel = "lrdd";
|
||||
[XmlAttribute("template")] public required string Template;
|
||||
[XmlAttribute("type")] public string Type = "application/jrd+json";
|
||||
|
||||
[SetsRequiredMembers]
|
||||
public HostMetaXmlResponseLink(string webDomain) : this() =>
|
||||
Template = $"https://{webDomain}/.well-known/webfinger?resource={{uri}}";
|
||||
}
|
|
@ -1,10 +1,6 @@
|
|||
using System.Net;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Net.Mime;
|
||||
using System.Text;
|
||||
using System.Xml.Serialization;
|
||||
using Iceshrimp.Backend.Controllers.Federation.Attributes;
|
||||
using Iceshrimp.Backend.Controllers.Federation.Schemas;
|
||||
using Iceshrimp.Backend.Controllers.Shared.Attributes;
|
||||
using Iceshrimp.Backend.Core.Configuration;
|
||||
using Iceshrimp.Backend.Core.Database;
|
||||
|
@ -24,7 +20,7 @@ namespace Iceshrimp.Backend.Controllers.Federation;
|
|||
public class WellKnownController(IOptions<Config.InstanceSection> config, DatabaseContext db) : ControllerBase
|
||||
{
|
||||
[HttpGet("webfinger")]
|
||||
[Produces("application/jrd+json", "application/json", "application/xrd+xml", "application/xml")]
|
||||
[Produces("application/jrd+json", "application/xrd+xml")]
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
[ProducesErrors(HttpStatusCode.NotFound)]
|
||||
public async Task<WebFingerResponse> WebFinger([FromQuery] string resource)
|
||||
|
@ -83,10 +79,7 @@ public class WellKnownController(IOptions<Config.InstanceSection> config, Databa
|
|||
Template = $"https://{config.Value.WebDomain}/authorize-follow?acct={{uri}}"
|
||||
}
|
||||
],
|
||||
Aliases = [
|
||||
user.GetPublicUrl(config.Value),
|
||||
user.GetPublicUri(config.Value)
|
||||
]
|
||||
Aliases = [user.GetPublicUrl(config.Value), user.GetPublicUri(config.Value)]
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -116,35 +109,14 @@ public class WellKnownController(IOptions<Config.InstanceSection> config, Databa
|
|||
[HttpGet("host-meta")]
|
||||
[Produces("application/xrd+xml", "application/jrd+json")]
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
public ActionResult<HostMetaJsonResponse> HostMeta()
|
||||
public HostMetaResponse HostMeta()
|
||||
{
|
||||
var accept = Request.Headers.Accept.OfType<string>()
|
||||
.SelectMany(p => p.Split(","))
|
||||
.Select(MediaTypeWithQualityHeaderValue.Parse)
|
||||
.Select(p => p.MediaType)
|
||||
.ToList();
|
||||
|
||||
if (accept.Contains("application/jrd+json") || accept.Contains("application/json"))
|
||||
return Ok(HostMetaJson());
|
||||
|
||||
var obj = new HostMetaXmlResponse(config.Value.WebDomain);
|
||||
var serializer = new XmlSerializer(obj.GetType());
|
||||
var writer = new Utf8StringWriter();
|
||||
|
||||
serializer.Serialize(writer, obj);
|
||||
return Content(writer.ToString(), "application/xrd+xml");
|
||||
if (Request.Headers.Accept is []) Request.Headers.Accept = "application/xrd+xml";
|
||||
return new HostMetaResponse(config.Value.WebDomain);
|
||||
}
|
||||
|
||||
[HttpGet("host-meta.json")]
|
||||
[Produces("application/jrd+json")]
|
||||
[ProducesResults(HttpStatusCode.OK)]
|
||||
public HostMetaJsonResponse HostMetaJson()
|
||||
{
|
||||
return new HostMetaJsonResponse(config.Value.WebDomain);
|
||||
}
|
||||
|
||||
private class Utf8StringWriter : StringWriter
|
||||
{
|
||||
public override Encoding Encoding => Encoding.UTF8;
|
||||
}
|
||||
public HostMetaResponse HostMetaJson() => new(config.Value.WebDomain);
|
||||
}
|
|
@ -1,11 +1,11 @@
|
|||
using System.Buffers;
|
||||
using System.Net;
|
||||
using System.Text.Encodings.Web;
|
||||
using System.Xml;
|
||||
using Iceshrimp.Backend.Controllers.Shared.Attributes;
|
||||
using Iceshrimp.Backend.Core.Middleware;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Formatters;
|
||||
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
using Microsoft.Extensions.ObjectPool;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
@ -15,6 +15,17 @@ namespace Iceshrimp.Backend.Core.Extensions;
|
|||
|
||||
public static class MvcBuilderExtensions
|
||||
{
|
||||
public static IMvcBuilder AddControllersWithOptions(this IServiceCollection services)
|
||||
{
|
||||
services.AddSingleton<OutputFormatterSelector, AcceptHeaderOutputFormatterSelector>();
|
||||
|
||||
return services.AddControllers(opts =>
|
||||
{
|
||||
opts.RespectBrowserAcceptHeader = true;
|
||||
opts.ReturnHttpNotAcceptable = true;
|
||||
});
|
||||
}
|
||||
|
||||
public static IMvcBuilder AddMultiFormatter(this IMvcBuilder builder)
|
||||
{
|
||||
builder.Services.AddOptions<MvcOptions>()
|
||||
|
@ -91,3 +102,26 @@ public static class MvcBuilderExtensions
|
|||
return builder;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Overrides the default OutputFormatterSelector, to make sure we return a body when content negotiation errors occur.
|
||||
/// </summary>
|
||||
public class AcceptHeaderOutputFormatterSelector(
|
||||
IOptions<MvcOptions> options,
|
||||
ILoggerFactory loggerFactory
|
||||
) : OutputFormatterSelector
|
||||
{
|
||||
private readonly DefaultOutputFormatterSelector _fallbackSelector = new(options, loggerFactory);
|
||||
private readonly List<IOutputFormatter> _formatters = [..options.Value.OutputFormatters];
|
||||
|
||||
public override IOutputFormatter? SelectFormatter(
|
||||
OutputFormatterCanWriteContext context, IList<IOutputFormatter> formatters, MediaTypeCollection mediaTypes
|
||||
)
|
||||
{
|
||||
var selectedFormatter = _fallbackSelector.SelectFormatter(context, formatters, mediaTypes);
|
||||
if (selectedFormatter == null)
|
||||
throw new GracefulException(HttpStatusCode.NotAcceptable, "The requested Content-Type is not acceptable.");
|
||||
|
||||
return selectedFormatter;
|
||||
}
|
||||
}
|
|
@ -8,7 +8,7 @@ using JI = System.Text.Json.Serialization.JsonIgnoreAttribute;
|
|||
namespace Iceshrimp.Backend.Core.Federation.WebFinger;
|
||||
|
||||
[SuppressMessage("ReSharper", "ClassNeverInstantiated.Global")]
|
||||
public sealed class WebFingerLink
|
||||
public class WebFingerLink
|
||||
{
|
||||
[XmlAttribute("rel")] [J("rel")] [JR] public required string Rel { get; set; } = null!;
|
||||
|
||||
|
@ -28,15 +28,10 @@ public sealed class WebFingerLink
|
|||
public string? Template { get; set; }
|
||||
}
|
||||
|
||||
[SuppressMessage("ReSharper", "CollectionNeverUpdated.Global")]
|
||||
[SuppressMessage("ReSharper", "ClassNeverInstantiated.Global")]
|
||||
[XmlRoot("XRD", Namespace = "http://docs.oasis-open.org/ns/xri/xrd-1.0", IsNullable = false)]
|
||||
public sealed class WebFingerResponse
|
||||
{
|
||||
[XmlElement("Link")]
|
||||
[J("links")]
|
||||
[JR]
|
||||
public required List<WebFingerLink> Links { get; set; } = null!;
|
||||
[XmlElement("Link")] [J("links")] [JR] public required List<WebFingerLink> Links { get; set; } = null!;
|
||||
|
||||
[XmlElement("Subject")]
|
||||
[J("subject")]
|
||||
|
@ -49,6 +44,33 @@ public sealed class WebFingerResponse
|
|||
public List<string>? Aliases { get; set; }
|
||||
}
|
||||
|
||||
[XmlRoot("XRD", Namespace = "http://docs.oasis-open.org/ns/xri/xrd-1.0", IsNullable = false)]
|
||||
public class HostMetaResponse()
|
||||
{
|
||||
[SetsRequiredMembers]
|
||||
public HostMetaResponse(string webDomain) : this()
|
||||
{
|
||||
Links =
|
||||
[
|
||||
new HostMetaResponseLink(webDomain, "application/jrd+json"),
|
||||
new HostMetaResponseLink(webDomain, "application/xrd+xml")
|
||||
];
|
||||
}
|
||||
|
||||
[XmlElement("Link")] [J("links")] [JR] public required List<HostMetaResponseLink> Links { get; set; }
|
||||
}
|
||||
|
||||
public sealed class HostMetaResponseLink() : WebFingerLink
|
||||
{
|
||||
[SetsRequiredMembers]
|
||||
public HostMetaResponseLink(string webDomain, string type) : this()
|
||||
{
|
||||
Rel = "lrdd";
|
||||
Type = type;
|
||||
Template = $"https://{webDomain}/.well-known/webfinger?resource={{uri}}";
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class NodeInfoIndexResponse
|
||||
{
|
||||
[J("links")] [JR] public List<WebFingerLink> Links { get; set; } = null!;
|
||||
|
|
|
@ -3,7 +3,6 @@ using System.Net;
|
|||
using System.Text.Encodings.Web;
|
||||
using System.Xml.Linq;
|
||||
using System.Xml.Serialization;
|
||||
using Iceshrimp.Backend.Controllers.Federation.Schemas;
|
||||
using Iceshrimp.Backend.Core.Configuration;
|
||||
using Iceshrimp.Backend.Core.Middleware;
|
||||
using Iceshrimp.Backend.Core.Services;
|
||||
|
@ -140,9 +139,9 @@ public class WebFingerService(
|
|||
var hostMetaUrl = $"{proto}://{domain}/.well-known/host-meta.json";
|
||||
using var res = await client.SendAsync(httpRqSvc.Get(hostMetaUrl, ["application/jrd+json"]),
|
||||
HttpCompletionOption.ResponseHeadersRead);
|
||||
var deserialized = await res.Content.ReadFromJsonAsync<HostMetaJsonResponse>();
|
||||
var deserialized = await res.Content.ReadFromJsonAsync<HostMetaResponse>();
|
||||
|
||||
var result = deserialized?.Links?.FirstOrDefault(p => p is
|
||||
var result = deserialized?.Links.FirstOrDefault(p => p is
|
||||
{
|
||||
Rel: "lrdd",
|
||||
//Type: "application/jrd+json",
|
||||
|
@ -162,9 +161,9 @@ public class WebFingerService(
|
|||
var hostMetaUrl = $"{proto}://{domain}/.well-known/host-meta";
|
||||
using var res = await client.SendAsync(httpRqSvc.Get(hostMetaUrl, ["application/jrd+json"]),
|
||||
HttpCompletionOption.ResponseHeadersRead);
|
||||
var deserialized = await res.Content.ReadFromJsonAsync<HostMetaJsonResponse>();
|
||||
var deserialized = await res.Content.ReadFromJsonAsync<HostMetaResponse>();
|
||||
|
||||
var result = deserialized?.Links?.FirstOrDefault(p => p is
|
||||
var result = deserialized?.Links.FirstOrDefault(p => p is
|
||||
{
|
||||
Rel: "lrdd",
|
||||
//Type: "application/jrd+json",
|
||||
|
|
|
@ -12,7 +12,8 @@ builder.Configuration.AddCustomConfiguration();
|
|||
|
||||
await PluginLoader.LoadPlugins();
|
||||
|
||||
builder.Services.AddControllers()
|
||||
builder.Services
|
||||
.AddControllersWithOptions()
|
||||
.AddNewtonsoftJson() //TODO: remove once dotNetRdf switches to System.Text.Json (or we switch to LinkedData.NET)
|
||||
.ConfigureNewtonsoftJson()
|
||||
.AddMultiFormatter()
|
||||
|
|
Loading…
Add table
Reference in a new issue