Implemented WebFinger flow and fixed incoming WebFinger
ci/woodpecker/push/ociImagePush Pipeline is running Details

This commit is contained in:
Natty 2024-04-01 01:33:58 +02:00
parent ffed556107
commit 0dff0c0785
Signed by: natty
GPG Key ID: BF6CB659ADEE60EC
14 changed files with 560 additions and 199 deletions

4
Cargo.lock generated
View File

@ -1679,6 +1679,7 @@ dependencies = [
"tokio",
"tokio-util",
"tracing",
"url",
]
[[package]]
@ -1708,6 +1709,7 @@ name = "magnetar_federation"
version = "0.3.0-alpha"
dependencies = [
"async-stream",
"async-trait",
"chrono",
"futures",
"futures-core",
@ -1727,6 +1729,8 @@ dependencies = [
"serde_json",
"thiserror",
"tokio",
"tracing",
"tracing-subscriber",
"url",
]

View File

@ -1,5 +1,8 @@
use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
use std::borrow::{Borrow, Cow};
use std::{
borrow::{Borrow, Cow},
fmt::Display,
};
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct Acct(String);
@ -23,6 +26,12 @@ impl AsRef<str> for Acct {
}
}
impl Display for Acct {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "acct:{}", self.0)
}
}
impl Serialize for Acct {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where

View File

@ -8,10 +8,12 @@ pub mod acct;
pub trait ContentType: Serialize {
fn mime_type(&self) -> &'static str;
fn alt_mime_types(&self) -> &[&'static str];
}
macro_rules! content_type {
($visib:vis $typ:ident, $expression:expr) => {
($visib:vis $typ:ident, $expression:expr $(,$alt:expr)*) => {
#[derive(Copy, Clone, Eq, PartialEq, Debug, Default)]
$visib struct $typ;
@ -31,13 +33,17 @@ macro_rules! content_type {
fn mime_type(&self) -> &'static str {
$expression
}
fn alt_mime_types(&self) -> &[&'static str] {
&[$($alt),*]
}
}
impl<'de> Deserialize<'de> for $typ {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let content_type = String::deserialize(deserializer)?;
if matches!(content_type.as_ref(), $expression) {
if matches!(content_type.as_ref(), $expression $(| $alt)*) {
Ok(Self)
} else {
Err(Error::custom(format!(
@ -54,7 +60,8 @@ pub mod content_type {
use serde::de::Error;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
content_type!(pub ContentActivityStreams, "application/activity+json");
content_type!(pub ContentActivityStreams, "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"", "application/activity+json");
content_type!(pub ContentActivityJson, "application/activity+json", "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"");
content_type!(pub ContentHtml, "text/html");
content_type!(pub ContentJson, "application/json");
content_type!(pub ContentXrdXml, "application/xrd+xml");

View File

@ -13,16 +13,21 @@ ext_calckey_model_migration = { path = "./migration" }
magnetar_common = { path = "../magnetar_common" }
magnetar_sdk = { path = "../magnetar_sdk" }
dotenvy = { workspace = true}
dotenvy = { workspace = true }
futures-core = { workspace = true }
futures-util = { workspace = true }
tokio = { workspace = true, features = ["full"] }
tokio-util = { workspace = true}
redis = { workspace = true, features = ["tokio-comp", "json", "serde_json"]}
sea-orm = { workspace = true, features = ["sqlx-postgres", "runtime-tokio-rustls", "macros"] }
tokio-util = { workspace = true }
redis = { workspace = true, features = ["tokio-comp", "json", "serde_json"] }
sea-orm = { workspace = true, features = [
"sqlx-postgres",
"runtime-tokio-rustls",
"macros",
] }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
strum = { workspace = true }
chrono = { workspace = true }
tracing = { workspace = true }
thiserror = { workspace = true }
url = { workspace = true }

View File

@ -8,6 +8,7 @@ pub mod user_model;
pub use ck;
use ck::*;
pub use sea_orm;
use url::Host;
use user_model::UserResolver;
use crate::model_ext::IdShape;
@ -73,10 +74,10 @@ impl CalckeyModel {
pub async fn get_user_by_tag(
&self,
name: &str,
instance: Option<&str>,
instance: Option<&Host>,
) -> Result<Option<user::Model>, CalckeyDbError> {
let name = name.to_lowercase();
let instance = instance.map(str::to_lowercase);
let instance = instance.map(Host::to_string);
let user = if let Some(instance) = instance {
user::Entity::find()

View File

@ -13,6 +13,7 @@ magnetar_common = { path = "../magnetar_common" }
magnetar_host_meta = { path = "../ext_host_meta" }
magnetar_webfinger = { path = "../ext_webfinger" }
async-trait = { workspace = true }
async-stream = { workspace = true }
futures = { workspace = true }
futures-core = { workspace = true }
@ -33,6 +34,8 @@ percent-encoding = { workspace = true }
reqwest = { workspace = true, features = ["stream"] }
tokio = { workspace = true, features = ["full"] }
tracing = { workspace = true }
[dev-dependencies]
tracing-subscriber = { workspace = true }
miette = { workspace = true, features = ["fancy"] }

View File

@ -1,56 +1,43 @@
use std::{io::Cursor, sync::Arc};
use client::federation_client::{FederationClient, FederationClientError};
use magnetar_common::{config::MagnetarNetworkingProtocol, util::FediverseTag};
use magnetar_host_meta::{Xrd, XrdXml};
use magnetar_common::util::{FediverseTag, ValidName};
use magnetar_host_meta::Xrd;
use magnetar_webfinger::webfinger::WebFinger;
use serde::{Deserialize, Serialize};
use url::Url;
use url::{Host, Url};
pub mod client;
pub mod lookup_flow;
/// The *visible* domain of fediverse handles, that gets resolved by WebFinger
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(transparent)]
pub struct HostUnmapped(String);
pub struct HostUnmapped(Host);
impl AsRef<Host> for HostUnmapped {
fn as_ref(&self) -> &Host {
&self.0
}
}
/// The real domain of fediverse handles used for federation
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(transparent)]
pub struct HostMapped(String);
pub struct HostMapped(Host);
pub trait HostMetaResolverService {
impl AsRef<Host> for HostMapped {
fn as_ref(&self) -> &Host {
&self.0
}
}
#[async_trait::async_trait]
pub trait HostMetaResolverService: Send + Sync {
type Error;
async fn resolve(&self, host: &HostUnmapped) -> Result<Xrd, Self::Error>;
}
pub struct HostMetaResolverProviderDefault {
client: Arc<FederationClient>,
protocol: MagnetarNetworkingProtocol,
}
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 trait WebFingerResolverService {
#[async_trait::async_trait]
pub trait WebFingerResolverService: Send + Sync {
type Error;
async fn resolve_url(&self, url: &str) -> Result<WebFinger, Self::Error>;
@ -62,7 +49,7 @@ pub trait WebFingerResolverService {
) -> Result<WebFinger, Self::Error> {
self.resolve_url(
&template_url.replace(
"{}",
"{uri}",
&percent_encoding::utf8_percent_encode(
resolved_uri,
percent_encoding::NON_ALPHANUMERIC,
@ -74,29 +61,32 @@ pub trait WebFingerResolverService {
}
}
pub struct WebFingerResolverProviderDefault {
client: Arc<FederationClient>,
#[derive(Debug)]
pub struct UnmappedUser {
pub name: ValidName,
pub host: HostUnmapped,
}
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)?).send().await?;
let webfinger = serde_json::from_reader(Cursor::new(host_meta_xml))?;
Ok(webfinger)
impl From<&UnmappedUser> for FediverseTag {
fn from(user: &UnmappedUser) -> Self {
FediverseTag {
name: user.name.clone(),
host: Some(user.host.as_ref().clone()),
}
}
}
#[derive(Debug)]
pub struct MappedUser {
pub host_meta: Xrd,
pub host_meta: Option<Xrd>,
pub webfinger: WebFinger,
pub tag_mapped: FediverseTag,
pub ap_url: Url,
}
pub trait FederationService {
#[async_trait::async_trait]
pub trait FederationLookupService {
type Error;
async fn map_fedi_tag(&self, host: &HostUnmapped) -> Result<MappedUser, Self::Error>;
async fn map_fedi_tag(&self, user: &UnmappedUser) -> Result<MappedUser, Self::Error>;
}

View File

@ -0,0 +1,217 @@
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);
}
}

View File

@ -34,7 +34,10 @@ impl Xrd {
pub fn get_webfinger_template(&self) -> Option<&str> {
self.links
.iter()
.find(|l| l.rel.as_deref() == Some(ContentXrdXml.as_ref()))
.find(|l| {
l.r#type.as_deref() == Some(ContentXrdXml.as_ref())
&& l.rel.as_deref() == Some(RelLrdd.as_ref())
})
.and_then(|l| l.template.as_deref())
}
}

View File

@ -1,7 +1,10 @@
use magnetar_core::web_model::acct::Acct;
use magnetar_core::web_model::content_type::{ContentActivityStreams, ContentHtml};
use magnetar_core::web_model::content_type::{
ContentActivityJson, ContentActivityStreams, ContentHtml,
};
use magnetar_core::web_model::rel::{RelOStatusSubscribe, RelSelf, RelWebFingerProfilePage};
use serde::{Deserialize, Serialize};
use serde_json::Value;
#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
pub struct WebFinger {
@ -34,10 +37,18 @@ pub enum WebFingerRel {
content_type: ContentActivityStreams,
href: String,
},
// Alternative content-type for some implementations
RelSelfAlt {
rel: RelSelf,
#[serde(rename = "type")]
content_type: ContentActivityJson,
href: String,
},
RelOStatusSubscribe {
rel: RelOStatusSubscribe,
template: String,
},
Other(Value),
}
#[cfg(test)]

View File

@ -14,7 +14,7 @@ pub struct MagnetarNetworking {
pub proxy_remote_files: bool,
}
#[derive(Deserialize, Debug)]
#[derive(Deserialize, Debug, Clone, Copy)]
#[serde(rename_all = "snake_case")]
pub enum MagnetarNetworkingProtocol {
Http,

View File

@ -2,21 +2,166 @@ use magnetar_core::web_model::acct::Acct;
use magnetar_sdk::mmm;
use magnetar_sdk::mmm::Token;
use percent_encoding::percent_decode_str;
use std::borrow::Cow;
use std::fmt::Display;
use std::str::FromStr;
use thiserror::Error;
use url::Host;
#[derive(Clone, Debug)]
pub struct ValidName(String);
impl FromStr for ValidName {
type Err = FediverseTagParseError;
fn from_str(name: &str) -> Result<Self, Self::Err> {
if name
.chars()
.any(|c| !c.is_alphanumeric() && c != '-' && c != '_' && c != '.')
{
return Err(FediverseTagParseError::InvalidChar(name.to_owned()));
}
Ok(Self(name.to_owned()))
}
}
impl AsRef<str> for ValidName {
fn as_ref(&self) -> &str {
&self.0
}
}
impl Display for ValidName {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Clone, Debug)]
pub struct FediverseTag {
pub name: String,
pub name: ValidName,
pub host: Option<Host>,
}
impl FediverseTag {
pub fn from_parts(parts: (&str, Option<&str>)) -> Result<Self, FediverseTagParseError> {
Self::try_from(FediverseTagDisplay::from_parts(parts)?)
}
}
impl TryFrom<FediverseTagDisplay> for FediverseTag {
type Error = FediverseTagParseError;
fn try_from(
FediverseTagDisplay { name, host }: FediverseTagDisplay,
) -> Result<Self, FediverseTagParseError> {
Ok(FediverseTag {
name,
host: host.as_deref().map(Host::parse).transpose()?,
})
}
}
impl TryFrom<&FediverseTagDisplay> for FediverseTag {
type Error = FediverseTagParseError;
fn try_from(
FediverseTagDisplay { name, host }: &FediverseTagDisplay,
) -> Result<Self, FediverseTagParseError> {
Ok(FediverseTag {
name: name.clone(),
host: host.as_deref().map(Host::parse).transpose()?,
})
}
}
impl From<&FediverseTag> for Acct {
fn from(tag: &FediverseTag) -> Self {
Acct::new(tag.to_string().into())
}
}
impl FromStr for FediverseTag {
type Err = FediverseTagParseError;
fn from_str(tag: &str) -> Result<Self, Self::Err> {
Self::try_from(FediverseTagDisplay::from_str(tag)?)
}
}
impl Display for FediverseTag {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if let Some(ref host) = self.host {
write!(f, "{}@{}", self.name, host)
} else {
write!(f, "{}", self.name)
}
}
}
#[derive(Clone, Debug)]
pub struct FediverseTagDisplay {
pub name: ValidName,
pub host: Option<String>,
}
impl FediverseTagDisplay {
pub fn from_parts((name, host): (&str, Option<&str>)) -> Result<Self, FediverseTagParseError> {
Ok(FediverseTagDisplay {
name: name.parse()?,
host: host.map(str::to_owned),
})
}
}
impl From<&FediverseTag> for FediverseTagDisplay {
fn from(FediverseTag { name, host }: &FediverseTag) -> Self {
FediverseTagDisplay {
name: name.clone(),
host: host
.as_ref()
.map(Host::to_string)
.as_deref()
.map(idna::domain_to_unicode)
.map(|v| v.0),
}
}
}
impl TryFrom<&Acct> for FediverseTagDisplay {
type Error = FediverseTagParseError;
fn try_from(acct: &Acct) -> Result<Self, Self::Error> {
acct.as_ref().parse()
}
}
impl FromStr for FediverseTagDisplay {
type Err = FediverseTagParseError;
fn from_str(tag: &str) -> Result<Self, Self::Err> {
Self::from_parts(split_tag_inner(tag))
}
}
impl Display for FediverseTagDisplay {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if let Some(ref host) = self.host {
write!(f, "{}@{}", self.name, host)
} else {
write!(f, "{}", self.name)
}
}
}
#[derive(Debug, Error)]
pub enum FediverseTagParseError {
#[error("Invalid char in tag: {0}")]
InvalidChar(String),
#[error("Invalid UTF-8 in tag: {0}")]
InvalidUtf8(#[from] std::str::Utf8Error),
#[error("Invalid host in tag: {0}")]
InvalidHost(#[from] url::ParseError),
}
impl From<&FediverseTagParseError> for &str {
@ -25,94 +170,30 @@ impl From<&FediverseTagParseError> for &str {
}
}
impl<S1: AsRef<str>, S2: AsRef<str>> From<(S1, Option<S2>)> for FediverseTag {
fn from((name, host): (S1, Option<S2>)) -> Self {
Self {
name: name.as_ref().to_owned(),
host: host.as_ref().map(S2::as_ref).map(str::to_owned),
}
}
}
impl From<FediverseTag> for Acct {
fn from(value: FediverseTag) -> Self {
value.to_string().into()
}
}
impl ToString for FediverseTag {
fn to_string(&self) -> String {
if let Some(ref host) = self.host {
format!("{}@{host}", self.name)
} else {
self.name.clone()
}
}
}
pub fn lenient_parse_acct(acct: &Acct) -> Result<FediverseTag, FediverseTagParseError> {
lenient_parse_tag(acct.as_ref())
}
fn split_tag_inner(tag: impl AsRef<str>) -> (String, Option<String>) {
let tag = tag.as_ref();
fn split_tag_inner(tag: &str) -> (&str, Option<&str>) {
let tag = tag.strip_prefix('@').unwrap_or(tag.as_ref());
match tag.split_once('@') {
Some((name, "")) => (name.to_owned(), None),
Some((name, host)) => (name.to_owned(), Some(host.to_owned())),
None => (tag.to_owned(), None),
Some((name, "")) => (name, None),
Some((name, host)) => (name, Some(host)),
None => (tag, None),
}
}
fn validate_tag_inner((name, host): (&str, Option<&str>)) -> Result<(), FediverseTagParseError> {
if name
.chars()
.any(|c| !c.is_alphanumeric() && c != '-' && c != '_' && c != '.')
{
return Err(FediverseTagParseError::InvalidChar(name.to_owned()));
}
if let Some(host_str) = host {
if host_str
.chars()
.any(|c| c.is_control() || c.is_whitespace() || c == '/' || c == '#')
{
return Err(FediverseTagParseError::InvalidChar(name.to_owned()));
}
}
Ok(())
}
pub fn lenient_parse_tag(tag: impl AsRef<str>) -> Result<FediverseTag, FediverseTagParseError> {
let (name, host) = split_tag_inner(tag);
validate_tag_inner((&name, host.as_ref().map(String::as_ref)))?;
Ok(FediverseTag { name, host })
}
pub fn lenient_parse_acct_decode(acct: &Acct) -> Result<FediverseTag, FediverseTagParseError> {
lenient_parse_tag_decode(acct.as_ref())
}
pub fn lenient_parse_tag_decode(
tag: impl AsRef<str>,
) -> Result<FediverseTag, FediverseTagParseError> {
let (name, host) = split_tag_inner(tag);
percent_decode_str(tag.as_ref()).decode_utf8()?.parse()
}
let name_decoded = percent_decode_str(&name).decode_utf8()?;
let host_decoded = host
.map(|host| percent_decode_str(&host).decode_utf8().map(Cow::into_owned))
.transpose()?;
validate_tag_inner((&name_decoded, host_decoded.as_deref()))?;
Ok(FediverseTag {
name: name_decoded.into_owned(),
host: host_decoded,
})
pub fn lenient_parse_acct_decode(acct: &Acct) -> Result<FediverseTag, FediverseTagParseError> {
lenient_parse_tag_decode(acct.as_ref())
}
#[derive(Debug)]

View File

@ -79,14 +79,14 @@ pub async fn handle_user_info_by_acct(
MaybeUser(self_user): MaybeUser,
) -> Result<Json<Res<GetUserByAcct>>, ApiError> {
let mut tag = lenient_parse_tag_decode(&tag_str)?;
if matches!(&tag.host, Some(host) if host == &service.config.networking.host) {
if matches!(&tag.host, Some(host) if host.to_string() == service.config.networking.host) {
tag.host = None;
}
let ctx = PackingContext::new(service.clone(), self_user).await?;
let user_model = service
.db
.get_user_by_tag(&tag.name, tag.host.as_deref())
.get_user_by_tag(tag.name.as_ref(), tag.host.as_ref())
.await?
.ok_or(ObjectNotFound(tag_str))?;

View File

@ -7,11 +7,14 @@ use magnetar_calckey_model::CalckeyModel;
use magnetar_common::config::MagnetarConfig;
use magnetar_common::util::{lenient_parse_acct_decode, FediverseTag};
use magnetar_core::web_model::acct::Acct;
use magnetar_core::web_model::content_type::{ContentActivityStreams, ContentHtml, ContentJrdJson};
use magnetar_core::web_model::content_type::{
ContentActivityJson, ContentActivityStreams, ContentHtml, ContentJrdJson,
};
use magnetar_core::web_model::rel::{RelOStatusSubscribe, RelSelf, RelWebFingerProfilePage};
use magnetar_webfinger::webfinger::{WebFinger, WebFingerRel, WebFingerSubject};
use serde::Deserialize;
use tracing::error;
use url::Url;
#[derive(Deserialize)]
pub struct WebFingerQuery {
@ -19,11 +22,13 @@ pub struct WebFingerQuery {
rel: Option<Vec<String>>,
}
// TODO: We don't have this endpoint in Magnetar yet, but make sure it's not hardcoded when
// we do
const USER_AP_ENDPOINT: &str = "/users/";
// TODO: Filter by rel
pub async fn handle_webfinger(
Query(WebFingerQuery {
resource, rel: _, ..
}): Query<WebFingerQuery>,
Query(WebFingerQuery { resource, .. }): Query<WebFingerQuery>,
State((config, ck)): State<(&'static MagnetarConfig, CalckeyModel)>,
) -> Result<impl IntoResponse, StatusCode> {
let resource = match resource {
@ -42,79 +47,104 @@ pub async fn handle_webfinger(
StatusCode::UNPROCESSABLE_ENTITY
})?;
ck.get_user_by_tag(
&tag.name,
tag.host
.filter(|host| *host != config.networking.host)
.as_deref(),
)
.await
.map_err(|e| {
error!("Data error: {e}");
StatusCode::INTERNAL_SERVER_ERROR
})?
ck.get_user_by_tag(tag.name.as_ref(), tag.host.as_ref())
.await
.map_err(|e| {
error!("Data error: {e}");
StatusCode::INTERNAL_SERVER_ERROR
})?
}
WebFingerSubject::Url(url) => {
let object_url = url.parse::<Url>().map_err(|e| {
error!("URL parse error: {e}");
StatusCode::UNPROCESSABLE_ENTITY
})?;
// FIXME: Jank
let path = object_url.path().strip_prefix(USER_AP_ENDPOINT);
match path {
Some(user_id) if !user_id.is_empty() && user_id.chars().all(|c| c != '/') => {
ck.get_user_by_id(user_id).await.map_err(|e| {
error!("Data error: {e}");
StatusCode::INTERNAL_SERVER_ERROR
})?
}
_ => ck.get_user_by_uri(&url).await.map_err(|e| {
error!("Data error: {e}");
StatusCode::INTERNAL_SERVER_ERROR
})?,
}
}
// TODO: Make this work for local users
WebFingerSubject::Url(url) => ck.get_user_by_uri(&url).await.map_err(|e| {
error!("Data error: {e}");
StatusCode::INTERNAL_SERVER_ERROR
})?,
};
if user.is_none() {
let Some(user) = user else {
return Err(StatusCode::NOT_FOUND);
}
};
let user = user.unwrap();
let tag = FediverseTag::from((
let tag = FediverseTag::from_parts((
&user.username,
user.host.as_ref().or(Some(&config.networking.host)),
));
user.host.as_deref().or(Some(&config.networking.host)),
))
.map_err(|e| {
error!("URL parse error: {e}");
StatusCode::INTERNAL_SERVER_ERROR
})?;
let mut links = Vec::new();
let mut aliases = Vec::new();
match tag.host {
Some(ref host) if host != &config.networking.host => {
if let Some(uri) = user.uri {
links.push(WebFingerRel::RelSelf {
rel: RelSelf,
content_type: ContentActivityStreams,
href: uri,
});
}
}
_ => {
links.push(WebFingerRel::RelOStatusSubscribe {
rel: RelOStatusSubscribe,
template: format!(
"{}://{}/authorize-follow?acct={{uri}}",
config.networking.protocol, config.networking.host
),
});
let user_url = format!(
"{}://{}/@{}",
config.networking.protocol, config.networking.host, tag.name
);
links.push(WebFingerRel::RelWebFingerProfilePage {
rel: RelWebFingerProfilePage,
content_type: ContentHtml,
href: user_url.clone(),
});
aliases.push(WebFingerSubject::Url(user_url));
if user.host.is_some() {
if let Some(uri) = user.uri {
links.push(WebFingerRel::RelSelf {
rel: RelSelf,
content_type: ContentActivityStreams,
href: format!(
"{}://{}/users/{}",
config.networking.protocol, config.networking.host, user.id
),
href: uri.clone(),
});
links.push(WebFingerRel::RelSelfAlt {
rel: RelSelf,
content_type: ContentActivityJson,
href: uri,
});
}
} else {
links.push(WebFingerRel::RelOStatusSubscribe {
rel: RelOStatusSubscribe,
template: format!(
"{}://{}/authorize-follow?acct={{uri}}",
config.networking.protocol, config.networking.host
),
});
let user_url = format!(
"{}://{}/@{}",
config.networking.protocol, config.networking.host, tag.name
);
links.push(WebFingerRel::RelWebFingerProfilePage {
rel: RelWebFingerProfilePage,
content_type: ContentHtml,
href: user_url.clone(),
});
aliases.push(WebFingerSubject::Url(user_url));
let self_url = format!(
"{}://{}{}{}",
config.networking.protocol, config.networking.host, USER_AP_ENDPOINT, user.id
);
links.push(WebFingerRel::RelSelf {
rel: RelSelf,
content_type: ContentActivityStreams,
href: self_url.clone(),
});
links.push(WebFingerRel::RelSelfAlt {
rel: RelSelf,
content_type: ContentActivityJson,
href: self_url,
});
}
Ok((