diff --git a/Cargo.lock b/Cargo.lock index 0ab0b95..0384aa3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -100,7 +100,7 @@ dependencies = [ "actix-threadpool", "actix-tls", "actix-utils", - "base64", + "base64 0.11.0", "bitflags", "brotli2", "bytes", @@ -313,6 +313,19 @@ dependencies = [ "syn", ] +[[package]] +name = "actix-webfinger" +version = "0.3.0-alpha.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "221e03224c654d7c6f35cc7a8bb7bc01ce0e53eb61b7722de199c6182cd9f3fe" +dependencies = [ + "actix-http", + "actix-web", + "serde", + "serde_derive", + "thiserror", +] + [[package]] name = "actix_derive" version = "0.5.0" @@ -362,15 +375,23 @@ dependencies = [ "actix", "actix-rt", "actix-web", + "actix-webfinger", "anyhow", + "base64 0.12.0", "bb8-postgres", "dotenv", "futures", + "http-signature-normalization-actix", "log", "lru", "pretty_env_logger", + "rand", + "rsa", + "rsa-magic-public-key", + "rsa-pem", "serde", "serde_json", + "sha2", "thiserror", "tokio", "ttl_cache", @@ -427,7 +448,7 @@ dependencies = [ "actix-http", "actix-rt", "actix-service", - "base64", + "base64 0.11.0", "bytes", "derive_more", "futures-core", @@ -475,6 +496,12 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b41b7ea54a0c9d92199de89e20e58d49f02f8e699814ef3fdf266f6f748d15c7" +[[package]] +name = "base64" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5ca2cd0adc3f48f9e9ea5a6bbdf9ccc0bfade884847e484d452414c7ccffb3" + [[package]] name = "bb8" version = "0.4.0" @@ -674,7 +701,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4434400df11d95d556bac068ddfedd482915eb18fe8bea89bc80b6e4b1c179e5" dependencies = [ "generic-array 0.12.3", - "subtle", + "subtle 1.0.0", ] [[package]] @@ -1046,6 +1073,32 @@ dependencies = [ "itoa", ] +[[package]] +name = "http-signature-normalization" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2db9cb1c64aaabb27523433fc8df0467670df81642fd770086cf8a2eb11b24a" +dependencies = [ + "chrono", + "thiserror", +] + +[[package]] +name = "http-signature-normalization-actix" +version = "0.3.0-alpha.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "406cae6778fb05b34885ee9aaec4e55931ac763aac3edf9a1e25bd40d4490e86" +dependencies = [ + "actix-http", + "actix-web", + "base64 0.11.0", + "bytes", + "futures", + "http-signature-normalization", + "sha2", + "thiserror", +] + [[package]] name = "httparse" version = "1.3.4" @@ -1129,6 +1182,9 @@ name = "lazy_static" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +dependencies = [ + "spin", +] [[package]] name = "libc" @@ -1136,6 +1192,12 @@ version = "0.2.67" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eb147597cdf94ed43ab7a9038716637d2d1bf2bc571da995d0028dec06bd3018" +[[package]] +name = "libm" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7d73b3f436185384286bd8098d17ec07c9a7d2388a6599f824d8502b529702a" + [[package]] name = "linked-hash-map" version = "0.5.2" @@ -1276,6 +1338,36 @@ dependencies = [ "winapi 0.3.8", ] +[[package]] +name = "num-bigint" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "090c7f9998ee0ff65aa5b723e4009f7b217707f1fb5ea551329cc4d6231fb304" +dependencies = [ + "autocfg 1.0.0", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-bigint-dig" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3d03c330f9f7a2c19e3c0b42698e48141d0809c78cd9b6219f85bd7d7e892aa" +dependencies = [ + "autocfg 0.1.7", + "byteorder", + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand", + "serde", + "smallvec", + "zeroize", +] + [[package]] name = "num-integer" version = "0.1.42" @@ -1286,6 +1378,17 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-iter" +version = "0.1.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfb0800a0291891dd9f4fe7bd9c19384f98f7fbe0cd0f39a2c6b88b9868bbc00" +dependencies = [ + "autocfg 1.0.0", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.11" @@ -1362,6 +1465,17 @@ dependencies = [ "winapi 0.3.8", ] +[[package]] +name = "pem" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1581760c757a756a41f0ee3ff01256227bdf64cb752839779b95ffb01c59793" +dependencies = [ + "base64 0.11.0", + "lazy_static", + "regex", +] + [[package]] name = "percent-encoding" version = "2.1.0" @@ -1430,7 +1544,7 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a30f0e172ae0fb0653dbf777ad10a74b8e58d6de95a892f2e1d3e94a9df9a844" dependencies = [ - "base64", + "base64 0.11.0", "byteorder", "bytes", "fallible-iterator", @@ -1586,6 +1700,49 @@ dependencies = [ "quick-error", ] +[[package]] +name = "rsa" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ed8692d8e0ea3baae03f0f32ecfc13a6c6f1f85fcd6d9fdefcdf364e70f4df9" +dependencies = [ + "byteorder", + "failure", + "lazy_static", + "num-bigint-dig", + "num-integer", + "num-iter", + "num-traits", + "rand", + "subtle 2.2.2", + "zeroize", +] + +[[package]] +name = "rsa-magic-public-key" +version = "0.1.0" +source = "git+https://git.asonix.dog/Aardwolf/rsa-magic-public-key#82878d8274530c8184be8e82f0de36623b51d3f6" +dependencies = [ + "base64 0.11.0", + "num-bigint-dig", + "rsa", + "thiserror", +] + +[[package]] +name = "rsa-pem" +version = "0.1.0" +source = "git+https://git.asonix.dog/Aardwolf/rsa-pem#6c47c3fc377375a5bfedbb7457832fc013d3227d" +dependencies = [ + "num-bigint", + "num-bigint-dig", + "num-traits", + "pem", + "rsa", + "thiserror", + "yasna", +] + [[package]] name = "rustc-demangle" version = "0.1.16" @@ -1752,6 +1909,12 @@ dependencies = [ "winapi 0.3.8", ] +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + [[package]] name = "standback" version = "0.2.1" @@ -1823,6 +1986,12 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d67a5a62ba6e01cb2192ff309324cb4875d0c451d55fe2319433abe7a05a8ee" +[[package]] +name = "subtle" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c65d530b10ccaeac294f349038a597e435b18fb456aadd0840a623f83b9e941" + [[package]] name = "syn" version = "1.0.16" @@ -2260,3 +2429,33 @@ dependencies = [ "winapi 0.2.8", "winapi-build", ] + +[[package]] +name = "yasna" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a563d10ead87e2d798e357d44f40f495ad70bcee4d5c0d3f77a5b1b7376645d9" +dependencies = [ + "num-bigint", +] + +[[package]] +name = "zeroize" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cbac2ed2ba24cc90f5e06485ac8c7c1e5449fe8911aef4d8877218af021a5b8" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de251eec69fc7c1bc3923403d18ececb929380e016afe103da75f396704f8ca2" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] diff --git a/Cargo.toml b/Cargo.toml index 7fc43a9..bcef980 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,18 +9,29 @@ edition = "2018" [dependencies] anyhow = "1.0" actix = "0.10.0-alpha.2" -actix-web = { version = "3.0.0-alpha.1", features = ["openssl"] } actix-rt = "1.0.0" +actix-web = { version = "3.0.0-alpha.1", features = ["openssl"] } +actix-webfinger = { version = "0.3.0-alpha.2" } activitystreams = "0.5.0-alpha.6" +base64 = "0.12" bb8-postgres = "0.4.0" dotenv = "0.15.0" futures = "0.3.4" +http-signature-normalization-actix = { version = "0.3.0-alpha.1", default-features = false, features = ["sha-2"] } log = "0.4" lru = "0.4.3" pretty_env_logger = "0.4.0" +rand = "0.7" +rsa = "0.2" +rsa-magic-public-key = { version = "0.1.0", git = "https://git.asonix.dog/Aardwolf/rsa-magic-public-key" } +rsa-pem = { version = "0.1.0", git = "https://git.asonix.dog/Aardwolf/rsa-pem" } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" +sha2 = "0.8" thiserror = "1.0" tokio = { version = "0.2.13", features = ["sync"] } ttl_cache = "0.5.1" uuid = { version = "0.8", features = ["v4"] } + +[profile.dev.package.rsa] +opt-level = 3 diff --git a/migrations/2020-03-16-012053_create-settings/down.sql b/migrations/2020-03-16-012053_create-settings/down.sql new file mode 100644 index 0000000..f1463d7 --- /dev/null +++ b/migrations/2020-03-16-012053_create-settings/down.sql @@ -0,0 +1,3 @@ +-- This file should undo anything in `up.sql` +DROP INDEX settings_key_index; +DROP TABLE settings; diff --git a/migrations/2020-03-16-012053_create-settings/up.sql b/migrations/2020-03-16-012053_create-settings/up.sql new file mode 100644 index 0000000..9836a98 --- /dev/null +++ b/migrations/2020-03-16-012053_create-settings/up.sql @@ -0,0 +1,12 @@ +-- Your SQL goes here +CREATE TABLE settings ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + key TEXT UNIQUE NOT NULL, + value TEXT NOT NULL, + created_at TIMESTAMP NOT NULL, + updated_at TIMESTAMP +); + +CREATE INDEX settings_key_index ON settings(key); + +SELECT diesel_manage_updated_at('settings'); diff --git a/src/apub.rs b/src/apub.rs index 8757e02..269ca4d 100644 --- a/src/apub.rs +++ b/src/apub.rs @@ -5,6 +5,23 @@ use activitystreams::{ }; use std::collections::HashMap; +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +#[serde(rename_all = "camelCase")] +pub struct PublicKey { + pub id: XsdAnyUri, + pub owner: XsdAnyUri, + pub public_key_pem: String, +} + +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +#[serde(rename_all = "camelCase")] +pub struct PublicKeyExtension { + public_key: PublicKey, + + #[serde(flatten)] + extending: T, +} + #[derive(Clone, Debug, serde::Deserialize, serde::Serialize, PropRefs)] #[serde(rename_all = "camelCase")] #[prop_refs(Object)] @@ -72,6 +89,15 @@ pub struct Endpoints { shared_inbox: Option, } +impl PublicKey { + pub fn extend(self, extending: T) -> PublicKeyExtension { + PublicKeyExtension { + public_key: self, + extending, + } + } +} + impl ValidObjects { pub fn id(&self) -> &XsdAnyUri { match self { diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..f6f8eee --- /dev/null +++ b/src/error.rs @@ -0,0 +1,77 @@ +use activitystreams::primitives::XsdAnyUriError; +use actix_web::{error::ResponseError, http::StatusCode, HttpResponse}; +use log::error; +use rsa_pem::KeyError; +use std::{convert::Infallible, io::Error}; + +#[derive(Debug, thiserror::Error)] +pub enum MyError { + #[error("Couldn't parse key, {0}")] + Key(#[from] KeyError), + + #[error("Couldn't parse URI, {0}")] + Uri(#[from] XsdAnyUriError), + + #[error("Couldn't perform IO, {0}")] + Io(#[from] Error), + + #[error("Couldn't sign string")] + Rsa(rsa::errors::Error), + + #[error("Couldn't do the json thing")] + Json(#[from] serde_json::Error), + + #[error("Couldn't serialzize the signature header")] + HeaderSerialize(#[from] actix_web::http::header::ToStrError), + + #[error("Couldn't parse the signature header")] + HeaderValidation(#[from] actix_web::http::header::InvalidHeaderValue), + + #[error("Wrong ActivityPub kind")] + Kind, + + #[error("Object has already been relayed")] + Duplicate, + + #[error("Actor is blocked")] + Blocked, + + #[error("Actor is not whitelisted")] + Whitelist, + + #[error("Couldn't send request")] + SendRequest, + + #[error("Couldn't receive request response")] + ReceiveResponse, + + #[error("Response has invalid status code")] + Status, + + #[error("URI is missing domain field")] + Domain, +} + +impl ResponseError for MyError { + fn status_code(&self) -> StatusCode { + StatusCode::INTERNAL_SERVER_ERROR + } + + fn error_response(&self) -> HttpResponse { + HttpResponse::InternalServerError() + .header("Content-Type", "application/activity+json") + .json(serde_json::json!({})) + } +} + +impl From for MyError { + fn from(i: Infallible) -> Self { + match i {} + } +} + +impl From for MyError { + fn from(e: rsa::errors::Error) -> Self { + MyError::Rsa(e) + } +} diff --git a/src/inbox.rs b/src/inbox.rs index d12e048..30c7065 100644 --- a/src/inbox.rs +++ b/src/inbox.rs @@ -4,20 +4,17 @@ use activitystreams::{ primitives::XsdAnyUri, }; use actix::Addr; -use actix_web::{client::Client, http::StatusCode, web, HttpResponse}; +use actix_web::{client::Client, web, HttpResponse}; use futures::join; use log::error; use crate::{ apub::{AcceptedActors, AcceptedObjects, ValidTypes}, db_actor::{DbActor, DbQuery, Pool}, + error::MyError, state::{State, UrlKind}, }; -#[derive(Clone, Debug, thiserror::Error)] -#[error("Something went wrong :(")] -pub struct MyError; - pub async fn inbox( db_actor: web::Data>, state: web::Data, @@ -57,7 +54,7 @@ async fn handle_undo( actor: AcceptedActors, ) -> Result { if !input.object.is_kind("Follow") { - return Err(MyError); + return Err(MyError::Kind); } let inbox = actor.inbox().to_owned(); @@ -99,7 +96,7 @@ async fn handle_undo( let undo2 = undo.clone(); let client = client.into_inner(); actix::Arbiter::spawn(async move { - let _ = deliver(&client, actor.id, &undo2).await; + let _ = deliver(&state.into_inner(), &client, actor.id, &undo2).await; }); } @@ -116,7 +113,7 @@ async fn handle_forward( let inboxes = get_inboxes(&state, &actor, &object_id).await?; - deliver_many(client, inboxes, input.clone()); + deliver_many(state, client, inboxes, input.clone()); Ok(response(input)) } @@ -130,7 +127,7 @@ async fn handle_relay( let object_id = input.object.id(); if state.is_cached(object_id).await { - return Err(MyError); + return Err(MyError::Duplicate); } let activity_id: XsdAnyUri = state.generate_url(UrlKind::Activity).parse()?; @@ -149,10 +146,10 @@ async fn handle_relay( let inboxes = get_inboxes(&state, &actor, &object_id).await?; - deliver_many(client, inboxes, announce.clone()); - state.cache(object_id.to_owned(), activity_id).await; + deliver_many(state, client, inboxes, announce.clone()); + Ok(response(announce)) } @@ -171,12 +168,12 @@ async fn handle_follow( if is_blocked { error!("Follow from blocked listener, {}", actor.id); - return Err(MyError); + return Err(MyError::Blocked); } if !is_whitelisted { error!("Follow from non-whitelisted listener, {}", actor.id); - return Err(MyError); + return Err(MyError::Whitelist); } if !is_listener { @@ -220,7 +217,7 @@ async fn handle_follow( let client = client.into_inner(); let accept2 = accept.clone(); actix::Arbiter::spawn(async move { - let _ = deliver(&client, actor_inbox, &accept2).await; + let _ = deliver(&state.into_inner(), &client, actor_inbox, &accept2).await; }); Ok(response(accept)) @@ -242,13 +239,13 @@ async fn fetch_actor( .await .map_err(|e| { error!("Couldn't send request to {} for actor, {}", actor_id, e); - MyError + MyError::SendRequest })? .json() .await .map_err(|e| { error!("Coudn't fetch actor from {}, {}", actor_id, e); - MyError + MyError::ReceiveResponse })?; state.cache_actor(actor_id.to_owned(), actor.clone()).await; @@ -256,20 +253,24 @@ async fn fetch_actor( Ok(actor) } -fn deliver_many(client: web::Data, inboxes: Vec, item: T) -where +fn deliver_many( + state: web::Data, + client: web::Data, + inboxes: Vec, + item: T, +) where T: serde::ser::Serialize + 'static, { let client = client.into_inner(); + let state = state.into_inner(); actix::Arbiter::spawn(async move { use futures::stream::StreamExt; - let client = client.clone(); let mut unordered = futures::stream::FuturesUnordered::new(); for inbox in inboxes { - unordered.push(deliver(&client, inbox, &item)); + unordered.push(deliver(&state, &client, inbox, &item)); } while let Some(_) = unordered.next().await {} @@ -277,6 +278,7 @@ where } async fn deliver( + state: &std::sync::Arc, client: &std::sync::Arc, inbox: XsdAnyUri, item: &T, @@ -284,20 +286,38 @@ async fn deliver( where T: serde::ser::Serialize, { + use http_signature_normalization_actix::prelude::*; + use sha2::{Digest, Sha256}; + + let config = Config::default(); + let mut digest = Sha256::new(); + + let key_id = state.generate_url(UrlKind::Actor); + + let item_string = serde_json::to_string(item)?; + let res = client .post(inbox.as_str()) .header("Accept", "application/activity+json") .header("Content-Type", "application/activity+json") - .send_json(item) + .header("User-Agent", "Aode Relay v0.1.0") + .signature_with_digest( + &config, + &key_id, + &mut digest, + item_string, + |signing_string| state.sign(signing_string.as_bytes()), + )? + .send() .await .map_err(|e| { error!("Couldn't send deliver request to {}, {}", inbox, e); - MyError + MyError::SendRequest })?; if !res.status().is_success() { error!("Invalid response status from {}, {}", inbox, res.status()); - return Err(MyError); + return Err(MyError::Status); } Ok(()) @@ -308,41 +328,13 @@ async fn get_inboxes( actor: &AcceptedActors, object_id: &XsdAnyUri, ) -> Result, MyError> { - let domain = object_id.as_url().host().ok_or(MyError)?.to_string(); + let domain = object_id + .as_url() + .host() + .ok_or(MyError::Domain)? + .to_string(); let inbox = actor.inbox(); Ok(state.listeners_without(&inbox, &domain).await) } - -impl actix_web::error::ResponseError for MyError { - fn status_code(&self) -> StatusCode { - StatusCode::INTERNAL_SERVER_ERROR - } - - fn error_response(&self) -> HttpResponse { - HttpResponse::InternalServerError() - .header("Content-Type", "application/activity+json") - .json(serde_json::json!({})) - } -} - -impl From for MyError { - fn from(_: std::convert::Infallible) -> Self { - MyError - } -} - -impl From for MyError { - fn from(_: activitystreams::primitives::XsdAnyUriError) -> Self { - error!("Error parsing URI"); - MyError - } -} - -impl From for MyError { - fn from(e: std::io::Error) -> Self { - error!("JSON Error, {}", e); - MyError - } -} diff --git a/src/main.rs b/src/main.rs index 69e31b9..36199cb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,16 +2,20 @@ use activitystreams::{actor::apub::Application, context, endpoint::EndpointProperties}; use actix_web::{client::Client, middleware::Logger, web, App, HttpServer, Responder}; use bb8_postgres::tokio_postgres; +use rsa_pem::KeyExt; mod apub; mod db_actor; +mod error; mod inbox; mod label; mod state; +mod webfinger; use self::{ + apub::PublicKey, db_actor::DbActor, - inbox::MyError, + error::MyError, label::ArbiterLabelFactory, state::{State, UrlKind}, }; @@ -42,7 +46,13 @@ async fn actor_route(state: web::Data) -> Result .set_inbox(state.generate_url(UrlKind::Inbox))? .set_endpoints(endpoint)?; - Ok(inbox::response(application)) + let public_key = PublicKey { + id: state.generate_url(UrlKind::MainKey).parse()?, + owner: state.generate_url(UrlKind::Actor).parse()?, + public_key_pem: state.settings.public_key.to_pem_pkcs8()?, + }; + + Ok(inbox::response(public_key.extend(application))) } #[actix_rt::main] @@ -81,6 +91,7 @@ async fn main() -> Result<(), anyhow::Error> { .service(web::resource("/").route(web::get().to(index))) .service(web::resource("/inbox").route(web::post().to(inbox::inbox))) .service(web::resource("/actor").route(web::get().to(actor_route))) + .service(actix_webfinger::resource::<_, webfinger::RelayResolver>()) }) .bind("127.0.0.1:8080")? .run() diff --git a/src/schema.rs b/src/schema.rs index 9222afd..4d9d3e0 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -16,6 +16,16 @@ table! { } } +table! { + settings (id) { + id -> Uuid, + key -> Text, + value -> Text, + created_at -> Timestamp, + updated_at -> Nullable, + } +} + table! { whitelists (id) { id -> Uuid, @@ -28,5 +38,6 @@ table! { allow_tables_to_appear_in_same_query!( blocks, listeners, + settings, whitelists, ); diff --git a/src/state.rs b/src/state.rs index 6558566..1a0f146 100644 --- a/src/state.rs +++ b/src/state.rs @@ -2,7 +2,11 @@ use activitystreams::primitives::XsdAnyUri; use anyhow::Error; use bb8_postgres::tokio_postgres::{row::Row, Client}; use futures::try_join; +use log::{error, info}; use lru::LruCache; +use rand::thread_rng; +use rsa::{RSAPrivateKey, RSAPublicKey}; +use rsa_pem::KeyExt; use std::{collections::HashSet, sync::Arc}; use tokio::sync::RwLock; use ttl_cache::TtlCache; @@ -12,9 +16,7 @@ use crate::{apub::AcceptedActors, db_actor::Pool}; #[derive(Clone)] pub struct State { - use_https: bool, - hostname: String, - whitelist_enabled: bool, + pub settings: Settings, actor_cache: Arc>>, actor_id_cache: Arc>>, blocks: Arc>>, @@ -22,20 +24,73 @@ pub struct State { listeners: Arc>>, } +#[derive(Clone)] +pub struct Settings { + pub use_https: bool, + pub whitelist_enabled: bool, + pub hostname: String, + pub public_key: RSAPublicKey, + private_key: RSAPrivateKey, +} + pub enum UrlKind { Activity, Actor, Followers, Following, Inbox, + MainKey, } #[derive(Clone, Debug, thiserror::Error)] #[error("No host present in URI")] pub struct HostError; -impl State { - pub fn generate_url(&self, kind: UrlKind) -> String { +#[derive(Clone, Debug, thiserror::Error)] +#[error("Error generating RSA key")] +pub struct RsaError; + +impl Settings { + async fn hydrate( + client: &Client, + use_https: bool, + whitelist_enabled: bool, + hostname: String, + ) -> Result { + info!("SELECT value FROM settings WHERE key = 'private_key'"); + let rows = client + .query("SELECT value FROM settings WHERE key = 'private_key'", &[]) + .await?; + + let private_key = if let Some(row) = rows.into_iter().next() { + let key_str: String = row.get(0); + KeyExt::from_pem_pkcs8(&key_str)? + } else { + info!("Generating new keys"); + let mut rng = thread_rng(); + let key = RSAPrivateKey::new(&mut rng, 4096).map_err(|e| { + error!("Error generating RSA key, {}", e); + RsaError + })?; + let pem_pkcs8 = key.to_pem_pkcs8()?; + + info!("INSERT INTO settings (key, value, created_at) VALUES ('private_key', $1::TEXT, 'now');"); + client.execute("INSERT INTO settings (key, value, created_at) VALUES ('private_key', $1::TEXT, 'now');", &[&pem_pkcs8]).await?; + key + }; + + let public_key = private_key.to_public_key(); + + Ok(Settings { + use_https, + whitelist_enabled, + hostname, + private_key, + public_key, + }) + } + + fn generate_url(&self, kind: UrlKind) -> String { let scheme = if self.use_https { "https" } else { "http" }; match kind { @@ -46,13 +101,40 @@ impl State { UrlKind::Followers => format!("{}://{}/followers", scheme, self.hostname), UrlKind::Following => format!("{}://{}/following", scheme, self.hostname), UrlKind::Inbox => format!("{}://{}/inbox", scheme, self.hostname), + UrlKind::MainKey => format!("{}://{}/actor#main-key", scheme, self.hostname), } } + fn generate_resource(&self) -> String { + format!("relay@{}", self.hostname) + } + + fn sign(&self, bytes: &[u8]) -> Result { + use rsa::{hash::Hashes, padding::PaddingScheme}; + let bytes = + self.private_key + .sign(PaddingScheme::PKCS1v15, Some(&Hashes::SHA2_256), bytes)?; + Ok(base64::encode_config(bytes, base64::URL_SAFE)) + } +} + +impl State { + pub fn generate_url(&self, kind: UrlKind) -> String { + self.settings.generate_url(kind) + } + + pub fn generate_resource(&self) -> String { + self.settings.generate_resource() + } + + pub fn sign(&self, bytes: &[u8]) -> Result { + self.settings.sign(bytes) + } + pub async fn remove_listener(&self, client: &Client, inbox: &XsdAnyUri) -> Result<(), Error> { let hs = self.listeners.clone(); - log::info!("DELETE FROM listeners WHERE actor_id = {};", inbox.as_str()); + info!("DELETE FROM listeners WHERE actor_id = {};", inbox.as_str()); client .execute( "DELETE FROM listeners WHERE actor_id = $1::TEXT;", @@ -86,7 +168,7 @@ impl State { } pub async fn is_whitelisted(&self, actor_id: &XsdAnyUri) -> bool { - if !self.whitelist_enabled { + if !self.settings.whitelist_enabled { return true; } @@ -155,7 +237,7 @@ impl State { return Err(HostError.into()); }; - log::info!( + info!( "INSERT INTO blocks (domain_name, created_at) VALUES ($1::TEXT, 'now'); [{}]", host.to_string() ); @@ -181,7 +263,7 @@ impl State { return Err(HostError.into()); }; - log::info!( + info!( "INSERT INTO whitelists (domain_name, created_at) VALUES ($1::TEXT, 'now'); [{}]", host.to_string() ); @@ -201,7 +283,7 @@ impl State { pub async fn add_listener(&self, client: &Client, listener: XsdAnyUri) -> Result<(), Error> { let listeners = self.listeners.clone(); - log::info!( + info!( "INSERT INTO listeners (actor_id, created_at) VALUES ($1::TEXT, 'now'); [{}]", listener.as_str(), ); @@ -226,6 +308,7 @@ impl State { ) -> Result { let pool1 = pool.clone(); let pool2 = pool.clone(); + let pool3 = pool.clone(); let f1 = async move { let conn = pool.get().await?; @@ -245,12 +328,16 @@ impl State { hydrate_listeners(&conn).await }; - let (blocks, whitelists, listeners) = try_join!(f1, f2, f3)?; + let f4 = async move { + let conn = pool3.get().await?; + + Settings::hydrate(&conn, use_https, whitelist_enabled, hostname).await + }; + + let (blocks, whitelists, listeners, settings) = try_join!(f1, f2, f3, f4)?; Ok(State { - use_https, - whitelist_enabled, - hostname, + settings, actor_cache: Arc::new(RwLock::new(TtlCache::new(1024 * 8))), actor_id_cache: Arc::new(RwLock::new(LruCache::new(1024 * 8))), blocks: Arc::new(RwLock::new(blocks)), @@ -261,14 +348,14 @@ impl State { } pub async fn hydrate_blocks(client: &Client) -> Result, Error> { - log::info!("SELECT domain_name FROM blocks"); + info!("SELECT domain_name FROM blocks"); let rows = client.query("SELECT domain_name FROM blocks", &[]).await?; parse_rows(rows) } pub async fn hydrate_whitelists(client: &Client) -> Result, Error> { - log::info!("SELECT domain_name FROM whitelists"); + info!("SELECT domain_name FROM whitelists"); let rows = client .query("SELECT domain_name FROM whitelists", &[]) .await?; @@ -277,7 +364,7 @@ pub async fn hydrate_whitelists(client: &Client) -> Result, Erro } pub async fn hydrate_listeners(client: &Client) -> Result, Error> { - log::info!("SELECT actor_id FROM listeners"); + info!("SELECT actor_id FROM listeners"); let rows = client.query("SELECT actor_id FROM listeners", &[]).await?; parse_rows(rows) diff --git a/src/webfinger.rs b/src/webfinger.rs new file mode 100644 index 0000000..8d8f14e --- /dev/null +++ b/src/webfinger.rs @@ -0,0 +1,52 @@ +use crate::state::{State, UrlKind}; +use activitystreams::context; +use actix_web::web::Data; +use actix_webfinger::{Link, Resolver, Webfinger}; +use rsa_magic_public_key::AsMagicPublicKey; +use std::{future::Future, pin::Pin}; + +pub struct RelayResolver; + +#[derive(Clone, Debug, thiserror::Error)] +#[error("Error resolving webfinger data")] +pub struct RelayError; + +impl Resolver> for RelayResolver { + type Error = RelayError; + + fn find( + account: &str, + domain: &str, + state: Data, + ) -> Pin, Self::Error>>>> { + let domain = domain.to_owned(); + let account = account.to_owned(); + + let fut = async move { + if domain != state.settings.hostname { + return Ok(None); + } + + if account != "relay" { + return Ok(None); + } + + let mut wf = Webfinger::new(&state.generate_resource()); + wf.add_alias(&state.generate_url(UrlKind::Actor)) + .add_activitypub(&state.generate_url(UrlKind::Actor)) + .add_magic_public_key(&state.settings.public_key.as_magic_public_key()) + .add_link(Link { + rel: "self".to_owned(), + href: Some(state.generate_url(UrlKind::Actor)), + template: None, + kind: Some(format!("application/ld+json; profile=\"{}\"", context())), + }); + + Ok(Some(wf)) + }; + + Box::pin(fut) + } +} + +impl actix_web::error::ResponseError for RelayError {}