Compare commits

...

22 Commits

Author SHA1 Message Date
Kitaiti Makoto
2fe705a712 Append semicolon at end of SQL 2021-09-24 05:38:16 +09:00
Kitaiti Makoto
3a448e9e17 Sign GET request to external instances 2021-09-24 04:27:22 +09:00
Kitaiti Makoto
6e4def4cc5 Implement Signer for Instance 2021-09-24 01:06:19 +09:00
Kitaiti Makoto
34b8fd83c1 Fix SQL to drop fields 2021-09-12 04:20:10 +09:00
Kitaiti Makoto
d5774078e0 Revert "Move Rocket-unreleated code from init_rocket() to main()"
This reverts commit 64f0333497.
2021-09-12 04:16:30 +09:00
Kitaiti Makoto
a3623412f9 Use Instance::get_locals() in ensure_local_instance_keys() 2021-09-12 03:57:30 +09:00
Kitaiti Makoto
1ed60537cf Define Instance::get_locals() 2021-09-12 03:57:12 +09:00
Kitaiti Makoto
037d670fb7 Make tests follow field addition to instances table 2021-09-12 03:09:28 +09:00
Kitaiti Makoto
8b817d50c5 Add URI /!/<public_domain> 2021-09-12 02:14:49 +09:00
Kitaiti Makoto
fa48060a94 Define Instance::to_activity() 2021-09-12 02:14:17 +09:00
Kitaiti Makoto
b41e982daf Run plume_models::migrate_data() on initialization 2021-09-11 23:06:49 +09:00
Kitaiti Makoto
2fcb449ed8 Define plume_models::migrate_data() 2021-09-11 23:06:02 +09:00
Kitaiti Makoto
218bc54a5f Define Instance::set_keypair() 2021-09-11 23:05:31 +09:00
Kitaiti Makoto
64f0333497 Move Rocket-unreleated code from init_rocket() to main() 2021-09-11 22:23:26 +09:00
Kitaiti Makoto
8aa7a5780d Use NewInstance::new_local in cli instance command 2021-09-11 22:20:34 +09:00
Kitaiti Makoto
17c398bcee Define NewInstance::new_local() 2021-09-11 22:17:40 +09:00
Kitaiti Makoto
76f1455372 Follow addition of key fields to instances table 2021-09-11 21:59:48 +09:00
Kitaiti Makoto
1bcad6d7cd Run diesel migration run 2021-09-11 21:58:15 +09:00
Kitaiti Makoto
9a58d9bcb7 Copy instances_add_keys migration for SQLite 2021-09-09 13:57:12 +09:00
Kitaiti Makoto
c2dcac4413 Add private_key and public_key fields to instances table 2021-09-09 13:56:28 +09:00
Kitaiti Makoto
394273e866 Generate migration files to add keys to instances
% diesel migration generate instances_add_keys
2021-09-08 22:12:07 +09:00
Kitaiti Makoto
1bcc70c174 [REFACTORING]Use headers utility function for deref() 2021-09-08 20:29:26 +09:00
27 changed files with 603 additions and 165 deletions

View File

@ -0,0 +1,2 @@
ALTER TABLE instances DROP COLUMN private_key;
ALTER TABLE instances DROP COLUMN public_key;

View File

@ -0,0 +1,2 @@
ALTER TABLE instances ADD COLUMN private_key TEXT;
ALTER TABLE instances ADD COLUMN public_key TEXT;

View File

@ -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;

View File

@ -0,0 +1,2 @@
ALTER TABLE instances ADD COLUMN private_key TEXT;
ALTER TABLE instances ADD COLUMN public_key TEXT;

View File

@ -1,6 +1,6 @@
use clap::{App, Arg, ArgMatches, SubCommand}; use clap::{App, Arg, ArgMatches, SubCommand};
use plume_models::{instance::*, safe_string::SafeString, Connection}; use plume_models::{instance::NewInstance, Connection};
use std::env; use std::env;
pub fn command<'a, 'b>() -> App<'a, 'b> { 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")); .unwrap_or_else(|| String::from("CC-BY-SA"));
let open_reg = !args.is_present("private"); let open_reg = !args.is_present("private");
Instance::insert( NewInstance::new_local(conn, domain, name, open_reg, license).expect("Couldn't save instance");
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");
} }

View File

