magnetar/ext_federation/src/lookup_flow.rs

218 lines
6.8 KiB
Rust

use std::{io::Cursor, sync::Arc};
use magnetar_common::{
config::MagnetarNetworkingProtocol,
util::{FediverseTag, FediverseTagDisplay, FediverseTagParseError},
};
use magnetar_core::web_model::{acct::Acct, content_type::ContentJrdJson};
use magnetar_host_meta::{Xrd, XrdXml};
use magnetar_webfinger::webfinger::{WebFinger, WebFingerRel, WebFingerSubject};
use thiserror::Error;
use tracing::trace;
use url::Url;
use crate::{
client::federation_client::{FederationClient, FederationClientError},
FederationLookupService, HostMetaResolverService, HostUnmapped, MappedUser, UnmappedUser,
WebFingerResolverService,
};
pub struct HostMetaResolverProviderDefault {
client: Arc<FederationClient>,
protocol: MagnetarNetworkingProtocol,
}
#[async_trait::async_trait]
impl HostMetaResolverService for HostMetaResolverProviderDefault {
type Error = FederationClientError;
async fn resolve(&self, HostUnmapped(host): &HostUnmapped) -> Result<Xrd, Self::Error> {
let host_meta_xml = self
.client
.get(Url::parse(&format!(
"{}://{}/.well-known/host-meta",
self.protocol.as_ref(),
host
))?)
.send()
.await?;
let XrdXml::Xrd(xrd) = quick_xml::de::from_reader(Cursor::new(host_meta_xml))?;
Ok(xrd)
}
}
pub struct WebFingerResolverProviderDefault {
client: Arc<FederationClient>,
}
#[async_trait::async_trait]
impl WebFingerResolverService for WebFingerResolverProviderDefault {
type Error = FederationClientError;
async fn resolve_url(&self, url: &str) -> Result<WebFinger, Self::Error> {
let host_meta_xml = self
.client
.get(Url::parse(url)?)
.content_type(ContentJrdJson)
.send()
.await?;
let webfinger = serde_json::from_reader(Cursor::new(host_meta_xml))?;
Ok(webfinger)
}
}
#[derive(Debug, Error)]
pub enum FederationLookupErrror {
#[error("Federation client error: {0}")]
FederationClientError(#[from] FederationClientError),
#[error("Fediverse tag parse error: {0}")]
FediverseTagParseError(#[from] FediverseTagParseError),
#[error("URL parse error: {0}")]
UrlParseError(#[from] url::ParseError),
#[error("Missing ActivityStreams URL in WebFinger")]
MissingApUrl,
#[error("Missing Acct URI in WebFinger")]
MissingAcctUri,
}
#[derive(Clone)]
pub struct FederationLookupServiceProviderDefault {
host_meta_resolver: Arc<dyn HostMetaResolverService<Error = FederationClientError>>,
webfinger_resolver: Arc<dyn WebFingerResolverService<Error = FederationClientError>>,
protocol: MagnetarNetworkingProtocol,
}
impl FederationLookupServiceProviderDefault {
pub fn new(
host_meta_resolver: Arc<dyn HostMetaResolverService<Error = FederationClientError>>,
webfinger_resolver: Arc<dyn WebFingerResolverService<Error = FederationClientError>>,
protocol: MagnetarNetworkingProtocol,
) -> Self {
Self {
host_meta_resolver,
webfinger_resolver,
protocol,
}
}
}
#[async_trait::async_trait]
impl FederationLookupService for FederationLookupServiceProviderDefault {
type Error = FederationLookupErrror;
#[tracing::instrument(level = "trace", skip(self))]
async fn map_fedi_tag(
&self,
user: &UnmappedUser,
) -> Result<MappedUser, FederationLookupErrror> {
trace!("Fetching flow initiated");
let host_meta = self.host_meta_resolver.resolve(&user.host).await;
let webfinger_template = match &host_meta {
Ok(h) => {
trace!("host-meta found: {:?}", h);
h.get_webfinger_template().map(str::to_owned)
}
Err(e) => {
trace!("host-meta fetch failed: {}", e);
None
}
}
.unwrap_or_else(|| {
Xrd::default_host_meta(self.protocol.as_ref(), &user.host.as_ref().to_string())
.get_webfinger_template()
.expect("default WebFinger template")
.to_owned()
});
let webfinger = self
.webfinger_resolver
.resolve(
&webfinger_template,
Acct::from(FediverseTag::from(user)).as_ref(),
)
.await?;
trace!("Webfinger fetched: {:?}", webfinger);
let real_tag = match &webfinger.subject {
WebFingerSubject::Acct(acct) => Some(acct.clone()),
_ => webfinger
.aliases
.iter()
.flatten()
.find_map(|alias| match alias {
WebFingerSubject::Acct(acct) => Some(acct.clone()),
_ => None,
}),
}
.ok_or(FederationLookupErrror::MissingAcctUri)?;
let ap_url = webfinger
.links
.iter()
.find_map(|link| match link {
WebFingerRel::RelSelf { href, .. } | WebFingerRel::RelSelfAlt { href, .. } => {
Some(href)
}
_ => None,
})
.ok_or(FederationLookupErrror::MissingApUrl)?
.parse()?;
Ok(MappedUser {
host_meta: host_meta.ok(),
webfinger,
tag_mapped: FediverseTag::try_from(&FediverseTagDisplay::try_from(&real_tag)?)?,
ap_url,
})
}
}
#[cfg(test)]
mod test {
use std::sync::Arc;
use magnetar_common::{config::MagnetarNetworkingProtocol, util::FediverseTag};
use tracing::{info, Level};
use crate::{
client::federation_client::FederationClient, FederationLookupService, HostUnmapped,
UnmappedUser,
};
use super::{
FederationLookupServiceProviderDefault, HostMetaResolverProviderDefault,
WebFingerResolverProviderDefault,
};
#[tokio::test]
async fn should_resolve() {
tracing_subscriber::fmt()
.with_max_level(Level::TRACE)
.init();
let client = Arc::new(FederationClient::new(true, 64000, 20).unwrap());
let federation_lookup = FederationLookupServiceProviderDefault::new(
Arc::new(HostMetaResolverProviderDefault {
protocol: MagnetarNetworkingProtocol::Https,
client: client.clone(),
}),
Arc::new(WebFingerResolverProviderDefault { client }),
MagnetarNetworkingProtocol::Https,
);
let tag: FediverseTag = "@natty@astolfo.social".parse().unwrap();
info!("Resolving: {}", tag);
let resolved = federation_lookup
.map_fedi_tag(&UnmappedUser {
name: tag.name,
host: HostUnmapped(tag.host.unwrap()),
})
.await
.unwrap();
info!("Resolved: {:#?}", resolved);
}
}