gourami

[UNMAINTAINED] Activitypub server in Rust
Log | Files | Refs | README | LICENSE

commit 77834a23cd728d6b233943dbb7ee6d5a9e454a8d
parent 98110e30bde49bc48f3245a6e18262b86d282c24
Author: alex wennerberg <alex@alexwennerberg.com>
Date:   Mon,  4 May 2020 09:11:57 -0500

Begin signature normalization

Diffstat:
MCargo.lock | 31+++++++++++++++++++++++++++++++
MCargo.toml | 1+
MTODO | 84+++++++------------------------------------------------------------------------
Msrc/ap.rs | 93+++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------
4 files changed, 101 insertions(+), 108 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -632,6 +632,7 @@ dependencies = [ "cookie", "diesel", "env_logger", + "http-signature-normalization", "hyper", "lazy_static", "log 0.4.8", @@ -735,6 +736,16 @@ dependencies = [ ] [[package]] +name = "http-signature-normalization" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "648233553603e7bb55bc1ea08a514661e212c09c10f6434507894273d8b5e773" +dependencies = [ + "chrono", + "thiserror", +] + +[[package]] name = "httparse" version = "1.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2016,6 +2027,26 @@ dependencies = [ ] [[package]] +name = "thiserror" +version = "1.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d12a1dae4add0f0d568eebc7bf142f145ba1aa2544cafb195c76f0f409091b60" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f34e0c1caaa462fd840ec6b768946ea1e7842620d94fe29d5b847138f521269" +dependencies = [ + "proc-macro2 1.0.10", + "quote 1.0.3", + "syn 1.0.17", +] + +[[package]] name = "thread_local" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" diff --git a/Cargo.toml b/Cargo.toml @@ -12,6 +12,7 @@ clap = "2.33.0" chrono = "0.4.11" diesel = { version = "1.4.4", features = ["sqlite", "r2d2"] } env_logger = "0.7.1" +http-signature-normalization = "0.5.1" lazy_static = "1.4.0" log = "0.4.8" rand = "0.7.3" diff --git a/TODO b/TODO @@ -1,93 +1,23 @@ v0.1.0 goals: sharing with a small community -major - -minor -- better tagging / notifications -- better session management? -- Change to GPL v3 license due to dependencies +NOTES +https://flak.tedunangst.com/post/ActivityPub-as-it-has-been-understood -Proper threading -- Render all DESCENDENTS -(allow for replies to multiple items...) - +major Federation / ActivityPub retryeets -following/followers & separate timelines -text cant exist after URLs - -REFACTORING -- +DMs with expiration on read? +minor Add limit of 255 characters for usernames and other limits to get urls to render - -askama stuff -MIDDLEWARE - -A nice big new feature would be event planning - -https://github.com/seanmonstar/warp/blob/8b8c9950260ef7312e29c4af72b0753619a5ad5c/examples/tls.rs - -sanitize on write to db - -parse markdown? - -send notes as cleartext and html? cleanup html parsing. - -understand fn vs async fn in tokio - -marketing tagline: - -"An intentionally small, lightweight activitypub community" - -NOTES-- - -NO DMS -- not really secure in AP -NO IMAGES -- issue described in https://flak.tedunangst.com/post/honk - -mastodon doesnt implement client-to-server - -outbox -? - -TODO -- - -First, client to server protocol, then federation - -Person -Note -Follow/Unfollow -Inbox -Outbox --> not really strictly necessary? - -crates-- -logging -json and serde json - -activitypub to implement: - -Person -https://www.w3.org/TR/activitystreams-vocabulary/#dfn-person -MUST have -inbox, outbox - -Note -- Represents a short written work typically less than a single paragraph in length. - -Object properties: https://www.w3.org/TR/activitystreams-vocabulary/#dfn-object - -used https://github.com/kaj/warp-diesel-ructe-sample +dotenv setup +file attachments Webfinger Cite this https://git.cypr.io/oz/autolink-rust/src/branch/master/src/lib.rs -Profiles: - -Follow - -AP key verification - -Status == ActivityPub "Note" - authentication https://github.com/messense/otpauth-rs diff --git a/src/ap.rs b/src/ap.rs @@ -7,6 +7,11 @@ use diesel::sqlite::SqliteConnection; use serde_json::json; use serde_json::Value; use std::env; +use std::collections::BTreeMap; +use reqwest::Request; +use chrono::Duration; +use http_signature_normalization::Config; + /// Users don't follow users in Gourami. Instead the server does hte following /// There are a number of reasons for this: /// Gives it a more 'community' feel -- everyone shares the same timeline @@ -25,6 +30,9 @@ fn send_to_outbox(activity: bool) { // activitystreams object fetch/store from db. db objects need to serialize/deserialize this object if get -> fetch from db if post -> put to db, send to inbox of followers send to inbox of followers } +fn verify_incoming_message() { +} + enum Action { CreateNote, DoNothing, @@ -32,25 +40,27 @@ enum Action { } /// get the server user json -fn server_actor() -> Value { - let domain = env::var("GOURAMI_DOMAIN").unwrap(); - let actor = format!("{}/actor", domain); - let inbox = format!("{}/inbox", domain); - let public_key = fs::read_to_string(env::var("SIGNATURE_PUBKEY").unwrap()).unwrap(); +fn server_actor_json() -> Value { + // TODO figure out how to get lazy static working + let DOMAIN: &str = &env::var("GOURAMI_DOMAIN").unwrap(); + let SERVER_ACTOR: &str = &format!("{}/actor", &env::var("GOURAMI_DOMAIN").unwrap()); + let SERVER_INBOX: &str = &format!("{}/inbox", &env::var("GOURAMI_DOMAIN").unwrap()); + let SERVER_KEY_ID: &str = &format!("{}/inbox", &env::var("GOURAMI_DOMAIN").unwrap()); + let SERVER_PUBLIC_KEY: &str = &fs::read_to_string(env::var("SIGNATURE_PUBKEY").unwrap()).unwrap(); json!({ "@context": [ "https://www.w3.org/ns/activitystreams", "https://w3id.org/security/v1" ], - "id": actor, + "id": SERVER_ACTOR, "type": "Organization", // application? - "preferredUsername": domain, // think about it - "inbox": inbox, + "preferredUsername": DOMAIN, // think about it + "inbox": SERVER_INBOX, "publicKey": { - "id": format!("{}#main-key", actor), - "owner": actor, - "publicKeyPem": public_key + "id": SERVER_KEY_ID, + "owner": SERVER_ACTOR, + "publicKeyPem": SERVER_PUBLIC_KEY }}) } @@ -164,7 +174,6 @@ fn generate_server_follow(remote_url: String) -> Value { /// Generate an AP create message from a new note pub fn new_note_to_ap_message(note: &NoteInput, user: &User) -> Value { // we need note, user. note noteinput but note obj - let conn = &POOL.get().unwrap(); // Do a bunch of db queries to get the info I need json!({ "@context": "https://www.w3.org/ns/activitystreams", @@ -180,7 +189,7 @@ pub fn new_note_to_ap_message(note: &NoteInput, user: &User) -> Value { "type": "note", "url": "abc", "inReplyTo": "none", - "attributedTo": "joe", + "attributedTo": user.username, "content": note.content } }) @@ -189,10 +198,49 @@ pub fn new_note_to_ap_message(note: &NoteInput, user: &User) -> Value { // /// used to send to others // fn generate_ap(activity: Activity) { // } +pub trait HttpSignature { + fn http_sign_outgoing(self) -> Result<reqwest::Request, Box<dyn std::error::Error>>; +} + +impl HttpSignature for reqwest::RequestBuilder { + fn http_sign_outgoing(self) -> Result<reqwest::Request, Box<dyn std::error::Error>> { + let req = self.build().unwrap(); + let config = Config::default().set_expiration(Duration::seconds(5)); + // let server_key_id = + let server_key_id: &str = &format!("{}/inbox", &env::var("GOURAMI_DOMAIN").unwrap()); + let mut bt = std::collections::BTreeMap::new(); + for (k, v) in req.headers().iter() { + bt.insert(k.as_str().to_owned(), v.to_str()?.to_owned()); + } + let path_and_query = if let Some(query) = req.url().query() { + format!("{}?{}", req.url().path(), query) + } else { + req.url().path().to_string() + }; + let unsigned = config.begin_sign(req.method().as_str(), &path_and_query, bt)?; + let sig_header = unsigned.sign(server_key_id.to_owned(), |signing_string| { + // sign here + Ok(signing_string.to_owned()) as Result<_, Box<dyn std::error::Error>> + })? + .signature_header(); + println!("{:?}", sig_header); + Ok(req) + } +} #[cfg(test)] mod tests { use super::*; + + #[test] + fn test_sign_outgoing_msg() { + let body: Value = serde_json::from_str(r#"{"foo": "bar"}"#).unwrap(); + let req = reqwest::Client::new() + .post("https://localhost:3030") + .json(&body) + .http_sign_outgoing(); + } + #[test] fn test_empty_string() { // to write @@ -200,7 +248,7 @@ mod tests { // #[test] fn test_mastodon_create_status_example() { - let create_note_mastodon = serde_json::from_str(r#"{ + let create_note_mastodon: Value = serde_json::from_str(r#"{ "id": "https://mastodon.social/users/alexwennerberg/statuses/104028309437021899/activity", "type": "Create", "actor": "https://mastodon.social/users/alexwennerberg", @@ -247,22 +295,5 @@ mod tests { } } }"#).unwrap(); - assert_eq!( - process_create_note(create_note_mastodon).unwrap(), - RemoteNoteInput { - content: String::from("hello world"), - in_reply_to: None, - neighborhood: true, - is_remote: true, - user_id: -1, // for remote. placeholder. not sure what to do with this ultimately - remote_creator: String::from("https://mastodon.social/users/alexwennerberg"), - remote_id: String::from( - "https://mastodon.social/users/alexwennerberg/statuses/104028309437021899" - ), - remote_url: String::from( - "https://mastodon.social/@alexwennerberg/104028309437021899" - ), - } - ) } }