Compare commits

...

25 commits

Author SHA1 Message Date
754b2a0f3d Revert Object trait id() ref 2026-04-25 22:40:47 -07:00
Felix Ableitner
588f431266 Version 0.7.0-beta.11 2026-04-24 11:31:10 +02:00
Hong Minhee (洪 民憙)
838dd9e501
Add a public-aware deserializer for recipient URLs (#165)
* Accept Public aliases in URL deserializer

Update deserialize_one_or_many to deserialize recipient URL fields while
accepting `Public` and `as:Public` as aliases for the canonical
ActivityStreams public URL.

Add focused tests for single and array inputs, and verify that unrelated
string fields such as `content` are left unchanged.

https://github.com/LemmyNet/lemmy/issues/6465

* Deduplicate deserialized recipients

Drop repeated recipient URLs after deserialization so equivalent public
aliases such as `Public`, `as:Public`, and the canonical public URL do
not produce duplicate entries.

Update the helper documentation and tests to match the deduplicated
result.
2026-04-24 11:25:06 +02:00
Felix Ableitner
279d29d350 Version 0.7.0-beta.10 2026-04-15 13:39:03 +02:00
Nutomic
fcb69ebffe
Make IP check public (#164)
* Make IP check public

* change
2026-04-15 13:38:29 +02:00
Felix Ableitner
5e8e918003 Version 0.7.0-beta.9 2026-03-16 11:39:46 +01:00
Nutomic
4ae8532b17
Add some more IP checks (#162) 2026-03-16 11:11:01 +01:00
Nutomic
f47fe58285
Better IP check (#161) 2026-02-05 07:04:08 -05:00
Nutomic
f60afae428
Add to_canonical() for ip check (#160) 2026-02-04 12:05:59 +01:00
Nutomic
11f95ff384
Improve error message, allow local IP federation via env var (#158)
* Improve error message, allow local IP federation via env var (fixes #152)

* fix
2026-01-28 08:44:39 -05:00
Nutomic
9d7bd965a4
Upgrade reqwest (#159) 2026-01-28 14:40:03 +01:00
Nutomic
b5dd86ab07
Update deps (#157) 2026-01-12 08:44:16 -05:00
Nutomic
a7da04c2d8
Revert parse order for webfinger results so community comes first (#156) 2025-12-05 08:42:53 -05:00
Brad Dunbar
2acf037d79
Fix example path params (#153)
Resolves the following error:

    thread 'main' (6023907) panicked at examples/live_federation/main.rs:58:10:
    Path segments must not start with `:`. For capture groups, use `{capture}`. If you meant to literally match a segment starting with a colon, call `without_v07_checks` on the router.
2025-11-21 10:03:20 +01:00
Brad Dunbar
99505b9567
Webfinger: impl PartialEq (#155)
It'd be nice to be able to compare these in tests.
2025-11-21 09:53:13 +01:00
Brad Dunbar
06df2bc1d1
Fix future incompatibility warning (#154)
These warnings are fixed in the [0.8.x branch][commits] of
`num-bigint-dig`.

    warning: the following packages contain code that will be rejected by a future version of Rust: num-bigint-dig v0.8.4

[commits]: https://github.com/dignifiedquire/num-bigint/commits/0-8
2025-11-21 09:52:53 +01:00
Nutomic
8b2b746707
Handle null values with deserialize_last (#151) 2025-10-17 21:09:32 +08:00
Felix Ableitner
545afcc719 Version 0.7.0-beta.8 2025-10-15 12:40:30 +02:00
Nutomic
105d13003a
Increase reqwest max body size (#150) 2025-10-15 18:39:56 +08:00
Felix Ableitner
ec098cfaed 0.7.0-beta.7 2025-10-02 11:35:55 +02:00
Nutomic
1df24ab781
Return deleted object on resolve (#149) 2025-10-02 17:30:12 +08:00
Felix Ableitner
6c97312f25 Version 0.7.0-beta.6 2025-07-28 11:23:03 +02:00
Nutomic
cd0f009f5f
Add helper deserialize_last() (#148) 2025-07-28 11:22:36 +02:00
Felix Ableitner
0d0f498ddd 0.7.0-beta.5 2025-07-10 10:29:02 +02:00
Nutomic
fa27a0c0b4
Various additions and changes (#147)
* Add methods Object.id(), Object.deleted()

* Rename ActivityHandler to Activity

* comments

* fix

* comment
2025-07-10 10:26:45 +02:00
37 changed files with 1736 additions and 1238 deletions

View file

@ -1,5 +1,5 @@
variables: variables:
- &rust_image "rust:1.81-bullseye" - &rust_image "rust:1.91-bullseye"
steps: steps:
cargo_fmt: cargo_fmt:

1821
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
[package] [package]
name = "activitypub_federation" name = "activitypub_federation"
version = "0.7.0-beta.4" version = "0.7.0-beta.11"
edition = "2021" edition = "2021"
description = "High-level Activitypub framework" description = "High-level Activitypub framework"
keywords = ["activitypub", "activitystreams", "federation", "fediverse"] keywords = ["activitypub", "activitystreams", "federation", "fediverse"]
@ -14,10 +14,6 @@ actix-web = ["dep:actix-web", "dep:http02"]
axum = ["dep:axum", "dep:tower"] axum = ["dep:axum", "dep:tower"]
axum-original-uri = ["dep:axum", "axum/original-uri"] axum-original-uri = ["dep:axum", "axum/original-uri"]
[lints.rust]
warnings = "deny"
deprecated = "deny"
[lints.clippy] [lints.clippy]
perf = { level = "deny", priority = -1 } perf = { level = "deny", priority = -1 }
complexity = { level = "deny", priority = -1 } complexity = { level = "deny", priority = -1 }
@ -32,69 +28,71 @@ redundant_closure_for_method_calls = "deny"
unwrap_used = "deny" unwrap_used = "deny"
[dependencies] [dependencies]
chrono = { version = "0.4.41", features = ["clock"], default-features = false } chrono = { version = "0.4.42", features = ["clock"], default-features = false }
serde = { version = "1.0.219", features = ["derive"] } serde = { version = "1.0.228", features = ["derive"] }
async-trait = "0.1.88" async-trait = "0.1.89"
url = { version = "2.5.4", features = ["serde"] } url = { version = "2.5.8", features = ["serde"] }
serde_json = { version = "1.0.140", features = ["preserve_order"] } serde_json = { version = "1.0.149", features = ["preserve_order"] }
reqwest = { version = "0.12.18", default-features = false, features = [ reqwest = { version = "0.13.1", default-features = false, features = [
"json", "json",
"stream", "stream",
"rustls-tls",
] } ] }
reqwest-middleware = "0.4.2" reqwest-middleware = "0.5.0"
tracing = "0.1.41" tracing = "0.1.44"
base64 = "0.22.1" base64 = "0.22.1"
rand = "0.8.5" rand = "0.8.5"
rsa = "0.9.8" rsa = "0.9.10"
http = "1.3.1" http = "1.4.0"
sha2 = { version = "0.10.9", features = ["oid"] } sha2 = { version = "0.10.9", features = ["oid"] }
thiserror = "2.0.12" thiserror = "2.0.17"
derive_builder = "0.20.2" derive_builder = "0.20.2"
itertools = "0.14.0" itertools = "0.14.0"
dyn-clone = "1.0.19" dyn-clone = "1.0.20"
enum_delegate = "0.2.0" enum_delegate = "0.2.0"
httpdate = "1.0.3" httpdate = "1.0.3"
http-signature-normalization-reqwest = { version = "0.13.0", default-features = false, features = [ http-signature-normalization-reqwest = { version = "0.14.0", default-features = false, features = [
"sha-2", "sha-2",
"middleware", "middleware",
"default-spawner", "default-spawner",
] } ] }
http-signature-normalization = "0.7.0" http-signature-normalization = "0.7.0"
bytes = "1.10.1" bytes = "1.11.0"
futures-core = { version = "0.3.31", default-features = false } futures-core = { version = "0.3.31", default-features = false }
pin-project-lite = "0.2.16" pin-project-lite = "0.2.16"
activitystreams-kinds = "0.3.0" activitystreams-kinds = "0.3.0"
regex = { version = "1.11.1", default-features = false, features = [ regex = { version = "1.12.2", default-features = false, features = [
"std", "std",
"unicode", "unicode",
] } ] }
tokio = { version = "1.45.0", features = [ tokio = { version = "1.49.0", features = [
"sync", "sync",
"rt", "rt",
"rt-multi-thread", "rt-multi-thread",
"time", "time",
] } ] }
futures = "0.3.31" futures = "0.3.31"
moka = { version = "0.12.10", features = ["future"] } moka = { version = "0.12.12", features = ["future"] }
either = "1.15.0" either = "1.15.0"
# Actix-web # Actix-web
actix-web = { version = "4.11.0", default-features = false, optional = true } actix-web = { version = "4.12.1", default-features = false, optional = true }
http02 = { package = "http", version = "0.2.12", optional = true } http02 = { package = "http", version = "0.2.12", optional = true }
# Axum # Axum
axum = { version = "0.8.4", features = [ axum = { version = "0.8.8", features = [
"json", "json",
], default-features = false, optional = true } ], default-features = false, optional = true }
tower = { version = "0.5.2", optional = true } tower = { version = "0.5.2", optional = true }
[dev-dependencies] [dev-dependencies]
anyhow = "1.0.98" anyhow = "1.0.100"
axum = { version = "0.8.4", features = ["macros"] } axum = { version = "0.8.8", features = ["macros"] }
axum-extra = { version = "0.10.1", features = ["typed-header"] } axum-extra = { version = "0.12.5", features = ["typed-header"] }
env_logger = "0.11.8" env_logger = "0.11.8"
tokio = { version = "1.45.0", features = ["full"] } tokio = { version = "1.49.0", features = ["full"] }
reqwest = { version = "0.13.1",features = [
"rustls"
] }
[profile.dev] [profile.dev]
strip = "symbols" strip = "symbols"

View file

@ -1,6 +1,6 @@
## Sending and receiving activities ## Sending and receiving activities
Activitypub propagates actions across servers using `Activities`. For this each actor has an inbox and a public/private key pair. We already defined a `Person` actor with keypair. Whats left is to define an activity. This is similar to the way we defined `Person` and `Note` structs before. In this case we need to implement the [ActivityHandler](trait@crate::traits::ActivityHandler) trait. Activitypub propagates actions across servers using `Activities`. For this each actor has an inbox and a public/private key pair. We already defined a `Person` actor with keypair. Whats left is to define an activity. This is similar to the way we defined `Person` and `Note` structs before. In this case we need to implement the [Activity](trait@crate::traits::Activity) trait.
``` ```
# use serde::{Deserialize, Serialize}; # use serde::{Deserialize, Serialize};
@ -10,7 +10,7 @@ Activitypub propagates actions across servers using `Activities`. For this each
# use activitypub_federation::fetch::object_id::ObjectId; # use activitypub_federation::fetch::object_id::ObjectId;
# use activitypub_federation::traits::tests::{DbConnection, DbUser}; # use activitypub_federation::traits::tests::{DbConnection, DbUser};
# use activitystreams_kinds::activity::FollowType; # use activitystreams_kinds::activity::FollowType;
# use activitypub_federation::traits::ActivityHandler; # use activitypub_federation::traits::Activity;
# use activitypub_federation::config::Data; # use activitypub_federation::config::Data;
# async fn send_accept() -> Result<(), Error> { Ok(()) } # async fn send_accept() -> Result<(), Error> { Ok(()) }
@ -25,7 +25,7 @@ pub struct Follow {
} }
#[async_trait] #[async_trait]
impl ActivityHandler for Follow { impl Activity for Follow {
type DataType = DbConnection; type DataType = DbConnection;
type Error = Error; type Error = Error;
@ -59,14 +59,14 @@ Next its time to setup the actual HTTP handler for the inbox. For this we first
# use activitypub_federation::axum::inbox::{ActivityData, receive_activity}; # use activitypub_federation::axum::inbox::{ActivityData, receive_activity};
# use activitypub_federation::config::Data; # use activitypub_federation::config::Data;
# use activitypub_federation::protocol::context::WithContext; # use activitypub_federation::protocol::context::WithContext;
# use activitypub_federation::traits::ActivityHandler; # use activitypub_federation::traits::Activity;
# use activitypub_federation::traits::tests::{DbConnection, DbUser, Follow}; # use activitypub_federation::traits::tests::{DbConnection, DbUser, Follow};
# use serde::{Deserialize, Serialize}; # use serde::{Deserialize, Serialize};
# use url::Url; # use url::Url;
#[derive(Deserialize, Serialize, Debug)] #[derive(Deserialize, Serialize, Debug)]
#[serde(untagged)] #[serde(untagged)]
#[enum_delegate::implement(ActivityHandler)] #[enum_delegate::implement(Activity)]
pub enum PersonAcceptedActivities { pub enum PersonAcceptedActivities {
Follow(Follow), Follow(Follow),
} }

View file

@ -32,6 +32,13 @@ impl Object for SearchableDbObjects {
type Kind = SearchableObjects; type Kind = SearchableObjects;
type Error = anyhow::Error; type Error = anyhow::Error;
fn id(&self) -> Url {
match self {
SearchableDbObjects::User(p) => p.federation_id.clone(),
SearchableDbObjects::Post(n) => n.federation_id.clone(),
}
}
async fn read_from_id( async fn read_from_id(
object_id: Url, object_id: Url,
data: &Data<Self::DataType>, data: &Data<Self::DataType>,

View file

@ -11,7 +11,7 @@ use activitypub_federation::{
fetch::object_id::ObjectId, fetch::object_id::ObjectId,
kinds::activity::CreateType, kinds::activity::CreateType,
protocol::{context::WithContext, helpers::deserialize_one_or_many}, protocol::{context::WithContext, helpers::deserialize_one_or_many},
traits::{ActivityHandler, Object}, traits::{Activity, Object},
}; };
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use url::Url; use url::Url;
@ -50,7 +50,7 @@ impl CreatePost {
} }
#[async_trait::async_trait] #[async_trait::async_trait]
impl ActivityHandler for CreatePost { impl Activity for CreatePost {
type DataType = DatabaseHandle; type DataType = DatabaseHandle;
type Error = crate::error::Error; type Error = crate::error::Error;

View file

@ -55,8 +55,8 @@ async fn main() -> Result<(), Error> {
info!("Listen with HTTP server on {BIND_ADDRESS}"); info!("Listen with HTTP server on {BIND_ADDRESS}");
let config = config.clone(); let config = config.clone();
let app = Router::new() let app = Router::new()
.route("/:user", get(http_get_user)) .route("/{user}", get(http_get_user))
.route("/:user/inbox", post(http_post_user_inbox)) .route("/{user}/inbox", post(http_post_user_inbox))
.route("/.well-known/webfinger", get(webfinger)) .route("/.well-known/webfinger", get(webfinger))
.layer(FederationMiddleware::new(config)); .layer(FederationMiddleware::new(config));

View file

@ -5,7 +5,7 @@ use activitypub_federation::{
http_signatures::generate_actor_keypair, http_signatures::generate_actor_keypair,
kinds::actor::PersonType, kinds::actor::PersonType,
protocol::{public_key::PublicKey, verification::verify_domains_match}, protocol::{public_key::PublicKey, verification::verify_domains_match},
traits::{ActivityHandler, Actor, Object}, traits::{Activity, Actor, Object},
}; };
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -29,7 +29,7 @@ pub struct DbUser {
/// List of all activities which this actor can receive. /// List of all activities which this actor can receive.
#[derive(Deserialize, Serialize, Debug)] #[derive(Deserialize, Serialize, Debug)]
#[serde(untagged)] #[serde(untagged)]
#[enum_delegate::implement(ActivityHandler)] #[enum_delegate::implement(Activity)]
pub enum PersonAcceptedActivities { pub enum PersonAcceptedActivities {
CreateNote(CreatePost), CreateNote(CreatePost),
} }
@ -69,6 +69,10 @@ impl Object for DbUser {
type Kind = Person; type Kind = Person;
type Error = Error; type Error = Error;
fn id(&self) -> Url {
self.ap_id.inner().clone()
}
fn last_refreshed_at(&self) -> Option<DateTime<Utc>> { fn last_refreshed_at(&self) -> Option<DateTime<Utc>> {
Some(self.last_refreshed_at) Some(self.last_refreshed_at)
} }
@ -122,10 +126,6 @@ impl Object for DbUser {
} }
impl Actor for DbUser { impl Actor for DbUser {
fn id(&self) -> Url {
self.ap_id.inner().clone()
}
fn public_key_pem(&self) -> &str { fn public_key_pem(&self) -> &str {
&self.public_key &self.public_key
} }

View file

@ -50,6 +50,10 @@ impl Object for DbPost {
type Kind = Note; type Kind = Note;
type Error = Error; type Error = Error;
fn id(&self) -> Url {
self.ap_id.inner().clone()
}
async fn read_from_id( async fn read_from_id(
_object_id: Url, _object_id: Url,
_data: &Data<Self::DataType>, _data: &Data<Self::DataType>,

View file

@ -3,7 +3,7 @@ use activitypub_federation::{
config::Data, config::Data,
fetch::object_id::ObjectId, fetch::object_id::ObjectId,
kinds::activity::AcceptType, kinds::activity::AcceptType,
traits::ActivityHandler, traits::Activity,
}; };
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use url::Url; use url::Url;
@ -30,7 +30,7 @@ impl Accept {
} }
#[async_trait::async_trait] #[async_trait::async_trait]
impl ActivityHandler for Accept { impl Activity for Accept {
type DataType = DatabaseHandle; type DataType = DatabaseHandle;
type Error = crate::error::Error; type Error = crate::error::Error;

View file

@ -8,7 +8,7 @@ use activitypub_federation::{
fetch::object_id::ObjectId, fetch::object_id::ObjectId,
kinds::activity::CreateType, kinds::activity::CreateType,
protocol::helpers::deserialize_one_or_many, protocol::helpers::deserialize_one_or_many,
traits::{ActivityHandler, Object}, traits::{Activity, Object},
}; };
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use url::Url; use url::Url;
@ -38,7 +38,7 @@ impl CreatePost {
} }
#[async_trait::async_trait] #[async_trait::async_trait]
impl ActivityHandler for CreatePost { impl Activity for CreatePost {
type DataType = DatabaseHandle; type DataType = DatabaseHandle;
type Error = crate::error::Error; type Error = crate::error::Error;

View file

@ -8,7 +8,7 @@ use activitypub_federation::{
config::Data, config::Data,
fetch::object_id::ObjectId, fetch::object_id::ObjectId,
kinds::activity::FollowType, kinds::activity::FollowType,
traits::{ActivityHandler, Actor}, traits::{Activity, Actor},
}; };
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use url::Url; use url::Url;
@ -35,7 +35,7 @@ impl Follow {
} }
#[async_trait::async_trait] #[async_trait::async_trait]
impl ActivityHandler for Follow { impl Activity for Follow {
type DataType = DatabaseHandle; type DataType = DatabaseHandle;
type Error = crate::error::Error; type Error = crate::error::Error;

View file

@ -8,7 +8,7 @@ use activitypub_federation::{
config::{Data, FederationConfig, FederationMiddleware}, config::{Data, FederationConfig, FederationMiddleware},
fetch::webfinger::{build_webfinger_response, extract_webfinger_name}, fetch::webfinger::{build_webfinger_response, extract_webfinger_name},
protocol::context::WithContext, protocol::context::WithContext,
traits::{Actor, Object}, traits::Object,
FEDERATION_CONTENT_TYPE, FEDERATION_CONTENT_TYPE,
}; };
use actix_web::{web, web::Bytes, App, HttpRequest, HttpResponse, HttpServer}; use actix_web::{web, web::Bytes, App, HttpRequest, HttpResponse, HttpServer};

View file

@ -13,7 +13,7 @@ use activitypub_federation::{
http_signatures::generate_actor_keypair, http_signatures::generate_actor_keypair,
kinds::actor::PersonType, kinds::actor::PersonType,
protocol::{context::WithContext, public_key::PublicKey, verification::verify_domains_match}, protocol::{context::WithContext, public_key::PublicKey, verification::verify_domains_match},
traits::{ActivityHandler, Actor, Object}, traits::{Activity, Actor, Object},
}; };
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -37,7 +37,7 @@ pub struct DbUser {
/// List of all activities which this actor can receive. /// List of all activities which this actor can receive.
#[derive(Deserialize, Serialize, Debug)] #[derive(Deserialize, Serialize, Debug)]
#[serde(untagged)] #[serde(untagged)]
#[enum_delegate::implement(ActivityHandler)] #[enum_delegate::implement(Activity)]
pub enum PersonAcceptedActivities { pub enum PersonAcceptedActivities {
Follow(Follow), Follow(Follow),
Accept(Accept), Accept(Accept),
@ -103,16 +103,16 @@ impl DbUser {
Ok(()) Ok(())
} }
pub(crate) async fn send<Activity>( pub(crate) async fn send<A>(
&self, &self,
activity: Activity, activity: A,
recipients: Vec<Url>, recipients: Vec<Url>,
use_queue: bool, use_queue: bool,
data: &Data<DatabaseHandle>, data: &Data<DatabaseHandle>,
) -> Result<(), Error> ) -> Result<(), Error>
where where
Activity: ActivityHandler + Serialize + Debug + Send + Sync, A: Activity + Serialize + Debug + Send + Sync,
<Activity as ActivityHandler>::Error: From<anyhow::Error> + From<serde_json::Error>, <A as Activity>::Error: From<anyhow::Error> + From<serde_json::Error>,
{ {
let activity = WithContext::new_default(activity); let activity = WithContext::new_default(activity);
// Send through queue in some cases and bypass it in others to test both code paths // Send through queue in some cases and bypass it in others to test both code paths
@ -134,6 +134,10 @@ impl Object for DbUser {
type Kind = Person; type Kind = Person;
type Error = Error; type Error = Error;
fn id(&self) -> Url {
self.ap_id.inner().clone()
}
fn last_refreshed_at(&self) -> Option<DateTime<Utc>> { fn last_refreshed_at(&self) -> Option<DateTime<Utc>> {
Some(self.last_refreshed_at) Some(self.last_refreshed_at)
} }
@ -187,10 +191,6 @@ impl Object for DbUser {
} }
impl Actor for DbUser { impl Actor for DbUser {
fn id(&self) -> Url {
self.ap_id.inner().clone()
}
fn public_key_pem(&self) -> &str { fn public_key_pem(&self) -> &str {
&self.public_key &self.public_key
} }

View file

@ -47,6 +47,10 @@ impl Object for DbPost {
type Kind = Note; type Kind = Note;
type Error = Error; type Error = Error;
fn id(&self) -> Url {
self.ap_id.inner().clone()
}
async fn read_from_id( async fn read_from_id(
object_id: Url, object_id: Url,
data: &Data<Self::DataType>, data: &Data<Self::DataType>,

View file

@ -6,7 +6,7 @@ use crate::{
activity_sending::{build_tasks, SendActivityTask}, activity_sending::{build_tasks, SendActivityTask},
config::Data, config::Data,
error::Error, error::Error,
traits::{ActivityHandler, Actor}, traits::{Activity, Actor},
}; };
use futures_core::Future; use futures_core::Future;
@ -37,14 +37,14 @@ use url::Url;
/// - `inboxes`: List of remote actor inboxes that should receive the activity. Ignores local actor /// - `inboxes`: List of remote actor inboxes that should receive the activity. Ignores local actor
/// inboxes. Should be built by calling [crate::traits::Actor::shared_inbox_or_inbox] /// inboxes. Should be built by calling [crate::traits::Actor::shared_inbox_or_inbox]
/// for each target actor. /// for each target actor.
pub async fn queue_activity<Activity, Datatype, ActorType>( pub async fn queue_activity<A, Datatype, ActorType>(
activity: &Activity, activity: &A,
actor: &ActorType, actor: &ActorType,
inboxes: Vec<Url>, inboxes: Vec<Url>,
data: &Data<Datatype>, data: &Data<Datatype>,
) -> Result<(), Error> ) -> Result<(), Error>
where where
Activity: ActivityHandler + Serialize + Debug, A: Activity + Serialize + Debug,
Datatype: Clone, Datatype: Clone,
ActorType: Actor, ActorType: Actor,
{ {

View file

@ -7,7 +7,7 @@ use crate::{
error::Error, error::Error,
http_signatures::sign_request, http_signatures::sign_request,
reqwest_shim::ResponseExt, reqwest_shim::ResponseExt,
traits::{ActivityHandler, Actor}, traits::{Activity, Actor},
FEDERATION_CONTENT_TYPE, FEDERATION_CONTENT_TYPE,
}; };
use bytes::Bytes; use bytes::Bytes;
@ -54,14 +54,14 @@ impl SendActivityTask {
/// - `inboxes`: List of remote actor inboxes that should receive the activity. Ignores local actor /// - `inboxes`: List of remote actor inboxes that should receive the activity. Ignores local actor
/// inboxes. Should be built by calling [crate::traits::Actor::shared_inbox_or_inbox] /// inboxes. Should be built by calling [crate::traits::Actor::shared_inbox_or_inbox]
/// for each target actor. /// for each target actor.
pub async fn prepare<Activity, Datatype, ActorType>( pub async fn prepare<A, Datatype, ActorType>(
activity: &Activity, activity: &A,
actor: &ActorType, actor: &ActorType,
inboxes: Vec<Url>, inboxes: Vec<Url>,
data: &Data<Datatype>, data: &Data<Datatype>,
) -> Result<Vec<SendActivityTask>, Error> ) -> Result<Vec<SendActivityTask>, Error>
where where
Activity: ActivityHandler + Serialize + Debug, A: Activity + Serialize + Debug,
Datatype: Clone, Datatype: Clone,
ActorType: Actor, ActorType: Actor,
{ {
@ -136,14 +136,14 @@ impl SendActivityTask {
} }
} }
pub(crate) async fn build_tasks<Activity, Datatype, ActorType>( pub(crate) async fn build_tasks<A, Datatype, ActorType>(
activity: &Activity, activity: &A,
actor: &ActorType, actor: &ActorType,
inboxes: Vec<Url>, inboxes: Vec<Url>,
data: &Data<Datatype>, data: &Data<Datatype>,
) -> Result<Vec<SendActivityTask>, Error> ) -> Result<Vec<SendActivityTask>, Error>
where where
Activity: ActivityHandler + Serialize + Debug, A: Activity + Serialize + Debug,
Datatype: Clone, Datatype: Clone,
ActorType: Actor, ActorType: Actor,
{ {

View file

@ -6,7 +6,7 @@ use crate::{
error::Error, error::Error,
http_signatures::{verify_body_hash, verify_signature}, http_signatures::{verify_body_hash, verify_signature},
parse_received_activity, parse_received_activity,
traits::{ActivityHandler, Actor, Object}, traits::{Activity, Actor, Object},
}; };
use actix_web::{web::Bytes, HttpRequest, HttpResponse}; use actix_web::{web::Bytes, HttpRequest, HttpResponse};
use serde::de::DeserializeOwned; use serde::de::DeserializeOwned;
@ -14,77 +14,77 @@ use tracing::debug;
/// Handles incoming activities, verifying HTTP signatures and other checks /// Handles incoming activities, verifying HTTP signatures and other checks
/// ///
/// After successful validation, activities are passed to respective [trait@ActivityHandler]. /// After successful validation, activities are passed to respective [trait@Activity].
pub async fn receive_activity<Activity, ActorT, Datatype>( pub async fn receive_activity<A, ActorT, Datatype>(
request: HttpRequest, request: HttpRequest,
body: Bytes, body: Bytes,
data: &Data<Datatype>, data: &Data<Datatype>,
) -> Result<HttpResponse, <Activity as ActivityHandler>::Error> ) -> Result<HttpResponse, <A as Activity>::Error>
where where
Activity: ActivityHandler<DataType = Datatype> + DeserializeOwned + Send + 'static, A: Activity<DataType = Datatype> + DeserializeOwned + Send + 'static,
ActorT: Object<DataType = Datatype> + Actor + Send + 'static, ActorT: Object<DataType = Datatype> + Actor + Send + Sync + 'static,
for<'de2> <ActorT as Object>::Kind: serde::Deserialize<'de2>, for<'de2> <ActorT as Object>::Kind: serde::Deserialize<'de2>,
<Activity as ActivityHandler>::Error: From<Error> + From<<ActorT as Object>::Error>, <A as Activity>::Error: From<Error> + From<<ActorT as Object>::Error>,
<ActorT as Object>::Error: From<Error>, <ActorT as Object>::Error: From<Error>,
Datatype: Clone, Datatype: Clone,
{ {
let (activity, _) = do_stuff::<Activity, ActorT, Datatype>(request, body, data).await?; let (activity, _) = do_stuff::<A, ActorT, Datatype>(request, body, data).await?;
do_more_stuff(activity, data).await do_more_stuff(activity, data).await
} }
/// Workaround required so we can use references for the hook, instead of cloning data. /// Workaround required so we can use references for the hook, instead of cloning data.
pub trait ReceiveActivityHook<Activity, ActorT, Datatype> pub trait ReceiveActivityHook<A, ActorT, Datatype>
where where
Activity: ActivityHandler<DataType = Datatype> + DeserializeOwned + Send + Clone + 'static, A: Activity<DataType = Datatype> + DeserializeOwned + Send + Clone + 'static,
ActorT: Object<DataType = Datatype> + Actor + Send + Clone + 'static, ActorT: Object<DataType = Datatype> + Actor + Send + Clone + 'static,
for<'de2> <ActorT as Object>::Kind: serde::Deserialize<'de2>, for<'de2> <ActorT as Object>::Kind: serde::Deserialize<'de2>,
<Activity as ActivityHandler>::Error: From<Error> + From<<ActorT as Object>::Error>, <A as Activity>::Error: From<Error> + From<<ActorT as Object>::Error>,
<ActorT as Object>::Error: From<Error>, <ActorT as Object>::Error: From<Error>,
Datatype: Clone, Datatype: Clone,
{ {
/// Called when a new activity is recived /// Called when a new activity is recived
fn hook( fn hook(
self, self,
activity: &Activity, activity: &A,
actor: &ActorT, actor: &ActorT,
data: &Data<Datatype>, data: &Data<Datatype>,
) -> impl std::future::Future<Output = Result<(), <Activity as ActivityHandler>::Error>>; ) -> impl std::future::Future<Output = Result<(), <A as Activity>::Error>>;
} }
/// Same as [receive_activity], only that it calls the provided hook function before /// Same as [receive_activity], only that it calls the provided hook function before
/// calling activity verify and receive functions. /// calling activity verify and receive functions.
pub async fn receive_activity_with_hook<Activity, ActorT, Datatype>( pub async fn receive_activity_with_hook<A, ActorT, Datatype>(
request: HttpRequest, request: HttpRequest,
body: Bytes, body: Bytes,
hook: impl ReceiveActivityHook<Activity, ActorT, Datatype>, hook: impl ReceiveActivityHook<A, ActorT, Datatype>,
data: &Data<Datatype>, data: &Data<Datatype>,
) -> Result<HttpResponse, <Activity as ActivityHandler>::Error> ) -> Result<HttpResponse, <A as Activity>::Error>
where where
Activity: ActivityHandler<DataType = Datatype> + DeserializeOwned + Send + Clone + 'static, A: Activity<DataType = Datatype> + DeserializeOwned + Send + Clone + 'static,
ActorT: Object<DataType = Datatype> + Actor + Send + Clone + 'static, ActorT: Object<DataType = Datatype> + Actor + Send + Sync + Clone + 'static,
for<'de2> <ActorT as Object>::Kind: serde::Deserialize<'de2>, for<'de2> <ActorT as Object>::Kind: serde::Deserialize<'de2>,
<Activity as ActivityHandler>::Error: From<Error> + From<<ActorT as Object>::Error>, <A as Activity>::Error: From<Error> + From<<ActorT as Object>::Error>,
<ActorT as Object>::Error: From<Error>, <ActorT as Object>::Error: From<Error>,
Datatype: Clone, Datatype: Clone,
{ {
let (activity, actor) = do_stuff::<Activity, ActorT, Datatype>(request, body, data).await?; let (activity, actor) = do_stuff::<A, ActorT, Datatype>(request, body, data).await?;
hook.hook(&activity, &actor, data).await?; hook.hook(&activity, &actor, data).await?;
do_more_stuff(activity, data).await do_more_stuff(activity, data).await
} }
async fn do_stuff<Activity, ActorT, Datatype>( async fn do_stuff<A, ActorT, Datatype>(
request: HttpRequest, request: HttpRequest,
body: Bytes, body: Bytes,
data: &Data<Datatype>, data: &Data<Datatype>,
) -> Result<(Activity, ActorT), <Activity as ActivityHandler>::Error> ) -> Result<(A, ActorT), <A as Activity>::Error>
where where
Activity: ActivityHandler<DataType = Datatype> + DeserializeOwned + Send + 'static, A: Activity<DataType = Datatype> + DeserializeOwned + Send + 'static,
ActorT: Object<DataType = Datatype> + Actor + Send + 'static, ActorT: Object<DataType = Datatype> + Actor + Send + Sync + 'static,
for<'de2> <ActorT as Object>::Kind: serde::Deserialize<'de2>, for<'de2> <ActorT as Object>::Kind: serde::Deserialize<'de2>,
<Activity as ActivityHandler>::Error: From<Error> + From<<ActorT as Object>::Error>, <A as Activity>::Error: From<Error> + From<<ActorT as Object>::Error>,
<ActorT as Object>::Error: From<Error>, <ActorT as Object>::Error: From<Error>,
Datatype: Clone, Datatype: Clone,
{ {
@ -94,7 +94,7 @@ where
.map(http_compat::header_value); .map(http_compat::header_value);
verify_body_hash(digest_header.as_ref(), &body)?; verify_body_hash(digest_header.as_ref(), &body)?;
let (activity, actor) = parse_received_activity::<Activity, ActorT, _>(&body, data).await?; let (activity, actor) = parse_received_activity::<A, ActorT, _>(&body, data).await?;
let headers = http_compat::header_map(request.headers()); let headers = http_compat::header_map(request.headers());
let method = http_compat::method(request.method()); let method = http_compat::method(request.method());
@ -104,12 +104,12 @@ where
Ok((activity, actor)) Ok((activity, actor))
} }
async fn do_more_stuff<Activity, Datatype>( async fn do_more_stuff<A, Datatype>(
activity: Activity, activity: A,
data: &Data<Datatype>, data: &Data<Datatype>,
) -> Result<HttpResponse, <Activity as ActivityHandler>::Error> ) -> Result<HttpResponse, <A as Activity>::Error>
where where
Activity: ActivityHandler<DataType = Datatype> + DeserializeOwned + Send + 'static, A: Activity<DataType = Datatype> + DeserializeOwned + Send + 'static,
Datatype: Clone, Datatype: Clone,
{ {
debug!("Receiving activity {}", activity.id().to_string()); debug!("Receiving activity {}", activity.id().to_string());
@ -160,21 +160,21 @@ mod test {
struct Dummy; struct Dummy;
impl<Activity, ActorT, Datatype> ReceiveActivityHook<Activity, ActorT, Datatype> for Dummy impl<A, ActorT, Datatype> ReceiveActivityHook<A, ActorT, Datatype> for Dummy
where where
Activity: ActivityHandler<DataType = Datatype> + DeserializeOwned + Send + Clone + 'static, A: Activity<DataType = Datatype> + DeserializeOwned + Send + Clone + 'static,
ActorT: Object<DataType = Datatype> + Actor + Send + Clone + 'static, ActorT: Object<DataType = Datatype> + Actor + Send + Clone + 'static,
for<'de2> <ActorT as Object>::Kind: serde::Deserialize<'de2>, for<'de2> <ActorT as Object>::Kind: serde::Deserialize<'de2>,
<Activity as ActivityHandler>::Error: From<Error> + From<<ActorT as Object>::Error>, <A as Activity>::Error: From<Error> + From<<ActorT as Object>::Error>,
<ActorT as Object>::Error: From<Error>, <ActorT as Object>::Error: From<Error>,
Datatype: Clone, Datatype: Clone,
{ {
async fn hook( async fn hook(
self, self,
_activity: &Activity, _activity: &A,
_actor: &ActorT, _actor: &ActorT,
_data: &Data<Datatype>, _data: &Data<Datatype>,
) -> Result<(), <Activity as ActivityHandler>::Error> { ) -> Result<(), <A as Activity>::Error> {
// ensure that hook gets called by returning this value // ensure that hook gets called by returning this value
Err(Error::Other("test-error".to_string()).into()) Err(Error::Other("test-error".to_string()).into())
} }

View file

@ -4,6 +4,7 @@ mod http_compat;
pub mod inbox; pub mod inbox;
#[doc(hidden)] #[doc(hidden)]
pub mod middleware; pub mod middleware;
pub mod response;
use crate::{ use crate::{
config::Data, config::Data,
@ -22,7 +23,7 @@ pub async fn signing_actor<A>(
data: &Data<<A as Object>::DataType>, data: &Data<<A as Object>::DataType>,
) -> Result<A, <A as Object>::Error> ) -> Result<A, <A as Object>::Error>
where where
A: Object + Actor, A: Object + Actor + Send + Sync,
<A as Object>::Error: From<Error>, <A as Object>::Error: From<Error>,
for<'de2> <A as Object>::Kind: Deserialize<'de2>, for<'de2> <A as Object>::Kind: Deserialize<'de2>,
{ {

50
src/actix_web/response.rs Normal file
View file

@ -0,0 +1,50 @@
//! Generate HTTP responses for Activitypub ojects
use crate::{
protocol::{context::WithContext, tombstone::Tombstone},
FEDERATION_CONTENT_TYPE,
};
use actix_web::HttpResponse;
use http02::header::VARY;
use serde::Serialize;
use serde_json::Value;
use url::Url;
/// Generates HTTP response to serve the object for fetching from other instances.
///
/// If possible use [Object.http_response]
/// which also handles redirects for remote objects and deletions.
///
/// `federation_context` is the value of `@context`.
pub fn create_http_response<T: Serialize>(
data: T,
federation_context: &Value,
) -> Result<HttpResponse, serde_json::Error> {
let json = serde_json::to_string_pretty(&WithContext::new(data, federation_context.clone()))?;
Ok(HttpResponse::Ok()
.content_type(FEDERATION_CONTENT_TYPE)
.insert_header((VARY, "Accept"))
.body(json))
}
pub(crate) fn create_tombstone_response(
id: Url,
federation_context: &Value,
) -> Result<HttpResponse, serde_json::Error> {
let tombstone = Tombstone::new(id);
let json =
serde_json::to_string_pretty(&WithContext::new(tombstone, federation_context.clone()))?;
Ok(HttpResponse::Gone()
.content_type(FEDERATION_CONTENT_TYPE)
.status(actix_web::http::StatusCode::GONE)
.insert_header((VARY, "Accept"))
.body(json))
}
pub(crate) fn redirect_remote_object(url: &Url) -> HttpResponse {
let mut res = HttpResponse::PermanentRedirect();
res.insert_header((actix_web::http::header::LOCATION, url.as_str()));
res.finish()
}

View file

@ -7,7 +7,7 @@ use crate::{
error::Error, error::Error,
http_signatures::verify_signature, http_signatures::verify_signature,
parse_received_activity, parse_received_activity,
traits::{ActivityHandler, Actor, Object}, traits::{Activity, Actor, Object},
}; };
use axum::{ use axum::{
body::Body, body::Body,
@ -20,20 +20,20 @@ use serde::de::DeserializeOwned;
use tracing::debug; use tracing::debug;
/// Handles incoming activities, verifying HTTP signatures and other checks /// Handles incoming activities, verifying HTTP signatures and other checks
pub async fn receive_activity<Activity, ActorT, Datatype>( pub async fn receive_activity<A, ActorT, Datatype>(
activity_data: ActivityData, activity_data: ActivityData,
data: &Data<Datatype>, data: &Data<Datatype>,
) -> Result<(), <Activity as ActivityHandler>::Error> ) -> Result<(), <A as Activity>::Error>
where where
Activity: ActivityHandler<DataType = Datatype> + DeserializeOwned + Send + 'static, A: Activity<DataType = Datatype> + DeserializeOwned + Send + 'static,
ActorT: Object<DataType = Datatype> + Actor + Send + 'static, ActorT: Object<DataType = Datatype> + Actor + Send + Sync + 'static,
for<'de2> <ActorT as Object>::Kind: serde::Deserialize<'de2>, for<'de2> <ActorT as Object>::Kind: serde::Deserialize<'de2>,
<Activity as ActivityHandler>::Error: From<Error> + From<<ActorT as Object>::Error>, <A as Activity>::Error: From<Error> + From<<ActorT as Object>::Error>,
<ActorT as Object>::Error: From<Error>, <ActorT as Object>::Error: From<Error>,
Datatype: Clone, Datatype: Clone,
{ {
let (activity, actor) = let (activity, actor) =
parse_received_activity::<Activity, ActorT, _>(&activity_data.body, data).await?; parse_received_activity::<A, ActorT, _>(&activity_data.body, data).await?;
verify_signature( verify_signature(
&activity_data.headers, &activity_data.headers,

View file

@ -19,7 +19,8 @@ use crate::{
error::Error, error::Error,
http_signatures::sign_request, http_signatures::sign_request,
protocol::verification::verify_domains_match, protocol::verification::verify_domains_match,
traits::{ActivityHandler, Actor}, traits::{Activity, Actor},
utils::validate_ip,
}; };
use async_trait::async_trait; use async_trait::async_trait;
use bytes::Bytes; use bytes::Bytes;
@ -32,7 +33,6 @@ use reqwest_middleware::{ClientWithMiddleware, RequestBuilder};
use rsa::{pkcs8::DecodePrivateKey, RsaPrivateKey}; use rsa::{pkcs8::DecodePrivateKey, RsaPrivateKey};
use serde::de::DeserializeOwned; use serde::de::DeserializeOwned;
use std::{ use std::{
net::IpAddr,
ops::Deref, ops::Deref,
sync::{ sync::{
atomic::{AtomicU32, Ordering}, atomic::{AtomicU32, Ordering},
@ -41,7 +41,6 @@ use std::{
}, },
time::Duration, time::Duration,
}; };
use tokio::net::lookup_host;
use url::Url; use url::Url;
/// Configuration for this library, with various federation related settings /// Configuration for this library, with various federation related settings
@ -125,12 +124,9 @@ impl<T: Clone> FederationConfig<T> {
FederationConfigBuilder::default() FederationConfigBuilder::default()
} }
pub(crate) async fn verify_url_and_domain<Activity, Datatype>( pub(crate) async fn verify_url_and_domain<A, Datatype>(&self, activity: &A) -> Result<(), Error>
&self,
activity: &Activity,
) -> Result<(), Error>
where where
Activity: ActivityHandler<DataType = Datatype> + DeserializeOwned + Send + 'static, A: Activity<DataType = Datatype> + DeserializeOwned + Send + 'static,
{ {
verify_domains_match(activity.id(), activity.actor())?; verify_domains_match(activity.id(), activity.actor())?;
self.verify_url_valid(activity.id()).await?; self.verify_url_valid(activity.id()).await?;
@ -186,30 +182,9 @@ impl<T: Clone> FederationConfig<T> {
return Err(Error::UrlVerificationError("Explicit port is not allowed")); return Err(Error::UrlVerificationError("Explicit port is not allowed"));
} }
// Resolve domain and see if it points to private IP let allow_local = std::env::var("DANGER_FEDERATION_ALLOW_LOCAL_IP").is_ok();
// TODO: Use is_global() once stabilized if !allow_local && validate_ip(&url).await.is_err() {
// https://doc.rust-lang.org/std/net/enum.IpAddr.html#method.is_global return Err(Error::DomainResolveError(domain.to_string()));
let invalid_ip =
lookup_host((domain.to_owned(), 80))
.await?
.any(|addr| match addr.ip() {
IpAddr::V4(addr) => {
addr.is_private()
|| addr.is_link_local()
|| addr.is_loopback()
|| addr.is_multicast()
}
IpAddr::V6(addr) => {
addr.is_loopback()
|| addr.is_multicast()
|| ((addr.segments()[0] & 0xfe00) == 0xfc00) // is_unique_local
|| ((addr.segments()[0] & 0xffc0) == 0xfe80) // is_unicast_link_local
}
});
if invalid_ip {
return Err(Error::UrlVerificationError(
"Localhost is only allowed in debug mode",
));
} }
} }
@ -258,7 +233,7 @@ impl<T: Clone> FederationConfigBuilder<T> {
let private_key = let private_key =
RsaPrivateKey::from_pkcs8_pem(&private_key_pem).expect("Could not decode PEM data"); RsaPrivateKey::from_pkcs8_pem(&private_key_pem).expect("Could not decode PEM data");
self.signed_fetch_actor = Some(Some(Arc::new((actor.id(), private_key)))); self.signed_fetch_actor = Some(Some(Arc::new((actor.id().clone(), private_key))));
self self
} }
@ -403,6 +378,15 @@ impl<T: Clone> Data<T> {
) )
.await .await
} }
/// Resolve domain of the url and throw error if it points to local/private IP.
pub async fn is_valid_ip(&self, url: &Url) -> Result<(), Error> {
if self.config.debug {
return Ok(());
}
validate_ip(url).await
}
} }
impl<T: Clone> Deref for Data<T> { impl<T: Clone> Deref for Data<T> {

View file

@ -28,6 +28,9 @@ pub enum Error {
/// url verification error /// url verification error
#[error("URL failed verification: {0}")] #[error("URL failed verification: {0}")]
UrlVerificationError(&'static str), UrlVerificationError(&'static str),
/// Resolving domain points to local IP.
#[error("Resolving domain {0} points to local IP address. This may indicate an attacker attempting to access internal services. If intentional, you can ignore this error by setting DANGER_FEDERATION_ALLOW_LOCAL_IP=1")]
DomainResolveError(String),
/// Incoming activity has invalid digest for body /// Incoming activity has invalid digest for body
#[error("Incoming activity has invalid digest for body")] #[error("Incoming activity has invalid digest for body")]
ActivityBodyDigestInvalid, ActivityBodyDigestInvalid,

View file

@ -10,7 +10,7 @@ use url::Url;
impl<T> FromStr for ObjectId<T> impl<T> FromStr for ObjectId<T>
where where
T: Object + Send + Debug + 'static, T: Object + Send + Sync + Debug + 'static,
for<'de2> <T as Object>::Kind: Deserialize<'de2>, for<'de2> <T as Object>::Kind: Deserialize<'de2>,
{ {
type Err = url::ParseError; type Err = url::ParseError;
@ -61,7 +61,7 @@ where
impl<Kind> ObjectId<Kind> impl<Kind> ObjectId<Kind>
where where
Kind: Object + Send + Debug + 'static, Kind: Object + Send + Sync + Debug + 'static,
for<'de2> <Kind as Object>::Kind: Deserialize<'de2>, for<'de2> <Kind as Object>::Kind: Deserialize<'de2>,
{ {
/// Construct a new objectid instance /// Construct a new objectid instance
@ -164,6 +164,7 @@ where
if let Err(Error::ObjectDeleted(url)) = res { if let Err(Error::ObjectDeleted(url)) = res {
if let Some(db_object) = db_object { if let Some(db_object) = db_object {
db_object.delete(data).await?; db_object.delete(data).await?;
return Ok(db_object);
} }
return Err(Error::ObjectDeleted(url).into()); return Err(Error::ObjectDeleted(url).into());
} }

View file

@ -45,7 +45,7 @@ pub async fn webfinger_resolve_actor<T: Clone, Kind>(
data: &Data<T>, data: &Data<T>,
) -> Result<Kind, <Kind as Object>::Error> ) -> Result<Kind, <Kind as Object>::Error>
where where
Kind: Object + Actor + Send + 'static + Object<DataType = T>, Kind: Object + Actor + Send + Sync + 'static + Object<DataType = T>,
for<'de2> <Kind as Object>::Kind: serde::Deserialize<'de2>, for<'de2> <Kind as Object>::Kind: serde::Deserialize<'de2>,
<Kind as Object>::Error: From<crate::error::Error> + Send + Sync + Display, <Kind as Object>::Error: From<crate::error::Error> + Send + Sync + Display,
{ {
@ -88,6 +88,7 @@ where
} }
}) })
.filter_map(|l| l.href.clone()) .filter_map(|l| l.href.clone())
.rev()
.collect(); .collect();
for l in links { for l in links {
@ -222,7 +223,7 @@ pub fn build_webfinger_response_with_type(
} }
/// A webfinger response with information about a `Person` or other type of actor. /// A webfinger response with information about a `Person` or other type of actor.
#[derive(Serialize, Deserialize, Debug, Default)] #[derive(Serialize, Deserialize, Debug, Default, PartialEq)]
pub struct Webfinger { pub struct Webfinger {
/// The actor which is described here, for example `acct:LemmyDev@mastodon.social` /// The actor which is described here, for example `acct:LemmyDev@mastodon.social`
pub subject: String, pub subject: String,
@ -237,7 +238,7 @@ pub struct Webfinger {
} }
/// A single link included as part of a [Webfinger] response. /// A single link included as part of a [Webfinger] response.
#[derive(Serialize, Deserialize, Debug, Default)] #[derive(Serialize, Deserialize, Debug, Default, PartialEq)]
pub struct WebfingerLink { pub struct WebfingerLink {
/// Relationship of the link, such as `self` or `http://webfinger.net/rel/profile-page` /// Relationship of the link, such as `self` or `http://webfinger.net/rel/profile-page`
pub rel: Option<String>, pub rel: Option<String>,

View file

@ -149,7 +149,7 @@ pub(crate) async fn signing_actor<'a, A, H>(
data: &Data<<A as Object>::DataType>, data: &Data<<A as Object>::DataType>,
) -> Result<A, <A as Object>::Error> ) -> Result<A, <A as Object>::Error>
where where
A: Object + Actor, A: Object + Actor + Send + Sync,
<A as Object>::Error: From<Error>, <A as Object>::Error: From<Error>,
for<'de2> <A as Object>::Kind: Deserialize<'de2>, for<'de2> <A as Object>::Kind: Deserialize<'de2>,
H: IntoIterator<Item = (&'a HeaderName, &'a HeaderValue)>, H: IntoIterator<Item = (&'a HeaderName, &'a HeaderValue)>,

View file

@ -23,12 +23,13 @@ pub mod http_signatures;
pub mod protocol; pub mod protocol;
pub(crate) mod reqwest_shim; pub(crate) mod reqwest_shim;
pub mod traits; pub mod traits;
mod utils;
use crate::{ use crate::{
config::Data, config::Data,
error::Error, error::Error,
fetch::object_id::ObjectId, fetch::object_id::ObjectId,
traits::{ActivityHandler, Actor, Object}, traits::{Activity, Actor, Object},
}; };
pub use activitystreams_kinds as kinds; pub use activitystreams_kinds as kinds;
@ -40,19 +41,19 @@ pub const FEDERATION_CONTENT_TYPE: &str = "application/activity+json";
/// Deserialize incoming inbox activity to the given type, perform basic /// Deserialize incoming inbox activity to the given type, perform basic
/// validation and extract the actor. /// validation and extract the actor.
async fn parse_received_activity<Activity, ActorT, Datatype>( async fn parse_received_activity<A, ActorT, Datatype>(
body: &[u8], body: &[u8],
data: &Data<Datatype>, data: &Data<Datatype>,
) -> Result<(Activity, ActorT), <Activity as ActivityHandler>::Error> ) -> Result<(A, ActorT), <A as Activity>::Error>
where where
Activity: ActivityHandler<DataType = Datatype> + DeserializeOwned + Send + 'static, A: Activity<DataType = Datatype> + DeserializeOwned + Send + 'static,
ActorT: Object<DataType = Datatype> + Actor + Send + 'static, ActorT: Object<DataType = Datatype> + Actor + Send + Sync + 'static,
for<'de2> <ActorT as Object>::Kind: serde::Deserialize<'de2>, for<'de2> <ActorT as Object>::Kind: serde::Deserialize<'de2>,
<Activity as ActivityHandler>::Error: From<Error> + From<<ActorT as Object>::Error>, <A as Activity>::Error: From<Error> + From<<ActorT as Object>::Error>,
<ActorT as Object>::Error: From<Error>, <ActorT as Object>::Error: From<Error>,
Datatype: Clone, Datatype: Clone,
{ {
let activity: Activity = serde_json::from_slice(body).map_err(|err| { let activity: A = serde_json::from_slice(body).map_err(|err| {
// Attempt to include activity id in error message // Attempt to include activity id in error message
let id = extract_id(body).ok(); let id = extract_id(body).ok();
Error::ParseReceivedActivity { err, id } Error::ParseReceivedActivity { err, id }

View file

@ -19,7 +19,7 @@
//! Ok::<(), serde_json::error::Error>(()) //! Ok::<(), serde_json::error::Error>(())
//! ``` //! ```
use crate::{config::Data, traits::ActivityHandler}; use crate::{config::Data, traits::Activity};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_json::Value; use serde_json::Value;
use url::Url; use url::Url;
@ -55,12 +55,12 @@ impl<T> WithContext<T> {
} }
#[async_trait::async_trait] #[async_trait::async_trait]
impl<T> ActivityHandler for WithContext<T> impl<T> Activity for WithContext<T>
where where
T: ActivityHandler + Send + Sync, T: Activity + Send + Sync,
{ {
type DataType = <T as ActivityHandler>::DataType; type DataType = <T as Activity>::DataType;
type Error = <T as ActivityHandler>::Error; type Error = <T as Activity>::Error;
fn id(&self) -> &Url { fn id(&self) -> &Url {
self.inner.id() self.inner.id()

View file

@ -1,13 +1,22 @@
//! Serde deserialization functions which help to receive differently shaped data //! Serde deserialization functions which help to receive differently shaped data
use serde::{Deserialize, Deserializer}; use activitystreams_kinds::public;
use itertools::Itertools;
use serde::{de::Error, Deserialize, Deserializer};
use serde_json::Value;
use url::Url;
/// Deserialize JSON single value or array into Vec. /// Deserialize JSON single value or array into `Vec<Url>`.
/// ///
/// Useful if your application can handle multiple values for a field, but another federated /// Useful if your application can handle multiple values for a field, but another federated
/// platform only sends a single one. /// platform only sends a single one.
/// ///
/// Also accepts common `Public` aliases for recipient fields. Some implementations send `Public`
/// or `as:Public` instead of the canonical `https://www.w3.org/ns/activitystreams#Public` URL
/// in fields such as `to` and `cc`.
///
/// ``` /// ```
/// # use activitypub_federation::kinds::public;
/// # use activitypub_federation::protocol::helpers::deserialize_one_or_many; /// # use activitypub_federation::protocol::helpers::deserialize_one_or_many;
/// # use url::Url; /// # use url::Url;
/// #[derive(serde::Deserialize)] /// #[derive(serde::Deserialize)]
@ -25,24 +34,39 @@ use serde::{Deserialize, Deserializer};
/// "https://lemmy.ml/u/bob" /// "https://lemmy.ml/u/bob"
/// ]}"#)?; /// ]}"#)?;
/// assert_eq!(multiple.to.len(), 2); /// assert_eq!(multiple.to.len(), 2);
/// Ok::<(), anyhow::Error>(()) ///
pub fn deserialize_one_or_many<'de, T, D>(deserializer: D) -> Result<Vec<T>, D::Error> /// let note: Note = serde_json::from_str(r#"{"to": ["Public", "as:Public"]}"#)?;
/// assert_eq!(note.to, vec![public()]);
/// # Ok::<(), anyhow::Error>(())
/// ```
pub fn deserialize_one_or_many<'de, D>(deserializer: D) -> Result<Vec<Url>, D::Error>
where where
T: Deserialize<'de>,
D: Deserializer<'de>, D: Deserializer<'de>,
{ {
#[derive(Deserialize)] #[derive(Deserialize)]
#[serde(untagged)] #[serde(untagged)]
enum OneOrMany<T> { enum OneOrMany {
One(T), Many(Vec<Value>),
Many(Vec<T>), One(Value),
} }
let result: OneOrMany<T> = Deserialize::deserialize(deserializer)?; let result: OneOrMany = Deserialize::deserialize(deserializer)?;
Ok(match result { let values = match result {
OneOrMany::Many(list) => list,
OneOrMany::One(value) => vec![value], OneOrMany::One(value) => vec![value],
OneOrMany::Many(values) => values,
};
values
.into_iter()
.map(|value| match value {
Value::String(value) if matches!(value.as_str(), "Public" | "as:Public") => {
Ok(public())
}
Value::String(value) => Url::parse(&value).map_err(D::Error::custom),
value => Url::deserialize(value).map_err(D::Error::custom),
}) })
.collect::<Result<Vec<_>, _>>()
.map(|values| values.into_iter().unique().collect())
} }
/// Deserialize JSON single value or single element array into single value. /// Deserialize JSON single value or single element array into single value.
@ -116,8 +140,35 @@ where
Ok(inner) Ok(inner)
} }
/// Deserialize either single value or last item from an array into an optional field.
pub fn deserialize_last<'de, T, D>(deserializer: D) -> Result<Option<T>, D::Error>
where
T: Deserialize<'de>,
D: Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(untagged)]
enum MaybeArray<T> {
Simple(T),
Array(Vec<T>),
None,
}
let result = Deserialize::deserialize(deserializer)?;
Ok(match result {
MaybeArray::Simple(value) => Some(value),
MaybeArray::Array(value) => value.into_iter().last(),
MaybeArray::None => None,
})
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::deserialize_one_or_many;
use activitystreams_kinds::public;
use anyhow::Result;
use serde::Deserialize;
#[test] #[test]
fn deserialize_one_multiple_values() { fn deserialize_one_multiple_values() {
use crate::protocol::helpers::deserialize_one; use crate::protocol::helpers::deserialize_one;
@ -133,4 +184,70 @@ mod tests {
); );
assert!(note.is_err()); assert!(note.is_err());
} }
#[test]
fn deserialize_one_or_many_single_public_aliases() -> Result<()> {
use url::Url;
#[derive(Deserialize)]
struct Note {
#[serde(deserialize_with = "deserialize_one_or_many")]
to: Vec<Url>,
}
for alias in ["Public", "as:Public"] {
let note = serde_json::from_str::<Note>(&format!(r#"{{"to": "{alias}"}}"#))?;
assert_eq!(note.to, vec![public()]);
}
Ok(())
}
#[test]
fn deserialize_one_or_many_array() -> Result<()> {
use url::Url;
#[derive(Deserialize)]
struct Note {
#[serde(deserialize_with = "deserialize_one_or_many")]
to: Vec<Url>,
}
let note = serde_json::from_str::<Note>(
r#"{
"to": [
"https://example.com/c/main",
"Public",
"as:Public",
"https://www.w3.org/ns/activitystreams#Public"
]
}"#,
)?;
assert_eq!(
note.to,
vec![Url::parse("https://example.com/c/main")?, public(),]
);
Ok(())
}
#[test]
fn deserialize_one_or_many_leaves_other_strings_unchanged() -> Result<()> {
use url::Url;
#[derive(Deserialize)]
struct Note {
#[serde(deserialize_with = "deserialize_one_or_many")]
to: Vec<Url>,
content: String,
}
let note = serde_json::from_str::<Note>(r#"{"to": "Public", "content": "Public"}"#)?;
assert_eq!(note.to, vec![public()]);
assert_eq!(note.content, "Public");
Ok(())
}
} }

View file

@ -3,5 +3,6 @@
pub mod context; pub mod context;
pub mod helpers; pub mod helpers;
pub mod public_key; pub mod public_key;
pub mod tombstone;
pub mod values; pub mod values;
pub mod verification; pub mod verification;

27
src/protocol/tombstone.rs Normal file
View file

@ -0,0 +1,27 @@
//! Tombstone is used to serve deleted objects
use crate::kinds::object::TombstoneType;
use serde::{Deserialize, Serialize};
use url::Url;
/// Represents a local object that was deleted
///
/// <https://www.w3.org/TR/activitystreams-vocabulary/#dfn-tombstone>
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Tombstone {
/// Id of the deleted object
pub id: Url,
#[serde(rename = "type")]
pub(crate) kind: TombstoneType,
}
impl Tombstone {
/// Create a new tombstone for the given object id
pub fn new(id: Url) -> Tombstone {
Tombstone {
id,
kind: TombstoneType::Tombstone,
}
}
}

View file

@ -63,7 +63,7 @@ pub fn verify_is_remote_object<Kind, R: Clone>(
data: &Data<<Kind as Object>::DataType>, data: &Data<<Kind as Object>::DataType>,
) -> Result<(), Error> ) -> Result<(), Error>
where where
Kind: Object<DataType = R> + Send + 'static, Kind: Object<DataType = R> + Send + Sync + 'static,
for<'de2> <Kind as Object>::Kind: Deserialize<'de2>, for<'de2> <Kind as Object>::Kind: Deserialize<'de2>,
{ {
if id.is_local(data) { if id.is_local(data) {

View file

@ -10,8 +10,8 @@ use std::{
task::{Context, Poll}, task::{Context, Poll},
}; };
/// 200KB /// 1 MB
const MAX_BODY_SIZE: usize = 204800; const MAX_BODY_SIZE: usize = 1024 * 1024;
pin_project! { pin_project! {
pub struct BytesFuture { pub struct BytesFuture {
@ -66,7 +66,7 @@ impl Future for TextFuture {
/// Reqwest doesn't limit the response body size by default nor does it offer an option to configure one. /// Reqwest doesn't limit the response body size by default nor does it offer an option to configure one.
/// Since we have to fetch data from untrusted sources, not restricting the maximum size is a DoS hazard for us. /// Since we have to fetch data from untrusted sources, not restricting the maximum size is a DoS hazard for us.
/// ///
/// This shim reimplements the `bytes`, `json`, and `text` functions and restricts the bodies to 100KB. /// This shim reimplements the `bytes`, `json`, and `text` functions and restricts the bodies length.
/// ///
/// TODO: Remove this shim as soon as reqwest gets support for size-limited bodies. /// TODO: Remove this shim as soon as reqwest gets support for size-limited bodies.
pub trait ResponseExt { pub trait ResponseExt {

View file

@ -18,8 +18,8 @@ pub enum UntaggedEither<L, R> {
#[async_trait] #[async_trait]
impl<T, R, E, D> Object for Either<T, R> impl<T, R, E, D> Object for Either<T, R>
where where
T: Object + Object<Error = E, DataType = D> + Send, T: Object + Object<Error = E, DataType = D> + Send + Sync,
R: Object + Object<Error = E, DataType = D> + Send, R: Object + Object<Error = E, DataType = D> + Send + Sync,
<T as Object>::Kind: Send + Sync, <T as Object>::Kind: Send + Sync,
<R as Object>::Kind: Send + Sync, <R as Object>::Kind: Send + Sync,
D: Sync + Send + Clone, D: Sync + Send + Clone,
@ -29,6 +29,14 @@ where
type Kind = UntaggedEither<T::Kind, R::Kind>; type Kind = UntaggedEither<T::Kind, R::Kind>;
type Error = E; type Error = E;
/// `id` field of the object
fn id(&self) -> Url {
match self {
Either::Left(l) => l.id(),
Either::Right(r) => r.id(),
}
}
fn last_refreshed_at(&self) -> Option<DateTime<Utc>> { fn last_refreshed_at(&self) -> Option<DateTime<Utc>> {
match self { match self {
Either::Left(l) => l.last_refreshed_at(), Either::Left(l) => l.last_refreshed_at(),
@ -51,13 +59,20 @@ where
Ok(None) Ok(None)
} }
async fn delete(self, data: &Data<Self::DataType>) -> Result<(), Self::Error> { async fn delete(&self, data: &Data<Self::DataType>) -> Result<(), Self::Error> {
match self { match self {
Either::Left(l) => l.delete(data).await, Either::Left(l) => l.delete(data).await,
Either::Right(r) => r.delete(data).await, Either::Right(r) => r.delete(data).await,
} }
} }
fn is_deleted(&self) -> bool {
match self {
Either::Left(l) => l.is_deleted(),
Either::Right(r) => r.is_deleted(),
}
}
async fn into_json(self, data: &Data<Self::DataType>) -> Result<Self::Kind, Self::Error> { async fn into_json(self, data: &Data<Self::DataType>) -> Result<Self::Kind, Self::Error> {
Ok(match self { Ok(match self {
Either::Left(l) => UntaggedEither::Left(l.into_json(data).await?), Either::Left(l) => UntaggedEither::Left(l.into_json(data).await?),
@ -88,20 +103,13 @@ where
#[async_trait] #[async_trait]
impl<T, R, E, D> Actor for Either<T, R> impl<T, R, E, D> Actor for Either<T, R>
where where
T: Actor + Object + Object<Error = E, DataType = D> + Send + 'static, T: Actor + Object + Object<Error = E, DataType = D> + Send + Sync + 'static,
R: Actor + Object + Object<Error = E, DataType = D> + Send + 'static, R: Actor + Object + Object<Error = E, DataType = D> + Send + Sync + 'static,
<T as Object>::Kind: Send + Sync, <T as Object>::Kind: Send + Sync,
<R as Object>::Kind: Send + Sync, <R as Object>::Kind: Send + Sync,
D: Sync + Send + Clone, D: Sync + Send + Clone,
E: From<Error> + Debug, E: From<Error> + Debug,
{ {
fn id(&self) -> Url {
match self {
Either::Left(l) => l.id(),
Either::Right(r) => r.id(),
}
}
fn public_key_pem(&self) -> &str { fn public_key_pem(&self) -> &str {
match self { match self {
Either::Left(l) => l.public_key_pem(), Either::Left(l) => l.public_key_pem(),

View file

@ -9,6 +9,7 @@ use url::Url;
/// `Either` implementations for traits /// `Either` implementations for traits
pub mod either; pub mod either;
pub mod tests;
/// Helper for converting between database structs and federated protocol structs. /// Helper for converting between database structs and federated protocol structs.
/// ///
@ -52,6 +53,8 @@ pub mod either;
/// type Kind = Note; /// type Kind = Note;
/// type Error = anyhow::Error; /// type Error = anyhow::Error;
/// ///
/// fn id(&self) -> Url { self.ap_id.inner().clone() }
///
/// async fn read_from_id(object_id: Url, data: &Data<Self::DataType>) -> Result<Option<Self>, Self::Error> { /// async fn read_from_id(object_id: Url, data: &Data<Self::DataType>) -> Result<Option<Self>, Self::Error> {
/// // Attempt to read object from local database. Return Ok(None) if not found. /// // Attempt to read object from local database. Return Ok(None) if not found.
/// let post: Option<DbPost> = data.read_post_from_json_id(object_id).await?; /// let post: Option<DbPost> = data.read_post_from_json_id(object_id).await?;
@ -106,6 +109,9 @@ pub trait Object: Sized + Debug {
/// Error type returned by handler methods /// Error type returned by handler methods
type Error; type Error;
/// `id` field of the object
fn id(&self) -> Url;
/// Returns the last time this object was updated. /// Returns the last time this object was updated.
/// ///
/// If this returns `Some` and the value is too long ago, the object is refetched from the /// If this returns `Some` and the value is too long ago, the object is refetched from the
@ -130,10 +136,15 @@ pub trait Object: Sized + Debug {
/// Mark remote object as deleted in local database. /// Mark remote object as deleted in local database.
/// ///
/// Called when a `Delete` activity is received, or if fetch returns a `Tombstone` object. /// Called when a `Delete` activity is received, or if fetch returns a `Tombstone` object.
async fn delete(self, _data: &Data<Self::DataType>) -> Result<(), Self::Error> { async fn delete(&self, _data: &Data<Self::DataType>) -> Result<(), Self::Error> {
Ok(()) Ok(())
} }
/// Returns true if the object was deleted
fn is_deleted(&self) -> bool {
false
}
/// Convert database type to Activitypub type. /// Convert database type to Activitypub type.
/// ///
/// Called when a local object gets fetched by another instance over HTTP, or when an object /// Called when a local object gets fetched by another instance over HTTP, or when an object
@ -159,6 +170,40 @@ pub trait Object: Sized + Debug {
/// should write the received object to database. Note that there is no distinction between /// should write the received object to database. Note that there is no distinction between
/// create and update, so an `upsert` operation should be used. /// create and update, so an `upsert` operation should be used.
async fn from_json(json: Self::Kind, data: &Data<Self::DataType>) -> Result<Self, Self::Error>; async fn from_json(json: Self::Kind, data: &Data<Self::DataType>) -> Result<Self, Self::Error>;
/// Generates HTTP response to serve the object for fetching from other instances.
///
/// - If the object has a remote domain, sends a redirect to the original instance.
/// - If [Object.is_deleted] returns true, returns a [crate::protocol::tombstone::Tombstone] instead.
/// - Otherwise serves the object JSON using [Object.into_json] and pretty-print
///
/// `federation_context` is the value of `@context`.
#[cfg(feature = "actix-web")]
async fn http_response(
self,
federation_context: &serde_json::Value,
data: &Data<Self::DataType>,
) -> Result<actix_web::HttpResponse, Self::Error>
where
Self::Error: From<serde_json::Error>,
Self::Kind: serde::Serialize + Send,
{
use crate::actix_web::response::{
create_http_response,
create_tombstone_response,
redirect_remote_object,
};
let id = self.id();
let res = if !data.config.is_local_url(&id) {
redirect_remote_object(&id)
} else if !self.is_deleted() {
let json = self.into_json(data).await?;
create_http_response(json, federation_context)?
} else {
create_tombstone_response(id.clone(), federation_context)?
};
Ok(res)
}
} }
/// Handler for receiving incoming activities. /// Handler for receiving incoming activities.
@ -168,7 +213,7 @@ pub trait Object: Sized + Debug {
/// # use url::Url; /// # use url::Url;
/// # use activitypub_federation::fetch::object_id::ObjectId; /// # use activitypub_federation::fetch::object_id::ObjectId;
/// # use activitypub_federation::config::Data; /// # use activitypub_federation::config::Data;
/// # use activitypub_federation::traits::ActivityHandler; /// # use activitypub_federation::traits::Activity;
/// # use activitypub_federation::traits::tests::{DbConnection, DbUser}; /// # use activitypub_federation::traits::tests::{DbConnection, DbUser};
/// #[derive(serde::Deserialize)] /// #[derive(serde::Deserialize)]
/// struct Follow { /// struct Follow {
@ -180,7 +225,7 @@ pub trait Object: Sized + Debug {
/// } /// }
/// ///
/// #[async_trait::async_trait] /// #[async_trait::async_trait]
/// impl ActivityHandler for Follow { /// impl Activity for Follow {
/// type DataType = DbConnection; /// type DataType = DbConnection;
/// type Error = anyhow::Error; /// type Error = anyhow::Error;
/// ///
@ -206,7 +251,7 @@ pub trait Object: Sized + Debug {
/// ``` /// ```
#[async_trait] #[async_trait]
#[enum_delegate::register] #[enum_delegate::register]
pub trait ActivityHandler { pub trait Activity {
/// App data type passed to handlers. Must be identical to /// App data type passed to handlers. Must be identical to
/// [crate::config::FederationConfigBuilder::app_data] type. /// [crate::config::FederationConfigBuilder::app_data] type.
type DataType: Clone + Send + Sync; type DataType: Clone + Send + Sync;
@ -234,9 +279,6 @@ pub trait ActivityHandler {
/// Trait to allow retrieving common Actor data. /// Trait to allow retrieving common Actor data.
pub trait Actor: Object + Send + 'static { pub trait Actor: Object + Send + 'static {
/// `id` field of the actor
fn id(&self) -> Url;
/// The actor's public key for verifying signatures of incoming activities. /// The actor's public key for verifying signatures of incoming activities.
/// ///
/// Use [generate_actor_keypair](crate::http_signatures::generate_actor_keypair) to create the /// Use [generate_actor_keypair](crate::http_signatures::generate_actor_keypair) to create the
@ -254,7 +296,7 @@ pub trait Actor: Object + Send + 'static {
/// Generates a public key struct for use in the actor json representation /// Generates a public key struct for use in the actor json representation
fn public_key(&self) -> PublicKey { fn public_key(&self) -> PublicKey {
PublicKey::new(self.id(), self.public_key_pem().to_string()) PublicKey::new(self.id().clone(), self.public_key_pem().to_string())
} }
/// The actor's shared inbox, if any /// The actor's shared inbox, if any
@ -270,9 +312,9 @@ pub trait Actor: Object + Send + 'static {
/// Allow for boxing of enum variants /// Allow for boxing of enum variants
#[async_trait] #[async_trait]
impl<T> ActivityHandler for Box<T> impl<T> Activity for Box<T>
where where
T: ActivityHandler + Send + Sync, T: Activity + Send + Sync,
{ {
type DataType = T::DataType; type DataType = T::DataType;
type Error = T::Error; type Error = T::Error;
@ -334,208 +376,3 @@ pub trait Collection: Sized {
data: &Data<Self::DataType>, data: &Data<Self::DataType>,
) -> Result<Self, Self::Error>; ) -> Result<Self, Self::Error>;
} }
/// Some impls of these traits for use in tests. Dont use this from external crates.
///
/// TODO: Should be using `cfg[doctest]` but blocked by <https://github.com/rust-lang/rust/issues/67295>
#[doc(hidden)]
#[allow(clippy::unwrap_used)]
pub mod tests {
use super::{async_trait, ActivityHandler, Actor, Data, Debug, Object, PublicKey, Url};
use crate::{
error::Error,
fetch::object_id::ObjectId,
http_signatures::{generate_actor_keypair, Keypair},
protocol::verification::verify_domains_match,
};
use activitystreams_kinds::{activity::FollowType, actor::PersonType};
use serde::{Deserialize, Serialize};
use std::sync::LazyLock;
#[derive(Clone)]
pub struct DbConnection;
impl DbConnection {
pub async fn read_post_from_json_id<T>(&self, _: Url) -> Result<Option<T>, Error> {
Ok(None)
}
pub async fn read_local_user(&self, _: &str) -> Result<DbUser, Error> {
todo!()
}
pub async fn upsert<T>(&self, _: &T) -> Result<(), Error> {
Ok(())
}
pub async fn add_follower(&self, _: DbUser, _: DbUser) -> Result<(), Error> {
Ok(())
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Person {
#[serde(rename = "type")]
pub kind: PersonType,
pub preferred_username: String,
pub id: ObjectId<DbUser>,
pub inbox: Url,
pub public_key: PublicKey,
}
#[derive(Debug, Clone)]
pub struct DbUser {
pub name: String,
pub federation_id: Url,
pub inbox: Url,
pub public_key: String,
#[allow(dead_code)]
private_key: Option<String>,
pub followers: Vec<Url>,
pub local: bool,
}
pub static DB_USER_KEYPAIR: LazyLock<Keypair> =
LazyLock::new(|| generate_actor_keypair().unwrap());
pub static DB_USER: LazyLock<DbUser> = LazyLock::new(|| DbUser {
name: String::new(),
federation_id: "https://localhost/123".parse().unwrap(),
inbox: "https://localhost/123/inbox".parse().unwrap(),
public_key: DB_USER_KEYPAIR.public_key.clone(),
private_key: Some(DB_USER_KEYPAIR.private_key.clone()),
followers: vec![],
local: false,
});
#[async_trait]
impl Object for DbUser {
type DataType = DbConnection;
type Kind = Person;
type Error = Error;
async fn read_from_id(
_object_id: Url,
_data: &Data<Self::DataType>,
) -> Result<Option<Self>, Self::Error> {
Ok(Some(DB_USER.clone()))
}
async fn into_json(self, _data: &Data<Self::DataType>) -> Result<Self::Kind, Self::Error> {
Ok(Person {
preferred_username: self.name.clone(),
kind: Default::default(),
id: self.federation_id.clone().into(),
inbox: self.inbox.clone(),
public_key: self.public_key(),
})
}
async fn verify(
json: &Self::Kind,
expected_domain: &Url,
_data: &Data<Self::DataType>,
) -> Result<(), Self::Error> {
verify_domains_match(json.id.inner(), expected_domain)?;
Ok(())
}
async fn from_json(
json: Self::Kind,
_data: &Data<Self::DataType>,
) -> Result<Self, Self::Error> {
Ok(DbUser {
name: json.preferred_username,
federation_id: json.id.into(),
inbox: json.inbox,
public_key: json.public_key.public_key_pem,
private_key: None,
followers: vec![],
local: false,
})
}
}
impl Actor for DbUser {
fn id(&self) -> Url {
self.federation_id.clone()
}
fn public_key_pem(&self) -> &str {
&self.public_key
}
fn private_key_pem(&self) -> Option<String> {
self.private_key.clone()
}
fn inbox(&self) -> Url {
self.inbox.clone()
}
}
#[derive(Deserialize, Serialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct Follow {
pub actor: ObjectId<DbUser>,
pub object: ObjectId<DbUser>,
#[serde(rename = "type")]
pub kind: FollowType,
pub id: Url,
}
#[async_trait]
impl ActivityHandler for Follow {
type DataType = DbConnection;
type Error = Error;
fn id(&self) -> &Url {
&self.id
}
fn actor(&self) -> &Url {
self.actor.inner()
}
async fn verify(&self, _: &Data<Self::DataType>) -> Result<(), Self::Error> {
Ok(())
}
async fn receive(self, _data: &Data<Self::DataType>) -> Result<(), Self::Error> {
Ok(())
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Note {}
#[derive(Debug, Clone)]
pub struct DbPost {}
#[async_trait]
impl Object for DbPost {
type DataType = DbConnection;
type Kind = Note;
type Error = Error;
async fn read_from_id(
_: Url,
_: &Data<Self::DataType>,
) -> Result<Option<Self>, Self::Error> {
todo!()
}
async fn into_json(self, _: &Data<Self::DataType>) -> Result<Self::Kind, Self::Error> {
todo!()
}
async fn verify(
_: &Self::Kind,
_: &Url,
_: &Data<Self::DataType>,
) -> Result<(), Self::Error> {
todo!()
}
async fn from_json(_: Self::Kind, _: &Data<Self::DataType>) -> Result<Self, Self::Error> {
todo!()
}
}
}

201
src/traits/tests.rs Normal file
View file

@ -0,0 +1,201 @@
#![doc(hidden)]
#![allow(clippy::unwrap_used)]
//! Some impls of these traits for use in tests. Dont use this from external crates.
//!
//! TODO: Should be using `cfg[doctest]` but blocked by <https://github.com/rust-lang/rust/issues/67295>
use super::{async_trait, Activity, Actor, Data, Debug, Object, PublicKey, Url};
use crate::{
error::Error,
fetch::object_id::ObjectId,
http_signatures::{generate_actor_keypair, Keypair},
protocol::verification::verify_domains_match,
};
use activitystreams_kinds::{activity::FollowType, actor::PersonType};
use serde::{Deserialize, Serialize};
use std::sync::LazyLock;
#[derive(Clone)]
pub struct DbConnection;
impl DbConnection {
pub async fn read_post_from_json_id<T>(&self, _: Url) -> Result<Option<T>, Error> {
Ok(None)
}
pub async fn read_local_user(&self, _: &str) -> Result<DbUser, Error> {
todo!()
}
pub async fn upsert<T>(&self, _: &T) -> Result<(), Error> {
Ok(())
}
pub async fn add_follower(&self, _: DbUser, _: DbUser) -> Result<(), Error> {
Ok(())
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Person {
#[serde(rename = "type")]
pub kind: PersonType,
pub preferred_username: String,
pub id: ObjectId<DbUser>,
pub inbox: Url,
pub public_key: PublicKey,
}
#[derive(Debug, Clone)]
pub struct DbUser {
pub name: String,
pub federation_id: Url,
pub inbox: Url,
pub public_key: String,
#[allow(dead_code)]
private_key: Option<String>,
pub followers: Vec<Url>,
pub local: bool,
}
pub static DB_USER_KEYPAIR: LazyLock<Keypair> = LazyLock::new(|| generate_actor_keypair().unwrap());
pub static DB_USER: LazyLock<DbUser> = LazyLock::new(|| DbUser {
name: String::new(),
federation_id: "https://localhost/123".parse().unwrap(),
inbox: "https://localhost/123/inbox".parse().unwrap(),
public_key: DB_USER_KEYPAIR.public_key.clone(),
private_key: Some(DB_USER_KEYPAIR.private_key.clone()),
followers: vec![],
local: false,
});
#[async_trait]
impl Object for DbUser {
type DataType = DbConnection;
type Kind = Person;
type Error = Error;
fn id(&self) -> Url {
self.federation_id.clone()
}
async fn read_from_id(
_object_id: Url,
_data: &Data<Self::DataType>,
) -> Result<Option<Self>, Self::Error> {
Ok(Some(DB_USER.clone()))
}
async fn into_json(self, _data: &Data<Self::DataType>) -> Result<Self::Kind, Self::Error> {
Ok(Person {
preferred_username: self.name.clone(),
kind: Default::default(),
id: self.federation_id.clone().into(),
inbox: self.inbox.clone(),
public_key: self.public_key(),
})
}
async fn verify(
json: &Self::Kind,
expected_domain: &Url,
_data: &Data<Self::DataType>,
) -> Result<(), Self::Error> {
verify_domains_match(json.id.inner(), expected_domain)?;
Ok(())
}
async fn from_json(
json: Self::Kind,
_data: &Data<Self::DataType>,
) -> Result<Self, Self::Error> {
Ok(DbUser {
name: json.preferred_username,
federation_id: json.id.into(),
inbox: json.inbox,
public_key: json.public_key.public_key_pem,
private_key: None,
followers: vec![],
local: false,
})
}
}
impl Actor for DbUser {
fn public_key_pem(&self) -> &str {
&self.public_key
}
fn private_key_pem(&self) -> Option<String> {
self.private_key.clone()
}
fn inbox(&self) -> Url {
self.inbox.clone()
}
}
#[derive(Deserialize, Serialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct Follow {
pub actor: ObjectId<DbUser>,
pub object: ObjectId<DbUser>,
#[serde(rename = "type")]
pub kind: FollowType,
pub id: Url,
}
#[async_trait]
impl Activity for Follow {
type DataType = DbConnection;
type Error = Error;
fn id(&self) -> &Url {
&self.id
}
fn actor(&self) -> &Url {
self.actor.inner()
}
async fn verify(&self, _: &Data<Self::DataType>) -> Result<(), Self::Error> {
Ok(())
}
async fn receive(self, _data: &Data<Self::DataType>) -> Result<(), Self::Error> {
Ok(())
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Note {}
#[derive(Debug, Clone)]
pub struct DbPost {
pub federation_id: Url,
}
#[async_trait]
impl Object for DbPost {
type DataType = DbConnection;
type Kind = Note;
type Error = Error;
fn id(&self) -> Url {
todo!()
}
async fn read_from_id(_: Url, _: &Data<Self::DataType>) -> Result<Option<Self>, Self::Error> {
todo!()
}
async fn into_json(self, _: &Data<Self::DataType>) -> Result<Self::Kind, Self::Error> {
todo!()
}
async fn verify(_: &Self::Kind, _: &Url, _: &Data<Self::DataType>) -> Result<(), Self::Error> {
todo!()
}
async fn from_json(_: Self::Kind, _: &Data<Self::DataType>) -> Result<Self, Self::Error> {
todo!()
}
}

78
src/utils.rs Normal file
View file

@ -0,0 +1,78 @@
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
use crate::error::Error;
use tokio::net::lookup_host;
use url::{Host, Url};
// TODO: Use is_global() once stabilized
// https://doc.rust-lang.org/std/net/enum.IpAddr.html#method.is_global
pub(crate) async fn validate_ip(url: &Url) -> Result<(), Error> {
let mut ip = vec![];
let host = url
.host()
.ok_or(Error::UrlVerificationError("Url must have a domain"))?;
match host {
Host::Domain(domain) => ip.extend(
lookup_host((domain.to_owned(), 80))
.await?
.map(|s| s.ip().to_canonical()),
),
Host::Ipv4(ipv4) => ip.push(ipv4.into()),
Host::Ipv6(ipv6) => ip.push(ipv6.into()),
};
let invalid_ip = ip.into_iter().any(|addr| match addr {
IpAddr::V4(addr) => v4_is_invalid(addr),
IpAddr::V6(addr) => v6_is_invalid(addr),
});
if invalid_ip {
return Err(Error::DomainResolveError(host.to_string()));
}
Ok(())
}
fn v4_is_invalid(v4: Ipv4Addr) -> bool {
v4.is_private()
|| v4.is_loopback()
|| v4.is_link_local()
|| v4.is_multicast()
|| v4.is_documentation()
|| v4.is_unspecified()
|| v4.is_broadcast()
}
fn v6_is_invalid(v6: Ipv6Addr) -> bool {
v6.is_loopback()
|| v6.is_multicast()
|| v6.is_unique_local()
|| v6.is_unicast_link_local()
|| v6.is_unspecified()
|| v6_is_documentation(v6)
|| v6.to_ipv4_mapped().is_some_and(v4_is_invalid)
}
fn v6_is_documentation(v6: std::net::Ipv6Addr) -> bool {
matches!(
v6.segments(),
[0x2001, 0xdb8, ..] | [0x3fff, 0..=0x0fff, ..]
)
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod test {
use super::*;
#[tokio::test]
async fn test_is_valid_ip() -> Result<(), Error> {
assert!(validate_ip(&Url::parse("http://example.com")?)
.await
.is_ok());
assert!(validate_ip(&Url::parse("http://172.66.147.243")?)
.await
.is_ok());
assert!(validate_ip(&Url::parse("http://localhost")?).await.is_err());
assert!(validate_ip(&Url::parse("http://127.0.0.1")?).await.is_err());
Ok(())
}
}