Big repository reorganization

The code is divided in three crates:
- plume-common, for the ActivityPub module, and some common utils
- plume-models, for the models and database-related code
- plume, the app itself

This new organization will allow to test it more easily, but also to create other tools that only reuse a little part of
the code (for instance a Wordpress import tool, that would just use the plume-models crate)
This commit is contained in:
Bat
2018-06-23 17:36:11 +01:00
parent 0a1edba4b0
commit 68c7aad179
40 changed files with 411 additions and 259 deletions
+41
View File
@@ -0,0 +1,41 @@
use activitypub::{Object, activity::Create};
use activity_pub::Id;
#[derive(Fail, Debug)]
pub enum InboxError {
#[fail(display = "The `type` property is required, but was not present")]
NoType,
#[fail(display = "Invalid activity type")]
InvalidType,
#[fail(display = "Couldn't undo activity")]
CantUndo
}
pub trait FromActivity<T: Object, C>: Sized {
fn from_activity(conn: &C, obj: T, actor: Id) -> Self;
fn try_from_activity(conn: &C, act: Create) -> bool {
if let Ok(obj) = act.create_props.object_object() {
Self::from_activity(conn, obj, act.create_props.actor_link::<Id>().unwrap());
true
} else {
false
}
}
}
pub trait Notify<C> {
fn notify(&self, conn: &C);
}
pub trait Deletable<C> {
/// true if success
fn delete_activity(conn: &C, id: Id) -> bool;
}
pub trait WithInbox {
fn get_inbox_url(&self) -> String;
fn get_shared_inbox_url(&self) -> Option<String>;
}
+137
View File
@@ -0,0 +1,137 @@
use activitypub::{Activity, Actor, Object, Link};
use array_tool::vec::Uniq;
use reqwest::Client;
use rocket::{
http::Status,
response::{Response, Responder},
request::Request
};
use serde_json;
use self::sign::Signable;
pub mod inbox;
pub mod request;
pub mod sign;
pub const CONTEXT_URL: &'static str = "https://www.w3.org/ns/activitystreams";
pub const PUBLIC_VISIBILTY: &'static str = "https://www.w3.org/ns/activitystreams#Public";
#[cfg(debug_assertions)]
pub fn ap_url(url: String) -> String {
format!("http://{}", url)
}
#[cfg(not(debug_assertions))]
pub fn ap_url(url: String) -> String {
format!("https://{}", url)
}
pub fn context() -> serde_json::Value {
json!([
CONTEXT_URL,
"https://w3id.org/security/v1",
{
"manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
"sensitive": "as:sensitive",
"movedTo": "as:movedTo",
"Hashtag": "as:Hashtag",
"ostatus":"http://ostatus.org#",
"atomUri":"ostatus:atomUri",
"inReplyToAtomUri":"ostatus:inReplyToAtomUri",
"conversation":"ostatus:conversation",
"toot":"http://joinmastodon.org/ns#",
"Emoji":"toot:Emoji",
"focalPoint": {
"@container":"@list",
"@id":"toot:focalPoint"
},
"featured":"toot:featured"
}
])
}
pub struct ActivityStream<T> (T);
impl<T> ActivityStream<T> {
pub fn new(t: T) -> ActivityStream<T> {
ActivityStream(t)
}
}
impl<'r, O: Object> Responder<'r> for ActivityStream<O> {
fn respond_to(self, request: &Request) -> Result<Response<'r>, Status> {
let mut json = serde_json::to_value(&self.0).map_err(|_| Status::InternalServerError)?;
json["@context"] = context();
serde_json::to_string(&json).respond_to(request).map(|r| Response::build_from(r)
.raw_header("Content-Type", "application/activity+json")
.finalize())
}
}
pub fn broadcast<A: Activity, S: sign::Signer, T: inbox::WithInbox + Actor>(sender: &S, act: A, to: Vec<T>) {
let boxes = to.into_iter()
.map(|u| u.get_shared_inbox_url().unwrap_or(u.get_inbox_url()))
.collect::<Vec<String>>()
.unique();
let mut act = serde_json::to_value(act).unwrap();
act["@context"] = context();
let signed = act.sign(sender);
for inbox in boxes {
// TODO: run it in Sidekiq or something like that
let res = Client::new()
.post(&inbox[..])
.headers(request::headers())
.header(request::signature(sender, request::headers()))
.header(request::digest(signed.to_string()))
.body(signed.to_string())
.send();
match res {
Ok(mut r) => println!("Successfully sent activity to inbox ({})\n\n{:?}", inbox, r.text().unwrap()),
Err(e) => println!("Error while sending to inbox ({:?})", e)
}
}
}
#[derive(Clone, Serialize, Deserialize)]
pub struct Id(String);
impl Id {
pub fn new<T: Into<String>>(id: T) -> Id {
Id(id.into())
}
}
impl Into<String> for Id {
fn into(self) -> String {
self.0.clone()
}
}
pub trait IntoId {
fn into_id(self) -> Id;
}
impl Link for Id {}
#[derive(Clone, Debug, Default, Deserialize, Serialize, Properties)]
#[serde(rename_all = "camelCase")]
pub struct ApSignature {
#[activitystreams(concrete(PublicKey), functional)]
pub public_key: Option<serde_json::Value>
}
#[derive(Clone, Debug, Default, Deserialize, Serialize, Properties)]
#[serde(rename_all = "camelCase")]
pub struct PublicKey {
#[activitystreams(concrete(String), functional)]
pub id: Option<serde_json::Value>,
#[activitystreams(concrete(String), functional)]
pub owner: Option<serde_json::Value>,
#[activitystreams(concrete(String), functional)]
pub public_key_pem: Option<serde_json::Value>
}
+52
View File
@@ -0,0 +1,52 @@
use base64;
use openssl::hash::{Hasher, MessageDigest};
use reqwest::{
mime::Mime,
header::{ContentType, Date, Headers, UserAgent}
};
use std::{
str::FromStr,
time::SystemTime
};
use activity_pub::sign::Signer;
const USER_AGENT: &'static str = "Plume/0.1.0";
header! {
(Signature, "Signature") => [String]
}
header! {
(Digest, "Digest") => [String]
}
pub fn headers() -> Headers {
let mut headers = Headers::new();
headers.set(UserAgent::new(USER_AGENT));
headers.set(Date(SystemTime::now().into()));
headers.set(ContentType(Mime::from_str("application/activity+json").unwrap()));
headers
}
pub fn signature<S: Signer>(signer: &S, headers: Headers) -> Signature {
let signed_string = headers.iter().map(|h| format!("{}: {}", h.name().to_lowercase(), h.value_string())).collect::<Vec<String>>().join("\n");
let signed_headers = headers.iter().map(|h| h.name().to_string()).collect::<Vec<String>>().join(" ").to_lowercase();
let data = signer.sign(signed_string);
let sign = base64::encode(&data[..]);
Signature(format!(
"keyId=\"{key_id}\",algorithm=\"rsa-sha256\",headers=\"{signed_headers}\",signature=\"{signature}\"",
key_id = signer.get_key_id(),
signed_headers = signed_headers,
signature = sign
))
}
pub fn digest(body: String) -> Digest {
let mut hasher = Hasher::new(MessageDigest::sha256()).unwrap();
hasher.update(&body.into_bytes()[..]).unwrap();
let res = base64::encode(&hasher.finish().unwrap());
Digest(format!("SHA-256={}", res))
}
+56
View File
@@ -0,0 +1,56 @@
use base64;
use chrono::Utc;
use hex;
use openssl::{
pkey::PKey,
rsa::Rsa,
sha::sha256
};
use serde_json;
/// Returns (public key, private key)
pub fn gen_keypair() -> (Vec<u8>, Vec<u8>) {
let keypair = Rsa::generate(2048).unwrap();
let keypair = PKey::from_rsa(keypair).unwrap();
(keypair.public_key_to_pem().unwrap(), keypair.private_key_to_pem_pkcs8().unwrap())
}
pub trait Signer {
fn get_key_id(&self) -> String;
/// Sign some data with the signer keypair
fn sign(&self, to_sign: String) -> Vec<u8>;
}
pub trait Signable {
fn sign<T>(&mut self, creator: &T) -> &mut Self where T: Signer;
fn hash(data: String) -> String {
let bytes = data.into_bytes();
hex::encode(sha256(&bytes[..]))
}
}
impl Signable for serde_json::Value {
fn sign<T: Signer>(&mut self, creator: &T) -> &mut serde_json::Value {
let creation_date = Utc::now().to_rfc3339();
let mut options = json!({
"type": "RsaSignature2017",
"creator": creator.get_key_id(),
"created": creation_date
});
let options_hash = Self::hash(json!({
"@context": "https://w3id.org/identity/v1",
"created": creation_date
}).to_string());
let document_hash = Self::hash(self.to_string());
let to_be_signed = options_hash + &document_hash;
let signature = base64::encode(&creator.sign(to_be_signed));
options["signaureValue"] = serde_json::Value::String(signature);
self["signature"] = options;
self
}
}