@ -1,6 +1,11 @@
use reqwest::header::{HeaderValue, ACCEPT};
use std::fmt::Debug; use std::fmt::Debug;
use super::{request, sign::Signer};
use reqwest::{
header::{HeaderValue, HOST},
Url,
};
/// Represents an ActivityPub inbox. /// Represents an ActivityPub inbox.
/// ///
/// It routes an incoming Activity through the registered handlers. /// It routes an incoming Activity through the registered handlers.
@ -10,7 +15,8 @@ use std::fmt::Debug;
/// ```rust /// ```rust
/// # extern crate activitypub; /// # extern crate activitypub;
/// # use activitypub::{actor::Person, activity::{Announce, Create}, object::Note}; /// # 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; /// # struct User;
/// # impl FromId<()> for User { /// # impl FromId<()> for User {
/// # type Error = (); /// # type Error = ();
@ -59,6 +65,42 @@ use std::fmt::Debug;
/// # Ok(()) /// # 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(); /// # let mut act = Create::default();
/// # act.object_props.set_id_string(String::from("https://test.ap/activity")).unwrap(); /// # 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 activity_json = serde_json::to_value(act).unwrap();
/// # /// #
/// # let conn = (); /// # 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, Announce, Message>(None)
/// .with::<User, Create, Message>(None) /// .with::<User, Create, Message>(None)
/// .done(); /// .done();
@ -84,9 +127,10 @@ where
/// # Structure /// # Structure
/// ///
/// - the context to be passed to each handler. /// - the context to be passed to each handler.
/// - the sender actor to sign request
/// - the activity /// - the activity
/// - the reason it has not been handled yet /// - 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 /// A matching handler have been found but failed
/// ///
@ -139,8 +183,12 @@ where
/// ///
/// - `ctx`: the context to pass to each handler /// - `ctx`: the context to pass to each handler
/// - `json`: the JSON representation of the incoming activity /// - `json`: the JSON representation of the incoming activity
pub fn handle(ctx: &'a C, json: serde_json::Value) -> Inbox<'a, C, E, R> { pub fn handle(
Inbox::NotHandled(ctx, json, InboxError::NoMatch) 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. /// Registers an handler on this Inbox.
@ -151,27 +199,30 @@ where
M: AsObject<A, V, &'a C, Error = E> + FromId<C, Error = E>, M: AsObject<A, V, &'a C, Error = E> + FromId<C, Error = E>,
M::Output: Into<R>, 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() { if serde_json::from_value::<V>(act.clone()).is_ok() {
let act_clone = act.clone(); let act_clone = act.clone();
let act_id = match act_clone["id"].as_str() { let act_id = match act_clone["id"].as_str() {
Some(x) => x, Some(x) => x,
None => return Inbox::NotHandled(ctx, act, InboxError::InvalidID), None => return Inbox::NotHandled(ctx, sender, act, InboxError::InvalidID),
}; };
// Get the actor ID // Get the actor ID
let actor_id = match get_id(act["actor"].clone()) { let actor_id = match get_id(act["actor"].clone()) {
Some(x) => x, 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) { 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) // Transform this actor to a model (see FromId for details about the from_id function)
let actor = match A::from_id( let actor = match A::from_id(
ctx, ctx,
sender,
&actor_id, &actor_id,
serde_json::from_value(act["actor"].clone()).ok(), serde_json::from_value(act["actor"].clone()).ok(),
proxy, proxy,
@ -182,17 +233,25 @@ where
if let Some(json) = json { if let Some(json) = json {
act["actor"] = 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" // Same logic for "object"
let obj_id = match get_id(act["object"].clone()) { let obj_id = match get_id(act["object"].clone()) {
Some(x) => x, 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( let obj = match M::from_id(
ctx, ctx,
sender,
&obj_id, &obj_id,
serde_json::from_value(act["object"].clone()).ok(), serde_json::from_value(act["object"].clone()).ok(),
proxy, proxy,
@ -202,7 +261,12 @@ where
if let Some(json) = json { if let Some(json) = json {
act["object"] = 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 { } else {
// If the Activity type is not matching the expected one for // If the Activity type is not matching the expected one for
// this handler, try with the next one. // this handler, try with the next one.
Inbox::NotHandled(ctx, act, e) Inbox::NotHandled(ctx, sender, act, e)
} }
} else { } else {
self self
@ -225,7 +289,7 @@ where
pub fn done(self) -> Result<R, E> { pub fn done(self) -> Result<R, E> {
match self { match self {
Inbox::Handled(res) => Ok(res), Inbox::Handled(res) => Ok(res),
Inbox::NotHandled(_, _, err) => Err(E::from(err)), Inbox::NotHandled(_, _, _, err) => Err(E::from(err)),
Inbox::Failed(err) => Err(err), Inbox::Failed(err) => Err(err),
} }
} }
@ -292,6 +356,7 @@ pub trait FromId<C>: Sized {
/// If absent, the ID will be dereferenced. /// If absent, the ID will be dereferenced.
fn from_id( fn from_id(
ctx: &C, ctx: &C,
sender: &dyn Signer,
id: &str, id: &str,
object: Option<Self::Object>, object: Option<Self::Object>,
proxy: Option<&reqwest::Proxy>, proxy: Option<&reqwest::Proxy>,
@ -300,7 +365,7 @@ pub trait FromId<C>: Sized {
Ok(x) => Ok(x), Ok(x) => Ok(x),
_ => match object { _ => match object {
Some(o) => Self::from_activity(ctx, o).map_err(|e| (None, e)), 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)), .map_err(|e| (None, e)),
}, },
} }
@ -309,8 +374,17 @@ pub trait FromId<C>: Sized {
/// Dereferences an ID /// Dereferences an ID
fn deref( fn deref(
id: &str, id: &str,
sender: &dyn Signer,
proxy: Option<reqwest::Proxy>, proxy: Option<reqwest::Proxy>,
) -> Result<Self::Object, (Option<serde_json::Value>, Self::Error)> { ) -> 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 { if let Some(proxy) = proxy {
reqwest::ClientBuilder::new().proxy(proxy) reqwest::ClientBuilder::new().proxy(proxy)
} else { } else {
@ -320,14 +394,10 @@ pub trait FromId<C>: Sized {
.build() .build()
.map_err(|_| (None, InboxError::DerefError.into()))? .map_err(|_| (None, InboxError::DerefError.into()))?
.get(id) .get(id)
.headers(headers.clone())
.header( .header(
ACCEPT, "Signature",
HeaderValue::from_str( request::signature(sender, &headers, ("get", url.path(), url.query()))
&super::ap_accept_header()
.into_iter()
.collect::<Vec<_>>()
.join(", "),
)
.map_err(|_| (None, InboxError::DerefError.into()))?, .map_err(|_| (None, InboxError::DerefError.into()))?,
) )
.send() .send()
@ -458,8 +528,10 @@ where
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::super::sign::{gen_keypair, Error as SignatureError, Result as SignatureResult};
use super::*; use super::*;
use activitypub::{activity::*, actor::Person, object::Note}; use activitypub::{activity::*, actor::Person, object::Note};
use openssl::{hash::MessageDigest, pkey::PKey, rsa::Rsa};
struct MyActor; struct MyActor;
impl FromId<()> for MyActor { impl FromId<()> for MyActor {
@ -557,10 +629,47 @@ mod tests {
act 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] #[test]
fn test_inbox_basic() { fn test_inbox_basic() {
let act = serde_json::to_value(build_create()).unwrap(); 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) .with::<MyActor, Create, MyObject>(None)
.done(); .done();
assert!(res.is_ok()); assert!(res.is_ok());
@ -569,7 +678,7 @@ mod tests {
#[test] #[test]
fn test_inbox_multi_handlers() { fn test_inbox_multi_handlers() {
let act = serde_json::to_value(build_create()).unwrap(); 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, Announce, MyObject>(None)
.with::<MyActor, Delete, MyObject>(None) .with::<MyActor, Delete, MyObject>(None)
.with::<MyActor, Create, MyObject>(None) .with::<MyActor, Create, MyObject>(None)
@ -582,7 +691,7 @@ mod tests {
fn test_inbox_failure() { fn test_inbox_failure() {
let act = serde_json::to_value(build_create()).unwrap(); let act = serde_json::to_value(build_create()).unwrap();
// Create is not handled by this inbox // 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, Announce, MyObject>(None)
.with::<MyActor, Like, MyObject>(None) .with::<MyActor, Like, MyObject>(None)
.done(); .done();
@ -631,12 +740,12 @@ mod tests {
fn test_inbox_actor_failure() { fn test_inbox_actor_failure() {
let act = serde_json::to_value(build_create()).unwrap(); 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) .with::<FailingActor, Create, MyObject>(None)
.done(); .done();
assert!(res.is_err()); 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::<FailingActor, Create, MyObject>(None)
.with::<MyActor, Create, MyObject>(None) .with::<MyActor, Create, MyObject>(None)
.done(); .done();

View File

@ -118,8 +118,8 @@ type Path<'a> = &'a str;
type Query<'a> = &'a str; type Query<'a> = &'a str;
type RequestTarget<'a> = (Method<'a>, Path<'a>, Option<Query<'a>>); type RequestTarget<'a> = (Method<'a>, Path<'a>, Option<Query<'a>>);
pub fn signature<S: Signer>( pub fn signature(
signer: &S, signer: &dyn Signer,
headers: &HeaderMap, headers: &HeaderMap,
request_target: RequestTarget, request_target: RequestTarget,
) -> Result<HeaderValue, Error> { ) -> Result<HeaderValue, Error> {
@ -166,8 +166,10 @@ pub fn signature<S: Signer>(
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::{signature, Error}; use super::signature;
use crate::activity_pub::sign::{gen_keypair, Signer}; use crate::activity_pub::sign::{
gen_keypair, Error as SignatureError, Result as SignatureResult, Signer,
};
use openssl::{hash::MessageDigest, pkey::PKey, rsa::Rsa}; use openssl::{hash::MessageDigest, pkey::PKey, rsa::Rsa};
use reqwest::header::HeaderMap; use reqwest::header::HeaderMap;
@ -187,26 +189,24 @@ mod tests {
} }
impl Signer for MySigner { impl Signer for MySigner {
type Error = Error;
fn get_key_id(&self) -> String { fn get_key_id(&self) -> String {
"mysigner".into() "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()) let key = PKey::from_rsa(Rsa::private_key_from_pem(self.private_key.as_ref()).unwrap())
.unwrap(); .unwrap();
let mut signer = openssl::sign::Signer::new(MessageDigest::sha256(), &key).unwrap(); let mut signer = openssl::sign::Signer::new(MessageDigest::sha256(), &key).unwrap();
signer.update(to_sign.as_bytes()).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()) let key = PKey::from_rsa(Rsa::public_key_from_pem(self.public_key.as_ref()).unwrap())
.unwrap(); .unwrap();
let mut verifier = openssl::sign::Verifier::new(MessageDigest::sha256(), &key).unwrap(); let mut verifier = openssl::sign::Verifier::new(MessageDigest::sha256(), &key).unwrap();
verifier.update(data.as_bytes()).unwrap(); verifier.update(data.as_bytes()).unwrap();
verifier.verify(&signature).map_err(|_| Error()) verifier.verify(&signature).map_err(|_| SignatureError())
} }
} }

View File

@ -19,20 +19,25 @@ pub fn gen_keypair() -> (Vec<u8>, Vec<u8>) {
#[derive(Debug)] #[derive(Debug)]
pub struct Error(); 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 { pub trait Signer {
type Error;
fn get_key_id(&self) -> String; fn get_key_id(&self) -> String;
/// Sign some data with the signer keypair /// 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 /// 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 { 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 where
T: Signer; T: Signer;
fn verify<T>(self, creator: &T) -> bool fn verify<T>(self, creator: &T) -> bool
@ -46,7 +51,7 @@ pub trait Signable {
} }
impl Signable for serde_json::Value { 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 creation_date = Utc::now().to_rfc3339();
let mut options = json!({ let mut options = json!({
"type": "RsaSignature2017", "type": "RsaSignature2017",

View File

@ -18,7 +18,8 @@ use openssl::{
}; };
use plume_common::activity_pub::{ use plume_common::activity_pub::{
inbox::{AsActor, FromId}, 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 url::Url;
use webfinger::*; use webfinger::*;
@ -149,7 +150,16 @@ impl Blog {
.into_iter() .into_iter()
.find(|l| l.mime_type == Some(String::from("application/activity+json"))) .find(|l| l.mime_type == Some(String::from("application/activity+json")))
.ok_or(Error::Webfinger) .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> { pub fn to_activity(&self, conn: &Connection) -> Result<CustomGroup> {
@ -359,6 +369,8 @@ impl FromId<DbConn> for Blog {
open_registrations: true, open_registrations: true,
short_description_html: String::new(), short_description_html: String::new(),
long_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( Media::save_remote(
conn, conn,
icon.object_props.url_string().ok()?, 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() .ok()
}) })
@ -388,7 +407,14 @@ impl FromId<DbConn> for Blog {
Media::save_remote( Media::save_remote(
conn, conn,
banner.object_props.url_string().ok()?, 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() .ok()
}) })
@ -451,24 +477,22 @@ impl AsActor<&PlumeRocket> for Blog {
} }
impl sign::Signer for Blog { impl sign::Signer for Blog {
type Error = Error;
fn get_key_id(&self) -> String { fn get_key_id(&self) -> String {
format!("{}#main-key", self.ap_url) 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 key = self.get_keypair()?;
let mut signer = Signer::new(MessageDigest::sha256(), &key)?; let mut signer = Signer::new(MessageDigest::sha256(), &key)?;
signer.update(to_sign.as_bytes())?; 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 key = PKey::from_rsa(Rsa::public_key_from_pem(self.public_key.as_ref())?)?;
let mut verifier = Verifier::new(MessageDigest::sha256(), &key)?; let mut verifier = Verifier::new(MessageDigest::sha256(), &key)?;
verifier.update(data.as_bytes())?; verifier.update(data.as_bytes())?;
verifier.verify(&signature).map_err(Error::from) verifier.verify(&signature).map_err(SignatureError::from)
} }
} }

View File

@ -236,6 +236,7 @@ impl FromId<DbConn> for Comment {
})?, })?,
author_id: User::from_id( author_id: User::from_id(
conn, conn,
&Instance::get_local().expect("Failed to get local instance"),
&note.object_props.attributed_to_link::<Id>()?, &note.object_props.attributed_to_link::<Id>()?,
None, None,
CONFIG.proxy(), CONFIG.proxy(),
@ -294,7 +295,13 @@ impl FromId<DbConn> for Comment {
.collect::<HashSet<_>>() // remove duplicates (don't do a query more than once) .collect::<HashSet<_>>() // remove duplicates (don't do a query more than once)
.into_iter() .into_iter()
.map(|v| { .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] vec![user]
} else { } else {
vec![] // TODO try to fetch collection vec![] // TODO try to fetch collection

View File

@ -1,6 +1,6 @@
use crate::{ use crate::{
ap_url, db_conn::DbConn, notifications::*, schema::follows, users::User, Connection, Error, ap_url, db_conn::DbConn, instance::Instance, notifications::*, schema::follows, users::User,
Result, CONFIG, Connection, Error, Result, CONFIG,
}; };
use activitypub::activity::{Accept, Follow as FollowAct, Undo}; use activitypub::activity::{Accept, Follow as FollowAct, Undo};
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl, SaveChangesDsl}; 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> { fn from_activity(conn: &DbConn, follow: FollowAct) -> Result<Self> {
let actor = User::from_id( let actor = User::from_id(
conn, conn,
&Instance::get_local().expect("Failed to get local instance"),
&follow.follow_props.actor_link::<Id>()?, &follow.follow_props.actor_link::<Id>()?,
None, None,
CONFIG.proxy(), CONFIG.proxy(),
@ -176,6 +177,7 @@ impl FromId<DbConn> for Follow {
let target = User::from_id( let target = User::from_id(
conn, conn,
&Instance::get_local().expect("Failed to get local instance"),
&follow.follow_props.object_link::<Id>()?, &follow.follow_props.object_link::<Id>()?,
None, None,
CONFIG.proxy(), CONFIG.proxy(),

View File

@ -3,7 +3,9 @@ use activitypub::activity::*;
use crate::{ use crate::{
comments::Comment, comments::Comment,
db_conn::DbConn, db_conn::DbConn,
follows, likes, follows,
instance::Instance,
likes,
posts::{Post, PostUpdate}, posts::{Post, PostUpdate},
reshares::Reshare, reshares::Reshare,
users::User, users::User,
@ -47,7 +49,11 @@ impl_into_inbox_result! {
} }
pub fn inbox(conn: &DbConn, act: serde_json::Value) -> Result<InboxResult, Error> { 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, Announce, Post>(CONFIG.proxy())
.with::<User, Create, Comment>(CONFIG.proxy()) .with::<User, Create, Comment>(CONFIG.proxy())
.with::<User, Create, Post>(CONFIG.proxy()) .with::<User, Create, Post>(CONFIG.proxy())

View File

@ -6,10 +6,26 @@ use crate::{
users::{Role, User}, users::{Role, User},
Connection, Error, Result, Connection, Error, Result,
}; };
use activitypub::{actor::Service, CustomObject};
use chrono::NaiveDateTime; use chrono::NaiveDateTime;
use diesel::{self, ExpressionMethods, QueryDsl, RunQueryDsl}; 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 std::sync::RwLock;
use tracing::warn;
pub type CustomService = CustomObject<ApSignature, Service>;
#[derive(Clone, Identifiable, Queryable)] #[derive(Clone, Identifiable, Queryable)]
pub struct Instance { pub struct Instance {
@ -25,6 +41,8 @@ pub struct Instance {
pub default_license: String, pub default_license: String,
pub long_description_html: SafeString, pub long_description_html: SafeString,
pub short_description_html: SafeString, pub short_description_html: SafeString,
pub private_key: Option<String>,
pub public_key: Option<String>,
} }
#[derive(Clone, Insertable)] #[derive(Clone, Insertable)]
@ -39,6 +57,8 @@ pub struct NewInstance {
pub default_license: String, pub default_license: String,
pub long_description_html: String, pub long_description_html: String,
pub short_description_html: String, pub short_description_html: String,
pub private_key: Option<String>,
pub public_key: Option<String>,
} }
lazy_static! { lazy_static! {
@ -69,6 +89,13 @@ impl Instance {
*LOCAL_INSTANCE.write().unwrap() = Instance::get_local_uncached(conn).ok(); *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>> { pub fn get_remotes(conn: &Connection) -> Result<Vec<Instance>> {
instances::table instances::table
.filter(instances::local.eq(false)) .filter(instances::local.eq(false))
@ -238,6 +265,112 @@ impl Instance {
}) })
.map_err(Error::from) .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)] #[cfg(test)]
@ -259,6 +392,8 @@ pub(crate) mod tests {
name: "My instance".to_string(), name: "My instance".to_string(),
open_registrations: true, open_registrations: true,
public_domain: "plu.me".to_string(), public_domain: "plu.me".to_string(),
private_key: None,
public_key: None,
}, },
NewInstance { NewInstance {
default_license: "WTFPL".to_string(), default_license: "WTFPL".to_string(),
@ -270,6 +405,8 @@ pub(crate) mod tests {
name: "An instance".to_string(), name: "An instance".to_string(),
open_registrations: true, open_registrations: true,
public_domain: "1plu.me".to_string(), public_domain: "1plu.me".to_string(),
private_key: None,
public_key: None,
}, },
NewInstance { NewInstance {
default_license: "CC-0".to_string(), default_license: "CC-0".to_string(),
@ -281,6 +418,8 @@ pub(crate) mod tests {
name: "Someone instance".to_string(), name: "Someone instance".to_string(),
open_registrations: false, open_registrations: false,
public_domain: "2plu.me".to_string(), public_domain: "2plu.me".to_string(),
private_key: None,
public_key: None,
}, },
NewInstance { NewInstance {
default_license: "CC-0-BY-SA".to_string(), default_license: "CC-0-BY-SA".to_string(),
@ -292,6 +431,8 @@ pub(crate) mod tests {
name: "Nice day".to_string(), name: "Nice day".to_string(),
open_registrations: true, open_registrations: true,
public_domain: "3plu.me".to_string(), public_domain: "3plu.me".to_string(),
private_key: None,
public_key: None,
}, },
] ]
.into_iter() .into_iter()

View File

@ -17,8 +17,10 @@ extern crate serde_json;
#[macro_use] #[macro_use]
extern crate tantivy; extern crate tantivy;
use db_conn::DbPool;
use instance::Instance;
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use plume_common::activity_pub::inbox::InboxError; use plume_common::activity_pub::{inbox::InboxError, sign};
use posts::PostEvent; use posts::PostEvent;
use riker::actors::{channel, ActorSystem, ChannelRef, SystemBuilder}; use riker::actors::{channel, ActorSystem, ChannelRef, SystemBuilder};
use users::UserEvent; 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 { impl From<diesel::result::Error> for Error {
fn from(err: diesel::result::Error) -> Self { fn from(err: diesel::result::Error) -> Self {
Error::Db(err) Error::Db(err)
@ -160,6 +168,12 @@ impl From<InboxError<Error>> for Error {
pub type Result<T> = std::result::Result<T, 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 /// Adds a function to a model, that returns the first
/// matching row for a given list of fields. /// matching row for a given list of fields.
/// ///
@ -295,6 +309,17 @@ pub fn ap_url(url: &str) -> String {
format!("https://{}", url) 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)] #[cfg(test)]
#[macro_use] #[macro_use]
mod tests { mod tests {

View File

@ -1,6 +1,6 @@
use crate::{ use crate::{
db_conn::DbConn, notifications::*, posts::Post, schema::likes, timeline::*, users::User, db_conn::DbConn, instance::Instance, notifications::*, posts::Post, schema::likes, timeline::*,
Connection, Error, Result, CONFIG, users::User, Connection, Error, Result, CONFIG,
}; };
use activitypub::activity; use activitypub::activity;
use chrono::NaiveDateTime; use chrono::NaiveDateTime;
@ -117,6 +117,7 @@ impl FromId<DbConn> for Like {
NewLike { NewLike {
post_id: Post::from_id( post_id: Post::from_id(
conn, conn,
&Instance::get_local().expect("Failed to get local instance"),
&act.like_props.object_link::<Id>()?, &act.like_props.object_link::<Id>()?,
None, None,
CONFIG.proxy(), CONFIG.proxy(),
@ -125,6 +126,7 @@ impl FromId<DbConn> for Like {
.id, .id,
user_id: User::from_id( user_id: User::from_id(
conn, conn,
&Instance::get_local().expect("Failed to get local instance"),
&act.like_props.actor_link::<Id>()?, &act.like_props.actor_link::<Id>()?,
None, None,
CONFIG.proxy(), CONFIG.proxy(),

View File

@ -272,6 +272,7 @@ impl Media {
content_warning: image.object_props.summary_string().ok(), content_warning: image.object_props.summary_string().ok(),
owner_id: User::from_id( owner_id: User::from_id(
conn, conn,
&Instance::get_local().expect("Failed to get local instance"),
image image
.object_props .object_props
.attributed_to_link_vec::<Id>() .attributed_to_link_vec::<Id>()

View File

@ -630,13 +630,28 @@ impl FromId<DbConn> for Post {
.into_iter() .into_iter()
.fold((None, vec![]), |(blog, mut authors), link| { .fold((None, vec![]), |(blog, mut authors), link| {
let url = 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) => { Ok(u) => {
authors.push(u); authors.push(u);
(blog, authors) (blog, authors)
} }
Err(_) => ( 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, authors,
), ),
} }
@ -837,8 +852,14 @@ impl AsObject<User, Update, &DbConn> for PostUpdate {
type Output = (); type Output = ();
fn activity(self, conn: &DbConn, actor: User, _id: &str) -> Result<()> { fn activity(self, conn: &DbConn, actor: User, _id: &str) -> Result<()> {
let mut post = let mut post = Post::from_id(
Post::from_id(conn, &self.ap_url, None, CONFIG.proxy()).map_err(|(_, e)| e)?; 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)? { if !post.is_author(conn, actor.id)? {
// TODO: maybe the author was added in the meantime // TODO: maybe the author was added in the meantime

View File

@ -1,6 +1,7 @@
use crate::{ use crate::{
db_conn::{DbConn, DbPool}, db_conn::{DbConn, DbPool},
follows, follows,
instance::Instance,
posts::{LicensedArticle, Post}, posts::{LicensedArticle, Post},
users::{User, UserEvent}, users::{User, UserEvent},
ACTOR_SYS, CONFIG, USER_CHAN, ACTOR_SYS, CONFIG, USER_CHAN,
@ -89,7 +90,13 @@ fn fetch_and_cache_followers(user: &Arc<User>, conn: &DbConn) {
match follower_ids { match follower_ids {
Ok(user_ids) => { Ok(user_ids) => {
for user_id in 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 { match follower {
Ok(follower) => { Ok(follower) => {
let inserted = follows::Follow::insert( let inserted = follows::Follow::insert(

View File

@ -1,6 +1,6 @@
use crate::{ use crate::{
db_conn::DbConn, notifications::*, posts::Post, schema::reshares, timeline::*, users::User, db_conn::DbConn, instance::Instance, notifications::*, posts::Post, schema::reshares,
Connection, Error, Result, CONFIG, timeline::*, users::User, Connection, Error, Result, CONFIG,
}; };
use activitypub::activity::{Announce, Undo}; use activitypub::activity::{Announce, Undo};
use chrono::NaiveDateTime; use chrono::NaiveDateTime;
@ -142,6 +142,7 @@ impl FromId<DbConn> for Reshare {
NewReshare { NewReshare {
post_id: Post::from_id( post_id: Post::from_id(
conn, conn,
&Instance::get_local().expect("Failed to get local instance"),
&act.announce_props.object_link::<Id>()?, &act.announce_props.object_link::<Id>()?,
None, None,
CONFIG.proxy(), CONFIG.proxy(),
@ -150,6 +151,7 @@ impl FromId<DbConn> for Reshare {
.id, .id,
user_id: User::from_id( user_id: User::from_id(
conn, conn,
&Instance::get_local().expect("Failed to get local instance"),
&act.announce_props.actor_link::<Id>()?, &act.announce_props.actor_link::<Id>()?,
None, None,
CONFIG.proxy(), CONFIG.proxy(),

View File

@ -106,6 +106,8 @@ table! {
default_license -> Text, default_license -> Text,
long_description_html -> Varchar, long_description_html -> Varchar,
short_description_html -> Varchar, short_description_html -> Varchar,
private_key -> Nullable<Text>,
public_key -> Nullable<Text>,
} }
} }

View File

@ -164,6 +164,8 @@ mod tests {
name: random_hex().to_string(), name: random_hex().to_string(),
open_registrations: true, open_registrations: true,
public_domain: random_hex().to_string(), public_domain: random_hex().to_string(),
private_key: None,
public_key: None,
}, },
) )
.unwrap(); .unwrap();

View File

@ -24,7 +24,7 @@ use plume_common::{
activity_pub::{ activity_pub::{
ap_accept_header, ap_accept_header,
inbox::{AsActor, AsObject, FromId}, 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, ActivityStream, ApSignature, Id, IntoId, PublicKey, PUBLIC_VISIBILITY,
}, },
utils, utils,
@ -210,7 +210,14 @@ impl User {
.into_iter() .into_iter()
.find(|l| l.mime_type == Some(String::from("application/activity+json"))) .find(|l| l.mime_type == Some(String::from("application/activity+json")))
.ok_or(Error::Webfinger)?; .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> { pub fn fetch_remote_interact_uri(acct: &str) -> Result<String> {
@ -958,6 +965,8 @@ impl FromId<DbConn> for User {
open_registrations: true, open_registrations: true,
short_description_html: String::new(), short_description_html: String::new(),
long_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 { impl Signer for User {
type Error = Error;
fn get_key_id(&self) -> String { fn get_key_id(&self) -> String {
format!("{}#main-key", self.ap_url) 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 key = self.get_keypair()?;
let mut signer = sign::Signer::new(MessageDigest::sha256(), &key)?; let mut signer = sign::Signer::new(MessageDigest::sha256(), &key)?;
signer.update(to_sign.as_bytes())?; 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 key = PKey::from_rsa(Rsa::public_key_from_pem(self.public_key.as_ref())?)?;
let mut verifier = sign::Verifier::new(MessageDigest::sha256(), &key)?; let mut verifier = sign::Verifier::new(MessageDigest::sha256(), &key)?;
verifier.update(data.as_bytes())?; verifier.update(data.as_bytes())?;
verifier.verify(&signature).map_err(Error::from) verifier.verify(&signature).map_err(SignatureError::from)
} }
} }

View File

@ -60,83 +60,83 @@ msgstr ""
msgid "Optional" msgid "Optional"
msgstr "" msgstr ""
# src/routes/blogs.rs:63 # src/routes/blogs.rs:67
msgid "To create a new blog, you need to be logged in" msgid "To create a new blog, you need to be logged in"
msgstr "" msgstr ""
# src/routes/blogs.rs:102 # src/routes/blogs.rs:109
msgid "A blog with the same name already exists." msgid "A blog with the same name already exists."
msgstr "" msgstr ""
# src/routes/blogs.rs:140 # src/routes/blogs.rs:147
msgid "Your blog was successfully created!" msgid "Your blog was successfully created!"
msgstr "" msgstr ""
# src/routes/blogs.rs:159 # src/routes/blogs.rs:165
msgid "Your blog was deleted." msgid "Your blog was deleted."
msgstr "" msgstr ""
# src/routes/blogs.rs:167 # src/routes/blogs.rs:173
msgid "You are not allowed to delete this blog." msgid "You are not allowed to delete this blog."
msgstr "" msgstr ""
# src/routes/blogs.rs:218 # src/routes/blogs.rs:223
msgid "You are not allowed to edit this blog." msgid "You are not allowed to edit this blog."
msgstr "" msgstr ""
# src/routes/blogs.rs:274 # src/routes/blogs.rs:279
msgid "You can't use this media as a blog icon." msgid "You can't use this media as a blog icon."
msgstr "" msgstr ""
# src/routes/blogs.rs:292 # src/routes/blogs.rs:297
msgid "You can't use this media as a blog banner." msgid "You can't use this media as a blog banner."
msgstr "" msgstr ""
# src/routes/blogs.rs:326 # src/routes/blogs.rs:331
msgid "Your blog information have been updated." msgid "Your blog information have been updated."
msgstr "" msgstr ""
# src/routes/comments.rs:99 # src/routes/comments.rs:100
msgid "Your comment has been posted." msgid "Your comment has been posted."
msgstr "" msgstr ""
# src/routes/comments.rs:178 # src/routes/comments.rs:177
msgid "Your comment has been deleted." msgid "Your comment has been deleted."
msgstr "" msgstr ""
# src/routes/instance.rs:118 # src/routes/instance.rs:147
msgid "Instance settings have been saved." msgid "Instance settings have been saved."
msgstr "" msgstr ""
# src/routes/instance.rs:150 # src/routes/instance.rs:180
msgid "{} has been unblocked." msgid "{} has been unblocked."
msgstr "" msgstr ""
# src/routes/instance.rs:152 # src/routes/instance.rs:182
msgid "{} has been blocked." msgid "{} has been blocked."
msgstr "" msgstr ""
# src/routes/instance.rs:201 # src/routes/instance.rs:233
msgid "Blocks deleted" msgid "Blocks deleted"
msgstr "" msgstr ""
# src/routes/instance.rs:216 # src/routes/instance.rs:249
msgid "Email already blocked" msgid "Email already blocked"
msgstr "" msgstr ""
# src/routes/instance.rs:221 # src/routes/instance.rs:254
msgid "Email Blocked" msgid "Email Blocked"
msgstr "" msgstr ""
# src/routes/instance.rs:312 # src/routes/instance.rs:347
msgid "You can't change your own rights." msgid "You can't change your own rights."
msgstr "" msgstr ""
# src/routes/instance.rs:323 # src/routes/instance.rs:358
msgid "You are not allowed to take this action." msgid "You are not allowed to take this action."
msgstr "" msgstr ""
# src/routes/instance.rs:359 # src/routes/instance.rs:393
msgid "Done." msgid "Done."
msgstr "" msgstr ""
@ -144,23 +144,23 @@ msgstr ""
msgid "To like a post, you need to be logged in" msgid "To like a post, you need to be logged in"
msgstr "" msgstr ""
# src/routes/medias.rs:145 # src/routes/medias.rs:158
msgid "Your media have been deleted." msgid "Your media have been deleted."
msgstr "" msgstr ""
# src/routes/medias.rs:150 # src/routes/medias.rs:163
msgid "You are not allowed to delete this media." msgid "You are not allowed to delete this media."
msgstr "" msgstr ""
# src/routes/medias.rs:167 # src/routes/medias.rs:180
msgid "Your avatar has been updated." msgid "Your avatar has been updated."
msgstr "" msgstr ""
# src/routes/medias.rs:172 # src/routes/medias.rs:185
msgid "You are not allowed to use this media." msgid "You are not allowed to use this media."
msgstr "" msgstr ""
# src/routes/notifications.rs:28 # src/routes/notifications.rs:29
msgid "To see your notifications, you need to be logged in" msgid "To see your notifications, you need to be logged in"
msgstr "" msgstr ""
@ -168,51 +168,51 @@ msgstr ""
msgid "This post isn't published yet." msgid "This post isn't published yet."
msgstr "" msgstr ""
# src/routes/posts.rs:126 # src/routes/posts.rs:125
msgid "To write a new post, you need to be logged in" msgid "To write a new post, you need to be logged in"
msgstr "" msgstr ""
# src/routes/posts.rs:143 # src/routes/posts.rs:146
msgid "You are not an author of this blog." msgid "You are not an author of this blog."
msgstr "" msgstr ""
# src/routes/posts.rs:150 # src/routes/posts.rs:153
msgid "New post" msgid "New post"
msgstr "" msgstr ""
# src/routes/posts.rs:195 # src/routes/posts.rs:198
msgid "Edit {0}" msgid "Edit {0}"
msgstr "" msgstr ""
# src/routes/posts.rs:264 # src/routes/posts.rs:267
msgid "You are not allowed to publish on this blog." msgid "You are not allowed to publish on this blog."
msgstr "" msgstr ""
# src/routes/posts.rs:363 # src/routes/posts.rs:367
msgid "Your article has been updated." msgid "Your article has been updated."
msgstr "" msgstr ""
# src/routes/posts.rs:553 # src/routes/posts.rs:556
msgid "Your article has been saved." msgid "Your article has been saved."
msgstr "" msgstr ""
# src/routes/posts.rs:560 # src/routes/posts.rs:563
msgid "New article" msgid "New article"
msgstr "" msgstr ""
# src/routes/posts.rs:597 # src/routes/posts.rs:601
msgid "You are not allowed to delete this article." msgid "You are not allowed to delete this article."
msgstr "" msgstr ""
# src/routes/posts.rs:622 # src/routes/posts.rs:625
msgid "Your article has been deleted." msgid "Your article has been deleted."
msgstr "" 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?" msgid "It looks like the article you tried to delete doesn't exist. Maybe it is already gone?"
msgstr "" 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." msgid "Couldn't obtain enough information about your account. Please make sure your username is correct."
msgstr "" msgstr ""
@ -220,63 +220,63 @@ msgstr ""
msgid "To reshare a post, you need to be logged in" msgid "To reshare a post, you need to be logged in"
msgstr "" msgstr ""
# src/routes/session.rs:88 # src/routes/session.rs:95
msgid "You are now connected." msgid "You are now connected."
msgstr "" msgstr ""
# src/routes/session.rs:109 # src/routes/session.rs:116
msgid "You are now logged off." msgid "You are now logged off."
msgstr "" msgstr ""
# src/routes/session.rs:154 # src/routes/session.rs:162
msgid "Password reset" msgid "Password reset"
msgstr "" msgstr ""
# src/routes/session.rs:155 # src/routes/session.rs:163
msgid "Here is the link to reset your password: {0}" msgid "Here is the link to reset your password: {0}"
msgstr "" msgstr ""
# src/routes/session.rs:215 # src/routes/session.rs:235
msgid "Your password was successfully reset." msgid "Your password was successfully reset."
msgstr "" msgstr ""
# src/routes/user.rs:142 # src/routes/user.rs:74
msgid "To access your dashboard, you need to be logged in" msgid "To access your dashboard, you need to be logged in"
msgstr "" msgstr ""
# src/routes/user.rs:164 # src/routes/user.rs:96
msgid "You are no longer following {}." msgid "You are no longer following {}."
msgstr "" msgstr ""
# src/routes/user.rs:181 # src/routes/user.rs:113
msgid "You are now following {}." msgid "You are now following {}."
msgstr "" msgstr ""
# src/routes/user.rs:261 # src/routes/user.rs:190
msgid "To subscribe to someone, you need to be logged in" msgid "To subscribe to someone, you need to be logged in"
msgstr "" msgstr ""
# src/routes/user.rs:365 # src/routes/user.rs:299
msgid "To edit your profile, you need to be logged in" msgid "To edit your profile, you need to be logged in"
msgstr "" msgstr ""
# src/routes/user.rs:411 # src/routes/user.rs:345
msgid "Your profile has been updated." msgid "Your profile has been updated."
msgstr "" msgstr ""
# src/routes/user.rs:438 # src/routes/user.rs:373
msgid "Your account has been deleted." msgid "Your account has been deleted."
msgstr "" msgstr ""
# src/routes/user.rs:444 # src/routes/user.rs:379
msgid "You can't delete someone else's account." msgid "You can't delete someone else's account."
msgstr "" msgstr ""
# src/routes/user.rs:528 # src/routes/user.rs:463
msgid "Registrations are closed on this instance." msgid "Registrations are closed on this instance."
msgstr "" 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." msgid "Your account has been created. Now you just need to log in, before you can use it."
msgstr "" msgstr ""

View File

@ -26,7 +26,13 @@ pub fn handle_incoming(
.or_else(|| activity["actor"]["id"].as_str()) .or_else(|| activity["actor"]["id"].as_str())
.ok_or(status::BadRequest(Some("Missing actor id for activity")))?; .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"); .expect("instance::shared_inbox: user error");
if !verify_http_headers(&actor, &headers.0, &sig).is_secure() && !act.clone().verify(&actor) { if !verify_http_headers(&actor, &headers.0, &sig).is_secure() && !act.clone().verify(&actor) {
// maybe we just know an old key? // maybe we just know an old key?

View File

@ -15,6 +15,7 @@ use diesel::r2d2::ConnectionManager;
use plume_models::{ use plume_models::{
db_conn::{DbPool, PragmaForeignKey}, db_conn::{DbPool, PragmaForeignKey},
instance::Instance, instance::Instance,
migrate_data,
migrations::IMPORTED_MIGRATIONS, migrations::IMPORTED_MIGRATIONS,
remote_fetch_actor::RemoteFetchActor, remote_fetch_actor::RemoteFetchActor,
search::{actor::SearchActor, Searcher as UnmanagedSearcher}, 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()); let workpool = ScheduledThreadPool::with_name("worker {}", num_cpus::get());
// we want a fast exit here, so // we want a fast exit here, so
let searcher = Arc::new(UnmanagedSearcher::open_or_recreate( let searcher = Arc::new(UnmanagedSearcher::open_or_recreate(
@ -147,6 +149,7 @@ Then try to restart Plume.
routes::comments::delete, routes::comments::delete,
routes::comments::activity_pub, routes::comments::activity_pub,
routes::instance::index, routes::instance::index,
routes::instance::activity_details,
routes::instance::admin, routes::instance::admin,
routes::instance::admin_mod, routes::instance::admin_mod,
routes::instance::admin_instances, routes::instance::admin_instances,

View File

@ -438,6 +438,8 @@ mod tests {
name: random_hex().to_string(), name: random_hex().to_string(),
open_registrations: true, open_registrations: true,
public_domain: random_hex().to_string(), public_domain: random_hex().to_string(),
private_key: None,
public_key: None,
}, },
) )
.unwrap(); .unwrap();

View File

@ -11,7 +11,7 @@ use validator::{Validate, ValidationErrors};
use crate::inbox; use crate::inbox;
use crate::routes::{errors::ErrorPage, rocket_uri_macro_static_files, Page, RespondOrRedirect}; use crate::routes::{errors::ErrorPage, rocket_uri_macro_static_files, Page, RespondOrRedirect};
use crate::template_utils::{IntoContext, Ructe}; 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::{ use plume_models::{
admin::*, admin::*,
blocklisted_emails::*, 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")] #[get("/admin")]
pub fn admin(_admin: Admin, conn: DbConn, rockets: PlumeRocket) -> Result<Ructe, ErrorPage> { pub fn admin(_admin: Admin, conn: DbConn, rockets: PlumeRocket) -> Result<Ructe, ErrorPage> {
let local_inst = Instance::get_local()?; 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))); 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!( return Some(Redirect::to(uri!(
super::posts::details: blog = post.get_blog(&conn).expect("Can't retrieve blog").fqn, super::posts::details: blog = post.get_blog(&conn).expect("Can't retrieve blog").fqn,
slug = &post.slug, 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()) { if comment.can_see(&conn, user.as_ref()) {
let post = comment.get_post(&conn).expect("Can't retrieve post"); let post = comment.get_post(&conn).expect("Can't retrieve post");
return Some(Redirect::to(uri!( return Some(Redirect::to(uri!(