Finish up user resolver work
This commit is contained in:
parent
80d6147757
commit
8bd049cc94
4 changed files with 93 additions and 16 deletions
|
@ -4,11 +4,13 @@ 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; }
|
||||
[J("rel"), JR] public string Rel { get; set; } = null!;
|
||||
[J("type")] public string? Type { get; set; } = null!;
|
||||
[J("href")] public string? Href { get; set; }
|
||||
}
|
||||
|
||||
public sealed class WebFingerResponse {
|
||||
[J("links"), JR] public List<Link> Links { get; set; } = null!;
|
||||
[J("subject"), JR] public string Subject { get; set; } = null!;
|
||||
[J("links"), JR] public List<Link> Links { get; set; } = null!;
|
||||
[J("subject"), JR] public string Subject { get; set; } = null!;
|
||||
[J("aliases")] public List<string> Aliases { get; set; } = [];
|
||||
}
|
|
@ -1,31 +1,89 @@
|
|||
using Iceshrimp.Backend.Core.Database;
|
||||
using Iceshrimp.Backend.Core.Database.Tables;
|
||||
using Iceshrimp.Backend.Core.Services;
|
||||
|
||||
namespace Iceshrimp.Backend.Core.Federation.WebFinger;
|
||||
|
||||
public class UserResolver(ILogger<UserResolver> logger) {
|
||||
public class UserResolver(ILogger<UserResolver> logger, UserService userSvc, DatabaseContext db) {
|
||||
private static string AcctToDomain(string acct) =>
|
||||
acct.StartsWith("acct:") && acct.Contains('@')
|
||||
? acct[5..].Split('@')[1]
|
||||
: throw new Exception("Invalid acct");
|
||||
|
||||
/*
|
||||
* Split domain logic:
|
||||
* 1. Get WebFinger response for query
|
||||
* 2. [...]
|
||||
* TODO: finish description and implement the rest
|
||||
* The full web finger algorithm:
|
||||
*
|
||||
* 1. WebFinger(input_uri), find the rel=self type=application/activity+json entry, that's ap_uri
|
||||
* 2. WebFinger(ap_uri), find the first acct: URI in [subject] + aliases, that's candidate_acct_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 (TODO: implement this)
|
||||
*
|
||||
* Avoid repeating WebFinger's with same URI for performance, TODO: optimize away validation checks when the domain matches
|
||||
*/
|
||||
|
||||
public async Task<User> Resolve(string query) {
|
||||
private async Task<(string Acct, string Uri)> WebFinger(string query) {
|
||||
logger.LogDebug("Running WebFinger for query '{query}'", query);
|
||||
|
||||
var finger = new WebFinger(query);
|
||||
var responses = new Dictionary<string, WebFingerResponse>();
|
||||
var fingerRes = await finger.Resolve();
|
||||
if (fingerRes == null) throw new Exception($"Failed to WebFinger '{query}'");
|
||||
if (finger.Domain != AcctToDomain(fingerRes.Subject)) {
|
||||
logger.LogInformation("possible split domain deployment detected, repeating webfinger");
|
||||
responses.Add(query, fingerRes);
|
||||
|
||||
finger = new WebFinger(fingerRes.Subject);
|
||||
var apUri = fingerRes.Links.FirstOrDefault(p => p.Rel == "self" && p.Type == "application/activity+json")
|
||||
?.Href;
|
||||
if (apUri == null) throw new Exception($"WebFinger response for '{query}' didn't contain a candidate link");
|
||||
|
||||
fingerRes = responses.GetValueOrDefault(apUri);
|
||||
if (fingerRes == null) {
|
||||
logger.LogDebug("AP uri didn't match query, re-running WebFinger for '{apUri}'", apUri);
|
||||
|
||||
finger = new WebFinger(apUri);
|
||||
fingerRes = await finger.Resolve();
|
||||
|
||||
if (fingerRes == null) throw new Exception($"Failed to WebFinger '{apUri}'");
|
||||
responses.Add(apUri, fingerRes);
|
||||
}
|
||||
|
||||
throw new NotImplementedException("stub method return");
|
||||
var acctUri = fingerRes.Aliases.Prepend(fingerRes.Subject).FirstOrDefault(p => p.StartsWith("acct:"));
|
||||
if (acctUri == null) throw new Exception($"WebFinger response for '{apUri}' didn't contain any acct uris");
|
||||
|
||||
fingerRes = responses.GetValueOrDefault(acctUri);
|
||||
if (fingerRes == null) {
|
||||
logger.LogDebug("Acct uri didn't match query, re-running WebFinger for '{acctUri}'", acctUri);
|
||||
|
||||
finger = new WebFinger(acctUri);
|
||||
fingerRes = await finger.Resolve();
|
||||
|
||||
if (fingerRes == null) throw new Exception($"Failed to WebFinger '{acctUri}'");
|
||||
responses.Add(acctUri, fingerRes);
|
||||
}
|
||||
|
||||
var finalAcct = fingerRes.Subject;
|
||||
var finalUri = fingerRes.Links.FirstOrDefault(p => p.Rel == "self" && p.Type == "application/activity+json")
|
||||
?.Href ?? throw new Exception("Final AP URI was null");
|
||||
|
||||
return (finalAcct, finalUri);
|
||||
}
|
||||
|
||||
private static string NormalizeQuery(string query) => query.StartsWith("acct:") ? query[5..] : query;
|
||||
|
||||
public async Task<User> Resolve(string query) {
|
||||
query = NormalizeQuery(query);
|
||||
|
||||
// First, let's see if we already know the user
|
||||
var user = await userSvc.GetUserFromQuery(query);
|
||||
if (user != null) return user;
|
||||
|
||||
// We don't, so we need to run WebFinger
|
||||
var (acct, uri) = await WebFinger(query);
|
||||
|
||||
// Check the database again with the new data
|
||||
if (uri != query) user = await userSvc.GetUserFromQuery(uri);
|
||||
if (user == null && acct != query) await userSvc.GetUserFromQuery(acct);
|
||||
if (user != null) return user;
|
||||
|
||||
// Pass the job on to userSvc, which will create the user
|
||||
return await userSvc.CreateUser(acct, uri);
|
||||
}
|
||||
}
|
|
@ -1,10 +1,12 @@
|
|||
using Iceshrimp.Backend.Core.Configuration;
|
||||
using Iceshrimp.Backend.Core.Federation.WebFinger;
|
||||
using Iceshrimp.Backend.Core.Services;
|
||||
|
||||
namespace Iceshrimp.Backend.Core.Helpers;
|
||||
|
||||
public static class ServiceExtensions {
|
||||
public static void AddServices(this IServiceCollection services) {
|
||||
services.AddScoped<UserResolver>();
|
||||
services.AddScoped<UserService>();
|
||||
services.AddScoped<NoteService>();
|
||||
}
|
||||
|
|
|
@ -1,10 +1,25 @@
|
|||
using Iceshrimp.Backend.Core.Database;
|
||||
using Iceshrimp.Backend.Core.Database.Tables;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Iceshrimp.Backend.Core.Services;
|
||||
|
||||
public class UserService(ILogger<UserService> logger, DatabaseContext db) {
|
||||
private static (string Username, string Host) AcctToTuple(string acct) {
|
||||
if (!acct.StartsWith("acct:")) throw new Exception("Invalid query");
|
||||
throw new NotImplementedException(); //FIXME
|
||||
}
|
||||
|
||||
public async Task CreateUser() {
|
||||
public Task<User?> GetUserFromQuery(string query) {
|
||||
if (query.StartsWith("http://") || query.StartsWith("https://")) {
|
||||
return db.Users.FirstOrDefaultAsync(p => p.Uri == query);
|
||||
}
|
||||
|
||||
var tuple = AcctToTuple(query);
|
||||
return db.Users.FirstOrDefaultAsync(p => p.Username == tuple.Username && p.Host == tuple.Host);
|
||||
}
|
||||
|
||||
public async Task<User> CreateUser(string uri, string acct) {
|
||||
throw new NotImplementedException(); //FIXME
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue