Reviewed-on: https://git.joinplu.me/Plume/Plume/pulls/957
This commit is contained in:
commit
0ede2ab3ab
@ -63,7 +63,7 @@ commands:
|
||||
type: boolean
|
||||
default: false
|
||||
steps:
|
||||
- run: cargo clippy <<^parameters.no_feature>>--no-default-features --features="${FEATURES}"<</parameters.no_feature>> --release -p <<parameters.package>> -- -D warnings
|
||||
- run: cargo clippy <<^parameters.no_feature>>--no-default-features --features="${FEATURES}"<</parameters.no_feature>> --release -p <<parameters.package>> -- -D warnings -A clippy::needless_borrow
|
||||
|
||||
run_with_coverage:
|
||||
description: run command with environment for coverage
|
||||
|
@ -20,6 +20,7 @@
|
||||
- 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)
|
||||
- Sign GET requests to other instances (#957)
|
||||
|
||||
### Fixed
|
||||
|
||||
|
1
Cargo.lock
generated
1
Cargo.lock
generated
@ -3038,6 +3038,7 @@ dependencies = [
|
||||
"heck",
|
||||
"hex",
|
||||
"hyper 0.12.36",
|
||||
"once_cell",
|
||||
"openssl",
|
||||
"pulldown-cmark",
|
||||
"regex-syntax 0.6.25",
|
||||
|
@ -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");
|
||||
}
|
||||
|
@ -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)) => {
|
||||
|
@ -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"
|
||||
|
@ -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<MySigner> = 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<Vec<u8>> {
|
||||
/// # 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<bool> {
|
||||
/// # 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<Self, Self::Error> {
|
||||
/// # 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<Self, Self::Error> {
|
||||
/// # Ok(Message)
|
||||
/// # }
|
||||
/// #
|
||||
/// # fn get_sender() -> &'static dyn Signer {
|
||||
/// # &*MY_SIGNER
|
||||
/// # }
|
||||
/// # }
|
||||
/// # impl AsObject<User, Create, &()> for Message {
|
||||
/// # type Error = ();
|
||||
@ -311,35 +364,16 @@ pub trait FromId<C>: Sized {
|
||||
id: &str,
|
||||
proxy: Option<reqwest::Proxy>,
|
||||
) -> Result<Self::Object, (Option<serde_json::Value>, 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::<Vec<_>>()
|
||||
.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<C>: Sized {
|
||||
|
||||
/// Tries to find a `Self` with a given ID (`id`), using `ctx` (a database)
|
||||
fn from_db(ctx: &C, id: &str) -> Result<Self, Self::Error>;
|
||||
|
||||
fn get_sender() -> &'static dyn Signer;
|
||||
}
|
||||
|
||||
/// Should be implemented by anything representing an ActivityPub actor.
|
||||
@ -385,6 +421,49 @@ pub trait AsActor<C> {
|
||||
/// # 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<MySigner> = 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<Vec<u8>> {
|
||||
/// # 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<bool> {
|
||||
/// # 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<C> {
|
||||
/// # fn from_activity(_: &(), obj: Person) -> Result<Self, Self::Error> {
|
||||
/// # 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<C> {
|
||||
/// fn from_activity(_: &(), obj: Note) -> Result<Self, Self::Error> {
|
||||
/// Ok(Message { text: obj.object_props.content_string().map_err(|_| ())? })
|
||||
/// }
|
||||
///
|
||||
/// fn get_sender() -> &'static dyn Signer {
|
||||
/// &*MY_SIGNER
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// impl AsObject<Account, Create, ()> 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<MySigner> = 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<Vec<u8>> {
|
||||
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<bool> {
|
||||
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<Self, Self::Error> {
|
||||
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<Self, Self::Error> {
|
||||
Ok(MyObject)
|
||||
}
|
||||
|
||||
fn get_sender() -> &'static dyn Signer {
|
||||
&*MY_SIGNER
|
||||
}
|
||||
}
|
||||
impl AsObject<MyActor, Create, &()> for MyObject {
|
||||
type Error = ();
|
||||
@ -601,6 +740,10 @@ mod tests {
|
||||
fn from_activity(_: &(), _obj: Person) -> Result<Self, Self::Error> {
|
||||
Err(())
|
||||
}
|
||||
|
||||
fn get_sender() -> &'static dyn Signer {
|
||||
&*MY_SIGNER
|
||||
}
|
||||
}
|
||||
impl AsActor<&()> for FailingActor {
|
||||
fn get_inbox_url(&self) -> String {
|
||||
|
@ -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<UrlError> for Error {
|
||||
fn from(_err: UrlError) -> Self {
|
||||
Error()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<InvalidHeaderValue> for Error {
|
||||
fn from(_err: InvalidHeaderValue) -> Self {
|
||||
Error()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<reqwest::Error> 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<Query<'a>>);
|
||||
|
||||
pub fn signature<S: Signer>(
|
||||
signer: &S,
|
||||
pub fn signature(
|
||||
signer: &dyn Signer,
|
||||
headers: &HeaderMap,
|
||||
request_target: RequestTarget,
|
||||
) -> Result<HeaderValue, Error> {
|
||||
@ -164,10 +187,35 @@ pub fn signature<S: Signer>(
|
||||
)).map_err(|_| Error())
|
||||
}
|
||||
|
||||
pub fn get(url_str: &str, sender: &dyn Signer, proxy: Option<Proxy>) -> Result<Response, Error> {
|
||||
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<Vec<u8>, Self::Error> {
|
||||
fn sign(&self, to_sign: &str) -> Result<Vec<u8>> {
|
||||
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<bool, Self::Error> {
|
||||
fn verify(&self, data: &str, signature: &[u8]) -> Result<bool> {
|
||||
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();
|
||||
|
@ -19,20 +19,25 @@ pub fn gen_keypair() -> (Vec<u8>, Vec<u8>) {
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Error();
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
impl From<openssl::error::ErrorStack> 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<Vec<u8>, Self::Error>;
|
||||
fn sign(&self, to_sign: &str) -> Result<Vec<u8>>;
|
||||
/// Verify if the signature is valid
|
||||
fn verify(&self, data: &str, signature: &[u8]) -> Result<bool, Self::Error>;
|
||||
fn verify(&self, data: &str, signature: &[u8]) -> Result<bool>;
|
||||
}
|
||||
|
||||
pub trait Signable {
|
||||
fn sign<T>(&mut self, creator: &T) -> Result<&mut Self, Error>
|
||||
fn sign<T>(&mut self, creator: &T) -> Result<&mut Self>
|
||||
where
|
||||
T: Signer;
|
||||
fn verify<T>(self, creator: &T) -> bool
|
||||
@ -46,7 +51,7 @@ pub trait Signable {
|
||||
}
|
||||
|
||||
impl Signable for serde_json::Value {
|
||||
fn sign<T: Signer>(&mut self, creator: &T) -> Result<&mut serde_json::Value, Error> {
|
||||
fn sign<T: Signer>(&mut self, creator: &T) -> Result<&mut serde_json::Value> {
|
||||
let creation_date = Utc::now().to_rfc3339();
|
||||
let mut options = json!({
|
||||
"type": "RsaSignature2017",
|
||||
|
@ -443,6 +443,10 @@ impl FromId<DbConn> 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<Vec<u8>> {
|
||||
let key = self.get_keypair()?;
|
||||
fn sign(&self, to_sign: &str) -> sign::Result<Vec<u8>> {
|
||||
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<bool> {
|
||||
fn verify(&self, data: &str, signature: &[u8]) -> sign::Result<bool> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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<DbConn> 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<User, Create, &DbConn> for Comment {
|
||||
|
@ -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<DbConn> 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<User, Undo, &DbConn> for Follow {
|
||||
|
@ -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<Option<Instance>> = RwLock::new(None);
|
||||
}
|
||||
|
||||
const LOCAL_INSTANCE_USERNAME: &str = "__instance__";
|
||||
static LOCAL_INSTANCE_USER: OnceCell<User> = 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<User> {
|
||||
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<User> {
|
||||
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<Vec<Instance>> {
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -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<openssl::error::ErrorStack> for Error {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<sign::Error> for Error {
|
||||
fn from(_: sign::Error) -> Self {
|
||||
Error::Signature
|
||||
}
|
||||
}
|
||||
|
||||
impl From<diesel::result::Error> for Error {
|
||||
fn from(err: diesel::result::Error) -> Self {
|
||||
Error::Db(err)
|
||||
@ -151,6 +157,12 @@ impl From<InboxError<Error>> for Error {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<request::Error> for Error {
|
||||
fn from(_err: request::Error) -> Error {
|
||||
Error::Request
|
||||
}
|
||||
}
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
/// Adds a function to a model, that returns the first
|
||||
|
@ -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<DbConn> 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<User, activity::Undo, &DbConn> for Like {
|
||||
|
@ -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)?)
|
||||
|
@ -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<DbConn> for Post {
|
||||
|
||||
Ok(post)
|
||||
}
|
||||
|
||||
fn get_sender() -> &'static dyn Signer {
|
||||
Instance::get_local_instance_user().expect("Failed to local instance user")
|
||||
}
|
||||
}
|
||||
|
||||
impl AsObject<User, Create, &DbConn> for Post {
|
||||
@ -830,6 +835,10 @@ impl FromId<DbConn> 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<User, Update, &DbConn> for PostUpdate {
|
||||
|
@ -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<DbConn> 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<User, Undo, &DbConn> for Reshare {
|
||||
|
@ -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<String>,
|
||||
@ -229,20 +227,7 @@ impl User {
|
||||
}
|
||||
|
||||
fn fetch(url: &str) -> Result<CustomPerson> {
|
||||
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::<Vec<_>>()
|
||||
.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::<ApSignature>(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<T: Activity>(&self, url: &str) -> Result<(Vec<T>, Option<String>)> {
|
||||
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::<Vec<_>>()
|
||||
.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<T: Activity>(&self) -> Result<Vec<T>> {
|
||||
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::<Vec<_>>()
|
||||
.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<Vec<String>> {
|
||||
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::<Vec<_>>()
|
||||
.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<DbConn> 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<User, Delete, &DbConn> 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<Vec<u8>> {
|
||||
let key = self.get_keypair()?;
|
||||
fn sign(&self, to_sign: &str) -> SignResult<Vec<u8>> {
|
||||
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<bool> {
|
||||
fn verify(&self, data: &str, signature: &[u8]) -> SignResult<bool> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -57,7 +57,10 @@ fn init_pool() -> Option<DbPool> {
|
||||
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)
|
||||
}
|
||||
|
||||
|
@ -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::<DbPool>().unwrap();
|
||||
|
Loading…
Reference in New Issue
Block a user