Compare commits

...

6 commits

Author SHA1 Message Date
Felix Ableitner
c5cec6cef6 simplify 2024-09-13 11:01:30 +02:00
Felix Ableitner
d066cc9e13 more fixes 2024-09-12 11:03:19 +02:00
Felix Ableitner
b6e461a51b fix 2024-09-12 10:10:18 +02:00
Felix Ableitner
bff0567c27 stuff 2024-09-11 15:46:19 +02:00
Felix Ableitner
d0f37c699f fmt 2024-09-11 15:08:47 +02:00
Felix Ableitner
62b7543299 Add wrapper type for url (fixes #58) 2024-09-11 15:01:14 +02:00
32 changed files with 183 additions and 99 deletions

View file

@ -40,7 +40,7 @@ Based on this we can define the following minimal struct to (de)serialize a `Per
# use activitypub_federation::fetch::object_id::ObjectId;
# use serde::{Deserialize, Serialize};
# use activitystreams_kinds::actor::PersonType;
# use url::Url;
# use activitypub_federation::url::Url;
# use activitypub_federation::traits::tests::DbUser;
#[derive(Deserialize, Serialize)]
@ -64,7 +64,7 @@ pub struct Person {
Besides we also need a second struct to represent the data which gets stored in our local database (for example PostgreSQL). This is necessary because the data format used by SQL is very different from that used by that from Activitypub. It is organized by an integer primary key instead of a link id. Nested structs are complicated to represent and easier if flattened. Some fields like `type` don't need to be stored at all. On the other hand, the database contains fields which can't be federated, such as the private key and a boolean indicating if the item is local or remote.
```rust
# use url::Url;
# use activitypub_federation::url::Url;
# use chrono::{DateTime, Utc};
pub struct DbUser {

View file

@ -4,7 +4,7 @@ Activitypub propagates actions across servers using `Activities`. For this each
```
# use serde::{Deserialize, Serialize};
# use url::Url;
# use activitypub_federation::url::Url;
# use anyhow::Error;
# use async_trait::async_trait;
# use activitypub_federation::fetch::object_id::ObjectId;
@ -62,7 +62,7 @@ Next its time to setup the actual HTTP handler for the inbox. For this we first
# use activitypub_federation::traits::ActivityHandler;
# use activitypub_federation::traits::tests::{DbConnection, DbUser, Follow};
# use serde::{Deserialize, Serialize};
# use url::Url;
# use activitypub_federation::url::Url;
#[derive(Deserialize, Serialize, Debug)]
#[serde(untagged)]

View file

@ -22,7 +22,7 @@ let activity = Follow {
actor: ObjectId::parse("https://lemmy.ml/u/nutomic")?,
object: recipient.federation_id.clone().into(),
kind: Default::default(),
id: "https://lemmy.ml/activities/321".try_into()?
id: "https://lemmy.ml/activities/321".parse()?
};
let inboxes = vec![recipient.shared_inbox_or_inbox()];
@ -62,7 +62,7 @@ let activity = Follow {
actor: ObjectId::parse("https://lemmy.ml/u/nutomic")?,
object: recipient.federation_id.clone().into(),
kind: Default::default(),
id: "https://lemmy.ml/activities/321".try_into()?
id: "https://lemmy.ml/activities/321".parse()?
};
let inboxes = vec![recipient.shared_inbox_or_inbox()];

View file

@ -10,7 +10,7 @@ It is sometimes necessary to fetch from a URL, but we don't know the exact type
# use serde::{Deserialize, Serialize};
# use activitypub_federation::traits::tests::DbConnection;
# use activitypub_federation::config::Data;
# use url::Url;
# use activitypub_federation::url::Url;
# use activitypub_federation::traits::tests::{Person, Note};
#[derive(Debug)]

View file

@ -12,9 +12,9 @@ use activitypub_federation::{
kinds::activity::CreateType,
protocol::{context::WithContext, helpers::deserialize_one_or_many},
traits::{ActivityHandler, Object},
url::Url,
};
use serde::{Deserialize, Serialize};
use url::Url;
#[derive(Deserialize, Serialize, Debug)]
#[serde(rename_all = "camelCase")]

View file

@ -6,11 +6,11 @@ use activitypub_federation::{
kinds::actor::PersonType,
protocol::{public_key::PublicKey, verification::verify_domains_match},
traits::{ActivityHandler, Actor, Object},
url::Url,
};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::fmt::Debug;
use url::Url;
use std::{fmt::Debug, str::FromStr};
#[derive(Debug, Clone)]
pub struct DbUser {
@ -36,8 +36,8 @@ pub enum PersonAcceptedActivities {
impl DbUser {
pub fn new(hostname: &str, name: &str) -> Result<DbUser, Error> {
let ap_id = Url::parse(&format!("https://{}/{}", hostname, &name))?.into();
let inbox = Url::parse(&format!("https://{}/{}/inbox", hostname, &name))?;
let ap_id = Url::from_str(&format!("https://{}/{}", hostname, &name))?.into();
let inbox = Url::from_str(&format!("https://{}/{}/inbox", hostname, &name))?;
let keypair = generate_actor_keypair()?;
Ok(DbUser {
name: name.to_string(),

View file

@ -11,10 +11,10 @@ use activitypub_federation::{
kinds::{object::NoteType, public},
protocol::{helpers::deserialize_one_or_many, verification::verify_domains_match},
traits::{Actor, Object},
url::Url,
};
use activitystreams_kinds::link::MentionType;
use serde::{Deserialize, Serialize};
use url::Url;
#[derive(Clone, Debug)]
pub struct DbPost {
@ -63,7 +63,7 @@ impl Object for DbPost {
id: self.ap_id,
content: self.text,
attributed_to: self.creator,
to: vec![public()],
to: vec![public().try_into()?],
tag: vec![],
in_reply_to: None,
})
@ -98,7 +98,7 @@ impl Object for DbPost {
kind: Default::default(),
id: generate_object_id(data.domain())?.into(),
attributed_to: data.local_user().ap_id,
to: vec![public()],
to: vec![public().try_into()?],
content: format!("Hello {}", creator.name),
in_reply_to: Some(json.id.clone()),
tag: vec![mention],

View file

@ -1,5 +1,8 @@
use std::str::FromStr;
use activitypub_federation::url::Url;
use rand::{distributions::Alphanumeric, thread_rng, Rng};
use url::{ParseError, Url};
use url::ParseError;
/// Just generate random url as object id. In a real project, you probably want to use
/// an url which contains the database id for easy retrieval (or store the random id in db).
@ -9,5 +12,5 @@ pub fn generate_object_id(domain: &str) -> Result<Url, ParseError> {
.take(7)
.map(char::from)
.collect();
Url::parse(&format!("https://{}/objects/{}", domain, id))
Url::from_str(&format!("https://{}/objects/{}", domain, id))
}

View file

@ -4,9 +4,9 @@ use activitypub_federation::{
fetch::object_id::ObjectId,
kinds::activity::AcceptType,
traits::ActivityHandler,
url::Url,
};
use serde::{Deserialize, Serialize};
use url::Url;
#[derive(Deserialize, Serialize, Debug)]
#[serde(rename_all = "camelCase")]

View file

@ -9,9 +9,9 @@ use activitypub_federation::{
kinds::activity::CreateType,
protocol::helpers::deserialize_one_or_many,
traits::{ActivityHandler, Object},
url::Url,
};
use serde::{Deserialize, Serialize};
use url::Url;
#[derive(Deserialize, Serialize, Debug)]
#[serde(rename_all = "camelCase")]

View file

@ -9,9 +9,9 @@ use activitypub_federation::{
fetch::object_id::ObjectId,
kinds::activity::FollowType,
traits::{ActivityHandler, Actor},
url::Url,
};
use serde::{Deserialize, Serialize};
use url::Url;
#[derive(Deserialize, Serialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]

View file

@ -2,21 +2,23 @@ use crate::{
objects::{person::DbUser, post::DbPost},
Error,
};
use activitypub_federation::config::{FederationConfig, UrlVerifier};
use activitypub_federation::{
config::{FederationConfig, UrlVerifier},
url::Url,
};
use anyhow::anyhow;
use async_trait::async_trait;
use std::{
str::FromStr,
sync::{Arc, Mutex},
};
use url::Url;
pub async fn new_instance(
hostname: &str,
name: String,
) -> Result<FederationConfig<DatabaseHandle>, Error> {
let mut system_user = DbUser::new(hostname, "system".into())?;
system_user.ap_id = Url::parse(&format!("http://{}/", hostname))?.into();
system_user.ap_id = Url::from_str(&format!("http://{}/", hostname))?.into();
let local_user = DbUser::new(hostname, name)?;
let database = Arc::new(Database {
@ -51,7 +53,7 @@ struct MyUrlVerifier();
#[async_trait]
impl UrlVerifier for MyUrlVerifier {
async fn verify(&self, url: &Url) -> Result<(), activitypub_federation::error::Error> {
if url.domain() == Some("malicious.com") {
if url.domain() == "malicious.com" {
Err(activitypub_federation::error::Error::Other(
"malicious domain".into(),
))

View file

@ -14,11 +14,11 @@ use activitypub_federation::{
kinds::actor::PersonType,
protocol::{context::WithContext, public_key::PublicKey, verification::verify_domains_match},
traits::{ActivityHandler, Actor, Object},
url::Url,
};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::fmt::Debug;
use url::Url;
use std::{fmt::Debug, str::FromStr};
#[derive(Debug, Clone)]
pub struct DbUser {
@ -46,8 +46,8 @@ pub enum PersonAcceptedActivities {
impl DbUser {
pub fn new(hostname: &str, name: String) -> Result<DbUser, Error> {
let ap_id = Url::parse(&format!("http://{}/{}", hostname, &name))?.into();
let inbox = Url::parse(&format!("http://{}/{}/inbox", hostname, &name))?;
let ap_id = Url::from_str(&format!("http://{}/{}", hostname, &name))?.into();
let inbox = Url::from_str(&format!("http://{}/{}/inbox", hostname, &name))?;
let keypair = generate_actor_keypair()?;
Ok(DbUser {
name,
@ -79,7 +79,7 @@ impl DbUser {
}
pub fn followers_url(&self) -> Result<Url, Error> {
Ok(Url::parse(&format!("{}/followers", self.ap_id.inner()))?)
Ok(Url::from_str(&format!("{}/followers", self.ap_id.inner()))?)
}
pub async fn follow(&self, other: &str, data: &Data<DatabaseHandle>) -> Result<(), Error> {

View file

@ -5,9 +5,9 @@ use activitypub_federation::{
kinds::{object::NoteType, public},
protocol::{helpers::deserialize_one_or_many, verification::verify_domains_match},
traits::Object,
url::Url,
};
use serde::{Deserialize, Serialize};
use url::Url;
#[derive(Clone, Debug)]
pub struct DbPost {
@ -19,7 +19,7 @@ pub struct DbPost {
impl DbPost {
pub fn new(text: String, creator: ObjectId<DbUser>) -> Result<DbPost, Error> {
let ap_id = generate_object_id(creator.inner().domain().unwrap())?.into();
let ap_id = generate_object_id(creator.inner().domain())?.into();
Ok(DbPost {
text,
ap_id,
@ -65,7 +65,7 @@ impl Object for DbPost {
kind: Default::default(),
id: self.ap_id,
attributed_to: self.creator,
to: vec![public(), creator.followers_url()?],
to: vec![public().try_into()?, creator.followers_url()?],
content: self.text,
})
}

View file

@ -1,5 +1,8 @@
use std::str::FromStr;
use activitypub_federation::url::Url;
use rand::{distributions::Alphanumeric, thread_rng, Rng};
use url::{ParseError, Url};
use url::ParseError;
/// Just generate random url as object id. In a real project, you probably want to use
/// an url which contains the database id for easy retrieval (or store the random id in db).
@ -9,5 +12,5 @@ pub fn generate_object_id(domain: &str) -> Result<Url, ParseError> {
.take(7)
.map(char::from)
.collect();
Url::parse(&format!("http://{}/objects/{}", domain, id))
Url::from_str(&format!("http://{}/objects/{}", domain, id))
}

View file

@ -11,6 +11,7 @@ use crate::{
use futures_core::Future;
use crate::url::Url;
use reqwest_middleware::ClientWithMiddleware;
use serde::Serialize;
use std::{
@ -26,7 +27,6 @@ use tokio::{
task::{JoinHandle, JoinSet},
};
use tracing::{info, warn};
use url::Url;
/// Send a new activity to the given inboxes with automatic retry on failure. Alternatively you
/// can implement your own queue and then send activities using [[crate::activity_sending::SendActivityTask]].

View file

@ -8,6 +8,7 @@ use crate::{
http_signatures::sign_request,
reqwest_shim::ResponseExt,
traits::{ActivityHandler, Actor},
url::Url,
FEDERATION_CONTENT_TYPE,
};
use bytes::Bytes;
@ -27,7 +28,6 @@ use std::{
time::{Duration, SystemTime},
};
use tracing::debug;
use url::Url;
#[derive(Clone, Debug)]
/// All info needed to sign and send one activity to one inbox. You should generally use
@ -202,7 +202,7 @@ where
}
pub(crate) fn generate_request_headers(inbox_url: &Url) -> HeaderMap {
let mut host = inbox_url.domain().expect("read inbox domain").to_string();
let mut host = inbox_url.domain().to_string();
if let Some(port) = inbox_url.port() {
host = format!("{}:{}", host, port);
}

View file

@ -47,6 +47,8 @@ where
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod test {
use std::str::FromStr;
use super::*;
use crate::{
activity_sending::generate_request_headers,
@ -54,12 +56,12 @@ mod test {
fetch::object_id::ObjectId,
http_signatures::sign_request,
traits::tests::{DbConnection, DbUser, Follow, DB_USER_KEYPAIR},
url::Url,
};
use actix_web::test::TestRequest;
use reqwest::Client;
use reqwest_middleware::ClientWithMiddleware;
use serde_json::json;
use url::Url;
#[tokio::test]
async fn test_receive_activity() {
@ -108,7 +110,7 @@ mod test {
async fn test_receive_unparseable_activity() {
let (_, _, config) = setup_receive_test().await;
let actor = Url::parse("http://ds9.lemmy.ml/u/lemmy_alpha").unwrap();
let actor = Url::from_str("http://ds9.lemmy.ml/u/lemmy_alpha").unwrap();
let id = "http://localhost:123/1";
let activity = json!({
"actor": actor.as_str(),
@ -140,7 +142,7 @@ mod test {
async fn construct_request(body: &Bytes, actor: &Url) -> TestRequest {
let inbox = "https://example.com/inbox";
let headers = generate_request_headers(&Url::parse(inbox).unwrap());
let headers = generate_request_headers(&Url::from_str(inbox).unwrap());
let request_builder = ClientWithMiddleware::from(Client::default())
.post(inbox)
.headers(headers);
@ -165,7 +167,7 @@ mod test {
actor: ObjectId::parse("http://localhost:123").unwrap(),
object: ObjectId::parse("http://localhost:124").unwrap(),
kind: Default::default(),
id: "http://localhost:123/1".try_into().unwrap(),
id: "http://localhost:123/1".parse().unwrap(),
};
let body: Bytes = serde_json::to_vec(&activity).unwrap().into();
let incoming_request = construct_request(&body, activity.actor.inner()).await;

View file

@ -19,6 +19,7 @@ use crate::{
error::Error,
protocol::verification::verify_domains_match,
traits::{ActivityHandler, Actor},
url::Url,
};
use async_trait::async_trait;
use derive_builder::Builder;
@ -35,7 +36,6 @@ use std::{
},
time::Duration,
};
use url::Url;
/// Configuration for this library, with various federation related settings
#[derive(Builder, Clone)]
@ -156,11 +156,7 @@ impl<T: Clone> FederationConfig<T> {
return Ok(());
}
if url.domain().is_none() {
return Err(Error::UrlVerificationError("Url must have a domain"));
}
if url.domain() == Some("localhost") && !self.debug {
if url.domain() == "localhost" && !self.debug {
return Err(Error::UrlVerificationError(
"Localhost is only allowed in debug mode",
));
@ -247,7 +243,7 @@ impl<T: Clone> Deref for FederationConfig<T> {
///
/// ```
/// # use async_trait::async_trait;
/// # use url::Url;
/// # use activitypub_federation::url::Url;
/// # use activitypub_federation::config::UrlVerifier;
/// # use activitypub_federation::error::Error;
/// # #[derive(Clone)]
@ -264,7 +260,7 @@ impl<T: Clone> Deref for FederationConfig<T> {
/// impl UrlVerifier for Verifier {
/// async fn verify(&self, url: &Url) -> Result<(), Error> {
/// let blocklist = get_blocklist(&self.db_connection).await;
/// let domain = url.domain().unwrap().to_string();
/// let domain = url.domain().to_string();
/// if blocklist.contains(&domain) {
/// Err(Error::Other("Domain is blocked".into()))
/// } else {
@ -351,6 +347,8 @@ impl<T: Clone> FederationMiddleware<T> {
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod test {
use std::str::FromStr;
use super::*;
async fn config() -> FederationConfig<i32> {
@ -365,10 +363,8 @@ mod test {
#[tokio::test]
async fn test_url_is_local() -> Result<(), Error> {
let config = config().await;
assert!(config.is_local_url(&Url::parse("http://example.com")?));
assert!(!config.is_local_url(&Url::parse("http://other.com")?));
// ensure that missing domain doesnt cause crash
assert!(!config.is_local_url(&Url::parse("http://127.0.0.1")?));
assert!(config.is_local_url(&Url::from_str("http://example.com")?));
assert!(!config.is_local_url(&Url::from_str("http://other.com")?));
Ok(())
}

View file

@ -1,6 +1,6 @@
//! Error messages returned by this library
use crate::fetch::webfinger::WebFingerError;
use crate::{fetch::webfinger::WebFingerError, url::Url};
use http_signature_normalization_reqwest::SignError;
use rsa::{
errors::Error as RsaError,
@ -8,7 +8,6 @@ use rsa::{
};
use std::string::FromUtf8Error;
use tokio::task::JoinError;
use url::Url;
/// Error messages returned by this library
#[derive(thiserror::Error, Debug)]

View file

@ -1,10 +1,10 @@
use crate::{config::Data, error::Error, fetch::fetch_object_http, traits::Collection};
use crate::{config::Data, error::Error, fetch::fetch_object_http, traits::Collection, url::Url};
use serde::{Deserialize, Serialize};
use std::{
fmt::{Debug, Display, Formatter},
marker::PhantomData,
str::FromStr,
};
use url::Url;
/// Typed wrapper for Activitypub Collection ID which helps with dereferencing.
#[derive(Serialize, Deserialize)]
@ -21,7 +21,7 @@ where
{
/// Construct a new CollectionId instance
pub fn parse(url: &str) -> Result<Self, url::ParseError> {
Ok(Self(Box::new(Url::parse(url)?), PhantomData::<Kind>))
Ok(Self(Box::new(Url::from_str(url)?), PhantomData::<Kind>))
}
/// Fetches collection over HTTP

View file

@ -8,6 +8,7 @@ use crate::{
extract_id,
http_signatures::sign_request,
reqwest_shim::ResponseExt,
url::Url,
FEDERATION_CONTENT_TYPE,
};
use bytes::Bytes;
@ -15,7 +16,6 @@ use http::{HeaderValue, StatusCode};
use serde::de::DeserializeOwned;
use std::sync::atomic::Ordering;
use tracing::info;
use url::Url;
/// Typed wrapper for collection IDs
pub mod collection_id;
@ -135,13 +135,13 @@ async fn fetch_object_http_with_accept<T: Clone, Kind: DeserializeOwned>(
match serde_json::from_slice(&text) {
Ok(object) => Ok(FetchObjectResponse {
object,
url,
url: url.try_into()?,
content_type,
object_id,
}),
Err(e) => Err(ParseFetchedObject(
e,
url,
url.try_into()?,
String::from_utf8(Vec::from(text))?,
)),
}

View file

@ -1,4 +1,4 @@
use crate::{config::Data, error::Error, fetch::fetch_object_http, traits::Object};
use crate::{config::Data, error::Error, fetch::fetch_object_http, traits::Object, url::Url};
use chrono::{DateTime, Duration as ChronoDuration, Utc};
use serde::{Deserialize, Serialize};
use std::{
@ -6,7 +6,6 @@ use std::{
marker::PhantomData,
str::FromStr,
};
use url::Url;
impl<T> FromStr for ObjectId<T>
where
@ -66,7 +65,7 @@ where
{
/// Construct a new objectid instance
pub fn parse(url: &str) -> Result<Self, url::ParseError> {
Ok(Self(Box::new(Url::parse(url)?), PhantomData::<Kind>))
Ok(Self(Box::new(Url::from_str(url)?), PhantomData::<Kind>))
}
/// Returns a reference to the wrapped URL value

View file

@ -3,6 +3,7 @@ use crate::{
error::Error,
fetch::{fetch_object_http_with_accept, object_id::ObjectId},
traits::{Actor, Object},
url::Url,
FEDERATION_CONTENT_TYPE,
};
use http::HeaderValue;
@ -10,9 +11,8 @@ use itertools::Itertools;
use once_cell::sync::Lazy;
use regex::Regex;
use serde::{Deserialize, Serialize};
use std::{collections::HashMap, fmt::Display};
use std::{collections::HashMap, fmt::Display, str::FromStr};
use tracing::debug;
use url::Url;
/// Errors relative to webfinger handling
#[derive(thiserror::Error, Debug)]
@ -60,7 +60,7 @@ where
debug!("Fetching webfinger url: {}", &fetch_url);
let res: Webfinger = fetch_object_http_with_accept(
&Url::parse(&fetch_url).map_err(Error::UrlParse)?,
&Url::from_str(&fetch_url).map_err(Error::UrlParse)?,
data,
&WEBFINGER_CONTENT_TYPE,
)
@ -143,10 +143,10 @@ where
/// of discovery.
///
/// ```
/// # use url::Url;
/// # use activitypub_federation::url::Url;
/// # use activitypub_federation::fetch::webfinger::build_webfinger_response;
/// let subject = "acct:nutomic@lemmy.ml".to_string();
/// let url = Url::parse("https://lemmy.ml/u/nutomic")?;
/// let url = "https://lemmy.ml/u/nutomic".parse()?;
/// build_webfinger_response(subject, url);
/// # Ok::<(), anyhow::Error>(())
/// ```
@ -162,11 +162,11 @@ pub fn build_webfinger_response(subject: String, url: Url) -> Webfinger {
/// will be empty.
///
/// ```
/// # use url::Url;
/// # use activitypub_federation::url::Url;
/// # use activitypub_federation::fetch::webfinger::build_webfinger_response_with_type;
/// let subject = "acct:nutomic@lemmy.ml".to_string();
/// let user = Url::parse("https://lemmy.ml/u/nutomic")?;
/// let group = Url::parse("https://lemmy.ml/c/asklemmy")?;
/// let user = "https://lemmy.ml/u/nutomic".parse()?;
/// let group = "https://lemmy.ml/c/asklemmy".parse()?;
/// build_webfinger_response_with_type(subject, vec![
/// (user, Some("Person")),
/// (group, Some("Group"))]);

View file

@ -11,6 +11,7 @@ use crate::{
fetch::object_id::ObjectId,
protocol::public_key::main_key_id,
traits::{Actor, Object},
url::Url,
};
use base64::{engine::general_purpose::STANDARD as Base64, Engine};
use bytes::Bytes;
@ -30,9 +31,8 @@ use rsa::{
};
use serde::Deserialize;
use sha2::{Digest, Sha256};
use std::{collections::BTreeMap, fmt::Debug, time::Duration};
use std::{collections::BTreeMap, fmt::Debug, str::FromStr, time::Duration};
use tracing::debug;
use url::Url;
/// A private/public key pair used for HTTP signatures
#[derive(Debug, Clone)]
@ -166,7 +166,7 @@ where
None => return Err(Error::ActivitySignatureInvalid.into()),
Some(caps) => caps.get(1).expect("regex error").as_str(),
};
let actor_url = Url::parse(actor_id).map_err(|_| Error::ActivitySignatureInvalid)?;
let actor_url = Url::from_str(actor_id).map_err(|_| Error::ActivitySignatureInvalid)?;
let actor_id: ObjectId<A> = actor_url.into();
let actor = actor_id.dereference(data).await?;
@ -287,9 +287,10 @@ pub mod test {
use rsa::{pkcs1::DecodeRsaPrivateKey, pkcs8::DecodePrivateKey};
use std::str::FromStr;
static ACTOR_ID: Lazy<Url> = Lazy::new(|| Url::parse("https://example.com/u/alice").unwrap());
static ACTOR_ID: Lazy<Url> =
Lazy::new(|| Url::from_str("https://example.com/u/alice").unwrap());
static INBOX_URL: Lazy<Url> =
Lazy::new(|| Url::parse("https://example.com/u/alice/inbox").unwrap());
Lazy::new(|| Url::from_str("https://example.com/u/alice/inbox").unwrap());
#[tokio::test]
async fn test_sign() {

View file

@ -23,6 +23,7 @@ pub mod http_signatures;
pub mod protocol;
pub(crate) mod reqwest_shim;
pub mod traits;
pub mod url;
use crate::{
config::Data,
@ -32,8 +33,8 @@ use crate::{
};
pub use activitystreams_kinds as kinds;
use crate::url::Url;
use serde::{de::DeserializeOwned, Deserialize};
use url::Url;
/// Mime type for Activitypub data, used for `Accept` and `Content-Type` HTTP headers
pub const FEDERATION_CONTENT_TYPE: &str = "application/activity+json";

View file

@ -19,10 +19,9 @@
//! Ok::<(), serde_json::error::Error>(())
//! ```
use crate::{config::Data, traits::ActivityHandler};
use crate::{config::Data, traits::ActivityHandler, url::Url};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use url::Url;
/// Default context used in Activitypub
const DEFAULT_CONTEXT: &str = "https://www.w3.org/ns/activitystreams";

View file

@ -9,7 +9,7 @@ use serde::{Deserialize, Deserializer};
///
/// ```
/// # use activitypub_federation::protocol::helpers::deserialize_one_or_many;
/// # use url::Url;
/// # use activitypub_federation::url::Url;
/// #[derive(serde::Deserialize)]
/// struct Note {
/// #[serde(deserialize_with = "deserialize_one_or_many")]
@ -52,7 +52,7 @@ where
///
/// ```
/// # use activitypub_federation::protocol::helpers::deserialize_one;
/// # use url::Url;
/// # use activitypub_federation::url::Url;
/// #[derive(serde::Deserialize)]
/// struct Note {
/// #[serde(deserialize_with = "deserialize_one")]
@ -88,7 +88,7 @@ where
///
/// ```
/// # use activitypub_federation::protocol::helpers::deserialize_skip_error;
/// # use url::Url;
/// # use activitypub_federation::url::Url;
/// #[derive(serde::Deserialize)]
/// struct Note {
/// content: String,
@ -120,8 +120,7 @@ where
mod tests {
#[test]
fn deserialize_one_multiple_values() {
use crate::protocol::helpers::deserialize_one;
use url::Url;
use crate::{protocol::helpers::deserialize_one, url::Url};
#[derive(serde::Deserialize)]
struct Note {
#[serde(deserialize_with = "deserialize_one")]

View file

@ -1,7 +1,7 @@
//! Struct which is used to federate actor key for HTTP signatures
use crate::url::Url;
use serde::{Deserialize, Serialize};
use url::Url;
/// Public key of actors which is used for HTTP signatures.
///

View file

@ -1,15 +1,14 @@
//! Verify that received data is valid
use crate::error::Error;
use url::Url;
use crate::{error::Error, url::Url};
/// Check that both urls have the same domain. If not, return UrlVerificationError.
///
/// ```
/// # use url::Url;
/// # use activitypub_federation::url::Url;
/// # use activitypub_federation::protocol::verification::verify_domains_match;
/// let a = Url::parse("https://example.com/abc")?;
/// let b = Url::parse("https://sample.net/abc")?;
/// let a = "https://example.com/abc".parse()?;
/// let b = "https://sample.net/abc".parse()?;
/// assert!(verify_domains_match(&a, &b).is_err());
/// # Ok::<(), url::ParseError>(())
/// ```
@ -23,10 +22,10 @@ pub fn verify_domains_match(a: &Url, b: &Url) -> Result<(), Error> {
/// Check that both urls are identical. If not, return UrlVerificationError.
///
/// ```
/// # use url::Url;
/// # use activitypub_federation::url::Url;
/// # use activitypub_federation::protocol::verification::verify_urls_match;
/// let a = Url::parse("https://example.com/abc")?;
/// let b = Url::parse("https://example.com/123")?;
/// let a = "https://example.com/abc".parse()?;
/// let b = "https://example.com/123".parse()?;
/// assert!(verify_urls_match(&a, &b).is_err());
/// # Ok::<(), url::ParseError>(())
/// ```

View file

@ -1,11 +1,10 @@
//! Traits which need to be implemented for federated data types
use crate::{config::Data, protocol::public_key::PublicKey};
use crate::{config::Data, protocol::public_key::PublicKey, url::Url};
use async_trait::async_trait;
use chrono::{DateTime, Utc};
use serde::Deserialize;
use std::{fmt::Debug, ops::Deref};
use url::Url;
/// Helper for converting between database structs and federated protocol structs.
///
@ -13,7 +12,7 @@ use url::Url;
/// # use activitystreams_kinds::{object::NoteType, public};
/// # use chrono::{Local, DateTime, Utc};
/// # use serde::{Deserialize, Serialize};
/// # use url::Url;
/// # use activitypub_federation::url::Url;
/// # use activitypub_federation::protocol::{public_key::PublicKey, helpers::deserialize_one_or_many};
/// # use activitypub_federation::config::Data;
/// # use activitypub_federation::fetch::object_id::ObjectId;
@ -62,7 +61,7 @@ use url::Url;
/// kind: Default::default(),
/// id: self.ap_id.clone().into(),
/// attributed_to: self.creator,
/// to: vec![public()],
/// to: vec![public().try_into()?],
/// content: self.text,
/// })
/// }
@ -162,7 +161,7 @@ pub trait Object: Sized + Debug {
///
/// ```
/// # use activitystreams_kinds::activity::FollowType;
/// # use url::Url;
/// # use activitypub_federation::url::Url;
/// # use activitypub_federation::fetch::object_id::ObjectId;
/// # use activitypub_federation::config::Data;
/// # use activitypub_federation::traits::ActivityHandler;

82
src/url.rs Normal file
View file

@ -0,0 +1,82 @@
//! Wrapper for `url::Url` type.
use serde::{Deserialize, Serialize};
use std::{
fmt::{Display, Formatter},
ops::Deref,
str::FromStr,
};
/// Wrapper for `url::Url` type. Has `domain` as mandatory field, and prints plain
/// string for debugging.
#[derive(Clone, PartialEq, Eq, Hash, Serialize, Deserialize, Debug)]
pub struct Url(url::Url);
impl Deref for Url {
type Target = url::Url;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl Display for Url {
#[allow(clippy::to_string_in_format_args)]
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0.to_string())
}
}
impl Url {
/// Returns domain of the url
pub fn domain(&self) -> &str {
self.0.domain().expect("has domain")
}
fn new(value: url::Url) -> Result<Self, url::ParseError> {
if value.domain().is_none() {
return Err(url::ParseError::EmptyHost);
}
Ok(Url(value))
}
}
impl TryFrom<url::Url> for Url {
type Error = url::ParseError;
fn try_from(value: url::Url) -> Result<Self, Self::Error> {
Self::new(value)
}
}
#[allow(clippy::from_over_into)]
impl Into<url::Url> for Url {
fn into(self) -> url::Url {
self.0
}
}
impl FromStr for Url {
type Err = url::ParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let value = url::Url::from_str(s)?;
Self::new(value)
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod test {
use super::*;
use crate::error::Error;
use std::str::FromStr;
#[tokio::test]
async fn test_url() -> Result<(), Error> {
assert!(Url::from_str("http://example.com").is_ok());
assert!(Url::try_from(url::Url::from_str("http://example.com")?).is_ok());
assert!(Url::from_str("http://127.0.0.1").is_err());
assert!(Url::try_from(url::Url::from_str("http://127.0.0.1")?).is_err());
Ok(())
}
}