[backend/federation] Support WebFinger reverse discovery (ISH-364)
This resolves federation issues with old versions of pixelfed, as well as various AP implementations that are not widely deployed.
This commit is contained in:
parent
2506e42733
commit
acbedd3bae
4 changed files with 62 additions and 13 deletions
|
@ -20,6 +20,9 @@ public static class Constants
|
|||
public const string MisskeyNs = "https://misskey-hub.net/ns";
|
||||
public static readonly string[] SystemUsers = ["instance.actor", "relay.actor"];
|
||||
|
||||
public const string ASMime = "application/activity+json";
|
||||
public const string ASMimeAlt = "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"";
|
||||
|
||||
public static readonly string[] BrowserSafeMimeTypes =
|
||||
[
|
||||
"image/png",
|
||||
|
|
|
@ -30,16 +30,26 @@ public class UserResolver(
|
|||
* The full web finger algorithm:
|
||||
*
|
||||
* 1. WebFinger(input_uri), find the rel=self type=application/activity+json entry, that's ap_uri
|
||||
* 1.1 Failing this, fetch the actor
|
||||
* 1.1.1 If the actor uri differs from the query, recurse (once!) with the new uri
|
||||
* 1.1.2 Otherwise, Perform reverse discovery for the actor uri
|
||||
* 2. WebFinger(ap_uri), find the first acct: URI in [subject] + aliases, that's candidate_acct_uri
|
||||
* 2.1 Failing this, perform reverse discovery for ap_uri
|
||||
* 3. WebFinger(candidate_acct_uri), validate it also points to ap_uri. If so, you have acct_uri
|
||||
* 4. Failing this, acct_uri = "acct:" + preferredUsername from AP actor + "@" + hostname from ap_uri
|
||||
* 3.1 Failing this, acct_uri = "acct:" + preferredUsername from AP actor + "@" + hostname from ap_uri
|
||||
*
|
||||
* Avoid repeating WebFinger's with same URI for performance, optimize away validation checks when the domain matches
|
||||
* Avoid repeating WebFinger's with same URI for performance, optimize away validation checks when the domain matches.
|
||||
* Skip step 2 when performing reverse discovery & ap_uri matches the actor uri.
|
||||
*/
|
||||
|
||||
private async Task<(string Acct, string Uri)> WebFingerAsync(string query, bool recurse = true)
|
||||
private async Task<(string Acct, string Uri)> WebFingerAsync(
|
||||
string query, bool recurse = true, string? actorUri = null
|
||||
)
|
||||
{
|
||||
logger.LogDebug("Running WebFinger for query '{query}'", query);
|
||||
if (actorUri == null)
|
||||
logger.LogDebug("Running WebFinger for query '{query}'", query);
|
||||
else
|
||||
logger.LogDebug("Running WebFinger reverse discovery for query '{query}' and uri '{uri}'", query, actorUri);
|
||||
|
||||
var responses = new Dictionary<string, WebFingerResponse>();
|
||||
var fingerRes = await webFingerSvc.ResolveAsync(query);
|
||||
|
@ -56,6 +66,12 @@ public class UserResolver(
|
|||
logger.LogDebug("Actor ID differs from query, retrying...");
|
||||
return await WebFingerAsync(actor.Id, false);
|
||||
}
|
||||
|
||||
logger.LogDebug("Actor ID matches query, performing reverse discovery...");
|
||||
actor.Normalize(query);
|
||||
var domain = new Uri(actor.Id).Host;
|
||||
var username = actor.Username!;
|
||||
return await WebFingerAsync($"acct:{username}@{domain}", false, actor.Id);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
|
@ -68,7 +84,10 @@ public class UserResolver(
|
|||
|
||||
responses.Add(query, fingerRes);
|
||||
|
||||
var apUri = fingerRes.Links.FirstOrDefault(p => p is { Rel: "self", Type: "application/activity+json" })?.Href;
|
||||
var apUri = fingerRes.Links
|
||||
.FirstOrDefault(p => p is { Rel: "self", Type: Constants.ASMime or Constants.ASMimeAlt })
|
||||
?.Href;
|
||||
|
||||
if (apUri == null)
|
||||
throw new GracefulException($"WebFinger response for '{query}' didn't contain a candidate link");
|
||||
var subjectUri = GetAcctUri(fingerRes) ??
|
||||
|
@ -80,12 +99,39 @@ public class UserResolver(
|
|||
if (subjectHost == apUriHost && subjectHost == queryHost)
|
||||
return (subjectUri, apUri);
|
||||
|
||||
fingerRes = responses.GetValueOrDefault(apUri);
|
||||
if (fingerRes == null)
|
||||
// We need to skip this step when performing reverse discovery & the uris match
|
||||
if (apUri != actorUri)
|
||||
{
|
||||
logger.LogDebug("AP uri didn't match query, re-running WebFinger for '{apUri}'", apUri);
|
||||
fingerRes = await webFingerSvc.ResolveAsync(apUri) ?? throw new Exception($"Failed to WebFinger '{apUri}'");
|
||||
responses.Add(apUri, fingerRes);
|
||||
if (actorUri != null) throw new Exception("Reverse discovery failed: uri mismatch");
|
||||
fingerRes = responses.GetValueOrDefault(apUri);
|
||||
if (fingerRes == null)
|
||||
{
|
||||
logger.LogDebug("AP uri didn't match query, re-running WebFinger for '{apUri}'", apUri);
|
||||
fingerRes = await webFingerSvc.ResolveAsync(apUri);
|
||||
if (fingerRes == null)
|
||||
{
|
||||
logger.LogDebug("Failed to validate apUri, falling back to reverse discovery");
|
||||
try
|
||||
{
|
||||
var actor = await fetchSvc.FetchActorAsync(apUri);
|
||||
if (apUri != actor.Id)
|
||||
throw new Exception("Reverse discovery fallback failed: uri mismatch");
|
||||
|
||||
logger.LogDebug("Actor ID matches apUri, performing reverse discovery...");
|
||||
actor.Normalize(apUri);
|
||||
var domain = new Uri(actor.Id).Host;
|
||||
var username = new Uri(actor.Username!).Host;
|
||||
return await WebFingerAsync($"acct:{username}@{domain}", false, apUri);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.LogDebug("Failed to fetch actor {uri}: {e}", query, e.Message);
|
||||
throw new GracefulException($"Failed to WebFinger '{query}'");
|
||||
}
|
||||
}
|
||||
|
||||
responses.Add(apUri, fingerRes);
|
||||
}
|
||||
}
|
||||
|
||||
var acctUri = GetAcctUri(fingerRes) ??
|
||||
|
|
|
@ -128,7 +128,7 @@ public class ASActor : ASObject
|
|||
|
||||
[JI] public bool IsBot => Type == $"{Constants.ActivityStreamsNs}#Service";
|
||||
|
||||
public void Normalize(string uri, string acct)
|
||||
public void Normalize(string uri)
|
||||
{
|
||||
if (Type == null || !ActorTypes.Contains(Type)) throw new Exception("Actor is of invalid type");
|
||||
|
||||
|
|
|
@ -116,7 +116,7 @@ public class UserService(
|
|||
var actor = await fetchSvc.FetchActorAsync(uri);
|
||||
logger.LogDebug("Got actor: {url}", actor.Url);
|
||||
|
||||
actor.Normalize(uri, acct);
|
||||
actor.Normalize(uri);
|
||||
|
||||
if (actor.Id != uri)
|
||||
throw GracefulException.UnprocessableEntity("Uri doesn't match id of fetched actor");
|
||||
|
@ -257,7 +257,7 @@ public class UserService(
|
|||
logger.LogDebug("Updating user with uri {uri}", uri);
|
||||
|
||||
actor ??= await fetchSvc.FetchActorAsync(user.Uri);
|
||||
actor.Normalize(uri, user.AcctWithPrefix);
|
||||
actor.Normalize(uri);
|
||||
|
||||
user.UserProfile ??= await db.UserProfiles.FirstOrDefaultAsync(p => p.User == user);
|
||||
user.UserProfile ??= new UserProfile { User = user };
|
||||
|
|
Loading…
Add table
Reference in a new issue