Compare commits
21 commits
resolve-de
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 754b2a0f3d | |||
|
|
588f431266 | ||
|
|
838dd9e501 | ||
|
|
279d29d350 | ||
|
|
fcb69ebffe | ||
|
|
5e8e918003 | ||
|
|
4ae8532b17 | ||
|
|
f47fe58285 | ||
|
|
f60afae428 | ||
|
|
11f95ff384 | ||
|
|
9d7bd965a4 | ||
|
|
b5dd86ab07 | ||
|
|
a7da04c2d8 | ||
|
|
2acf037d79 | ||
|
|
99505b9567 | ||
|
|
06df2bc1d1 | ||
|
|
8b2b746707 | ||
|
|
545afcc719 | ||
|
|
105d13003a | ||
|
|
ec098cfaed | ||
|
|
1df24ab781 |
27 changed files with 1282 additions and 941 deletions
|
|
@ -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
1821
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
56
Cargo.toml
56
Cargo.toml
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "activitypub_federation"
|
name = "activitypub_federation"
|
||||||
version = "0.7.0-beta.6"
|
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"
|
||||||
|
|
|
||||||
|
|
@ -32,10 +32,10 @@ impl Object for SearchableDbObjects {
|
||||||
type Kind = SearchableObjects;
|
type Kind = SearchableObjects;
|
||||||
type Error = anyhow::Error;
|
type Error = anyhow::Error;
|
||||||
|
|
||||||
fn id(&self) -> &Url {
|
fn id(&self) -> Url {
|
||||||
match self {
|
match self {
|
||||||
SearchableDbObjects::User(p) => &p.federation_id,
|
SearchableDbObjects::User(p) => p.federation_id.clone(),
|
||||||
SearchableDbObjects::Post(n) => &n.federation_id,
|
SearchableDbObjects::Post(n) => n.federation_id.clone(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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));
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -69,8 +69,8 @@ impl Object for DbUser {
|
||||||
type Kind = Person;
|
type Kind = Person;
|
||||||
type Error = Error;
|
type Error = Error;
|
||||||
|
|
||||||
fn id(&self) -> &Url {
|
fn id(&self) -> Url {
|
||||||
self.ap_id.inner()
|
self.ap_id.inner().clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn last_refreshed_at(&self) -> Option<DateTime<Utc>> {
|
fn last_refreshed_at(&self) -> Option<DateTime<Utc>> {
|
||||||
|
|
|
||||||
|
|
@ -50,8 +50,8 @@ impl Object for DbPost {
|
||||||
type Kind = Note;
|
type Kind = Note;
|
||||||
type Error = Error;
|
type Error = Error;
|
||||||
|
|
||||||
fn id(&self) -> &Url {
|
fn id(&self) -> Url {
|
||||||
self.ap_id.inner()
|
self.ap_id.inner().clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn read_from_id(
|
async fn read_from_id(
|
||||||
|
|
|
||||||
|
|
@ -134,8 +134,8 @@ impl Object for DbUser {
|
||||||
type Kind = Person;
|
type Kind = Person;
|
||||||
type Error = Error;
|
type Error = Error;
|
||||||
|
|
||||||
fn id(&self) -> &Url {
|
fn id(&self) -> Url {
|
||||||
self.ap_id.inner()
|
self.ap_id.inner().clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn last_refreshed_at(&self) -> Option<DateTime<Utc>> {
|
fn last_refreshed_at(&self) -> Option<DateTime<Utc>> {
|
||||||
|
|
|
||||||
|
|
@ -47,8 +47,8 @@ impl Object for DbPost {
|
||||||
type Kind = Note;
|
type Kind = Note;
|
||||||
type Error = Error;
|
type Error = Error;
|
||||||
|
|
||||||
fn id(&self) -> &Url {
|
fn id(&self) -> Url {
|
||||||
self.ap_id.inner()
|
self.ap_id.inner().clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn read_from_id(
|
async fn read_from_id(
|
||||||
|
|
|
||||||
|
|
@ -33,10 +33,10 @@ use url::Url;
|
||||||
///
|
///
|
||||||
/// - `activity`: The activity to be sent, gets converted to json
|
/// - `activity`: The activity to be sent, gets converted to json
|
||||||
/// - `private_key`: Private key belonging to the actor who sends the activity, for signing HTTP
|
/// - `private_key`: Private key belonging to the actor who sends the activity, for signing HTTP
|
||||||
/// signature. Generated with [crate::http_signatures::generate_actor_keypair].
|
/// signature. Generated with [crate::http_signatures::generate_actor_keypair].
|
||||||
/// - `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<A, Datatype, ActorType>(
|
pub async fn queue_activity<A, Datatype, ActorType>(
|
||||||
activity: &A,
|
activity: &A,
|
||||||
actor: &ActorType,
|
actor: &ActorType,
|
||||||
|
|
|
||||||
|
|
@ -52,8 +52,8 @@ impl SendActivityTask {
|
||||||
///
|
///
|
||||||
/// - `activity`: The activity to be sent, gets converted to json
|
/// - `activity`: The activity to be sent, gets converted to json
|
||||||
/// - `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<A, Datatype, ActorType>(
|
pub async fn prepare<A, Datatype, ActorType>(
|
||||||
activity: &A,
|
activity: &A,
|
||||||
actor: &ActorType,
|
actor: &ActorType,
|
||||||
|
|
@ -190,7 +190,7 @@ where
|
||||||
// PKey is internally like an Arc<>, so cloning is ok
|
// PKey is internally like an Arc<>, so cloning is ok
|
||||||
data.config
|
data.config
|
||||||
.actor_pkey_cache
|
.actor_pkey_cache
|
||||||
.try_get_with_by_ref(actor_id, async {
|
.try_get_with_by_ref(&actor_id, async {
|
||||||
let private_key_pem = actor.private_key_pem().ok_or_else(|| {
|
let private_key_pem = actor.private_key_pem().ok_or_else(|| {
|
||||||
Error::Other(format!(
|
Error::Other(format!(
|
||||||
"Actor {actor_id} does not contain a private key for signing"
|
"Actor {actor_id} does not contain a private key for signing"
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ pub async fn receive_activity<A, ActorT, Datatype>(
|
||||||
) -> Result<HttpResponse, <A as Activity>::Error>
|
) -> Result<HttpResponse, <A as Activity>::Error>
|
||||||
where
|
where
|
||||||
A: Activity<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>,
|
||||||
<A as Activity>::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>,
|
||||||
|
|
@ -62,7 +62,7 @@ pub async fn receive_activity_with_hook<A, ActorT, Datatype>(
|
||||||
) -> Result<HttpResponse, <A as Activity>::Error>
|
) -> Result<HttpResponse, <A as Activity>::Error>
|
||||||
where
|
where
|
||||||
A: Activity<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>,
|
||||||
<A as Activity>::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>,
|
||||||
|
|
@ -82,7 +82,7 @@ async fn do_stuff<A, ActorT, Datatype>(
|
||||||
) -> Result<(A, ActorT), <A as Activity>::Error>
|
) -> Result<(A, ActorT), <A as Activity>::Error>
|
||||||
where
|
where
|
||||||
A: Activity<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>,
|
||||||
<A as Activity>::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>,
|
||||||
|
|
|
||||||
|
|
@ -23,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>,
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ pub async fn receive_activity<A, ActorT, Datatype>(
|
||||||
) -> Result<(), <A as Activity>::Error>
|
) -> Result<(), <A as Activity>::Error>
|
||||||
where
|
where
|
||||||
A: Activity<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>,
|
||||||
<A as Activity>::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>,
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ use crate::{
|
||||||
http_signatures::sign_request,
|
http_signatures::sign_request,
|
||||||
protocol::verification::verify_domains_match,
|
protocol::verification::verify_domains_match,
|
||||||
traits::{Activity, 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
|
||||||
|
|
@ -183,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",
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -400,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> {
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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());
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>,
|
||||||
|
|
|
||||||
|
|
@ -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)>,
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ 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,
|
||||||
|
|
@ -46,7 +47,7 @@ async fn parse_received_activity<A, ActorT, Datatype>(
|
||||||
) -> Result<(A, ActorT), <A as Activity>::Error>
|
) -> Result<(A, ActorT), <A as Activity>::Error>
|
||||||
where
|
where
|
||||||
A: Activity<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>,
|
||||||
<A as Activity>::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>,
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
@ -127,17 +151,24 @@ where
|
||||||
enum MaybeArray<T> {
|
enum MaybeArray<T> {
|
||||||
Simple(T),
|
Simple(T),
|
||||||
Array(Vec<T>),
|
Array(Vec<T>),
|
||||||
|
None,
|
||||||
}
|
}
|
||||||
|
|
||||||
let result: MaybeArray<T> = Deserialize::deserialize(deserializer)?;
|
let result = Deserialize::deserialize(deserializer)?;
|
||||||
Ok(match result {
|
Ok(match result {
|
||||||
MaybeArray::Simple(value) => Some(value),
|
MaybeArray::Simple(value) => Some(value),
|
||||||
MaybeArray::Array(value) => value.into_iter().last(),
|
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;
|
||||||
|
|
@ -153,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(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
@ -30,7 +30,7 @@ where
|
||||||
type Error = E;
|
type Error = E;
|
||||||
|
|
||||||
/// `id` field of the object
|
/// `id` field of the object
|
||||||
fn id(&self) -> &Url {
|
fn id(&self) -> Url {
|
||||||
match self {
|
match self {
|
||||||
Either::Left(l) => l.id(),
|
Either::Left(l) => l.id(),
|
||||||
Either::Right(r) => r.id(),
|
Either::Right(r) => r.id(),
|
||||||
|
|
@ -59,7 +59,7 @@ 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,
|
||||||
|
|
@ -103,8 +103,8 @@ 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,
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,7 @@ pub mod tests;
|
||||||
/// type Kind = Note;
|
/// type Kind = Note;
|
||||||
/// type Error = anyhow::Error;
|
/// type Error = anyhow::Error;
|
||||||
///
|
///
|
||||||
/// fn id(&self) -> &Url { self.ap_id.inner() }
|
/// 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.
|
||||||
|
|
@ -110,7 +110,7 @@ pub trait Object: Sized + Debug {
|
||||||
type Error;
|
type Error;
|
||||||
|
|
||||||
/// `id` field of the object
|
/// `id` field of the object
|
||||||
fn id(&self) -> &Url;
|
fn id(&self) -> Url;
|
||||||
|
|
||||||
/// Returns the last time this object was updated.
|
/// Returns the last time this object was updated.
|
||||||
///
|
///
|
||||||
|
|
@ -136,7 +136,7 @@ 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(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -194,8 +194,8 @@ pub trait Object: Sized + Debug {
|
||||||
redirect_remote_object,
|
redirect_remote_object,
|
||||||
};
|
};
|
||||||
let id = self.id();
|
let id = self.id();
|
||||||
let res = if !data.config.is_local_url(id) {
|
let res = if !data.config.is_local_url(&id) {
|
||||||
redirect_remote_object(id)
|
redirect_remote_object(&id)
|
||||||
} else if !self.is_deleted() {
|
} else if !self.is_deleted() {
|
||||||
let json = self.into_json(data).await?;
|
let json = self.into_json(data).await?;
|
||||||
create_http_response(json, federation_context)?
|
create_http_response(json, federation_context)?
|
||||||
|
|
|
||||||
|
|
@ -73,8 +73,8 @@ impl Object for DbUser {
|
||||||
type Kind = Person;
|
type Kind = Person;
|
||||||
type Error = Error;
|
type Error = Error;
|
||||||
|
|
||||||
fn id(&self) -> &Url {
|
fn id(&self) -> Url {
|
||||||
&self.federation_id
|
self.federation_id.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn read_from_id(
|
async fn read_from_id(
|
||||||
|
|
@ -179,7 +179,7 @@ impl Object for DbPost {
|
||||||
type Kind = Note;
|
type Kind = Note;
|
||||||
type Error = Error;
|
type Error = Error;
|
||||||
|
|
||||||
fn id(&self) -> &Url {
|
fn id(&self) -> Url {
|
||||||
todo!()
|
todo!()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
78
src/utils.rs
Normal file
78
src/utils.rs
Normal 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(())
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue