(WIP)feat: add axum feature

This commit is contained in:
Paul Delafosse 2022-11-25 02:04:03 +01:00
parent 4d8caf9a85
commit e51cd50113
7 changed files with 364 additions and 93 deletions

162
Cargo.lock generated
View file

@ -11,6 +11,7 @@ dependencies = [
"actix-web",
"anyhow",
"async-trait",
"axum",
"background-jobs",
"base64",
"chrono",
@ -270,6 +271,68 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
[[package]]
name = "axum"
version = "0.6.0-rc.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21b6b56b1bdef08d4d50e63683d79c797dbf2ae467586c7502b283241b5e2b8a"
dependencies = [
"async-trait",
"axum-core",
"axum-macros",
"bitflags",
"bytes",
"futures-util",
"headers",
"http",
"http-body",
"hyper",
"itoa",
"matchit",
"memchr",
"mime",
"percent-encoding",
"pin-project-lite",
"serde",
"serde_json",
"serde_path_to_error",
"serde_urlencoded",
"sync_wrapper",
"tokio",
"tower",
"tower-http",
"tower-layer",
"tower-service",
]
[[package]]
name = "axum-core"
version = "0.3.0-rc.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d1aa274f0599e5100cbc24e1f184d437d8086ea0bba0b5f68326e2ad5a48567"
dependencies = [
"async-trait",
"bytes",
"futures-util",
"http",
"http-body",
"mime",
"tower-layer",
"tower-service",
]
[[package]]
name = "axum-macros"
version = "0.3.0-rc.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2185fff4d6f14de84dcc01b0ff8eee2ac5331a962cf85e60e080ce7db724cc9"
dependencies = [
"heck",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "background-jobs"
version = "0.13.0"
@ -753,6 +816,37 @@ version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
[[package]]
name = "headers"
version = "0.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3e372db8e5c0d213e0cd0b9be18be2aca3d44cf2fe30a9d46a65581cd454584"
dependencies = [
"base64",
"bitflags",
"bytes",
"headers-core",
"http",
"httpdate",
"mime",
"sha1",
]
[[package]]
name = "headers-core"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7f66481bfee273957b1f20485a4ff3362987f85b2c236580d81b4eb7a326429"
dependencies = [
"http",
]
[[package]]
name = "heck"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9"
[[package]]
name = "hermit-abi"
version = "0.1.19"
@ -784,6 +878,12 @@ dependencies = [
"pin-project-lite",
]
[[package]]
name = "http-range-header"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bfe8eed0a9285ef776bb792479ea3834e8b94e13d615c2f66d03dd50a435a29"
[[package]]
name = "http-signature-normalization"
version = "0.6.0"
@ -1031,6 +1131,12 @@ dependencies = [
"cfg-if",
]
[[package]]
name = "matchit"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3dfc802da7b1cf80aefffa0c7b2f77247c8b32206cc83c270b61264f5b360a80"
[[package]]
name = "memchr"
version = "2.5.0"
@ -1476,6 +1582,15 @@ dependencies = [
"serde",
]
[[package]]
name = "serde_path_to_error"
version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "184c643044780f7ceb59104cef98a5a6f12cb2288a7bc701ab93a362b49fd47d"
dependencies = [
"serde",
]
[[package]]
name = "serde_urlencoded"
version = "0.7.1"
@ -1570,6 +1685,12 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "sync_wrapper"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20518fe4a4c9acf048008599e464deb21beeae3d3578418951a189c235a7a9a8"
[[package]]
name = "task-local-extensions"
version = "0.1.3"
@ -1727,6 +1848,47 @@ dependencies = [
"tracing",
]
[[package]]
name = "tower"
version = "0.4.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c"
dependencies = [
"futures-core",
"futures-util",
"pin-project",
"pin-project-lite",
"tokio",
"tower-layer",
"tower-service",
"tracing",
]
[[package]]
name = "tower-http"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c530c8675c1dbf98facee631536fa116b5fb6382d7dd6dc1b118d970eafe3ba"
dependencies = [
"bitflags",
"bytes",
"futures-core",
"futures-util",
"http",
"http-body",
"http-range-header",
"pin-project-lite",
"tower",
"tower-layer",
"tower-service",
]
[[package]]
name = "tower-layer"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0"
[[package]]
name = "tower-service"
version = "0.3.2"

View file

@ -33,9 +33,11 @@ http-signature-normalization-reqwest = { version = "0.7.1", default-features = f
actix-web = { version = "4.2.1", default-features = false, optional = true }
http-signature-normalization-actix = { version = "0.6.1", default-features = false, features = ["server", "sha-2"], optional = true }
axum = { version = "0.6.0-rc.5", features = ["json", "headers", "macros"], optional = true }
[features]
actix = ["actix-web", "http-signature-normalization-actix"]
actix = ["dep:actix-web", "dep:http-signature-normalization-actix"]
axum = ["dep:axum"]
[dev-dependencies]
activitystreams-kinds = "0.2.1"

View file

@ -1,12 +1,10 @@
use actix_web::ResponseError;
use std::fmt::{Display, Formatter};
/// Necessary because of this issue: https://github.com/actix/actix-web/issues/1711
#[derive(Debug)]
pub struct Error(anyhow::Error);
impl ResponseError for Error {}
#[cfg(feature = "actix")]
impl Display for Error {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
std::fmt::Display::fmt(&self.0, f)
@ -21,3 +19,23 @@ where
Error(t.into())
}
}
#[cfg(feature = "actix")]
mod actix {
use crate::error::Error;
use actix_web::ResponseError;
impl ResponseError for Error {}
}
#[cfg(feature = "axum")]
mod axum {
use axum::response::{IntoResponse, Response};
use http::StatusCode;
impl IntoResponse for AppError {
fn into_response(self) -> Response {
(StatusCode::INTERNAL_SERVER_ERROR, format!("{}", self.0)).into_response()
}
}
}

View file

@ -6,6 +6,7 @@ use crate::{
person::{MyUser, PersonAcceptedActivities},
},
};
use activitypub_federation::{
core::{inbox::receive_activity, object_id::ObjectId, signatures::generate_actor_keypair},
data::Data,
@ -16,8 +17,10 @@ use activitypub_federation::{
UrlVerifier,
APUB_JSON_CONTENT_TYPE,
};
use actix_web::{web, App, HttpRequest, HttpResponse, HttpServer};
use async_trait::async_trait;
/// FIXME: actix
use http_signature_normalization_actix::prelude::VerifyDigest;
use reqwest::Client;
use sha2::{Digest, Sha256};

View file

@ -137,7 +137,7 @@ async fn do_send(
) -> Result<(), anyhow::Error> {
debug!("Sending {} to {}", task.activity_id, task.inbox);
let request_builder = client
.post(&task.inbox.to_string())
.post(task.inbox.to_string())
.timeout(timeout)
.headers(generate_request_headers(&task.inbox));
let request = sign_request(

View file

@ -1,53 +1,124 @@
use crate::{
core::{object_id::ObjectId, signatures::verify_signature},
data::Data,
traits::{ActivityHandler, Actor, ApubObject},
utils::{verify_domains_match, verify_url_valid},
Error,
LocalInstance,
};
use actix_web::{dev::Payload, FromRequest, HttpRequest, HttpResponse};
use http_signature_normalization_actix::prelude::DigestVerified;
use serde::de::DeserializeOwned;
use tracing::debug;
#[cfg(feature = "actix")]
pub use actix_imp::receive_activity;
/// Receive an activity and perform some basic checks, including HTTP signature verification.
pub async fn receive_activity<Activity, ActorT, Datatype>(
request: HttpRequest,
activity: Activity,
local_instance: &LocalInstance,
data: &Data<Datatype>,
) -> Result<HttpResponse, <Activity as ActivityHandler>::Error>
where
Activity: ActivityHandler<DataType = Datatype> + DeserializeOwned + Send + 'static,
ActorT: ApubObject<DataType = Datatype> + Actor + Send + 'static,
for<'de2> <ActorT as ApubObject>::ApubType: serde::Deserialize<'de2>,
<Activity as ActivityHandler>::Error: From<anyhow::Error>
+ From<Error>
+ From<<ActorT as ApubObject>::Error>
+ From<serde_json::Error>
+ From<http_signature_normalization_actix::digest::middleware::VerifyError>,
<ActorT as ApubObject>::Error: From<Error> + From<anyhow::Error>,
{
// ensure that payload hash was checked against digest header by middleware
DigestVerified::from_request(&request, &mut Payload::None).await?;
#[cfg(feature = "axum")]
pub use axum_imp::receive_activity;
verify_domains_match(activity.id(), activity.actor())?;
verify_url_valid(activity.id(), &local_instance.settings).await?;
if local_instance.is_local_url(activity.id()) {
return Err(Error::UrlVerificationError("Activity was sent from local instance").into());
#[cfg(feature = "actix")]
mod actix_imp {
use crate::{
core::{object_id::ObjectId, signatures::actix::verify_signature},
data::Data,
traits::{ActivityHandler, Actor, ApubObject},
utils::{verify_domains_match, verify_url_valid},
Error,
LocalInstance,
};
use actix_web::{dev::Payload, FromRequest, HttpRequest, HttpResponse};
use http_signature_normalization_actix::prelude::DigestVerified;
use serde::de::DeserializeOwned;
use tracing::debug;
/// Receive an activity and perform some basic checks, including HTTP signature verification.
pub async fn receive_activity<Activity, ActorT, Datatype>(
request: HttpRequest,
activity: Activity,
local_instance: &LocalInstance,
data: &Data<Datatype>,
) -> Result<HttpResponse, <Activity as ActivityHandler>::Error>
where
Activity: ActivityHandler<DataType = Datatype> + DeserializeOwned + Send + 'static,
ActorT: ApubObject<DataType = Datatype> + Actor + Send + 'static,
for<'de2> <ActorT as ApubObject>::ApubType: serde::Deserialize<'de2>,
<Activity as ActivityHandler>::Error: From<anyhow::Error>
+ From<Error>
+ From<<ActorT as ApubObject>::Error>
+ From<serde_json::Error>
+ From<http_signature_normalization_actix::digest::middleware::VerifyError>,
<ActorT as ApubObject>::Error: From<Error> + From<anyhow::Error>,
{
// ensure that payload hash was checked against digest header by middleware
DigestVerified::from_request(&request, &mut Payload::None).await?;
verify_domains_match(activity.id(), activity.actor())?;
verify_url_valid(activity.id(), &local_instance.settings).await?;
if local_instance.is_local_url(activity.id()) {
return Err(
Error::UrlVerificationError("Activity was sent from local instance").into(),
);
}
let request_counter = &mut 0;
let actor = ObjectId::<ActorT>::new(activity.actor().clone())
.dereference(data, local_instance, request_counter)
.await?;
verify_signature(&request, actor.public_key())?;
debug!("Verifying activity {}", activity.id().to_string());
activity.verify(data, request_counter).await?;
debug!("Receiving activity {}", activity.id().to_string());
activity.receive(data, request_counter).await?;
Ok(HttpResponse::Ok().finish())
}
}
#[cfg(feature = "axum")]
mod axum_imp {
use crate::{
core::{object_id::ObjectId, signatures::axum::verify_signature},
data::Data,
traits::{ActivityHandler, Actor, ApubObject},
utils::{verify_domains_match, verify_url_valid},
Error,
LocalInstance,
};
use axum::{http::Request, Extension, Json};
use serde::de::DeserializeOwned;
use tracing::debug;
/// Receive an activity and perform some basic checks, including HTTP signature verification.
pub async fn receive_activity<Activity, ActorT, Datatype>(
request: Request<Activity>,
Json(activity): Json<Activity>,
Extension(local_instance): Extension<&LocalInstance>,
Extension(data): Extension<&Data<Datatype>>,
) -> Result<(), <Activity as ActivityHandler>::Error>
where
Activity: ActivityHandler<DataType = Datatype> + DeserializeOwned + Send + 'static,
ActorT: ApubObject<DataType = Datatype> + Actor + Send + 'static,
for<'de2> <ActorT as ApubObject>::ApubType: serde::Deserialize<'de2>,
<Activity as ActivityHandler>::Error: From<anyhow::Error>
+ From<Error>
+ From<<ActorT as ApubObject>::Error>
+ From<serde_json::Error>,
// FIXME
// + From<http_signature_normalization_actix::digest::middleware::VerifyError>,
<ActorT as ApubObject>::Error: From<Error> + From<anyhow::Error>,
{
// ensure that payload hash was checked against digest header by middleware
/// FIXME
// DigestVerified::from_request(&request, &mut Payload::None).await?;
verify_domains_match(activity.id(), activity.actor())?;
verify_url_valid(activity.id(), &local_instance.settings).await?;
if local_instance.is_local_url(activity.id()) {
return Err(
Error::UrlVerificationError("Activity was sent from local instance").into(),
);
}
let request_counter = &mut 0;
let actor = ObjectId::<ActorT>::new(activity.actor().clone())
.dereference(data, local_instance, request_counter)
.await?;
verify_signature(&request, actor.public_key())?;
debug!("Verifying activity {}", activity.id().to_string());
activity.verify(data, request_counter).await?;
debug!("Receiving activity {}", activity.id().to_string());
activity.receive(data, request_counter).await?;
Ok(())
}
let request_counter = &mut 0;
let actor = ObjectId::<ActorT>::new(activity.actor().clone())
.dereference(data, local_instance, request_counter)
.await?;
verify_signature(&request, actor.public_key())?;
debug!("Verifying activity {}", activity.id().to_string());
activity.verify(data, request_counter).await?;
debug!("Receiving activity {}", activity.id().to_string());
activity.receive(data, request_counter).await?;
Ok(HttpResponse::Ok().finish())
}

View file

@ -1,23 +1,13 @@
use actix_web::HttpRequest;
use anyhow::anyhow;
use http_signature_normalization_actix::Config as ConfigActix;
use http_signature_normalization_reqwest::prelude::{Config, SignExt};
use once_cell::sync::{Lazy, OnceCell};
use openssl::{
hash::MessageDigest,
pkey::PKey,
rsa::Rsa,
sign::{Signer, Verifier},
};
use once_cell::sync::OnceCell;
use openssl::{hash::MessageDigest, pkey::PKey, rsa::Rsa, sign::Signer};
use reqwest::Request;
use reqwest_middleware::RequestBuilder;
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use std::io::{Error, ErrorKind};
use tracing::debug;
use url::Url;
static CONFIG2: Lazy<ConfigActix> = Lazy::new(ConfigActix::new);
static HTTP_SIG_CONFIG: OnceCell<Config> = OnceCell::new();
/// A private/public key pair used for HTTP signatures
@ -80,33 +70,6 @@ pub(crate) async fn sign_request(
.await
}
/// Verifies the HTTP signature on an incoming inbox request.
pub fn verify_signature(request: &HttpRequest, public_key: &str) -> Result<(), anyhow::Error> {
let verified = CONFIG2
.begin_verify(
request.method(),
request.uri().path_and_query(),
request.headers().clone(),
)?
.verify(|signature, signing_string| -> Result<bool, anyhow::Error> {
debug!(
"Verifying with key {}, message {}",
&public_key, &signing_string
);
let public_key = PKey::public_key_from_pem(public_key.as_bytes())?;
let mut verifier = Verifier::new(MessageDigest::sha256(), &public_key)?;
verifier.update(signing_string.as_bytes())?;
Ok(verifier.verify(&base64::decode(signature)?)?)
})?;
if verified {
debug!("verified signature for {}", &request.uri());
Ok(())
} else {
Err(anyhow!("Invalid signature on request: {}", &request.uri()))
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct PublicKey {
@ -131,3 +94,55 @@ impl PublicKey {
}
}
}
#[cfg(feature = "actix")]
pub mod actix {
use actix_web::HttpRequest;
use anyhow::anyhow;
use http_signature_normalization_actix::Config as ConfigActix;
use once_cell::sync::Lazy;
use openssl::{hash::MessageDigest, pkey::PKey, sign::Verifier};
use tracing::debug;
static CONFIG2: Lazy<ConfigActix> = Lazy::new(ConfigActix::new);
/// Verifies the HTTP signature on an incoming inbox request.
pub fn verify_signature(request: &HttpRequest, public_key: &str) -> Result<(), anyhow::Error> {
let verified = CONFIG2
.begin_verify(
request.method(),
request.uri().path_and_query(),
request.headers().clone(),
)?
.verify(|signature, signing_string| -> Result<bool, anyhow::Error> {
debug!(
"Verifying with key {}, message {}",
&public_key, &signing_string
);
let public_key = PKey::public_key_from_pem(public_key.as_bytes())?;
let mut verifier = Verifier::new(MessageDigest::sha256(), &public_key)?;
verifier.update(signing_string.as_bytes())?;
Ok(verifier.verify(&base64::decode(signature)?)?)
})?;
if verified {
debug!("verified signature for {}", &request.uri());
Ok(())
} else {
Err(anyhow!("Invalid signature on request: {}", &request.uri()))
}
}
}
#[cfg(feature = "axum")]
pub mod axum {
use axum::http::Request;
/// Verifies the HTTP signature on an incoming inbox request.
pub fn verify_signature<B>(
request: &Request<B>,
public_key: &str,
) -> Result<(), anyhow::Error> {
todo!("Tower Http does not have this feature yet")
}
}