forked from mirrors/relay
Add keys, http-sigs, webfinger
This commit is contained in:
parent
d741e2b202
commit
eea0577686
11 changed files with 561 additions and 80 deletions
207
Cargo.lock
generated
207
Cargo.lock
generated
|
@ -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",
|
||||
]
|
||||
|
|
13
Cargo.toml
13
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
|
||||
|
|
3
migrations/2020-03-16-012053_create-settings/down.sql
Normal file
3
migrations/2020-03-16-012053_create-settings/down.sql
Normal file
|
@ -0,0 +1,3 @@
|
|||
-- This file should undo anything in `up.sql`
|
||||
DROP INDEX settings_key_index;
|
||||
DROP TABLE settings;
|
12
migrations/2020-03-16-012053_create-settings/up.sql
Normal file
12
migrations/2020-03-16-012053_create-settings/up.sql
Normal 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');
|
26
src/apub.rs
26
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<T> {
|
||||
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<XsdAnyUri>,
|
||||
}
|
||||
|
||||
impl PublicKey {
|
||||
pub fn extend<T>(self, extending: T) -> PublicKeyExtension<T> {
|
||||
PublicKeyExtension {
|
||||
public_key: self,
|
||||
extending,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ValidObjects {
|
||||
pub fn id(&self) -> &XsdAnyUri {
|
||||
match self {
|
||||
|
|
77
src/error.rs
Normal file
77
src/error.rs
Normal 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)
|
||||
}
|
||||
}
|
104
src/inbox.rs
104
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<Addr<DbActor>>,
|
||||
state: web::Data<State>,
|
||||
|
@ -57,7 +54,7 @@ async fn handle_undo(
|
|||
actor: AcceptedActors,
|
||||
) -> Result<HttpResponse, MyError> {
|
||||
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<T>(client: web::Data<Client>, inboxes: Vec<XsdAnyUri>, item: T)
|
||||
where
|
||||
fn deliver_many<T>(
|
||||
state: web::Data<State>,
|
||||
client: web::Data<Client>,
|
||||
inboxes: Vec<XsdAnyUri>,
|
||||
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<T>(
|
||||
state: &std::sync::Arc<State>,
|
||||
client: &std::sync::Arc<Client>,
|
||||
inbox: XsdAnyUri,
|
||||
item: &T,
|
||||
|
@ -284,20 +286,38 @@ async fn deliver<T>(
|
|||
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<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();
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
15
src/main.rs
15
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<State>) -> Result<impl Responder, MyError>
|
|||
.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()
|
||||
|
|
|
@ -16,6 +16,16 @@ table! {
|
|||
}
|
||||
}
|
||||
|
||||
table! {
|
||||
settings (id) {
|
||||
id -> Uuid,
|
||||
key -> Text,
|
||||
value -> Text,
|
||||
created_at -> Timestamp,
|
||||
updated_at -> Nullable<Timestamp>,
|
||||
}
|
||||
}
|
||||
|
||||
table! {
|
||||
whitelists (id) {
|
||||
id -> Uuid,
|
||||
|
@ -28,5 +38,6 @@ table! {
|
|||
allow_tables_to_appear_in_same_query!(
|
||||
blocks,
|
||||
listeners,
|
||||
settings,
|
||||
whitelists,
|
||||
);
|
||||
|
|
121
src/state.rs
121
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<RwLock<TtlCache<XsdAnyUri, AcceptedActors>>>,
|
||||
actor_id_cache: Arc<RwLock<LruCache<XsdAnyUri, XsdAnyUri>>>,
|
||||
blocks: Arc<RwLock<HashSet<String>>>,
|
||||
|
@ -22,20 +24,73 @@ pub struct State {
|
|||
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 {
|
||||
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<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" };
|
||||
|
||||
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<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> {
|
||||
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<Self, Error> {
|
||||
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<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?;
|
||||
|
||||
parse_rows(rows)
|
||||
}
|
||||
|
||||
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
|
||||
.query("SELECT domain_name FROM whitelists", &[])
|
||||
.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> {
|
||||
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)
|
||||
|
|
52
src/webfinger.rs
Normal file
52
src/webfinger.rs
Normal 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 {}
|
Loading…
Reference in a new issue