Implemented WebFinger flow and fixed incoming WebFinger
ci/woodpecker/push/ociImagePush Pipeline is running
Details
ci/woodpecker/push/ociImagePush Pipeline is running
Details
This commit is contained in:
parent
ffed556107
commit
0dff0c0785
|
@ -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",
|
||||
]
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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"] }
|
||||
|
|
|
@ -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>;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)]
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)]
|
||||
|
|
|
@ -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))?;
|
||||
|
||||
|
|
154
src/webfinger.rs
154
src/webfinger.rs
|
@ -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((
|
||||
|
|
Loading…
Reference in New Issue