Add keys, http-sigs, webfinger

This commit is contained in:
asonix 2020-03-15 22:36:46 -05:00
parent d741e2b202
commit eea0577686
11 changed files with 561 additions and 80 deletions

207
Cargo.lock generated
View file

@ -100,7 +100,7 @@ dependencies = [
"actix-threadpool", "actix-threadpool",
"actix-tls", "actix-tls",
"actix-utils", "actix-utils",
"base64", "base64 0.11.0",
"bitflags", "bitflags",
"brotli2", "brotli2",
"bytes", "bytes",
@ -313,6 +313,19 @@ dependencies = [
"syn", "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]] [[package]]
name = "actix_derive" name = "actix_derive"
version = "0.5.0" version = "0.5.0"
@ -362,15 +375,23 @@ dependencies = [
"actix", "actix",
"actix-rt", "actix-rt",
"actix-web", "actix-web",
"actix-webfinger",
"anyhow", "anyhow",
"base64 0.12.0",
"bb8-postgres", "bb8-postgres",
"dotenv", "dotenv",
"futures", "futures",
"http-signature-normalization-actix",
"log", "log",
"lru", "lru",
"pretty_env_logger", "pretty_env_logger",
"rand",
"rsa",
"rsa-magic-public-key",
"rsa-pem",
"serde", "serde",
"serde_json", "serde_json",
"sha2",
"thiserror", "thiserror",
"tokio", "tokio",
"ttl_cache", "ttl_cache",
@ -427,7 +448,7 @@ dependencies = [
"actix-http", "actix-http",
"actix-rt", "actix-rt",
"actix-service", "actix-service",
"base64", "base64 0.11.0",
"bytes", "bytes",
"derive_more", "derive_more",
"futures-core", "futures-core",
@ -475,6 +496,12 @@ version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b41b7ea54a0c9d92199de89e20e58d49f02f8e699814ef3fdf266f6f748d15c7" checksum = "b41b7ea54a0c9d92199de89e20e58d49f02f8e699814ef3fdf266f6f748d15c7"
[[package]]
name = "base64"
version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d5ca2cd0adc3f48f9e9ea5a6bbdf9ccc0bfade884847e484d452414c7ccffb3"
[[package]] [[package]]
name = "bb8" name = "bb8"
version = "0.4.0" version = "0.4.0"
@ -674,7 +701,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4434400df11d95d556bac068ddfedd482915eb18fe8bea89bc80b6e4b1c179e5" checksum = "4434400df11d95d556bac068ddfedd482915eb18fe8bea89bc80b6e4b1c179e5"
dependencies = [ dependencies = [
"generic-array 0.12.3", "generic-array 0.12.3",
"subtle", "subtle 1.0.0",
] ]
[[package]] [[package]]
@ -1046,6 +1073,32 @@ dependencies = [
"itoa", "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]] [[package]]
name = "httparse" name = "httparse"
version = "1.3.4" version = "1.3.4"
@ -1129,6 +1182,9 @@ name = "lazy_static"
version = "1.4.0" version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
dependencies = [
"spin",
]
[[package]] [[package]]
name = "libc" name = "libc"
@ -1136,6 +1192,12 @@ version = "0.2.67"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eb147597cdf94ed43ab7a9038716637d2d1bf2bc571da995d0028dec06bd3018" checksum = "eb147597cdf94ed43ab7a9038716637d2d1bf2bc571da995d0028dec06bd3018"
[[package]]
name = "libm"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7d73b3f436185384286bd8098d17ec07c9a7d2388a6599f824d8502b529702a"
[[package]] [[package]]
name = "linked-hash-map" name = "linked-hash-map"
version = "0.5.2" version = "0.5.2"
@ -1276,6 +1338,36 @@ dependencies = [
"winapi 0.3.8", "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]] [[package]]
name = "num-integer" name = "num-integer"
version = "0.1.42" version = "0.1.42"
@ -1286,6 +1378,17 @@ dependencies = [
"num-traits", "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]] [[package]]
name = "num-traits" name = "num-traits"
version = "0.2.11" version = "0.2.11"
@ -1362,6 +1465,17 @@ dependencies = [
"winapi 0.3.8", "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]] [[package]]
name = "percent-encoding" name = "percent-encoding"
version = "2.1.0" version = "2.1.0"
@ -1430,7 +1544,7 @@ version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a30f0e172ae0fb0653dbf777ad10a74b8e58d6de95a892f2e1d3e94a9df9a844" checksum = "a30f0e172ae0fb0653dbf777ad10a74b8e58d6de95a892f2e1d3e94a9df9a844"
dependencies = [ dependencies = [
"base64", "base64 0.11.0",
"byteorder", "byteorder",
"bytes", "bytes",
"fallible-iterator", "fallible-iterator",
@ -1586,6 +1700,49 @@ dependencies = [
"quick-error", "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]] [[package]]
name = "rustc-demangle" name = "rustc-demangle"
version = "0.1.16" version = "0.1.16"
@ -1752,6 +1909,12 @@ dependencies = [
"winapi 0.3.8", "winapi 0.3.8",
] ]
[[package]]
name = "spin"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d"
[[package]] [[package]]
name = "standback" name = "standback"
version = "0.2.1" version = "0.2.1"
@ -1823,6 +1986,12 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2d67a5a62ba6e01cb2192ff309324cb4875d0c451d55fe2319433abe7a05a8ee" checksum = "2d67a5a62ba6e01cb2192ff309324cb4875d0c451d55fe2319433abe7a05a8ee"
[[package]]
name = "subtle"
version = "2.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c65d530b10ccaeac294f349038a597e435b18fb456aadd0840a623f83b9e941"
[[package]] [[package]]
name = "syn" name = "syn"
version = "1.0.16" version = "1.0.16"
@ -2260,3 +2429,33 @@ dependencies = [
"winapi 0.2.8", "winapi 0.2.8",
"winapi-build", "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",
]

View file

@ -9,18 +9,29 @@ edition = "2018"
[dependencies] [dependencies]
anyhow = "1.0" anyhow = "1.0"
actix = "0.10.0-alpha.2" actix = "0.10.0-alpha.2"
actix-web = { version = "3.0.0-alpha.1", features = ["openssl"] }
actix-rt = "1.0.0" 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" activitystreams = "0.5.0-alpha.6"
base64 = "0.12"
bb8-postgres = "0.4.0" bb8-postgres = "0.4.0"
dotenv = "0.15.0" dotenv = "0.15.0"
futures = "0.3.4" futures = "0.3.4"
http-signature-normalization-actix = { version = "0.3.0-alpha.1", default-features = false, features = ["sha-2"] }
log = "0.4" log = "0.4"
lru = "0.4.3" lru = "0.4.3"
pretty_env_logger = "0.4.0" 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 = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"
sha2 = "0.8"
thiserror = "1.0" thiserror = "1.0"
tokio = { version = "0.2.13", features = ["sync"] } tokio = { version = "0.2.13", features = ["sync"] }
ttl_cache = "0.5.1" ttl_cache = "0.5.1"
uuid = { version = "0.8", features = ["v4"] } uuid = { version = "0.8", features = ["v4"] }
[profile.dev.package.rsa]
opt-level = 3

View file

@ -0,0 +1,3 @@
-- This file should undo anything in `up.sql`
DROP INDEX settings_key_index;
DROP TABLE settings;

View file

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

View file

@ -5,6 +5,23 @@ use activitystreams::{
}; };
use std::collections::HashMap; 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<T> {
public_key: PublicKey,
#[serde(flatten)]
extending: T,
}
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize, PropRefs)] #[derive(Clone, Debug, serde::Deserialize, serde::Serialize, PropRefs)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[prop_refs(Object)] #[prop_refs(Object)]
@ -72,6 +89,15 @@ pub struct Endpoints {
shared_inbox: Option<XsdAnyUri>, shared_inbox: Option<XsdAnyUri>,
} }
impl PublicKey {
pub fn extend<T>(self, extending: T) -> PublicKeyExtension<T> {
PublicKeyExtension {
public_key: self,
extending,
}
}
}
impl ValidObjects { impl ValidObjects {
pub fn id(&self) -> &XsdAnyUri { pub fn id(&self) -> &XsdAnyUri {
match self { match self {

77
src/error.rs Normal file
View file

@ -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<Infallible> for MyError {
fn from(i: Infallible) -> Self {
match i {}
}
}
impl From<rsa::errors::Error> for MyError {
fn from(e: rsa::errors::Error) -> Self {
MyError::Rsa(e)
}
}

View file

@ -4,20 +4,17 @@ use activitystreams::{
primitives::XsdAnyUri, primitives::XsdAnyUri,
}; };
use actix::Addr; use actix::Addr;
use actix_web::{client::Client, http::StatusCode, web, HttpResponse}; use actix_web::{client::Client, web, HttpResponse};
use futures::join; use futures::join;
use log::error; use log::error;
use crate::{ use crate::{
apub::{AcceptedActors, AcceptedObjects, ValidTypes}, apub::{AcceptedActors, AcceptedObjects, ValidTypes},
db_actor::{DbActor, DbQuery, Pool}, db_actor::{DbActor, DbQuery, Pool},
error::MyError,
state::{State, UrlKind}, state::{State, UrlKind},
}; };
#[derive(Clone, Debug, thiserror::Error)]
#[error("Something went wrong :(")]
pub struct MyError;
pub async fn inbox( pub async fn inbox(
db_actor: web::Data<Addr<DbActor>>, db_actor: web::Data<Addr<DbActor>>,
state: web::Data<State>, state: web::Data<State>,
@ -57,7 +54,7 @@ async fn handle_undo(
actor: AcceptedActors, actor: AcceptedActors,
) -> Result<HttpResponse, MyError> { ) -> Result<HttpResponse, MyError> {
if !input.object.is_kind("Follow") { if !input.object.is_kind("Follow") {
return Err(MyError); return Err(MyError::Kind);
} }
let inbox = actor.inbox().to_owned(); let inbox = actor.inbox().to_owned();
@ -99,7 +96,7 @@ async fn handle_undo(
let undo2 = undo.clone(); let undo2 = undo.clone();
let client = client.into_inner(); let client = client.into_inner();
actix::Arbiter::spawn(async move { 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?; 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)) Ok(response(input))
} }
@ -130,7 +127,7 @@ async fn handle_relay(
let object_id = input.object.id(); let object_id = input.object.id();
if state.is_cached(object_id).await { if state.is_cached(object_id).await {
return Err(MyError); return Err(MyError::Duplicate);
} }
let activity_id: XsdAnyUri = state.generate_url(UrlKind::Activity).parse()?; 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?; let inboxes = get_inboxes(&state, &actor, &object_id).await?;
deliver_many(client, inboxes, announce.clone());
state.cache(object_id.to_owned(), activity_id).await; state.cache(object_id.to_owned(), activity_id).await;
deliver_many(state, client, inboxes, announce.clone());
Ok(response(announce)) Ok(response(announce))
} }
@ -171,12 +168,12 @@ async fn handle_follow(
if is_blocked { if is_blocked {
error!("Follow from blocked listener, {}", actor.id); error!("Follow from blocked listener, {}", actor.id);
return Err(MyError); return Err(MyError::Blocked);
} }
if !is_whitelisted { if !is_whitelisted {
error!("Follow from non-whitelisted listener, {}", actor.id); error!("Follow from non-whitelisted listener, {}", actor.id);
return Err(MyError); return Err(MyError::Whitelist);
} }
if !is_listener { if !is_listener {
@ -220,7 +217,7 @@ async fn handle_follow(
let client = client.into_inner(); let client = client.into_inner();
let accept2 = accept.clone(); let accept2 = accept.clone();
actix::Arbiter::spawn(async move { 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)) Ok(response(accept))
@ -242,13 +239,13 @@ async fn fetch_actor(
.await .await
.map_err(|e| { .map_err(|e| {
error!("Couldn't send request to {} for actor, {}", actor_id, e); error!("Couldn't send request to {} for actor, {}", actor_id, e);
MyError MyError::SendRequest
})? })?
.json() .json()
.await .await
.map_err(|e| { .map_err(|e| {
error!("Coudn't fetch actor from {}, {}", actor_id, e); error!("Coudn't fetch actor from {}, {}", actor_id, e);
MyError MyError::ReceiveResponse
})?; })?;
state.cache_actor(actor_id.to_owned(), actor.clone()).await; state.cache_actor(actor_id.to_owned(), actor.clone()).await;
@ -256,20 +253,24 @@ async fn fetch_actor(
Ok(actor) Ok(actor)
} }
fn deliver_many<T>(client: web::Data<Client>, inboxes: Vec<XsdAnyUri>, item: T) fn deliver_many<T>(
where state: web::Data<State>,
client: web::Data<Client>,
inboxes: Vec<XsdAnyUri>,
item: T,
) where
T: serde::ser::Serialize + 'static, T: serde::ser::Serialize + 'static,
{ {
let client = client.into_inner(); let client = client.into_inner();
let state = state.into_inner();
actix::Arbiter::spawn(async move { actix::Arbiter::spawn(async move {
use futures::stream::StreamExt; use futures::stream::StreamExt;
let client = client.clone();
let mut unordered = futures::stream::FuturesUnordered::new(); let mut unordered = futures::stream::FuturesUnordered::new();
for inbox in inboxes { for inbox in inboxes {
unordered.push(deliver(&client, inbox, &item)); unordered.push(deliver(&state, &client, inbox, &item));
} }
while let Some(_) = unordered.next().await {} while let Some(_) = unordered.next().await {}
@ -277,6 +278,7 @@ where
} }
async fn deliver<T>( async fn deliver<T>(
state: &std::sync::Arc<State>,
client: &std::sync::Arc<Client>, client: &std::sync::Arc<Client>,
inbox: XsdAnyUri, inbox: XsdAnyUri,
item: &T, item: &T,
@ -284,20 +286,38 @@ async fn deliver<T>(
where where
T: serde::ser::Serialize, 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 let res = client
.post(inbox.as_str()) .post(inbox.as_str())
.header("Accept", "application/activity+json") .header("Accept", "application/activity+json")
.header("Content-Type", "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 .await
.map_err(|e| { .map_err(|e| {
error!("Couldn't send deliver request to {}, {}", inbox, e); error!("Couldn't send deliver request to {}, {}", inbox, e);
MyError MyError::SendRequest
})?; })?;
if !res.status().is_success() { if !res.status().is_success() {
error!("Invalid response status from {}, {}", inbox, res.status()); error!("Invalid response status from {}, {}", inbox, res.status());
return Err(MyError); return Err(MyError::Status);
} }
Ok(()) Ok(())
@ -308,41 +328,13 @@ async fn get_inboxes(
actor: &AcceptedActors, actor: &AcceptedActors,
object_id: &XsdAnyUri, object_id: &XsdAnyUri,
) -> Result<Vec<XsdAnyUri>, MyError> { ) -> Result<Vec<XsdAnyUri>, 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(); let inbox = actor.inbox();
Ok(state.listeners_without(&inbox, &domain).await) 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<std::convert::Infallible> for MyError {
fn from(_: std::convert::Infallible) -> Self {
MyError
}
}
impl From<activitystreams::primitives::XsdAnyUriError> for MyError {
fn from(_: activitystreams::primitives::XsdAnyUriError) -> Self {
error!("Error parsing URI");
MyError
}
}
impl From<std::io::Error> for MyError {
fn from(e: std::io::Error) -> Self {
error!("JSON Error, {}", e);
MyError
}
}

View file

@ -2,16 +2,20 @@
use activitystreams::{actor::apub::Application, context, endpoint::EndpointProperties}; use activitystreams::{actor::apub::Application, context, endpoint::EndpointProperties};
use actix_web::{client::Client, middleware::Logger, web, App, HttpServer, Responder}; use actix_web::{client::Client, middleware::Logger, web, App, HttpServer, Responder};
use bb8_postgres::tokio_postgres; use bb8_postgres::tokio_postgres;
use rsa_pem::KeyExt;
mod apub; mod apub;
mod db_actor; mod db_actor;
mod error;
mod inbox; mod inbox;
mod label; mod label;
mod state; mod state;
mod webfinger;
use self::{ use self::{
apub::PublicKey,
db_actor::DbActor, db_actor::DbActor,
inbox::MyError, error::MyError,
label::ArbiterLabelFactory, label::ArbiterLabelFactory,
state::{State, UrlKind}, state::{State, UrlKind},
}; };
@ -42,7 +46,13 @@ async fn actor_route(state: web::Data<State>) -> Result<impl Responder, MyError>
.set_inbox(state.generate_url(UrlKind::Inbox))? .set_inbox(state.generate_url(UrlKind::Inbox))?
.set_endpoints(endpoint)?; .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] #[actix_rt::main]
@ -81,6 +91,7 @@ async fn main() -> Result<(), anyhow::Error> {
.service(web::resource("/").route(web::get().to(index))) .service(web::resource("/").route(web::get().to(index)))
.service(web::resource("/inbox").route(web::post().to(inbox::inbox))) .service(web::resource("/inbox").route(web::post().to(inbox::inbox)))
.service(web::resource("/actor").route(web::get().to(actor_route))) .service(web::resource("/actor").route(web::get().to(actor_route)))
.service(actix_webfinger::resource::<_, webfinger::RelayResolver>())
}) })
.bind("127.0.0.1:8080")? .bind("127.0.0.1:8080")?
.run() .run()

View file

@ -16,6 +16,16 @@ table! {
} }
} }
table! {
settings (id) {
id -> Uuid,
key -> Text,
value -> Text,
created_at -> Timestamp,
updated_at -> Nullable<Timestamp>,
}
}
table! { table! {
whitelists (id) { whitelists (id) {
id -> Uuid, id -> Uuid,
@ -28,5 +38,6 @@ table! {
allow_tables_to_appear_in_same_query!( allow_tables_to_appear_in_same_query!(
blocks, blocks,
listeners, listeners,
settings,
whitelists, whitelists,
); );

View file

@ -2,7 +2,11 @@ use activitystreams::primitives::XsdAnyUri;
use anyhow::Error; use anyhow::Error;
use bb8_postgres::tokio_postgres::{row::Row, Client}; use bb8_postgres::tokio_postgres::{row::Row, Client};
use futures::try_join; use futures::try_join;
use log::{error, info};
use lru::LruCache; use lru::LruCache;
use rand::thread_rng;
use rsa::{RSAPrivateKey, RSAPublicKey};
use rsa_pem::KeyExt;
use std::{collections::HashSet, sync::Arc}; use std::{collections::HashSet, sync::Arc};
use tokio::sync::RwLock; use tokio::sync::RwLock;
use ttl_cache::TtlCache; use ttl_cache::TtlCache;
@ -12,9 +16,7 @@ use crate::{apub::AcceptedActors, db_actor::Pool};
#[derive(Clone)] #[derive(Clone)]
pub struct State { pub struct State {
use_https: bool, pub settings: Settings,
hostname: String,
whitelist_enabled: bool,
actor_cache: Arc<RwLock<TtlCache<XsdAnyUri, AcceptedActors>>>, actor_cache: Arc<RwLock<TtlCache<XsdAnyUri, AcceptedActors>>>,
actor_id_cache: Arc<RwLock<LruCache<XsdAnyUri, XsdAnyUri>>>, actor_id_cache: Arc<RwLock<LruCache<XsdAnyUri, XsdAnyUri>>>,
blocks: Arc<RwLock<HashSet<String>>>, blocks: Arc<RwLock<HashSet<String>>>,
@ -22,20 +24,73 @@ pub struct State {
listeners: Arc<RwLock<HashSet<XsdAnyUri>>>, listeners: Arc<RwLock<HashSet<XsdAnyUri>>>,
} }
#[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 { pub enum UrlKind {
Activity, Activity,
Actor, Actor,
Followers, Followers,
Following, Following,
Inbox, Inbox,
MainKey,
} }
#[derive(Clone, Debug, thiserror::Error)] #[derive(Clone, Debug, thiserror::Error)]
#[error("No host present in URI")] #[error("No host present in URI")]
pub struct HostError; pub struct HostError;
impl State { #[derive(Clone, Debug, thiserror::Error)]
pub fn generate_url(&self, kind: UrlKind) -> String { #[error("Error generating RSA key")]
pub struct RsaError;
impl Settings {
async fn hydrate(
client: &Client,
use_https: bool,
whitelist_enabled: bool,
hostname: String,
) -> Result<Self, Error> {
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" }; let scheme = if self.use_https { "https" } else { "http" };
match kind { match kind {
@ -46,13 +101,40 @@ impl State {
UrlKind::Followers => format!("{}://{}/followers", scheme, self.hostname), UrlKind::Followers => format!("{}://{}/followers", scheme, self.hostname),
UrlKind::Following => format!("{}://{}/following", scheme, self.hostname), UrlKind::Following => format!("{}://{}/following", scheme, self.hostname),
UrlKind::Inbox => format!("{}://{}/inbox", 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<String, crate::error::MyError> {
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<String, crate::error::MyError> {
self.settings.sign(bytes)
}
pub async fn remove_listener(&self, client: &Client, inbox: &XsdAnyUri) -> Result<(), Error> { pub async fn remove_listener(&self, client: &Client, inbox: &XsdAnyUri) -> Result<(), Error> {
let hs = self.listeners.clone(); 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 client
.execute( .execute(
"DELETE FROM listeners WHERE actor_id = $1::TEXT;", "DELETE FROM listeners WHERE actor_id = $1::TEXT;",
@ -86,7 +168,7 @@ impl State {
} }
pub async fn is_whitelisted(&self, actor_id: &XsdAnyUri) -> bool { pub async fn is_whitelisted(&self, actor_id: &XsdAnyUri) -> bool {
if !self.whitelist_enabled { if !self.settings.whitelist_enabled {
return true; return true;
} }
@ -155,7 +237,7 @@ impl State {
return Err(HostError.into()); return Err(HostError.into());
}; };
log::info!( info!(
"INSERT INTO blocks (domain_name, created_at) VALUES ($1::TEXT, 'now'); [{}]", "INSERT INTO blocks (domain_name, created_at) VALUES ($1::TEXT, 'now'); [{}]",
host.to_string() host.to_string()
); );
@ -181,7 +263,7 @@ impl State {
return Err(HostError.into()); return Err(HostError.into());
}; };
log::info!( info!(
"INSERT INTO whitelists (domain_name, created_at) VALUES ($1::TEXT, 'now'); [{}]", "INSERT INTO whitelists (domain_name, created_at) VALUES ($1::TEXT, 'now'); [{}]",
host.to_string() host.to_string()
); );
@ -201,7 +283,7 @@ impl State {
pub async fn add_listener(&self, client: &Client, listener: XsdAnyUri) -> Result<(), Error> { pub async fn add_listener(&self, client: &Client, listener: XsdAnyUri) -> Result<(), Error> {
let listeners = self.listeners.clone(); let listeners = self.listeners.clone();
log::info!( info!(
"INSERT INTO listeners (actor_id, created_at) VALUES ($1::TEXT, 'now'); [{}]", "INSERT INTO listeners (actor_id, created_at) VALUES ($1::TEXT, 'now'); [{}]",
listener.as_str(), listener.as_str(),
); );
@ -226,6 +308,7 @@ impl State {
) -> Result<Self, Error> { ) -> Result<Self, Error> {
let pool1 = pool.clone(); let pool1 = pool.clone();
let pool2 = pool.clone(); let pool2 = pool.clone();
let pool3 = pool.clone();
let f1 = async move { let f1 = async move {
let conn = pool.get().await?; let conn = pool.get().await?;
@ -245,12 +328,16 @@ impl State {
hydrate_listeners(&conn).await 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 { Ok(State {
use_https, settings,
whitelist_enabled,
hostname,
actor_cache: Arc::new(RwLock::new(TtlCache::new(1024 * 8))), actor_cache: Arc::new(RwLock::new(TtlCache::new(1024 * 8))),
actor_id_cache: Arc::new(RwLock::new(LruCache::new(1024 * 8))), actor_id_cache: Arc::new(RwLock::new(LruCache::new(1024 * 8))),
blocks: Arc::new(RwLock::new(blocks)), blocks: Arc::new(RwLock::new(blocks)),
@ -261,14 +348,14 @@ impl State {
} }
pub async fn hydrate_blocks(client: &Client) -> Result<HashSet<String>, Error> { pub async fn hydrate_blocks(client: &Client) -> Result<HashSet<String>, Error> {
log::info!("SELECT domain_name FROM blocks"); info!("SELECT domain_name FROM blocks");
let rows = client.query("SELECT domain_name FROM blocks", &[]).await?; let rows = client.query("SELECT domain_name FROM blocks", &[]).await?;
parse_rows(rows) parse_rows(rows)
} }
pub async fn hydrate_whitelists(client: &Client) -> Result<HashSet<String>, Error> { pub async fn hydrate_whitelists(client: &Client) -> Result<HashSet<String>, Error> {
log::info!("SELECT domain_name FROM whitelists"); info!("SELECT domain_name FROM whitelists");
let rows = client let rows = client
.query("SELECT domain_name FROM whitelists", &[]) .query("SELECT domain_name FROM whitelists", &[])
.await?; .await?;
@ -277,7 +364,7 @@ pub async fn hydrate_whitelists(client: &Client) -> Result<HashSet<String>, Erro
} }
pub async fn hydrate_listeners(client: &Client) -> Result<HashSet<XsdAnyUri>, Error> { pub async fn hydrate_listeners(client: &Client) -> Result<HashSet<XsdAnyUri>, Error> {
log::info!("SELECT actor_id FROM listeners"); info!("SELECT actor_id FROM listeners");
let rows = client.query("SELECT actor_id FROM listeners", &[]).await?; let rows = client.query("SELECT actor_id FROM listeners", &[]).await?;
parse_rows(rows) parse_rows(rows)

52
src/webfinger.rs Normal file
View file

@ -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<Data<State>> for RelayResolver {
type Error = RelayError;
fn find(
account: &str,
domain: &str,
state: Data<State>,
) -> Pin<Box<dyn Future<Output = Result<Option<Webfinger>, 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 {}