From bef53cc70912ce13dd3b5255c05c87ab6a396da4 Mon Sep 17 00:00:00 2001 From: Namekuji Date: Tue, 16 May 2023 14:13:57 -0400 Subject: [PATCH] adding fedration --- .../backend/crates/activitypub/Cargo.toml | 16 +++ .../activitypub/src/federation/config.rs | 0 .../activitypub/src/federation/error.rs | 42 ++++++ .../crates/activitypub/src/federation/mod.rs | 2 + .../src/federation/protocol/context.rs | 97 ++++++++++++++ .../src/federation/protocol/helper.rs | 120 ++++++++++++++++++ .../src/federation/protocol/kind.rs | 82 ++++++++++++ .../src/federation/protocol/mod.rs | 5 + .../src/federation/protocol/public_key.rs | 39 ++++++ .../src/federation/protocol/verification.rs | 41 ++++++ .../backend/crates/activitypub/src/lib.rs | 1 + 11 files changed, 445 insertions(+) create mode 100644 packages/backend/crates/activitypub/Cargo.toml create mode 100644 packages/backend/crates/activitypub/src/federation/config.rs create mode 100644 packages/backend/crates/activitypub/src/federation/error.rs create mode 100644 packages/backend/crates/activitypub/src/federation/mod.rs create mode 100644 packages/backend/crates/activitypub/src/federation/protocol/context.rs create mode 100644 packages/backend/crates/activitypub/src/federation/protocol/helper.rs create mode 100644 packages/backend/crates/activitypub/src/federation/protocol/kind.rs create mode 100644 packages/backend/crates/activitypub/src/federation/protocol/mod.rs create mode 100644 packages/backend/crates/activitypub/src/federation/protocol/public_key.rs create mode 100644 packages/backend/crates/activitypub/src/federation/protocol/verification.rs create mode 100644 packages/backend/crates/activitypub/src/lib.rs diff --git a/packages/backend/crates/activitypub/Cargo.toml b/packages/backend/crates/activitypub/Cargo.toml new file mode 100644 index 0000000000..0cf96652ba --- /dev/null +++ b/packages/backend/crates/activitypub/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "activitypub" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +anyhow = "1.0.71" +async-trait = "0.1.68" +displaydoc = "0.2.4" +parse-display = "0.8.0" +serde = { version = "1.0.163", features = ["derive"] } +serde_json = { version = "1.0.96", features = ["preserve_order"] } +thiserror = "1.0.40" +url = { version = "2.3.1", features = ["serde"] } diff --git a/packages/backend/crates/activitypub/src/federation/config.rs b/packages/backend/crates/activitypub/src/federation/config.rs new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/backend/crates/activitypub/src/federation/error.rs b/packages/backend/crates/activitypub/src/federation/error.rs new file mode 100644 index 0000000000..5d3dd49c3d --- /dev/null +++ b/packages/backend/crates/activitypub/src/federation/error.rs @@ -0,0 +1,42 @@ +//! Error messages returned by this library + +use displaydoc::Display; + +/// Error messages returned by this library +#[derive(thiserror::Error, Debug, Display)] +pub(crate) enum Error { + /// Requested object was not found in local database + NotFound, + /// Request limit was reached during fetch + RequestLimit, + /// Response body limit was reached during fetch + ResponseBodyLimit, + /// Object to be fetched was deleted + ObjectDeleted, + /// Url in object was invalid + UrlVerificationError, + /// Incoming activity has invalid digest for body + ActivityBodyDigestInvalid, + /// Incoming activity has invalid signature + ActivitySignatureInvalid, + /// Failed to resolve actor via webfinger + WebfingerResolveFailed, + /// Other errors which are not explicitly handled + #[error(transparent)] + Other(#[from] anyhow::Error), +} + +impl Error { + pub(crate) fn other(error: T) -> Self + where + T: Into, + { + Error::Other(error.into()) + } +} + +impl PartialEq for Error { + fn eq(&self, other: &Self) -> bool { + std::mem::discriminant(self) == std::mem::discriminant(other) + } +} diff --git a/packages/backend/crates/activitypub/src/federation/mod.rs b/packages/backend/crates/activitypub/src/federation/mod.rs new file mode 100644 index 0000000000..eddbd04152 --- /dev/null +++ b/packages/backend/crates/activitypub/src/federation/mod.rs @@ -0,0 +1,2 @@ +mod error; +mod protocol; diff --git a/packages/backend/crates/activitypub/src/federation/protocol/context.rs b/packages/backend/crates/activitypub/src/federation/protocol/context.rs new file mode 100644 index 0000000000..47cbd5a17f --- /dev/null +++ b/packages/backend/crates/activitypub/src/federation/protocol/context.rs @@ -0,0 +1,97 @@ +// GNU Affero General Public License v3.0 +// https://github.com/LemmyNet/activitypub-federation-rust + +//! Wrapper for federated structs which handles `@context` field. +//! +//! This wrapper can be used when sending Activitypub data, to automatically add `@context`. It +//! avoids having to repeat the `@context` property on every struct, and getting multiple contexts +//! in nested structs. +//! +//! ``` +//! # use activitypub_federation::protocol::context::WithContext; +//! #[derive(serde::Serialize)] +//! struct Note { +//! content: String +//! } +//! let note = Note { +//! content: "Hello world".to_string() +//! }; +//! let note_with_context = WithContext::new_default(note); +//! let serialized = serde_json::to_string(¬e_with_context)?; +//! assert_eq!(serialized, r#"{"@context":["https://www.w3.org/ns/activitystreams"],"content":"Hello world"}"#); +//! Ok::<(), serde_json::error::Error>(()) +//! ``` + +use crate::federation::protocol::helper::deserialize_one_or_many; +use crate::{config::Data, traits::ActivityHandler}; +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use url::Url; + +/// Default context used in Activitypub +const DEFAULT_CONTEXT: &str = "https://www.w3.org/ns/activitystreams"; + +/// Wrapper for federated structs which handles `@context` field. +#[derive(Serialize, Deserialize, Debug)] +pub struct WithContext { + #[serde(rename = "@context")] + #[serde(deserialize_with = "deserialize_one_or_many")] + context: Vec, + #[serde(flatten)] + inner: T, +} + +impl WithContext { + /// Create a new wrapper with the default Activitypub context. + pub fn new_default(inner: T) -> WithContext { + let context = vec![Value::String(DEFAULT_CONTEXT.to_string())]; + WithContext::new(inner, context) + } + + /// Create new wrapper with custom context. Use this in case you are implementing extensions. + pub fn new(inner: T, context: Vec) -> WithContext { + WithContext { context, inner } + } + + /// Returns the inner `T` object which this `WithContext` object is wrapping + pub fn inner(&self) -> &T { + &self.inner + } +} + +#[async_trait::async_trait] +impl ActivityHandler for WithContext +where + T: ActivityHandler + Send + Sync, +{ + type DataType = ::DataType; + type Error = ::Error; + + fn id(&self) -> &Url { + self.inner.id() + } + + fn actor(&self) -> &Url { + self.inner.actor() + } + + async fn verify(&self, data: &Data) -> Result<(), Self::Error> { + self.inner.verify(data).await + } + + async fn receive(self, data: &Data) -> Result<(), Self::Error> { + self.inner.receive(data).await + } +} + +impl Clone for WithContext +where + T: Clone, +{ + fn clone(&self) -> Self { + Self { + context: self.context.clone(), + inner: self.inner.clone(), + } + } +} diff --git a/packages/backend/crates/activitypub/src/federation/protocol/helper.rs b/packages/backend/crates/activitypub/src/federation/protocol/helper.rs new file mode 100644 index 0000000000..32abcbda38 --- /dev/null +++ b/packages/backend/crates/activitypub/src/federation/protocol/helper.rs @@ -0,0 +1,120 @@ +// GNU Affero General Public License v3.0 +// https://github.com/LemmyNet/activitypub-federation-rust + +//! Serde deserialization functions which help to receive differently shaped data + +use serde::{Deserialize, Deserializer}; + +/// Deserialize JSON single value or array into Vec. +/// +/// Useful if your application can handle multiple values for a field, but another federated +/// platform only sends a single one. +/// +/// ``` +/// # use activitypub_federation::protocol::helpers::deserialize_one_or_many; +/// # use url::Url; +/// #[derive(serde::Deserialize)] +/// struct Note { +/// #[serde(deserialize_with = "deserialize_one_or_many")] +/// to: Vec +/// } +/// +/// let single: Note = serde_json::from_str(r#"{"to": "https://example.com/u/alice" }"#)?; +/// assert_eq!(single.to.len(), 1); +/// +/// let multiple: Note = serde_json::from_str( +/// r#"{"to": [ +/// "https://example.com/u/alice", +/// "https://lemmy.ml/u/bob" +/// ]}"#)?; +/// assert_eq!(multiple.to.len(), 2); +/// Ok::<(), anyhow::Error>(()) +pub(crate) fn deserialize_one_or_many<'de, T, D>(deserializer: D) -> Result, D::Error> +where + T: Deserialize<'de>, + D: Deserializer<'de>, +{ + #[derive(Deserialize)] + #[serde(untagged)] + enum OneOrMany { + One(T), + Many(Vec), + } + + let result: OneOrMany = Deserialize::deserialize(deserializer)?; + Ok(match result { + OneOrMany::Many(list) => list, + OneOrMany::One(value) => vec![value], + }) +} + +/// Deserialize JSON single value or single element array into single value. +/// +/// Useful if your application can only handle a single value for a field, but another federated +/// platform sends single value wrapped in array. Fails if array contains multiple items. +/// +/// ``` +/// # use activitypub_federation::protocol::helpers::deserialize_one; +/// # use url::Url; +/// #[derive(serde::Deserialize)] +/// struct Note { +/// #[serde(deserialize_with = "deserialize_one")] +/// to: Url +/// } +/// +/// let note = serde_json::from_str::(r#"{"to": ["https://example.com/u/alice"] }"#); +/// assert!(note.is_ok()); +pub(crate) fn deserialize_one<'de, T, D>(deserializer: D) -> Result +where + T: Deserialize<'de>, + D: Deserializer<'de>, +{ + #[derive(Deserialize)] + #[serde(untagged)] + enum MaybeArray { + Simple(T), + Array([T; 1]), + } + + let result: MaybeArray = Deserialize::deserialize(deserializer)?; + Ok(match result { + MaybeArray::Simple(value) => value, + MaybeArray::Array([value]) => value, + }) +} + +/// Attempts to deserialize item, in case of error falls back to the type's default value. +/// +/// Useful for optional fields which are sent with a different type from another platform, +/// eg object instead of array. Should always be used together with `#[serde(default)]`, so +/// that a mssing value doesn't cause an error. +/// +/// ``` +/// # use activitypub_federation::protocol::helpers::deserialize_skip_error; +/// # use url::Url; +/// #[derive(serde::Deserialize)] +/// struct Note { +/// content: String, +/// #[serde(deserialize_with = "deserialize_skip_error", default)] +/// source: Option +/// } +/// +/// let note = serde_json::from_str::( +/// r#"{ +/// "content": "How are you?", +/// "source": { +/// "content": "How are you?", +/// "mediaType": "text/markdown" +/// } +/// }"#); +/// assert_eq!(note.unwrap().source, None); +/// # Ok::<(), anyhow::Error>(()) +pub(crate) fn deserialize_skip_error<'de, T, D>(deserializer: D) -> Result +where + T: Deserialize<'de> + Default, + D: Deserializer<'de>, +{ + let value = serde_json::Value::deserialize(deserializer)?; + let inner = T::deserialize(value).unwrap_or_default(); + Ok(inner) +} diff --git a/packages/backend/crates/activitypub/src/federation/protocol/kind.rs b/packages/backend/crates/activitypub/src/federation/protocol/kind.rs new file mode 100644 index 0000000000..1060b163f3 --- /dev/null +++ b/packages/backend/crates/activitypub/src/federation/protocol/kind.rs @@ -0,0 +1,82 @@ +//! Types of Activity, Actor, Collection, Link, and Object + +use parse_display::{Display, FromStr}; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, Display, FromStr, PartialEq, Serialize, Deserialize, Default)] +pub(crate) enum ActivityType { + Activity, + Accept, + Add, + Announce, + Arrive, + Block, + #[default] + Create, + Delete, + Dislike, + Flag, + Follow, + Ignore, + Invite, + Join, + Leave, + Like, + Listen, + Move, + Offer, + Question, + Read, + Reject, + Remove, + TentativeAccept, + TentativeReject, + Travel, + Undo, + Update, + View, +} + +#[derive(Clone, Debug, Display, FromStr, PartialEq, Serialize, Deserialize, Default)] +pub(crate) enum ActorType { + Application, + Group, + Organization, + #[default] + Person, + Service, +} + +#[derive(Clone, Debug, Display, FromStr, PartialEq, Serialize, Deserialize, Default)] +pub(crate) enum CollectionType { + Collection, + #[default] + OrderedCollection, + CollectionPage, + OrderedCollectionPage, +} + +#[derive(Clone, Debug, Display, FromStr, PartialEq, Serialize, Deserialize, Default)] +pub(crate) enum LinkType { + #[default] + Link, + Mention, +} + +#[derive(Clone, Debug, Display, FromStr, PartialEq, Serialize, Deserialize, Default)] +pub(crate) enum ObjectType { + Object, + Article, + Audio, + Document, + Event, + Image, + #[default] + Note, + Page, + Place, + Profile, + Relationship, + Tombstone, + Video, +} diff --git a/packages/backend/crates/activitypub/src/federation/protocol/mod.rs b/packages/backend/crates/activitypub/src/federation/protocol/mod.rs new file mode 100644 index 0000000000..c3291abc65 --- /dev/null +++ b/packages/backend/crates/activitypub/src/federation/protocol/mod.rs @@ -0,0 +1,5 @@ +mod context; +mod helper; +mod kind; +mod public_key; +mod verification; diff --git a/packages/backend/crates/activitypub/src/federation/protocol/public_key.rs b/packages/backend/crates/activitypub/src/federation/protocol/public_key.rs new file mode 100644 index 0000000000..9d3f106c94 --- /dev/null +++ b/packages/backend/crates/activitypub/src/federation/protocol/public_key.rs @@ -0,0 +1,39 @@ +// GNU Affero General Public License v3.0 +// https://github.com/LemmyNet/activitypub-federation-rust + +//! Struct which is used to federate actor key for HTTP signatures + +use serde::{Deserialize, Serialize}; +use url::Url; + +/// Public key of actors which is used for HTTP signatures. +/// +/// This needs to be federated in the `public_key` field of all actors. +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct PublicKey { + /// Id of this private key. + pub id: String, + /// ID of the actor that this public key belongs to + pub owner: Url, + /// The actual public key in PEM format + pub public_key_pem: String, +} + +impl PublicKey { + /// Create a new [PublicKey] struct for the `owner` with `public_key_pem`. + /// + /// It uses an standard key id of `{actor_id}#main-key` + pub(crate) fn new(owner: Url, public_key_pem: String) -> Self { + let id = main_key_id(&owner); + PublicKey { + id, + owner, + public_key_pem, + } + } +} + +pub(crate) fn main_key_id(owner: &Url) -> String { + format!("{}#main-key", &owner) +} diff --git a/packages/backend/crates/activitypub/src/federation/protocol/verification.rs b/packages/backend/crates/activitypub/src/federation/protocol/verification.rs new file mode 100644 index 0000000000..50fbca0926 --- /dev/null +++ b/packages/backend/crates/activitypub/src/federation/protocol/verification.rs @@ -0,0 +1,41 @@ +// GNU Affero General Public License v3.0 +// https://github.com/LemmyNet/activitypub-federation-rust + +//! Verify that received data is valid + +use crate::federation::error::Error; +use url::Url; + +/// Check that both urls have the same domain. If not, return UrlVerificationError. +/// +/// ``` +/// # use url::Url; +/// # use activitypub_federation::protocol::verification::verify_domains_match; +/// let a = Url::parse("https://example.com/abc")?; +/// let b = Url::parse("https://sample.net/abc")?; +/// assert!(verify_domains_match(&a, &b).is_err()); +/// # Ok::<(), url::ParseError>(()) +/// ``` +pub fn verify_domains_match(a: &Url, b: &Url) -> Result<(), Error> { + if a.domain() != b.domain() { + return Err(Error::UrlVerificationError); + } + Ok(()) +} + +/// Check that both urls are identical. If not, return UrlVerificationError. +/// +/// ``` +/// # use url::Url; +/// # use activitypub_federation::protocol::verification::verify_urls_match; +/// let a = Url::parse("https://example.com/abc")?; +/// let b = Url::parse("https://example.com/123")?; +/// assert!(verify_urls_match(&a, &b).is_err()); +/// # Ok::<(), url::ParseError>(()) +/// ``` +pub fn verify_urls_match(a: &Url, b: &Url) -> Result<(), Error> { + if a != b { + return Err(Error::UrlVerificationError); + } + Ok(()) +} diff --git a/packages/backend/crates/activitypub/src/lib.rs b/packages/backend/crates/activitypub/src/lib.rs new file mode 100644 index 0000000000..9979c2181c --- /dev/null +++ b/packages/backend/crates/activitypub/src/lib.rs @@ -0,0 +1 @@ +mod federation;