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 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");
}

View File

@ -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,15 +394,11 @@ 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(", "),
)
.map_err(|_| (None, InboxError::DerefError.into()))?,
"Signature",
request::signature(sender, &headers, ("get", url.path(), url.query()))
.map_err(|_| (None, InboxError::DerefError.into()))?,
)
.send()
.map_err(|_| (None, InboxError::DerefError))
@ -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();

View File

@ -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())
}
}

View File

@ -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",

View File

@ -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)
}
}

View File

@ -236,6 +236,7 @@ impl FromId<DbConn> for Comment {
})?,
author_id: User::from_id(
conn,
&Instance::get_local().expect("Failed to get local instance"),
&note.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

View File

@ -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(),

View File

@ -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,20 +49,24 @@ impl_into_inbox_result! {
}
pub fn inbox(conn: &DbConn, act: serde_json::Value) -> Result<InboxResult, Error> {
Inbox::handle(conn, act)
.with::<User, Announce, Post>(CONFIG.proxy())
.with::<User, Create, Comment>(CONFIG.proxy())
.with::<User, Create, Post>(CONFIG.proxy())
.with::<User, Delete, Comment>(CONFIG.proxy())
.with::<User, Delete, Post>(CONFIG.proxy())
.with::<User, Delete, User>(CONFIG.proxy())
.with::<User, Follow, User>(CONFIG.proxy())
.with::<User, Like, Post>(CONFIG.proxy())
.with::<User, Undo, Reshare>(CONFIG.proxy())
.with::<User, Undo, follows::Follow>(CONFIG.proxy())
.with::<User, Undo, likes::Like>(CONFIG.proxy())
.with::<User, Update, PostUpdate>(CONFIG.proxy())
.done()
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())
.with::<User, Delete, Comment>(CONFIG.proxy())
.with::<User, Delete, Post>(CONFIG.proxy())
.with::<User, Delete, User>(CONFIG.proxy())
.with::<User, Follow, User>(CONFIG.proxy())
.with::<User, Like, Post>(CONFIG.proxy())
.with::<User, Undo, Reshare>(CONFIG.proxy())
.with::<User, Undo, follows::Follow>(CONFIG.proxy())
.with::<User, Undo, likes::Like>(CONFIG.proxy())
.with::<User, Update, PostUpdate>(CONFIG.proxy())
.done()
}
#[cfg(test)]

View File

@ -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()

View File

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

View File

@ -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(),

View File

@ -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>()

View File

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

View File

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

View File

@ -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(),

View File

@ -106,6 +106,8 @@ table! {
default_license -> Text,
long_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(),
open_registrations: true,
public_domain: random_hex().to_string(),
private_key: None,
public_key: None,
},
)
.unwrap();

View File

@ -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)
}
}

View File

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

View File

@ -26,8 +26,14 @@ 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())
.expect("instance::shared_inbox: user error");
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?
actor

View File

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

View File

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

View File

@ -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!(