Compare commits
22 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
2fe705a712 | ||
|
3a448e9e17 | ||
|
6e4def4cc5 | ||
|
34b8fd83c1 | ||
|
d5774078e0 | ||
|
a3623412f9 | ||
|
1ed60537cf | ||
|
037d670fb7 | ||
|
8b817d50c5 | ||
|
fa48060a94 | ||
|
b41e982daf | ||
|
2fcb449ed8 | ||
|
218bc54a5f | ||
|
64f0333497 | ||
|
8aa7a5780d | ||
|
17c398bcee | ||
|
76f1455372 | ||
|
1bcad6d7cd | ||
|
9a58d9bcb7 | ||
|
c2dcac4413 | ||
|
394273e866 | ||
|
1bcc70c174 |
@ -0,0 +1,2 @@
|
||||
ALTER TABLE instances DROP COLUMN private_key;
|
||||
ALTER TABLE instances DROP COLUMN public_key;
|
@ -0,0 +1,2 @@
|
||||
ALTER TABLE instances ADD COLUMN private_key TEXT;
|
||||
ALTER TABLE instances ADD COLUMN public_key TEXT;
|
@ -0,0 +1,30 @@
|
||||
CREATE TABLE instances_old (
|
||||
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
public_domain VARCHAR NOT NULL,
|
||||
name VARCHAR NOT NULL,
|
||||
local BOOLEAN NOT NULL DEFAULT 'f',
|
||||
blocked BOOLEAN NOT NULL DEFAULT 'f',
|
||||
creation_date DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
open_registrations BOOLEAN NOT NULL DEFAULT 't',
|
||||
short_description TEXT NOT NULL DEFAULT '',
|
||||
long_description TEXT NOT NULL DEFAULT '',
|
||||
default_license TEXT NOT NULL DEFAULT 'CC-0',
|
||||
long_description_html VARCHAR NOT NULL DEFAULT '',
|
||||
short_description_html VARCHAR NOT NULL DEFAULT ''
|
||||
);
|
||||
INSERT INTO instances_old SELECT
|
||||
id,
|
||||
public_domain,
|
||||
name,
|
||||
local,
|
||||
blocked,
|
||||
creation_date,
|
||||
open_registrations,
|
||||
short_description,
|
||||
long_description,
|
||||
default_license,
|
||||
long_description_html,
|
||||
short_description_html
|
||||
FROM instances;
|
||||
DROP TABLE instances;
|
||||
ALTER TABLE instances_old RENAME TO instances;
|
@ -0,0 +1,2 @@
|
||||
ALTER TABLE instances ADD COLUMN private_key TEXT;
|
||||
ALTER TABLE instances ADD COLUMN public_key TEXT;
|
@ -1,6 +1,6 @@
|
||||
use clap::{App, Arg, ArgMatches, SubCommand};
|
||||
|
||||
use plume_models::{instance::*, safe_string::SafeString, Connection};
|
||||
use plume_models::{instance::NewInstance, Connection};
|
||||
use std::env;
|
||||
|
||||
pub fn command<'a, 'b>() -> App<'a, 'b> {
|
||||
@ -53,19 +53,5 @@ fn new<'a>(args: &ArgMatches<'a>, conn: &Connection) {
|
||||
.unwrap_or_else(|| String::from("CC-BY-SA"));
|
||||
let open_reg = !args.is_present("private");
|
||||
|
||||
Instance::insert(
|
||||
conn,
|
||||
NewInstance {
|
||||
public_domain: domain,
|
||||
name,
|
||||
local: true,
|
||||
long_description: SafeString::new(""),
|
||||
short_description: SafeString::new(""),
|
||||
default_license: license,
|
||||
open_registrations: open_reg,
|
||||
short_description_html: String::new(),
|
||||
long_description_html: String::new(),
|
||||
},
|
||||
)
|
||||
.expect("Couldn't save instance");
|
||||
NewInstance::new_local(conn, domain, name, open_reg, license).expect("Couldn't save instance");
|
||||
}
|
||||
|
@ -1,6 +1,11 @@
|
||||
use reqwest::header::{HeaderValue, ACCEPT};
|
||||
use std::fmt::Debug;
|
||||
|
||||
use super::{request, sign::Signer};
|
||||
use reqwest::{
|
||||
header::{HeaderValue, HOST},
|
||||
Url,
|
||||
};
|
||||
|
||||
/// Represents an ActivityPub inbox.
|
||||
///
|
||||
/// It routes an incoming Activity through the registered handlers.
|
||||
@ -10,7 +15,8 @@ use std::fmt::Debug;
|
||||
/// ```rust
|
||||
/// # extern crate activitypub;
|
||||
/// # use activitypub::{actor::Person, activity::{Announce, Create}, object::Note};
|
||||
/// # use plume_common::activity_pub::inbox::*;
|
||||
/// # use openssl::{hash::MessageDigest, pkey::PKey, rsa::Rsa};
|
||||
/// # use plume_common::activity_pub::{inbox::*, sign::{gen_keypair, Error as SignatureError, Result as SignatureResult, Signer}};
|
||||
/// # struct User;
|
||||
/// # impl FromId<()> for User {
|
||||
/// # type Error = ();
|
||||
@ -59,6 +65,42 @@ use std::fmt::Debug;
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// # }
|
||||
/// # 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) -> SignatureResult<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(|_| SignatureError())
|
||||
/// # }
|
||||
/// #
|
||||
/// # fn verify(&self, data: &str, signature: &[u8]) -> SignatureResult<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(|_| SignatureError())
|
||||
/// # }
|
||||
/// # }
|
||||
/// #
|
||||
/// # let mut act = Create::default();
|
||||
/// # act.object_props.set_id_string(String::from("https://test.ap/activity")).unwrap();
|
||||
@ -69,8 +111,9 @@ use std::fmt::Debug;
|
||||
/// # let activity_json = serde_json::to_value(act).unwrap();
|
||||
/// #
|
||||
/// # let conn = ();
|
||||
/// # let sender = MySigner::new();
|
||||
/// #
|
||||
/// let result: Result<(), ()> = Inbox::handle(&conn, activity_json)
|
||||
/// let result: Result<(), ()> = Inbox::handle(&conn, &sender, activity_json)
|
||||
/// .with::<User, Announce, Message>(None)
|
||||
/// .with::<User, Create, Message>(None)
|
||||
/// .done();
|
||||
@ -84,9 +127,10 @@ where
|
||||
/// # Structure
|
||||
///
|
||||
/// - the context to be passed to each handler.
|
||||
/// - the sender actor to sign request
|
||||
/// - the activity
|
||||
/// - the reason it has not been handled yet
|
||||
NotHandled(&'a C, serde_json::Value, InboxError<E>),
|
||||
NotHandled(&'a C, &'a dyn Signer, serde_json::Value, InboxError<E>),
|
||||
|
||||
/// A matching handler have been found but failed
|
||||
///
|
||||
@ -139,8 +183,12 @@ where
|
||||
///
|
||||
/// - `ctx`: the context to pass to each handler
|
||||
/// - `json`: the JSON representation of the incoming activity
|
||||
pub fn handle(ctx: &'a C, json: serde_json::Value) -> Inbox<'a, C, E, R> {
|
||||
Inbox::NotHandled(ctx, json, InboxError::NoMatch)
|
||||
pub fn handle(
|
||||
ctx: &'a C,
|
||||
sender: &'a dyn Signer,
|
||||
json: serde_json::Value,
|
||||
) -> Inbox<'a, C, E, R> {
|
||||
Inbox::NotHandled(ctx, sender, json, InboxError::NoMatch)
|
||||
}
|
||||
|
||||
/// Registers an handler on this Inbox.
|
||||
@ -151,27 +199,30 @@ where
|
||||
M: AsObject<A, V, &'a C, Error = E> + FromId<C, Error = E>,
|
||||
M::Output: Into<R>,
|
||||
{
|
||||
if let Inbox::NotHandled(ctx, mut act, e) = self {
|
||||
if let Inbox::NotHandled(ctx, sender, mut act, e) = self {
|
||||
if serde_json::from_value::<V>(act.clone()).is_ok() {
|
||||
let act_clone = act.clone();
|
||||
let act_id = match act_clone["id"].as_str() {
|
||||
Some(x) => x,
|
||||
None => return Inbox::NotHandled(ctx, act, InboxError::InvalidID),
|
||||
None => return Inbox::NotHandled(ctx, sender, act, InboxError::InvalidID),
|
||||
};
|
||||
|
||||
// Get the actor ID
|
||||
let actor_id = match get_id(act["actor"].clone()) {
|
||||
Some(x) => x,
|
||||
None => return Inbox::NotHandled(ctx, act, InboxError::InvalidActor(None)),
|
||||
None => {
|
||||
return Inbox::NotHandled(ctx, sender, act, InboxError::InvalidActor(None))
|
||||
}
|
||||
};
|
||||
|
||||
if Self::is_spoofed_activity(&actor_id, &act) {
|
||||
return Inbox::NotHandled(ctx, act, InboxError::InvalidObject(None));
|
||||
return Inbox::NotHandled(ctx, sender, act, InboxError::InvalidObject(None));
|
||||
}
|
||||
|
||||
// Transform this actor to a model (see FromId for details about the from_id function)
|
||||
let actor = match A::from_id(
|
||||
ctx,
|
||||
sender,
|
||||
&actor_id,
|
||||
serde_json::from_value(act["actor"].clone()).ok(),
|
||||
proxy,
|
||||
@ -182,17 +233,25 @@ where
|
||||
if let Some(json) = json {
|
||||
act["actor"] = json;
|
||||
}
|
||||
return Inbox::NotHandled(ctx, act, InboxError::InvalidActor(Some(e)));
|
||||
return Inbox::NotHandled(
|
||||
ctx,
|
||||
sender,
|
||||
act,
|
||||
InboxError::InvalidActor(Some(e)),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// Same logic for "object"
|
||||
let obj_id = match get_id(act["object"].clone()) {
|
||||
Some(x) => x,
|
||||
None => return Inbox::NotHandled(ctx, act, InboxError::InvalidObject(None)),
|
||||
None => {
|
||||
return Inbox::NotHandled(ctx, sender, act, InboxError::InvalidObject(None))
|
||||
}
|
||||
};
|
||||
let obj = match M::from_id(
|
||||
ctx,
|
||||
sender,
|
||||
&obj_id,
|
||||
serde_json::from_value(act["object"].clone()).ok(),
|
||||
proxy,
|
||||
@ -202,7 +261,12 @@ where
|
||||
if let Some(json) = json {
|
||||
act["object"] = json;
|
||||
}
|
||||
return Inbox::NotHandled(ctx, act, InboxError::InvalidObject(Some(e)));
|
||||
return Inbox::NotHandled(
|
||||
ctx,
|
||||
sender,
|
||||
act,
|
||||
InboxError::InvalidObject(Some(e)),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@ -214,7 +278,7 @@ where
|
||||
} else {
|
||||
// If the Activity type is not matching the expected one for
|
||||
// this handler, try with the next one.
|
||||
Inbox::NotHandled(ctx, act, e)
|
||||
Inbox::NotHandled(ctx, sender, act, e)
|
||||
}
|
||||
} else {
|
||||
self
|
||||
@ -225,7 +289,7 @@ where
|
||||
pub fn done(self) -> Result<R, E> {
|
||||
match self {
|
||||
Inbox::Handled(res) => Ok(res),
|
||||
Inbox::NotHandled(_, _, err) => Err(E::from(err)),
|
||||
Inbox::NotHandled(_, _, _, err) => Err(E::from(err)),
|
||||
Inbox::Failed(err) => Err(err),
|
||||
}
|
||||
}
|
||||
@ -292,6 +356,7 @@ pub trait FromId<C>: Sized {
|
||||
/// If absent, the ID will be dereferenced.
|
||||
fn from_id(
|
||||
ctx: &C,
|
||||
sender: &dyn Signer,
|
||||
id: &str,
|
||||
object: Option<Self::Object>,
|
||||
proxy: Option<&reqwest::Proxy>,
|
||||
@ -300,7 +365,7 @@ pub trait FromId<C>: Sized {
|
||||
Ok(x) => Ok(x),
|
||||
_ => match object {
|
||||
Some(o) => Self::from_activity(ctx, o).map_err(|e| (None, e)),
|
||||
None => Self::from_activity(ctx, Self::deref(id, proxy.cloned())?)
|
||||
None => Self::from_activity(ctx, Self::deref(id, sender, proxy.cloned())?)
|
||||
.map_err(|e| (None, e)),
|
||||
},
|
||||
}
|
||||
@ -309,8 +374,17 @@ pub trait FromId<C>: Sized {
|
||||
/// Dereferences an ID
|
||||
fn deref(
|
||||
id: &str,
|
||||
sender: &dyn Signer,
|
||||
proxy: Option<reqwest::Proxy>,
|
||||
) -> Result<Self::Object, (Option<serde_json::Value>, Self::Error)> {
|
||||
let mut headers = request::headers();
|
||||
let url = Url::parse(&id).map_err(|_| (None, InboxError::InvalidID.into()))?;
|
||||
if !url.has_host() {
|
||||
return Err((None, InboxError::InvalidID.into()));
|
||||
}
|
||||
let host_header_value = HeaderValue::from_str(&url.host_str().expect("Unreachable"))
|
||||
.map_err(|_| (None, InboxError::DerefError.into()))?;
|
||||
headers.insert(HOST, host_header_value);
|
||||
if let Some(proxy) = proxy {
|
||||
reqwest::ClientBuilder::new().proxy(proxy)
|
||||
} else {
|
||||
@ -320,14 +394,10 @@ pub trait FromId<C>: Sized {
|
||||
.build()
|
||||
.map_err(|_| (None, InboxError::DerefError.into()))?
|
||||
.get(id)
|
||||
.headers(headers.clone())
|
||||
.header(
|
||||
ACCEPT,
|
||||
HeaderValue::from_str(
|
||||
&super::ap_accept_header()
|
||||
.into_iter()
|
||||
.collect::<Vec<_>>()
|
||||
.join(", "),
|
||||
)
|
||||
"Signature",
|
||||
request::signature(sender, &headers, ("get", url.path(), url.query()))
|
||||
.map_err(|_| (None, InboxError::DerefError.into()))?,
|
||||
)
|
||||
.send()
|
||||
@ -458,8 +528,10 @@ where
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::super::sign::{gen_keypair, Error as SignatureError, Result as SignatureResult};
|
||||
use super::*;
|
||||
use activitypub::{activity::*, actor::Person, object::Note};
|
||||
use openssl::{hash::MessageDigest, pkey::PKey, rsa::Rsa};
|
||||
|
||||
struct MyActor;
|
||||
impl FromId<()> for MyActor {
|
||||
@ -557,10 +629,47 @@ mod tests {
|
||||
act
|
||||
}
|
||||
|
||||
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) -> SignatureResult<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(|_| SignatureError())
|
||||
}
|
||||
|
||||
fn verify(&self, data: &str, signature: &[u8]) -> SignatureResult<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(|_| SignatureError())
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_inbox_basic() {
|
||||
let act = serde_json::to_value(build_create()).unwrap();
|
||||
let res: Result<(), ()> = Inbox::handle(&(), act)
|
||||
let res: Result<(), ()> = Inbox::handle(&(), &MySigner::new(), act)
|
||||
.with::<MyActor, Create, MyObject>(None)
|
||||
.done();
|
||||
assert!(res.is_ok());
|
||||
@ -569,7 +678,7 @@ mod tests {
|
||||
#[test]
|
||||
fn test_inbox_multi_handlers() {
|
||||
let act = serde_json::to_value(build_create()).unwrap();
|
||||
let res: Result<(), ()> = Inbox::handle(&(), act)
|
||||
let res: Result<(), ()> = Inbox::handle(&(), &MySigner::new(), act)
|
||||
.with::<MyActor, Announce, MyObject>(None)
|
||||
.with::<MyActor, Delete, MyObject>(None)
|
||||
.with::<MyActor, Create, MyObject>(None)
|
||||
@ -582,7 +691,7 @@ mod tests {
|
||||
fn test_inbox_failure() {
|
||||
let act = serde_json::to_value(build_create()).unwrap();
|
||||
// Create is not handled by this inbox
|
||||
let res: Result<(), ()> = Inbox::handle(&(), act)
|
||||
let res: Result<(), ()> = Inbox::handle(&(), &MySigner::new(), act)
|
||||
.with::<MyActor, Announce, MyObject>(None)
|
||||
.with::<MyActor, Like, MyObject>(None)
|
||||
.done();
|
||||
@ -631,12 +740,12 @@ mod tests {
|
||||
fn test_inbox_actor_failure() {
|
||||
let act = serde_json::to_value(build_create()).unwrap();
|
||||
|
||||
let res: Result<(), ()> = Inbox::handle(&(), act.clone())
|
||||
let res: Result<(), ()> = Inbox::handle(&(), &MySigner::new(), act.clone())
|
||||
.with::<FailingActor, Create, MyObject>(None)
|
||||
.done();
|
||||
assert!(res.is_err());
|
||||
|
||||
let res: Result<(), ()> = Inbox::handle(&(), act.clone())
|
||||
let res: Result<(), ()> = Inbox::handle(&(), &MySigner::new(), act.clone())
|
||||
.with::<FailingActor, Create, MyObject>(None)
|
||||
.with::<MyActor, Create, MyObject>(None)
|
||||
.done();
|
||||
|
@ -118,8 +118,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> {
|
||||
@ -166,8 +166,10 @@ pub fn signature<S: Signer>(
|
||||
|
||||
#[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 as SignatureError, Result as SignatureResult, Signer,
|
||||
};
|
||||
use openssl::{hash::MessageDigest, pkey::PKey, rsa::Rsa};
|
||||
use reqwest::header::HeaderMap;
|
||||
|
||||
@ -187,26 +189,24 @@ 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) -> SignatureResult<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(|_| Error())
|
||||
signer.sign_to_vec().map_err(|_| SignatureError())
|
||||
}
|
||||
|
||||
fn verify(&self, data: &str, signature: &[u8]) -> Result<bool, Self::Error> {
|
||||
fn verify(&self, data: &str, signature: &[u8]) -> SignatureResult<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(|_| Error())
|
||||
verifier.verify(&signature).map_err(|_| SignatureError())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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",
|
||||
|
@ -18,7 +18,8 @@ use openssl::{
|
||||
};
|
||||
use plume_common::activity_pub::{
|
||||
inbox::{AsActor, FromId},
|
||||
sign, ActivityStream, ApSignature, Id, IntoId, PublicKey, Source,
|
||||
sign::{self, Error as SignatureError, Result as SignatureResult},
|
||||
ActivityStream, ApSignature, Id, IntoId, PublicKey, Source,
|
||||
};
|
||||
use url::Url;
|
||||
use webfinger::*;
|
||||
@ -149,7 +150,16 @@ impl Blog {
|
||||
.into_iter()
|
||||
.find(|l| l.mime_type == Some(String::from("application/activity+json")))
|
||||
.ok_or(Error::Webfinger)
|
||||
.and_then(|l| Blog::from_id(conn, &l.href?, None, CONFIG.proxy()).map_err(|(_, e)| e))
|
||||
.and_then(|l| {
|
||||
Blog::from_id(
|
||||
conn,
|
||||
&Instance::get_local().expect("Failed to get local instance"),
|
||||
&l.href?,
|
||||
None,
|
||||
CONFIG.proxy(),
|
||||
)
|
||||
.map_err(|(_, e)| e)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn to_activity(&self, conn: &Connection) -> Result<CustomGroup> {
|
||||
@ -359,6 +369,8 @@ impl FromId<DbConn> for Blog {
|
||||
open_registrations: true,
|
||||
short_description_html: String::new(),
|
||||
long_description_html: String::new(),
|
||||
private_key: None,
|
||||
public_key: None,
|
||||
},
|
||||
)
|
||||
})?;
|
||||
@ -372,7 +384,14 @@ impl FromId<DbConn> for Blog {
|
||||
Media::save_remote(
|
||||
conn,
|
||||
icon.object_props.url_string().ok()?,
|
||||
&User::from_id(conn, &owner, None, CONFIG.proxy()).ok()?,
|
||||
&User::from_id(
|
||||
conn,
|
||||
&Instance::get_local().expect("Failed to get local instance"),
|
||||
&owner,
|
||||
None,
|
||||
CONFIG.proxy(),
|
||||
)
|
||||
.ok()?,
|
||||
)
|
||||
.ok()
|
||||
})
|
||||
@ -388,7 +407,14 @@ impl FromId<DbConn> for Blog {
|
||||
Media::save_remote(
|
||||
conn,
|
||||
banner.object_props.url_string().ok()?,
|
||||
&User::from_id(conn, &owner, None, CONFIG.proxy()).ok()?,
|
||||
&User::from_id(
|
||||
conn,
|
||||
&Instance::get_local().expect("Failed to get local instance"),
|
||||
&owner,
|
||||
None,
|
||||
CONFIG.proxy(),
|
||||
)
|
||||
.ok()?,
|
||||
)
|
||||
.ok()
|
||||
})
|
||||
@ -451,24 +477,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>> {
|
||||
fn sign(&self, to_sign: &str) -> SignatureResult<Vec<u8>> {
|
||||
let key = self.get_keypair()?;
|
||||
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(SignatureError::from)
|
||||
}
|
||||
|
||||
fn verify(&self, data: &str, signature: &[u8]) -> Result<bool> {
|
||||
fn verify(&self, data: &str, signature: &[u8]) -> SignatureResult<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(SignatureError::from)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -236,6 +236,7 @@ impl FromId<DbConn> for Comment {
|
||||
})?,
|
||||
author_id: User::from_id(
|
||||
conn,
|
||||
&Instance::get_local().expect("Failed to get local instance"),
|
||||
¬e.object_props.attributed_to_link::<Id>()?,
|
||||
None,
|
||||
CONFIG.proxy(),
|
||||
@ -294,7 +295,13 @@ impl FromId<DbConn> for Comment {
|
||||
.collect::<HashSet<_>>() // remove duplicates (don't do a query more than once)
|
||||
.into_iter()
|
||||
.map(|v| {
|
||||
if let Ok(user) = User::from_id(conn, &v, None, CONFIG.proxy()) {
|
||||
if let Ok(user) = User::from_id(
|
||||
conn,
|
||||
&Instance::get_local().expect("Failed to get local instance"),
|
||||
&v,
|
||||
None,
|
||||
CONFIG.proxy(),
|
||||
) {
|
||||
vec![user]
|
||||
} else {
|
||||
vec![] // TODO try to fetch collection
|
||||
|
@ -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};
|
||||
@ -168,6 +168,7 @@ impl FromId<DbConn> for Follow {
|
||||
fn from_activity(conn: &DbConn, follow: FollowAct) -> Result<Self> {
|
||||
let actor = User::from_id(
|
||||
conn,
|
||||
&Instance::get_local().expect("Failed to get local instance"),
|
||||
&follow.follow_props.actor_link::<Id>()?,
|
||||
None,
|
||||
CONFIG.proxy(),
|
||||
@ -176,6 +177,7 @@ impl FromId<DbConn> for Follow {
|
||||
|
||||
let target = User::from_id(
|
||||
conn,
|
||||
&Instance::get_local().expect("Failed to get local instance"),
|
||||
&follow.follow_props.object_link::<Id>()?,
|
||||
None,
|
||||
CONFIG.proxy(),
|
||||
|
@ -3,7 +3,9 @@ use activitypub::activity::*;
|
||||
use crate::{
|
||||
comments::Comment,
|
||||
db_conn::DbConn,
|
||||
follows, likes,
|
||||
follows,
|
||||
instance::Instance,
|
||||
likes,
|
||||
posts::{Post, PostUpdate},
|
||||
reshares::Reshare,
|
||||
users::User,
|
||||
@ -47,7 +49,11 @@ impl_into_inbox_result! {
|
||||
}
|
||||
|
||||
pub fn inbox(conn: &DbConn, act: serde_json::Value) -> Result<InboxResult, Error> {
|
||||
Inbox::handle(conn, act)
|
||||
Inbox::handle(
|
||||
conn,
|
||||
&Instance::get_local().expect("Failed to get local instance"),
|
||||
act,
|
||||
)
|
||||
.with::<User, Announce, Post>(CONFIG.proxy())
|
||||
.with::<User, Create, Comment>(CONFIG.proxy())
|
||||
.with::<User, Create, Post>(CONFIG.proxy())
|
||||
|
@ -6,10 +6,26 @@ use crate::{
|
||||
users::{Role, User},
|
||||
Connection, Error, Result,
|
||||
};
|
||||
use activitypub::{actor::Service, CustomObject};
|
||||
use chrono::NaiveDateTime;
|
||||
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl};
|
||||
use plume_common::utils::md_to_html;
|
||||
use openssl::{
|
||||
hash::MessageDigest,
|
||||
pkey::{PKey, Private},
|
||||
rsa::Rsa,
|
||||
sign,
|
||||
};
|
||||
use plume_common::{
|
||||
activity_pub::{
|
||||
sign::{gen_keypair, Error as SignatureError, Result as SignatureResult, Signer},
|
||||
ApSignature, PublicKey,
|
||||
},
|
||||
utils::md_to_html,
|
||||
};
|
||||
use std::sync::RwLock;
|
||||
use tracing::warn;
|
||||
|
||||
pub type CustomService = CustomObject<ApSignature, Service>;
|
||||
|
||||
#[derive(Clone, Identifiable, Queryable)]
|
||||
pub struct Instance {
|
||||
@ -25,6 +41,8 @@ pub struct Instance {
|
||||
pub default_license: String,
|
||||
pub long_description_html: SafeString,
|
||||
pub short_description_html: SafeString,
|
||||
pub private_key: Option<String>,
|
||||
pub public_key: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Insertable)]
|
||||
@ -39,6 +57,8 @@ pub struct NewInstance {
|
||||
pub default_license: String,
|
||||
pub long_description_html: String,
|
||||
pub short_description_html: String,
|
||||
pub private_key: Option<String>,
|
||||
pub public_key: Option<String>,
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
@ -69,6 +89,13 @@ impl Instance {
|
||||
*LOCAL_INSTANCE.write().unwrap() = Instance::get_local_uncached(conn).ok();
|
||||
}
|
||||
|
||||
pub fn get_locals(conn: &Connection) -> Result<Vec<Instance>> {
|
||||
instances::table
|
||||
.filter(instances::local.eq(true))
|
||||
.load::<Instance>(conn)
|
||||
.map_err(Error::from)
|
||||
}
|
||||
|
||||
pub fn get_remotes(conn: &Connection) -> Result<Vec<Instance>> {
|
||||
instances::table
|
||||
.filter(instances::local.eq(false))
|
||||
@ -238,6 +265,112 @@ impl Instance {
|
||||
})
|
||||
.map_err(Error::from)
|
||||
}
|
||||
|
||||
pub fn set_keypair(&self, conn: &Connection) -> Result<()> {
|
||||
let (pub_key, priv_key) = gen_keypair();
|
||||
let private_key = String::from_utf8(priv_key).or(Err(Error::Signature))?;
|
||||
let public_key = String::from_utf8(pub_key).or(Err(Error::Signature))?;
|
||||
diesel::update(self)
|
||||
.set((
|
||||
instances::private_key.eq(Some(private_key)),
|
||||
instances::public_key.eq(Some(public_key)),
|
||||
))
|
||||
.execute(conn)
|
||||
.and(Ok(()))
|
||||
.map_err(Error::from)
|
||||
}
|
||||
|
||||
pub fn get_keypair(&self) -> Result<PKey<Private>> {
|
||||
PKey::from_rsa(Rsa::private_key_from_pem(
|
||||
self.private_key.clone()?.as_ref(),
|
||||
)?)
|
||||
.map_err(Error::from)
|
||||
}
|
||||
|
||||
/// This is experimental and might change in the future.
|
||||
/// Currently "!" sign is used but it's not decided.
|
||||
pub fn ap_url(&self) -> String {
|
||||
ap_url(&format!(
|
||||
"{}/!/{}",
|
||||
Self::get_local().unwrap().public_domain,
|
||||
self.public_domain
|
||||
))
|
||||
}
|
||||
|
||||
pub fn to_activity(&self) -> Result<CustomService> {
|
||||
let mut actor = Service::default();
|
||||
let id = self.ap_url();
|
||||
actor.object_props.set_id_string(id.clone())?;
|
||||
actor.object_props.set_name_string(self.name.clone())?;
|
||||
|
||||
let mut ap_signature = ApSignature::default();
|
||||
if self.local {
|
||||
if let Some(pub_key) = self.public_key.clone() {
|
||||
let mut public_key = PublicKey::default();
|
||||
public_key.set_id_string(format!("{}#main-key", id))?;
|
||||
public_key.set_owner_string(id)?;
|
||||
public_key.set_public_key_pem_string(pub_key)?;
|
||||
ap_signature.set_public_key_publickey(public_key)?;
|
||||
}
|
||||
};
|
||||
|
||||
Ok(CustomService::new(actor, ap_signature))
|
||||
}
|
||||
}
|
||||
|
||||
impl NewInstance {
|
||||
pub fn new_local(
|
||||
conn: &Connection,
|
||||
public_domain: String,
|
||||
name: String,
|
||||
open_registrations: bool,
|
||||
default_license: String,
|
||||
) -> Result<Instance> {
|
||||
let (pub_key, priv_key) = gen_keypair();
|
||||
|
||||
Instance::insert(
|
||||
conn,
|
||||
NewInstance {
|
||||
public_domain,
|
||||
name,
|
||||
local: true,
|
||||
open_registrations,
|
||||
short_description: SafeString::new(""),
|
||||
long_description: SafeString::new(""),
|
||||
default_license,
|
||||
long_description_html: String::new(),
|
||||
short_description_html: String::new(),
|
||||
private_key: Some(String::from_utf8(priv_key).or(Err(Error::Signature))?),
|
||||
public_key: Some(String::from_utf8(pub_key).or(Err(Error::Signature))?),
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Signer for Instance {
|
||||
fn get_key_id(&self) -> String {
|
||||
format!("{}#main-key", self.ap_url())
|
||||
}
|
||||
|
||||
fn sign(&self, to_sign: &str) -> SignatureResult<Vec<u8>> {
|
||||
let key = self.get_keypair()?;
|
||||
let mut signer = sign::Signer::new(MessageDigest::sha256(), &key)?;
|
||||
signer.update(to_sign.as_bytes())?;
|
||||
signer.sign_to_vec().map_err(SignatureError::from)
|
||||
}
|
||||
|
||||
fn verify(&self, data: &str, signature: &[u8]) -> SignatureResult<bool> {
|
||||
if self.public_key.is_none() {
|
||||
warn!("missing public key for {}", self.public_domain);
|
||||
return Err(SignatureError());
|
||||
}
|
||||
let key = PKey::from_rsa(Rsa::public_key_from_pem(
|
||||
self.public_key.clone().unwrap().as_ref(),
|
||||
)?)?;
|
||||
let mut verifier = sign::Verifier::new(MessageDigest::sha256(), &key)?;
|
||||
verifier.update(data.as_bytes())?;
|
||||
verifier.verify(&signature).map_err(SignatureError::from)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@ -259,6 +392,8 @@ pub(crate) mod tests {
|
||||
name: "My instance".to_string(),
|
||||
open_registrations: true,
|
||||
public_domain: "plu.me".to_string(),
|
||||
private_key: None,
|
||||
public_key: None,
|
||||
},
|
||||
NewInstance {
|
||||
default_license: "WTFPL".to_string(),
|
||||
@ -270,6 +405,8 @@ pub(crate) mod tests {
|
||||
name: "An instance".to_string(),
|
||||
open_registrations: true,
|
||||
public_domain: "1plu.me".to_string(),
|
||||
private_key: None,
|
||||
public_key: None,
|
||||
},
|
||||
NewInstance {
|
||||
default_license: "CC-0".to_string(),
|
||||
@ -281,6 +418,8 @@ pub(crate) mod tests {
|
||||
name: "Someone instance".to_string(),
|
||||
open_registrations: false,
|
||||
public_domain: "2plu.me".to_string(),
|
||||
private_key: None,
|
||||
public_key: None,
|
||||
},
|
||||
NewInstance {
|
||||
default_license: "CC-0-BY-SA".to_string(),
|
||||
@ -292,6 +431,8 @@ pub(crate) mod tests {
|
||||
name: "Nice day".to_string(),
|
||||
open_registrations: true,
|
||||
public_domain: "3plu.me".to_string(),
|
||||
private_key: None,
|
||||
public_key: None,
|
||||
},
|
||||
]
|
||||
.into_iter()
|
||||
|
@ -17,8 +17,10 @@ extern crate serde_json;
|
||||
#[macro_use]
|
||||
extern crate tantivy;
|
||||
|
||||
use db_conn::DbPool;
|
||||
use instance::Instance;
|
||||
use once_cell::sync::Lazy;
|
||||
use plume_common::activity_pub::inbox::InboxError;
|
||||
use plume_common::activity_pub::{inbox::InboxError, sign};
|
||||
use posts::PostEvent;
|
||||
use riker::actors::{channel, ActorSystem, ChannelRef, SystemBuilder};
|
||||
use users::UserEvent;
|
||||
@ -80,6 +82,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)
|
||||
@ -160,6 +168,12 @@ impl From<InboxError<Error>> for Error {
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
impl From<Error> for sign::Error {
|
||||
fn from(_: Error) -> Self {
|
||||
Self()
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds a function to a model, that returns the first
|
||||
/// matching row for a given list of fields.
|
||||
///
|
||||
@ -295,6 +309,17 @@ pub fn ap_url(url: &str) -> String {
|
||||
format!("https://{}", url)
|
||||
}
|
||||
|
||||
pub fn migrate_data(dbpool: &DbPool) -> Result<()> {
|
||||
ensure_local_instance_keys(&dbpool.get().unwrap())
|
||||
}
|
||||
|
||||
fn ensure_local_instance_keys(conn: &Connection) -> Result<()> {
|
||||
for instance in Instance::get_locals(conn)? {
|
||||
instance.set_keypair(conn)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[macro_use]
|
||||
mod tests {
|
||||
|
@ -1,6 +1,6 @@
|
||||
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;
|
||||
@ -117,6 +117,7 @@ impl FromId<DbConn> for Like {
|
||||
NewLike {
|
||||
post_id: Post::from_id(
|
||||
conn,
|
||||
&Instance::get_local().expect("Failed to get local instance"),
|
||||
&act.like_props.object_link::<Id>()?,
|
||||
None,
|
||||
CONFIG.proxy(),
|
||||
@ -125,6 +126,7 @@ impl FromId<DbConn> for Like {
|
||||
.id,
|
||||
user_id: User::from_id(
|
||||
conn,
|
||||
&Instance::get_local().expect("Failed to get local instance"),
|
||||
&act.like_props.actor_link::<Id>()?,
|
||||
None,
|
||||
CONFIG.proxy(),
|
||||
|
@ -272,6 +272,7 @@ impl Media {
|
||||
content_warning: image.object_props.summary_string().ok(),
|
||||
owner_id: User::from_id(
|
||||
conn,
|
||||
&Instance::get_local().expect("Failed to get local instance"),
|
||||
image
|
||||
.object_props
|
||||
.attributed_to_link_vec::<Id>()
|
||||
|
@ -630,13 +630,28 @@ impl FromId<DbConn> for Post {
|
||||
.into_iter()
|
||||
.fold((None, vec![]), |(blog, mut authors), link| {
|
||||
let url = link;
|
||||
match User::from_id(conn, &url, None, CONFIG.proxy()) {
|
||||
match User::from_id(
|
||||
conn,
|
||||
&Instance::get_local().expect("Failed to get local instance"),
|
||||
&url,
|
||||
None,
|
||||
CONFIG.proxy(),
|
||||
) {
|
||||
Ok(u) => {
|
||||
authors.push(u);
|
||||
(blog, authors)
|
||||
}
|
||||
Err(_) => (
|
||||
blog.or_else(|| Blog::from_id(conn, &url, None, CONFIG.proxy()).ok()),
|
||||
blog.or_else(|| {
|
||||
Blog::from_id(
|
||||
conn,
|
||||
&Instance::get_local().expect("Failed to get local instance"),
|
||||
&url,
|
||||
None,
|
||||
CONFIG.proxy(),
|
||||
)
|
||||
.ok()
|
||||
}),
|
||||
authors,
|
||||
),
|
||||
}
|
||||
@ -837,8 +852,14 @@ impl AsObject<User, Update, &DbConn> for PostUpdate {
|
||||
type Output = ();
|
||||
|
||||
fn activity(self, conn: &DbConn, actor: User, _id: &str) -> Result<()> {
|
||||
let mut post =
|
||||
Post::from_id(conn, &self.ap_url, None, CONFIG.proxy()).map_err(|(_, e)| e)?;
|
||||
let mut post = Post::from_id(
|
||||
conn,
|
||||
&Instance::get_local().expect("Failed to get local instance"),
|
||||
&self.ap_url,
|
||||
None,
|
||||
CONFIG.proxy(),
|
||||
)
|
||||
.map_err(|(_, e)| e)?;
|
||||
|
||||
if !post.is_author(conn, actor.id)? {
|
||||
// TODO: maybe the author was added in the meantime
|
||||
|
@ -1,6 +1,7 @@
|
||||
use crate::{
|
||||
db_conn::{DbConn, DbPool},
|
||||
follows,
|
||||
instance::Instance,
|
||||
posts::{LicensedArticle, Post},
|
||||
users::{User, UserEvent},
|
||||
ACTOR_SYS, CONFIG, USER_CHAN,
|
||||
@ -89,7 +90,13 @@ fn fetch_and_cache_followers(user: &Arc<User>, conn: &DbConn) {
|
||||
match follower_ids {
|
||||
Ok(user_ids) => {
|
||||
for user_id in user_ids {
|
||||
let follower = User::from_id(conn, &user_id, None, CONFIG.proxy());
|
||||
let follower = User::from_id(
|
||||
conn,
|
||||
&Instance::get_local().expect("Failed to get local instance"),
|
||||
&user_id,
|
||||
None,
|
||||
CONFIG.proxy(),
|
||||
);
|
||||
match follower {
|
||||
Ok(follower) => {
|
||||
let inserted = follows::Follow::insert(
|
||||
|
@ -1,6 +1,6 @@
|
||||
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;
|
||||
@ -142,6 +142,7 @@ impl FromId<DbConn> for Reshare {
|
||||
NewReshare {
|
||||
post_id: Post::from_id(
|
||||
conn,
|
||||
&Instance::get_local().expect("Failed to get local instance"),
|
||||
&act.announce_props.object_link::<Id>()?,
|
||||
None,
|
||||
CONFIG.proxy(),
|
||||
@ -150,6 +151,7 @@ impl FromId<DbConn> for Reshare {
|
||||
.id,
|
||||
user_id: User::from_id(
|
||||
conn,
|
||||
&Instance::get_local().expect("Failed to get local instance"),
|
||||
&act.announce_props.actor_link::<Id>()?,
|
||||
None,
|
||||
CONFIG.proxy(),
|
||||
|
@ -106,6 +106,8 @@ table! {
|
||||
default_license -> Text,
|
||||
long_description_html -> Varchar,
|
||||
short_description_html -> Varchar,
|
||||
private_key -> Nullable<Text>,
|
||||
public_key -> Nullable<Text>,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -164,6 +164,8 @@ mod tests {
|
||||
name: random_hex().to_string(),
|
||||
open_registrations: true,
|
||||
public_domain: random_hex().to_string(),
|
||||
private_key: None,
|
||||
public_key: None,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
@ -24,7 +24,7 @@ use plume_common::{
|
||||
activity_pub::{
|
||||
ap_accept_header,
|
||||
inbox::{AsActor, AsObject, FromId},
|
||||
sign::{gen_keypair, Signer},
|
||||
sign::{gen_keypair, Error as SignatureError, Result as SignatureResult, Signer},
|
||||
ActivityStream, ApSignature, Id, IntoId, PublicKey, PUBLIC_VISIBILITY,
|
||||
},
|
||||
utils,
|
||||
@ -210,7 +210,14 @@ impl User {
|
||||
.into_iter()
|
||||
.find(|l| l.mime_type == Some(String::from("application/activity+json")))
|
||||
.ok_or(Error::Webfinger)?;
|
||||
User::from_id(conn, link.href.as_ref()?, None, CONFIG.proxy()).map_err(|(_, e)| e)
|
||||
User::from_id(
|
||||
conn,
|
||||
&Instance::get_local().expect("Failed to get local instance"),
|
||||
link.href.as_ref()?,
|
||||
None,
|
||||
CONFIG.proxy(),
|
||||
)
|
||||
.map_err(|(_, e)| e)
|
||||
}
|
||||
|
||||
pub fn fetch_remote_interact_uri(acct: &str) -> Result<String> {
|
||||
@ -958,6 +965,8 @@ impl FromId<DbConn> for User {
|
||||
open_registrations: true,
|
||||
short_description_html: String::new(),
|
||||
long_description_html: String::new(),
|
||||
private_key: None,
|
||||
public_key: None,
|
||||
},
|
||||
)
|
||||
})?;
|
||||
@ -1063,24 +1072,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>> {
|
||||
fn sign(&self, to_sign: &str) -> SignatureResult<Vec<u8>> {
|
||||
let key = self.get_keypair()?;
|
||||
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(SignatureError::from)
|
||||
}
|
||||
|
||||
fn verify(&self, data: &str, signature: &[u8]) -> Result<bool> {
|
||||
fn verify(&self, data: &str, signature: &[u8]) -> SignatureResult<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(SignatureError::from)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -60,83 +60,83 @@ msgstr ""
|
||||
msgid "Optional"
|
||||
msgstr ""
|
||||
|
||||
# src/routes/blogs.rs:63
|
||||
# src/routes/blogs.rs:67
|
||||
msgid "To create a new blog, you need to be logged in"
|
||||
msgstr ""
|
||||
|
||||
# src/routes/blogs.rs:102
|
||||
# src/routes/blogs.rs:109
|
||||
msgid "A blog with the same name already exists."
|
||||
msgstr ""
|
||||
|
||||
# src/routes/blogs.rs:140
|
||||
# src/routes/blogs.rs:147
|
||||
msgid "Your blog was successfully created!"
|
||||
msgstr ""
|
||||
|
||||
# src/routes/blogs.rs:159
|
||||
# src/routes/blogs.rs:165
|
||||
msgid "Your blog was deleted."
|
||||
msgstr ""
|
||||
|
||||
# src/routes/blogs.rs:167
|
||||
# src/routes/blogs.rs:173
|
||||
msgid "You are not allowed to delete this blog."
|
||||
msgstr ""
|
||||
|
||||
# src/routes/blogs.rs:218
|
||||
# src/routes/blogs.rs:223
|
||||
msgid "You are not allowed to edit this blog."
|
||||
msgstr ""
|
||||
|
||||
# src/routes/blogs.rs:274
|
||||
# src/routes/blogs.rs:279
|
||||
msgid "You can't use this media as a blog icon."
|
||||
msgstr ""
|
||||
|
||||
# src/routes/blogs.rs:292
|
||||
# src/routes/blogs.rs:297
|
||||
msgid "You can't use this media as a blog banner."
|
||||
msgstr ""
|
||||
|
||||
# src/routes/blogs.rs:326
|
||||
# src/routes/blogs.rs:331
|
||||
msgid "Your blog information have been updated."
|
||||
msgstr ""
|
||||
|
||||
# src/routes/comments.rs:99
|
||||
# src/routes/comments.rs:100
|
||||
msgid "Your comment has been posted."
|
||||
msgstr ""
|
||||
|
||||
# src/routes/comments.rs:178
|
||||
# src/routes/comments.rs:177
|
||||
msgid "Your comment has been deleted."
|
||||
msgstr ""
|
||||
|
||||
# src/routes/instance.rs:118
|
||||
# src/routes/instance.rs:147
|
||||
msgid "Instance settings have been saved."
|
||||
msgstr ""
|
||||
|
||||
# src/routes/instance.rs:150
|
||||
# src/routes/instance.rs:180
|
||||
msgid "{} has been unblocked."
|
||||
msgstr ""
|
||||
|
||||
# src/routes/instance.rs:152
|
||||
# src/routes/instance.rs:182
|
||||
msgid "{} has been blocked."
|
||||
msgstr ""
|
||||
|
||||
# src/routes/instance.rs:201
|
||||
# src/routes/instance.rs:233
|
||||
msgid "Blocks deleted"
|
||||
msgstr ""
|
||||
|
||||
# src/routes/instance.rs:216
|
||||
# src/routes/instance.rs:249
|
||||
msgid "Email already blocked"
|
||||
msgstr ""
|
||||
|
||||
# src/routes/instance.rs:221
|
||||
# src/routes/instance.rs:254
|
||||
msgid "Email Blocked"
|
||||
msgstr ""
|
||||
|
||||
# src/routes/instance.rs:312
|
||||
# src/routes/instance.rs:347
|
||||
msgid "You can't change your own rights."
|
||||
msgstr ""
|
||||
|
||||
# src/routes/instance.rs:323
|
||||
# src/routes/instance.rs:358
|
||||
msgid "You are not allowed to take this action."
|
||||
msgstr ""
|
||||
|
||||
# src/routes/instance.rs:359
|
||||
# src/routes/instance.rs:393
|
||||
msgid "Done."
|
||||
msgstr ""
|
||||
|
||||
@ -144,23 +144,23 @@ msgstr ""
|
||||
msgid "To like a post, you need to be logged in"
|
||||
msgstr ""
|
||||
|
||||
# src/routes/medias.rs:145
|
||||
# src/routes/medias.rs:158
|
||||
msgid "Your media have been deleted."
|
||||
msgstr ""
|
||||
|
||||
# src/routes/medias.rs:150
|
||||
# src/routes/medias.rs:163
|
||||
msgid "You are not allowed to delete this media."
|
||||
msgstr ""
|
||||
|
||||
# src/routes/medias.rs:167
|
||||
# src/routes/medias.rs:180
|
||||
msgid "Your avatar has been updated."
|
||||
msgstr ""
|
||||
|
||||
# src/routes/medias.rs:172
|
||||
# src/routes/medias.rs:185
|
||||
msgid "You are not allowed to use this media."
|
||||
msgstr ""
|
||||
|
||||
# src/routes/notifications.rs:28
|
||||
# src/routes/notifications.rs:29
|
||||
msgid "To see your notifications, you need to be logged in"
|
||||
msgstr ""
|
||||
|
||||
@ -168,51 +168,51 @@ msgstr ""
|
||||
msgid "This post isn't published yet."
|
||||
msgstr ""
|
||||
|
||||
# src/routes/posts.rs:126
|
||||
# src/routes/posts.rs:125
|
||||
msgid "To write a new post, you need to be logged in"
|
||||
msgstr ""
|
||||
|
||||
# src/routes/posts.rs:143
|
||||
# src/routes/posts.rs:146
|
||||
msgid "You are not an author of this blog."
|
||||
msgstr ""
|
||||
|
||||
# src/routes/posts.rs:150
|
||||
# src/routes/posts.rs:153
|
||||
msgid "New post"
|
||||
msgstr ""
|
||||
|
||||
# src/routes/posts.rs:195
|
||||
# src/routes/posts.rs:198
|
||||
msgid "Edit {0}"
|
||||
msgstr ""
|
||||
|
||||
# src/routes/posts.rs:264
|
||||
# src/routes/posts.rs:267
|
||||
msgid "You are not allowed to publish on this blog."
|
||||
msgstr ""
|
||||
|
||||
# src/routes/posts.rs:363
|
||||
# src/routes/posts.rs:367
|
||||
msgid "Your article has been updated."
|
||||
msgstr ""
|
||||
|
||||
# src/routes/posts.rs:553
|
||||
# src/routes/posts.rs:556
|
||||
msgid "Your article has been saved."
|
||||
msgstr ""
|
||||
|
||||
# src/routes/posts.rs:560
|
||||
# src/routes/posts.rs:563
|
||||
msgid "New article"
|
||||
msgstr ""
|
||||
|
||||
# src/routes/posts.rs:597
|
||||
# src/routes/posts.rs:601
|
||||
msgid "You are not allowed to delete this article."
|
||||
msgstr ""
|
||||
|
||||
# src/routes/posts.rs:622
|
||||
# src/routes/posts.rs:625
|
||||
msgid "Your article has been deleted."
|
||||
msgstr ""
|
||||
|
||||
# src/routes/posts.rs:627
|
||||
# src/routes/posts.rs:630
|
||||
msgid "It looks like the article you tried to delete doesn't exist. Maybe it is already gone?"
|
||||
msgstr ""
|
||||
|
||||
# src/routes/posts.rs:667
|
||||
# src/routes/posts.rs:672
|
||||
msgid "Couldn't obtain enough information about your account. Please make sure your username is correct."
|
||||
msgstr ""
|
||||
|
||||
@ -220,63 +220,63 @@ msgstr ""
|
||||
msgid "To reshare a post, you need to be logged in"
|
||||
msgstr ""
|
||||
|
||||
# src/routes/session.rs:88
|
||||
# src/routes/session.rs:95
|
||||
msgid "You are now connected."
|
||||
msgstr ""
|
||||
|
||||
# src/routes/session.rs:109
|
||||
# src/routes/session.rs:116
|
||||
msgid "You are now logged off."
|
||||
msgstr ""
|
||||
|
||||
# src/routes/session.rs:154
|
||||
# src/routes/session.rs:162
|
||||
msgid "Password reset"
|
||||
msgstr ""
|
||||
|
||||
# src/routes/session.rs:155
|
||||
# src/routes/session.rs:163
|
||||
msgid "Here is the link to reset your password: {0}"
|
||||
msgstr ""
|
||||
|
||||
# src/routes/session.rs:215
|
||||
# src/routes/session.rs:235
|
||||
msgid "Your password was successfully reset."
|
||||
msgstr ""
|
||||
|
||||
# src/routes/user.rs:142
|
||||
# src/routes/user.rs:74
|
||||
msgid "To access your dashboard, you need to be logged in"
|
||||
msgstr ""
|
||||
|
||||
# src/routes/user.rs:164
|
||||
# src/routes/user.rs:96
|
||||
msgid "You are no longer following {}."
|
||||
msgstr ""
|
||||
|
||||
# src/routes/user.rs:181
|
||||
# src/routes/user.rs:113
|
||||
msgid "You are now following {}."
|
||||
msgstr ""
|
||||
|
||||
# src/routes/user.rs:261
|
||||
# src/routes/user.rs:190
|
||||
msgid "To subscribe to someone, you need to be logged in"
|
||||
msgstr ""
|
||||
|
||||
# src/routes/user.rs:365
|
||||
# src/routes/user.rs:299
|
||||
msgid "To edit your profile, you need to be logged in"
|
||||
msgstr ""
|
||||
|
||||
# src/routes/user.rs:411
|
||||
# src/routes/user.rs:345
|
||||
msgid "Your profile has been updated."
|
||||
msgstr ""
|
||||
|
||||
# src/routes/user.rs:438
|
||||
# src/routes/user.rs:373
|
||||
msgid "Your account has been deleted."
|
||||
msgstr ""
|
||||
|
||||
# src/routes/user.rs:444
|
||||
# src/routes/user.rs:379
|
||||
msgid "You can't delete someone else's account."
|
||||
msgstr ""
|
||||
|
||||
# src/routes/user.rs:528
|
||||
# src/routes/user.rs:463
|
||||
msgid "Registrations are closed on this instance."
|
||||
msgstr ""
|
||||
|
||||
# src/routes/user.rs:551
|
||||
# src/routes/user.rs:486
|
||||
msgid "Your account has been created. Now you just need to log in, before you can use it."
|
||||
msgstr ""
|
||||
|
||||
|
@ -26,7 +26,13 @@ pub fn handle_incoming(
|
||||
.or_else(|| activity["actor"]["id"].as_str())
|
||||
.ok_or(status::BadRequest(Some("Missing actor id for activity")))?;
|
||||
|
||||
let actor = User::from_id(&conn, actor_id, None, CONFIG.proxy())
|
||||
let actor = User::from_id(
|
||||
&conn,
|
||||
&Instance::get_local().expect("Failed to get local instance"),
|
||||
actor_id,
|
||||
None,
|
||||
CONFIG.proxy(),
|
||||
)
|
||||
.expect("instance::shared_inbox: user error");
|
||||
if !verify_http_headers(&actor, &headers.0, &sig).is_secure() && !act.clone().verify(&actor) {
|
||||
// maybe we just know an old key?
|
||||
|
@ -15,6 +15,7 @@ use diesel::r2d2::ConnectionManager;
|
||||
use plume_models::{
|
||||
db_conn::{DbPool, PragmaForeignKey},
|
||||
instance::Instance,
|
||||
migrate_data,
|
||||
migrations::IMPORTED_MIGRATIONS,
|
||||
remote_fetch_actor::RemoteFetchActor,
|
||||
search::{actor::SearchActor, Searcher as UnmanagedSearcher},
|
||||
@ -99,6 +100,7 @@ Then try to restart Plume.
|
||||
"#
|
||||
)
|
||||
}
|
||||
migrate_data(&dbpool).expect("Failed to migrate data");
|
||||
let workpool = ScheduledThreadPool::with_name("worker {}", num_cpus::get());
|
||||
// we want a fast exit here, so
|
||||
let searcher = Arc::new(UnmanagedSearcher::open_or_recreate(
|
||||
@ -147,6 +149,7 @@ Then try to restart Plume.
|
||||
routes::comments::delete,
|
||||
routes::comments::activity_pub,
|
||||
routes::instance::index,
|
||||
routes::instance::activity_details,
|
||||
routes::instance::admin,
|
||||
routes::instance::admin_mod,
|
||||
routes::instance::admin_instances,
|
||||
|
@ -438,6 +438,8 @@ mod tests {
|
||||
name: random_hex().to_string(),
|
||||
open_registrations: true,
|
||||
public_domain: random_hex().to_string(),
|
||||
private_key: None,
|
||||
public_key: None,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
@ -11,7 +11,7 @@ use validator::{Validate, ValidationErrors};
|
||||
use crate::inbox;
|
||||
use crate::routes::{errors::ErrorPage, rocket_uri_macro_static_files, Page, RespondOrRedirect};
|
||||
use crate::template_utils::{IntoContext, Ructe};
|
||||
use plume_common::activity_pub::{broadcast, inbox::FromId};
|
||||
use plume_common::activity_pub::{broadcast, inbox::FromId, ActivityStream, ApRequest};
|
||||
use plume_models::{
|
||||
admin::*,
|
||||
blocklisted_emails::*,
|
||||
@ -49,6 +49,36 @@ pub fn index(conn: DbConn, rockets: PlumeRocket) -> Result<Ructe, ErrorPage> {
|
||||
)))
|
||||
}
|
||||
|
||||
/// Experimental
|
||||
/// "!" might be change in the future
|
||||
///
|
||||
/// This exists for communication with Mastodon.
|
||||
/// Secure mode Mastodon requires HTTP signature for event GET requests.
|
||||
/// To sign requests, public key of request sender is required. We decided
|
||||
/// the sender is local instance.
|
||||
#[get("/!/<public_domain>")]
|
||||
pub fn activity_details(
|
||||
public_domain: String,
|
||||
conn: DbConn,
|
||||
_ap: ApRequest,
|
||||
) -> Option<ActivityStream<CustomService>> {
|
||||
if let Ok(instance) = Instance::find_by_domain(&conn, &public_domain) {
|
||||
if !instance.local {
|
||||
return None;
|
||||
}
|
||||
match instance.to_activity() {
|
||||
Ok(activity) => Some(ActivityStream::new(activity)),
|
||||
Err(plume_models::Error::NotFound) => None,
|
||||
Err(err) => {
|
||||
tracing::error!("{:?}", err);
|
||||
panic!();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/admin")]
|
||||
pub fn admin(_admin: Admin, conn: DbConn, rockets: PlumeRocket) -> Result<Ructe, ErrorPage> {
|
||||
let local_inst = Instance::get_local()?;
|
||||
@ -404,7 +434,13 @@ pub fn interact(conn: DbConn, user: Option<User>, target: String) -> Option<Redi
|
||||
return Some(Redirect::to(uri!(super::user::details: name = target)));
|
||||
}
|
||||
|
||||
if let Ok(post) = Post::from_id(&conn, &target, None, CONFIG.proxy()) {
|
||||
if let Ok(post) = Post::from_id(
|
||||
&conn,
|
||||
&Instance::get_local().expect("Failed to get local instance"),
|
||||
&target,
|
||||
None,
|
||||
CONFIG.proxy(),
|
||||
) {
|
||||
return Some(Redirect::to(uri!(
|
||||
super::posts::details: blog = post.get_blog(&conn).expect("Can't retrieve blog").fqn,
|
||||
slug = &post.slug,
|
||||
@ -412,7 +448,13 @@ pub fn interact(conn: DbConn, user: Option<User>, target: String) -> Option<Redi
|
||||
)));
|
||||
}
|
||||
|
||||
if let Ok(comment) = Comment::from_id(&conn, &target, None, CONFIG.proxy()) {
|
||||
if let Ok(comment) = Comment::from_id(
|
||||
&conn,
|
||||
&Instance::get_local().expect("Failed to get local instance"),
|
||||
&target,
|
||||
None,
|
||||
CONFIG.proxy(),
|
||||
) {
|
||||
if comment.can_see(&conn, user.as_ref()) {
|
||||
let post = comment.get_post(&conn).expect("Can't retrieve post");
|
||||
return Some(Redirect::to(uri!(
|
||||
|
Loading…
Reference in New Issue
Block a user