diff --git a/.circleci/config.yml b/.circleci/config.yml index cd7f2d06..b0f7e651 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -63,7 +63,7 @@ commands: type: boolean default: false steps: - - run: cargo clippy <<^parameters.no_feature>>--no-default-features --features="${FEATURES}"<> --release -p <> -- -D warnings + - run: cargo clippy <<^parameters.no_feature>>--no-default-features --features="${FEATURES}"<> --release -p <> -- -D warnings -A clippy::needless_borrow run_with_coverage: description: run command with environment for coverage diff --git a/CHANGELOG.md b/CHANGELOG.md index 9881a16f..49517023 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,9 +20,9 @@ - Update Rust version to nightly-2021-11-27 (#961) - Upgrade Tantivy to 0.13.3 and lindera-tantivy to 0.7.1 (#878) - Run searcher on actor system (#870) -- Use article title as its slug instead of capitalizing and inserting hyphens (#920) - Extract a function to calculate posts' ap_url and share it with some places (#918) -- Keep title in URI (#920) +- Use article title as its slug instead of capitalizing and inserting hyphens (#920) +- Sign GET requests to other instances (#957) ### Fixed diff --git a/Cargo.lock b/Cargo.lock index 122a9af2..0bd41319 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3038,6 +3038,7 @@ dependencies = [ "heck", "hex", "hyper 0.12.36", + "once_cell", "openssl", "pulldown-cmark", "regex-syntax 0.6.25", diff --git a/plume-cli/src/instance.rs b/plume-cli/src/instance.rs index 8fa48060..35793639 100644 --- a/plume-cli/src/instance.rs +++ b/plume-cli/src/instance.rs @@ -68,4 +68,6 @@ fn new<'a>(args: &ArgMatches<'a>, conn: &Connection) { }, ) .expect("Couldn't save instance"); + Instance::cache_local(conn); + Instance::create_local_instance_user(conn).expect("Couldn't save local instance user"); } diff --git a/plume-cli/src/main.rs b/plume-cli/src/main.rs index 35248a32..3615c10d 100644 --- a/plume-cli/src/main.rs +++ b/plume-cli/src/main.rs @@ -25,7 +25,7 @@ fn main() { e => e.map(|_| ()).unwrap(), } let conn = Conn::establish(CONFIG.database_url.as_str()); - let _ = conn.as_ref().map(|conn| Instance::cache_local(conn)); + let _ = conn.as_ref().map(Instance::cache_local); match matches.subcommand() { ("instance", Some(args)) => { diff --git a/plume-common/Cargo.toml b/plume-common/Cargo.toml index 06cf22f5..a67f748e 100644 --- a/plume-common/Cargo.toml +++ b/plume-common/Cargo.toml @@ -33,3 +33,6 @@ version = "0.4" default-features = false git = "https://git.joinplu.me/Plume/pulldown-cmark" branch = "bidi-plume" + +[dev-dependencies] +once_cell = "1.5.2" diff --git a/plume-common/src/activity_pub/inbox.rs b/plume-common/src/activity_pub/inbox.rs index 0f88b5d9..48ba6c5f 100644 --- a/plume-common/src/activity_pub/inbox.rs +++ b/plume-common/src/activity_pub/inbox.rs @@ -1,6 +1,8 @@ -use reqwest::header::{HeaderValue, ACCEPT}; +use reqwest; use std::fmt::Debug; +use super::{request, sign::Signer}; + /// Represents an ActivityPub inbox. /// /// It routes an incoming Activity through the registered handlers. @@ -10,7 +12,50 @@ use std::fmt::Debug; /// ```rust /// # extern crate activitypub; /// # use activitypub::{actor::Person, activity::{Announce, Create}, object::Note}; +/// # use openssl::{hash::MessageDigest, pkey::PKey, rsa::Rsa}; +/// # use once_cell::sync::Lazy; /// # use plume_common::activity_pub::inbox::*; +/// # use plume_common::activity_pub::sign::{gen_keypair, Error as SignError, Result as SignResult, Signer}; +/// # +/// # static MY_SIGNER: Lazy = Lazy::new(|| MySigner::new()); +/// # +/// # struct MySigner { +/// # public_key: String, +/// # private_key: String, +/// # } +/// # +/// # impl MySigner { +/// # fn new() -> Self { +/// # let (pub_key, priv_key) = gen_keypair(); +/// # Self { +/// # public_key: String::from_utf8(pub_key).unwrap(), +/// # private_key: String::from_utf8(priv_key).unwrap(), +/// # } +/// # } +/// # } +/// # +/// # impl Signer for MySigner { +/// # fn get_key_id(&self) -> String { +/// # "mysigner".into() +/// # } +/// # +/// # fn sign(&self, to_sign: &str) -> SignResult> { +/// # let key = PKey::from_rsa(Rsa::private_key_from_pem(self.private_key.as_ref()).unwrap()) +/// # .unwrap(); +/// # let mut signer = openssl::sign::Signer::new(MessageDigest::sha256(), &key).unwrap(); +/// # signer.update(to_sign.as_bytes()).unwrap(); +/// # signer.sign_to_vec().map_err(|_| SignError()) +/// # } +/// # +/// # fn verify(&self, data: &str, signature: &[u8]) -> SignResult { +/// # let key = PKey::from_rsa(Rsa::public_key_from_pem(self.public_key.as_ref()).unwrap()) +/// # .unwrap(); +/// # let mut verifier = openssl::sign::Verifier::new(MessageDigest::sha256(), &key).unwrap(); +/// # verifier.update(data.as_bytes()).unwrap(); +/// # verifier.verify(&signature).map_err(|_| SignError()) +/// # } +/// # } +/// # /// # struct User; /// # impl FromId<()> for User { /// # type Error = (); @@ -23,6 +68,10 @@ use std::fmt::Debug; /// # fn from_activity(_: &(), obj: Person) -> Result { /// # Ok(User) /// # } +/// # +/// # fn get_sender() -> &'static dyn Signer { +/// # &*MY_SIGNER +/// # } /// # } /// # impl AsActor<&()> for User { /// # fn get_inbox_url(&self) -> String { @@ -42,6 +91,10 @@ use std::fmt::Debug; /// # fn from_activity(_: &(), obj: Note) -> Result { /// # Ok(Message) /// # } +/// # +/// # fn get_sender() -> &'static dyn Signer { +/// # &*MY_SIGNER +/// # } /// # } /// # impl AsObject for Message { /// # type Error = (); @@ -311,35 +364,16 @@ pub trait FromId: Sized { id: &str, proxy: Option, ) -> Result, Self::Error)> { - if let Some(proxy) = proxy { - reqwest::ClientBuilder::new().proxy(proxy) - } else { - reqwest::ClientBuilder::new() - } - .connect_timeout(Some(std::time::Duration::from_secs(5))) - .build() - .map_err(|_| (None, InboxError::DerefError.into()))? - .get(id) - .header( - ACCEPT, - HeaderValue::from_str( - &super::ap_accept_header() - .into_iter() - .collect::>() - .join(", "), - ) - .map_err(|_| (None, InboxError::DerefError.into()))?, - ) - .send() - .map_err(|_| (None, InboxError::DerefError)) - .and_then(|mut r| { - let json: serde_json::Value = r - .json() - .map_err(|_| (None, InboxError::InvalidObject(None)))?; - serde_json::from_value(json.clone()) - .map_err(|_| (Some(json), InboxError::InvalidObject(None))) - }) - .map_err(|(json, e)| (json, e.into())) + request::get(id, Self::get_sender(), proxy) + .map_err(|_| (None, InboxError::DerefError)) + .and_then(|mut r| { + let json: serde_json::Value = r + .json() + .map_err(|_| (None, InboxError::InvalidObject(None)))?; + serde_json::from_value(json.clone()) + .map_err(|_| (Some(json), InboxError::InvalidObject(None))) + }) + .map_err(|(json, e)| (json, e.into())) } /// Builds a `Self` from its ActivityPub representation @@ -347,6 +381,8 @@ pub trait FromId: Sized { /// Tries to find a `Self` with a given ID (`id`), using `ctx` (a database) fn from_db(ctx: &C, id: &str) -> Result; + + fn get_sender() -> &'static dyn Signer; } /// Should be implemented by anything representing an ActivityPub actor. @@ -385,6 +421,49 @@ pub trait AsActor { /// # extern crate activitypub; /// # use activitypub::{activity::Create, actor::Person, object::Note}; /// # use plume_common::activity_pub::inbox::{AsActor, AsObject, FromId}; +/// # use plume_common::activity_pub::sign::{gen_keypair, Error as SignError, Result as SignResult, Signer}; +/// # use openssl::{hash::MessageDigest, pkey::PKey, rsa::Rsa}; +/// # use once_cell::sync::Lazy; +/// # +/// # static MY_SIGNER: Lazy = Lazy::new(|| MySigner::new()); +/// # +/// # struct MySigner { +/// # public_key: String, +/// # private_key: String, +/// # } +/// # +/// # impl MySigner { +/// # fn new() -> Self { +/// # let (pub_key, priv_key) = gen_keypair(); +/// # Self { +/// # public_key: String::from_utf8(pub_key).unwrap(), +/// # private_key: String::from_utf8(priv_key).unwrap(), +/// # } +/// # } +/// # } +/// # +/// # impl Signer for MySigner { +/// # fn get_key_id(&self) -> String { +/// # "mysigner".into() +/// # } +/// # +/// # fn sign(&self, to_sign: &str) -> SignResult> { +/// # let key = PKey::from_rsa(Rsa::private_key_from_pem(self.private_key.as_ref()).unwrap()) +/// # .unwrap(); +/// # let mut signer = openssl::sign::Signer::new(MessageDigest::sha256(), &key).unwrap(); +/// # signer.update(to_sign.as_bytes()).unwrap(); +/// # signer.sign_to_vec().map_err(|_| SignError()) +/// # } +/// # +/// # fn verify(&self, data: &str, signature: &[u8]) -> SignResult { +/// # let key = PKey::from_rsa(Rsa::public_key_from_pem(self.public_key.as_ref()).unwrap()) +/// # .unwrap(); +/// # let mut verifier = openssl::sign::Verifier::new(MessageDigest::sha256(), &key).unwrap(); +/// # verifier.update(data.as_bytes()).unwrap(); +/// # verifier.verify(&signature).map_err(|_| SignError()) +/// # } +/// # } +/// # /// # struct Account; /// # impl FromId<()> for Account { /// # type Error = (); @@ -397,6 +476,10 @@ pub trait AsActor { /// # fn from_activity(_: &(), obj: Person) -> Result { /// # Ok(Account) /// # } +/// # +/// # fn get_sender() -> &'static dyn Signer { +/// # &*MY_SIGNER +/// # } /// # } /// # impl AsActor<()> for Account { /// # fn get_inbox_url(&self) -> String { @@ -420,6 +503,10 @@ pub trait AsActor { /// fn from_activity(_: &(), obj: Note) -> Result { /// Ok(Message { text: obj.object_props.content_string().map_err(|_| ())? }) /// } +/// +/// fn get_sender() -> &'static dyn Signer { +/// &*MY_SIGNER +/// } /// } /// /// impl AsObject for Message { @@ -459,7 +546,51 @@ where #[cfg(test)] mod tests { use super::*; + use crate::activity_pub::sign::{ + gen_keypair, Error as SignError, Result as SignResult, Signer, + }; use activitypub::{activity::*, actor::Person, object::Note}; + use once_cell::sync::Lazy; + use openssl::{hash::MessageDigest, pkey::PKey, rsa::Rsa}; + + static MY_SIGNER: Lazy = Lazy::new(|| MySigner::new()); + + struct MySigner { + public_key: String, + private_key: String, + } + + impl MySigner { + fn new() -> Self { + let (pub_key, priv_key) = gen_keypair(); + Self { + public_key: String::from_utf8(pub_key).unwrap(), + private_key: String::from_utf8(priv_key).unwrap(), + } + } + } + + impl Signer for MySigner { + fn get_key_id(&self) -> String { + "mysigner".into() + } + + fn sign(&self, to_sign: &str) -> SignResult> { + let key = PKey::from_rsa(Rsa::private_key_from_pem(self.private_key.as_ref()).unwrap()) + .unwrap(); + let mut signer = openssl::sign::Signer::new(MessageDigest::sha256(), &key).unwrap(); + signer.update(to_sign.as_bytes()).unwrap(); + signer.sign_to_vec().map_err(|_| SignError()) + } + + fn verify(&self, data: &str, signature: &[u8]) -> SignResult { + let key = PKey::from_rsa(Rsa::public_key_from_pem(self.public_key.as_ref()).unwrap()) + .unwrap(); + let mut verifier = openssl::sign::Verifier::new(MessageDigest::sha256(), &key).unwrap(); + verifier.update(data.as_bytes()).unwrap(); + verifier.verify(&signature).map_err(|_| SignError()) + } + } struct MyActor; impl FromId<()> for MyActor { @@ -473,6 +604,10 @@ mod tests { fn from_activity(_: &(), _obj: Person) -> Result { Ok(MyActor) } + + fn get_sender() -> &'static dyn Signer { + &*MY_SIGNER + } } impl AsActor<&()> for MyActor { @@ -497,6 +632,10 @@ mod tests { fn from_activity(_: &(), _obj: Note) -> Result { Ok(MyObject) } + + fn get_sender() -> &'static dyn Signer { + &*MY_SIGNER + } } impl AsObject for MyObject { type Error = (); @@ -601,6 +740,10 @@ mod tests { fn from_activity(_: &(), _obj: Person) -> Result { Err(()) } + + fn get_sender() -> &'static dyn Signer { + &*MY_SIGNER + } } impl AsActor<&()> for FailingActor { fn get_inbox_url(&self) -> String { diff --git a/plume-common/src/activity_pub/request.rs b/plume-common/src/activity_pub/request.rs index 73e6d6ee..4258bd7b 100644 --- a/plume-common/src/activity_pub/request.rs +++ b/plume-common/src/activity_pub/request.rs @@ -1,6 +1,11 @@ use chrono::{offset::Utc, DateTime}; use openssl::hash::{Hasher, MessageDigest}; -use reqwest::header::{HeaderMap, HeaderValue, ACCEPT, CONTENT_TYPE, DATE, USER_AGENT}; +use reqwest::{ + header::{ + HeaderMap, HeaderValue, InvalidHeaderValue, ACCEPT, CONTENT_TYPE, DATE, HOST, USER_AGENT, + }, + ClientBuilder, Proxy, Response, Url, UrlError, +}; use std::ops::Deref; use std::time::SystemTime; use tracing::warn; @@ -13,6 +18,24 @@ const PLUME_USER_AGENT: &str = concat!("Plume/", env!("CARGO_PKG_VERSION")); #[derive(Debug)] pub struct Error(); +impl From for Error { + fn from(_err: UrlError) -> Self { + Error() + } +} + +impl From for Error { + fn from(_err: InvalidHeaderValue) -> Self { + Error() + } +} + +impl From for Error { + fn from(_err: reqwest::Error) -> Self { + Error() + } +} + pub struct Digest(String); impl Digest { @@ -118,8 +141,8 @@ type Path<'a> = &'a str; type Query<'a> = &'a str; type RequestTarget<'a> = (Method<'a>, Path<'a>, Option>); -pub fn signature( - signer: &S, +pub fn signature( + signer: &dyn Signer, headers: &HeaderMap, request_target: RequestTarget, ) -> Result { @@ -164,10 +187,35 @@ pub fn signature( )).map_err(|_| Error()) } +pub fn get(url_str: &str, sender: &dyn Signer, proxy: Option) -> Result { + let mut headers = headers(); + let url = Url::parse(url_str)?; + if !url.has_host() { + return Err(Error()); + } + let host_header_value = HeaderValue::from_str(url.host_str().expect("Unreachable"))?; + headers.insert(HOST, host_header_value); + if let Some(proxy) = proxy { + ClientBuilder::new().proxy(proxy) + } else { + ClientBuilder::new() + } + .connect_timeout(Some(std::time::Duration::from_secs(5))) + .build()? + .get(url_str) + .headers(headers.clone()) + .header( + "Signature", + signature(sender, &headers, ("get", url.path(), url.query()))?, + ) + .send() + .map_err(|_| Error()) +} + #[cfg(test)] mod tests { - use super::{signature, Error}; - use crate::activity_pub::sign::{gen_keypair, Signer}; + use super::signature; + use crate::activity_pub::sign::{gen_keypair, Error, Result, Signer}; use openssl::{hash::MessageDigest, pkey::PKey, rsa::Rsa}; use reqwest::header::HeaderMap; @@ -187,13 +235,11 @@ mod tests { } impl Signer for MySigner { - type Error = Error; - fn get_key_id(&self) -> String { "mysigner".into() } - fn sign(&self, to_sign: &str) -> Result, Self::Error> { + fn sign(&self, to_sign: &str) -> Result> { let key = PKey::from_rsa(Rsa::private_key_from_pem(self.private_key.as_ref()).unwrap()) .unwrap(); let mut signer = openssl::sign::Signer::new(MessageDigest::sha256(), &key).unwrap(); @@ -201,7 +247,7 @@ mod tests { signer.sign_to_vec().map_err(|_| Error()) } - fn verify(&self, data: &str, signature: &[u8]) -> Result { + fn verify(&self, data: &str, signature: &[u8]) -> Result { let key = PKey::from_rsa(Rsa::public_key_from_pem(self.public_key.as_ref()).unwrap()) .unwrap(); let mut verifier = openssl::sign::Verifier::new(MessageDigest::sha256(), &key).unwrap(); diff --git a/plume-common/src/activity_pub/sign.rs b/plume-common/src/activity_pub/sign.rs index c97cb5dd..22aaf9d6 100644 --- a/plume-common/src/activity_pub/sign.rs +++ b/plume-common/src/activity_pub/sign.rs @@ -19,20 +19,25 @@ pub fn gen_keypair() -> (Vec, Vec) { #[derive(Debug)] pub struct Error(); +pub type Result = std::result::Result; + +impl From for Error { + fn from(_: openssl::error::ErrorStack) -> Self { + Self() + } +} pub trait Signer { - type Error; - fn get_key_id(&self) -> String; /// Sign some data with the signer keypair - fn sign(&self, to_sign: &str) -> Result, Self::Error>; + fn sign(&self, to_sign: &str) -> Result>; /// Verify if the signature is valid - fn verify(&self, data: &str, signature: &[u8]) -> Result; + fn verify(&self, data: &str, signature: &[u8]) -> Result; } pub trait Signable { - fn sign(&mut self, creator: &T) -> Result<&mut Self, Error> + fn sign(&mut self, creator: &T) -> Result<&mut Self> where T: Signer; fn verify(self, creator: &T) -> bool @@ -46,7 +51,7 @@ pub trait Signable { } impl Signable for serde_json::Value { - fn sign(&mut self, creator: &T) -> Result<&mut serde_json::Value, Error> { + fn sign(&mut self, creator: &T) -> Result<&mut serde_json::Value> { let creation_date = Utc::now().to_rfc3339(); let mut options = json!({ "type": "RsaSignature2017", diff --git a/plume-models/src/blogs.rs b/plume-models/src/blogs.rs index 4043c85e..9d0839ae 100644 --- a/plume-models/src/blogs.rs +++ b/plume-models/src/blogs.rs @@ -443,6 +443,10 @@ impl FromId for Blog { }, ) } + + fn get_sender() -> &'static dyn sign::Signer { + Instance::get_local_instance_user().expect("Failed to local instance user") + } } impl AsActor<&PlumeRocket> for Blog { @@ -462,24 +466,22 @@ impl AsActor<&PlumeRocket> for Blog { } impl sign::Signer for Blog { - type Error = Error; - fn get_key_id(&self) -> String { format!("{}#main-key", self.ap_url) } - fn sign(&self, to_sign: &str) -> Result> { - let key = self.get_keypair()?; + fn sign(&self, to_sign: &str) -> sign::Result> { + let key = self.get_keypair().map_err(|_| sign::Error())?; let mut signer = Signer::new(MessageDigest::sha256(), &key)?; signer.update(to_sign.as_bytes())?; - signer.sign_to_vec().map_err(Error::from) + signer.sign_to_vec().map_err(sign::Error::from) } - fn verify(&self, data: &str, signature: &[u8]) -> Result { + fn verify(&self, data: &str, signature: &[u8]) -> sign::Result { let key = PKey::from_rsa(Rsa::public_key_from_pem(self.public_key.as_ref())?)?; let mut verifier = Verifier::new(MessageDigest::sha256(), &key)?; verifier.update(data.as_bytes())?; - verifier.verify(signature).map_err(Error::from) + verifier.verify(signature).map_err(sign::Error::from) } } diff --git a/plume-models/src/comments.rs b/plume-models/src/comments.rs index d92ab6c0..0809aab5 100644 --- a/plume-models/src/comments.rs +++ b/plume-models/src/comments.rs @@ -21,6 +21,7 @@ use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl, SaveChangesDsl}; use plume_common::{ activity_pub::{ inbox::{AsActor, AsObject, FromId}, + sign::Signer, Id, IntoId, PUBLIC_VISIBILITY, }, utils, @@ -328,6 +329,10 @@ impl FromId for Comment { comm.notify(conn)?; Ok(comm) } + + fn get_sender() -> &'static dyn Signer { + Instance::get_local_instance_user().expect("Failed to local instance user") + } } impl AsObject for Comment { diff --git a/plume-models/src/follows.rs b/plume-models/src/follows.rs index c1669e36..9450b59b 100644 --- a/plume-models/src/follows.rs +++ b/plume-models/src/follows.rs @@ -1,6 +1,6 @@ use crate::{ - ap_url, db_conn::DbConn, notifications::*, schema::follows, users::User, Connection, Error, - Result, CONFIG, + ap_url, db_conn::DbConn, instance::Instance, notifications::*, schema::follows, users::User, + Connection, Error, Result, CONFIG, }; use activitypub::activity::{Accept, Follow as FollowAct, Undo}; use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl, SaveChangesDsl}; @@ -183,6 +183,10 @@ impl FromId for Follow { .map_err(|(_, e)| e)?; Follow::accept_follow(conn, &actor, &target, follow, actor.id, target.id) } + + fn get_sender() -> &'static dyn Signer { + Instance::get_local_instance_user().expect("Failed to local instance user") + } } impl AsObject for Follow { diff --git a/plume-models/src/instance.rs b/plume-models/src/instance.rs index 80babed5..86cb3d46 100644 --- a/plume-models/src/instance.rs +++ b/plume-models/src/instance.rs @@ -3,11 +3,12 @@ use crate::{ medias::Media, safe_string::SafeString, schema::{instances, users}, - users::{Role, User}, + users::{NewUser, Role, User}, Connection, Error, Result, }; use chrono::NaiveDateTime; -use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl}; +use diesel::{self, result::Error::NotFound, ExpressionMethods, QueryDsl, RunQueryDsl}; +use once_cell::sync::OnceCell; use plume_common::utils::md_to_html; use std::sync::RwLock; @@ -45,6 +46,9 @@ lazy_static! { static ref LOCAL_INSTANCE: RwLock> = RwLock::new(None); } +const LOCAL_INSTANCE_USERNAME: &str = "__instance__"; +static LOCAL_INSTANCE_USER: OnceCell = OnceCell::new(); + impl Instance { pub fn set_local(self) { LOCAL_INSTANCE.write().unwrap().replace(self); @@ -76,6 +80,42 @@ impl Instance { .map_err(Error::from) } + pub fn create_local_instance_user(conn: &Connection) -> Result { + let instance = Instance::get_local()?; + let email = format!("{}@{}", LOCAL_INSTANCE_USERNAME, &instance.public_domain); + NewUser::new_local( + conn, + LOCAL_INSTANCE_USERNAME.into(), + instance.public_domain, + Role::Instance, + "Local instance", + email, + None, + ) + } + + pub fn get_local_instance_user() -> Option<&'static User> { + LOCAL_INSTANCE_USER.get() + } + + pub fn get_local_instance_user_uncached(conn: &Connection) -> Result { + users::table + .filter(users::role.eq(3)) + .first(conn) + .or_else(|err| match err { + NotFound => Self::create_local_instance_user(conn), + _ => Err(Error::Db(err)), + }) + } + + pub fn cache_local_instance_user(conn: &Connection) { + let _ = LOCAL_INSTANCE_USER.get_or_init(|| { + Self::get_local_instance_user_uncached(conn) + .or_else(|_| Self::create_local_instance_user(conn)) + .expect("Failed to cache local instance user") + }); + } + pub fn page(conn: &Connection, (min, max): (i32, i32)) -> Result> { instances::table .order(instances::public_domain.asc()) @@ -304,6 +344,7 @@ pub(crate) mod tests { }) .collect(); Instance::cache_local(conn); + Instance::cache_local_instance_user(conn); res } diff --git a/plume-models/src/lib.rs b/plume-models/src/lib.rs index bd1474ec..16d3a159 100755 --- a/plume-models/src/lib.rs +++ b/plume-models/src/lib.rs @@ -17,7 +17,7 @@ extern crate serde_json; extern crate tantivy; use once_cell::sync::Lazy; -use plume_common::activity_pub::inbox::InboxError; +use plume_common::activity_pub::{inbox::InboxError, request, sign}; use posts::PostEvent; use riker::actors::{channel, ActorSystem, ChannelRef, SystemBuilder}; use users::UserEvent; @@ -79,6 +79,12 @@ impl From for Error { } } +impl From for Error { + fn from(_: sign::Error) -> Self { + Error::Signature + } +} + impl From for Error { fn from(err: diesel::result::Error) -> Self { Error::Db(err) @@ -151,6 +157,12 @@ impl From> for Error { } } +impl From for Error { + fn from(_err: request::Error) -> Error { + Error::Request + } +} + pub type Result = std::result::Result; /// Adds a function to a model, that returns the first diff --git a/plume-models/src/likes.rs b/plume-models/src/likes.rs index b14e7c38..fa092af0 100644 --- a/plume-models/src/likes.rs +++ b/plume-models/src/likes.rs @@ -1,12 +1,13 @@ use crate::{ - db_conn::DbConn, notifications::*, posts::Post, schema::likes, timeline::*, users::User, - Connection, Error, Result, CONFIG, + db_conn::DbConn, instance::Instance, notifications::*, posts::Post, schema::likes, timeline::*, + users::User, Connection, Error, Result, CONFIG, }; use activitypub::activity; use chrono::NaiveDateTime; use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl}; use plume_common::activity_pub::{ inbox::{AsActor, AsObject, FromId}, + sign::Signer, Id, IntoId, PUBLIC_VISIBILITY, }; @@ -137,6 +138,10 @@ impl FromId for Like { res.notify(conn)?; Ok(res) } + + fn get_sender() -> &'static dyn Signer { + Instance::get_local_instance_user().expect("Failed to local instance user") + } } impl AsObject for Like { diff --git a/plume-models/src/medias.rs b/plume-models/src/medias.rs index 5be9bc70..68cb52c4 100644 --- a/plume-models/src/medias.rs +++ b/plume-models/src/medias.rs @@ -7,7 +7,7 @@ use askama_escape::escape; use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl}; use guid_create::GUID; use plume_common::{ - activity_pub::{inbox::FromId, Id}, + activity_pub::{inbox::FromId, request, Id}, utils::MediaProcessor, }; use std::{ @@ -220,13 +220,11 @@ impl Media { let mut dest = fs::File::create(path.clone())?; // TODO: conditional GET - if let Some(proxy) = CONFIG.proxy() { - reqwest::ClientBuilder::new().proxy(proxy.clone()).build()? - } else { - reqwest::Client::new() - } - .get(remote_url.as_str()) - .send()? + request::get( + remote_url.as_str(), + User::get_sender(), + CONFIG.proxy().cloned(), + )? .copy_to(&mut dest)?; Media::find_by_file_path(conn, path.to_str().ok_or(Error::InvalidValue)?) diff --git a/plume-models/src/posts.rs b/plume-models/src/posts.rs index c53f20c1..9a385db9 100644 --- a/plume-models/src/posts.rs +++ b/plume-models/src/posts.rs @@ -15,6 +15,7 @@ use once_cell::sync::Lazy; use plume_common::{ activity_pub::{ inbox::{AsActor, AsObject, FromId}, + sign::Signer, Hashtag, Id, IntoId, Licensed, Source, PUBLIC_VISIBILITY, }, utils::{iri_percent_encode_seg, md_to_html}, @@ -759,6 +760,10 @@ impl FromId for Post { Ok(post) } + + fn get_sender() -> &'static dyn Signer { + Instance::get_local_instance_user().expect("Failed to local instance user") + } } impl AsObject for Post { @@ -830,6 +835,10 @@ impl FromId for PostUpdate { tags: updated.object.object_props.tag, }) } + + fn get_sender() -> &'static dyn Signer { + Instance::get_local_instance_user().expect("Failed to local instance user") + } } impl AsObject for PostUpdate { diff --git a/plume-models/src/reshares.rs b/plume-models/src/reshares.rs index eec86a1e..58400196 100644 --- a/plume-models/src/reshares.rs +++ b/plume-models/src/reshares.rs @@ -1,12 +1,13 @@ use crate::{ - db_conn::DbConn, notifications::*, posts::Post, schema::reshares, timeline::*, users::User, - Connection, Error, Result, CONFIG, + db_conn::DbConn, instance::Instance, notifications::*, posts::Post, schema::reshares, + timeline::*, users::User, Connection, Error, Result, CONFIG, }; use activitypub::activity::{Announce, Undo}; use chrono::NaiveDateTime; use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl}; use plume_common::activity_pub::{ inbox::{AsActor, AsObject, FromId}, + sign::Signer, Id, IntoId, PUBLIC_VISIBILITY, }; @@ -162,6 +163,10 @@ impl FromId for Reshare { res.notify(conn)?; Ok(res) } + + fn get_sender() -> &'static dyn Signer { + Instance::get_local_instance_user().expect("Failed to local instance user") + } } impl AsObject for Reshare { diff --git a/plume-models/src/users.rs b/plume-models/src/users.rs index 0b40d63f..8c42701b 100644 --- a/plume-models/src/users.rs +++ b/plume-models/src/users.rs @@ -22,17 +22,13 @@ use openssl::{ }; use plume_common::{ activity_pub::{ - ap_accept_header, inbox::{AsActor, AsObject, FromId}, - sign::{gen_keypair, Signer}, + request::get, + sign::{gen_keypair, Error as SignError, Result as SignResult, Signer}, ActivityStream, ApSignature, Id, IntoId, PublicKey, PUBLIC_VISIBILITY, }, utils, }; -use reqwest::{ - header::{HeaderValue, ACCEPT}, - ClientBuilder, -}; use riker::actors::{Publish, Tell}; use rocket::{ outcome::IntoOutcome, @@ -52,6 +48,7 @@ pub enum Role { Admin = 0, Moderator = 1, Normal = 2, + Instance = 3, } #[derive(Queryable, Identifiable, Clone, Debug, AsChangeset)] @@ -78,6 +75,7 @@ pub struct User { pub summary_html: SafeString, /// 0 = admin /// 1 = moderator + /// 3 = local instance /// anything else = normal user pub role: i32, pub preferred_theme: Option, @@ -229,20 +227,7 @@ impl User { } fn fetch(url: &str) -> Result { - let mut res = ClientBuilder::new() - .connect_timeout(Some(std::time::Duration::from_secs(5))) - .build()? - .get(url) - .header( - ACCEPT, - HeaderValue::from_str( - &ap_accept_header() - .into_iter() - .collect::>() - .join(", "), - )?, - ) - .send()?; + let mut res = get(url, Self::get_sender(), CONFIG.proxy().cloned())?; let text = &res.text()?; // without this workaround, publicKey is not correctly deserialized let ap_sign = serde_json::from_str::(text)?; @@ -261,7 +246,7 @@ impl User { conn, json.object .object_props - .icon_image()? + .icon_image()? // FIXME: Fails when icon is not set .object_props .url_string()?, self, @@ -469,20 +454,7 @@ impl User { Ok(ActivityStream::new(coll)) } fn fetch_outbox_page(&self, url: &str) -> Result<(Vec, Option)> { - let mut res = ClientBuilder::new() - .connect_timeout(Some(std::time::Duration::from_secs(5))) - .build()? - .get(url) - .header( - ACCEPT, - HeaderValue::from_str( - &ap_accept_header() - .into_iter() - .collect::>() - .join(", "), - )?, - ) - .send()?; + let mut res = get(url, Self::get_sender(), CONFIG.proxy().cloned())?; let text = &res.text()?; let json: serde_json::Value = serde_json::from_str(text)?; let items = json["items"] @@ -496,20 +468,11 @@ impl User { Ok((items, next)) } pub fn fetch_outbox(&self) -> Result> { - let mut res = ClientBuilder::new() - .connect_timeout(Some(std::time::Duration::from_secs(5))) - .build()? - .get(&self.outbox_url[..]) - .header( - ACCEPT, - HeaderValue::from_str( - &ap_accept_header() - .into_iter() - .collect::>() - .join(", "), - )?, - ) - .send()?; + let mut res = get( + &self.outbox_url[..], + Self::get_sender(), + CONFIG.proxy().cloned(), + )?; let text = &res.text()?; let json: serde_json::Value = serde_json::from_str(text)?; if let Some(first) = json.get("first") { @@ -541,20 +504,11 @@ impl User { } pub fn fetch_followers_ids(&self) -> Result> { - let mut res = ClientBuilder::new() - .connect_timeout(Some(std::time::Duration::from_secs(5))) - .build()? - .get(&self.followers_endpoint[..]) - .header( - ACCEPT, - HeaderValue::from_str( - &ap_accept_header() - .into_iter() - .collect::>() - .join(", "), - )?, - ) - .send()?; + let mut res = get( + &self.followers_endpoint[..], + Self::get_sender(), + CONFIG.proxy().cloned(), + )?; let text = &res.text()?; let json: serde_json::Value = serde_json::from_str(text)?; Ok(json["items"] @@ -1037,6 +991,10 @@ impl FromId for User { Ok(user) } + + fn get_sender() -> &'static dyn Signer { + Instance::get_local_instance_user().expect("Failed to local instance user") + } } impl AsActor<&DbConn> for User { @@ -1069,24 +1027,22 @@ impl AsObject for User { } impl Signer for User { - type Error = Error; - fn get_key_id(&self) -> String { format!("{}#main-key", self.ap_url) } - fn sign(&self, to_sign: &str) -> Result> { - let key = self.get_keypair()?; + fn sign(&self, to_sign: &str) -> SignResult> { + let key = self.get_keypair().map_err(|_| SignError())?; let mut signer = sign::Signer::new(MessageDigest::sha256(), &key)?; signer.update(to_sign.as_bytes())?; - signer.sign_to_vec().map_err(Error::from) + signer.sign_to_vec().map_err(SignError::from) } - fn verify(&self, data: &str, signature: &[u8]) -> Result { + fn verify(&self, data: &str, signature: &[u8]) -> SignResult { let key = PKey::from_rsa(Rsa::public_key_from_pem(self.public_key.as_ref())?)?; let mut verifier = sign::Verifier::new(MessageDigest::sha256(), &key)?; verifier.update(data.as_bytes())?; - verifier.verify(signature).map_err(Error::from) + verifier.verify(signature).map_err(SignError::from) } } diff --git a/src/main.rs b/src/main.rs index 243a0b91..58cb3abe 100755 --- a/src/main.rs +++ b/src/main.rs @@ -57,7 +57,10 @@ fn init_pool() -> Option { builder = builder.max_size(max_size); }; let pool = builder.build(manager).ok()?; - Instance::cache_local(&pool.get().unwrap()); + let conn = pool.get().unwrap(); + Instance::cache_local(&conn); + let _ = Instance::create_local_instance_user(&conn); + Instance::cache_local_instance_user(&conn); Some(pool) } diff --git a/src/routes/blogs.rs b/src/routes/blogs.rs index df4da910..07a8e8f1 100644 --- a/src/routes/blogs.rs +++ b/src/routes/blogs.rs @@ -390,6 +390,7 @@ mod tests { posts::{NewPost, Post}, safe_string::SafeString, users::{NewUser, User, AUTH_COOKIE}, + Connection as Conn, CONFIG, }; use rocket::{ http::{Cookie, Cookies, SameSite}, @@ -398,6 +399,22 @@ mod tests { #[test] fn edit_link_within_post_card() { + let conn = Conn::establish(CONFIG.database_url.as_str()).unwrap(); + Instance::insert( + &conn, + NewInstance { + public_domain: "example.org".to_string(), + name: "Plume".to_string(), + local: true, + long_description: SafeString::new(""), + short_description: SafeString::new(""), + default_license: "CC-BY-SA".to_string(), + open_registrations: true, + short_description_html: String::new(), + long_description_html: String::new(), + }, + ) + .unwrap(); let rocket = init_rocket(); let client = Client::new(rocket).expect("valid rocket instance"); let dbpool = client.rocket().state::().unwrap();