diff --git a/Cargo.toml b/Cargo.toml index 58f596a..cdb90ee 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,7 @@ edition = "2021" [workspace] members = [ + "apub-actix-web", "apub-awc", "apub-background-jobs", "apub-breaker-session", @@ -17,6 +18,7 @@ members = [ "apub-openssl", "apub-reqwest", "apub-rustcrypto", + "examples/actix-web-example", "examples/awc-example", "examples/background-jobs-example", "examples/example-types", diff --git a/apub-actix-web/Cargo.toml b/apub-actix-web/Cargo.toml new file mode 100644 index 0000000..7c7c60f --- /dev/null +++ b/apub-actix-web/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "apub-actix-web" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +apub-core = { version = "0.1.0", path = "../apub-core/" } +actix-web = { version = "4.0.0-beta.11", default-features = false } +http-signature-normalization-actix = { version = "0.5.0-beta.12", default-features = false, features = ["server"] } +serde = { version = "1", features = ["derive"] } +thiserror = "1" +url = { version = "2", features = ["serde"] } diff --git a/apub-actix-web/src/lib.rs b/apub-actix-web/src/lib.rs new file mode 100644 index 0000000..50b4dbb --- /dev/null +++ b/apub-actix-web/src/lib.rs @@ -0,0 +1,308 @@ +use actix_web::{ + error::BlockingError, + web::{self, ServiceConfig}, + HttpRequest, HttpResponse, ResponseError, +}; +use apub_core::{ + deref::{Dereference, Repo}, + signature::Verify, +}; +use http_signature_normalization_actix::prelude::{ + Algorithm, DeprecatedAlgorithm, SignatureVerify, VerifySignature, +}; +use std::{future::Future, marker::PhantomData, pin::Pin}; +use url::Url; + +pub use http_signature_normalization_actix::Config as SignatureConfig; + +#[derive(Debug, thiserror::Error)] +pub enum VerifyError { + #[error("Unsupported algorithm: {0}")] + Algorithm(String), + + #[error("Invalid Key ID: {0}")] + KeyId(String), + + #[error("Actor {0} is not public key's owner")] + InvalidOwner(Url), + + #[error("Public Key {0} is not the expected key")] + InvalidKey(Url), + + #[error("No key associated with Key ID")] + KeyNotFound, + + #[error("Key verification panicked")] + Canceled, +} + +pub trait VerifierFactory { + type Error: ResponseError + From + From + 'static; + type VerifyError: Send + 'static; + type Verifier: for<'a> Verifier<'a, D, Error = Self::VerifyError> + 'static; + + fn verifier(&self) -> Self::Verifier; +} + +pub trait Verifier<'a, D: Dereference> { + type Error: From + From + Send + 'static; + type RepoError; + type VerifyError; + type Verify: Verify; + type Repo: for<'b> Repo<'b, D, Error = Self::RepoError> + 'a; + + fn repo(&'a self) -> Self::Repo; +} + +pub trait RepoFactory<'a, D: Dereference> { + type Repo: for<'b> Repo<'b, D> + 'a; + + fn repo(&'a self) -> Self::Repo; +} + +struct ServeInfo { + local_host: String, + repo_factory: R, +} + +/// Serve activitypub objects from a given endpoint +/// +/// ```rust,ignore +/// use actix_web::App; +/// use apub_actix_web::{serve_objects, SignatureConfig}; +/// +/// let repo_factory = DatabaseRepo::new(); +/// let verfier_factory = DatabaseVerifier::new(); +/// +/// App::new() +/// .service( +/// web::scope("/activites") +/// .configure(serve_objects::, _, _>( +/// repo_factory, +/// verfier_factory, +/// SignatureConfig::default(), +/// "https://my_server.com".to_string(), +/// true, +/// )) +/// ); +/// ``` +pub fn serve_objects( + repo_factory: R, + verifier_factory: V, + config: SignatureConfig, + local_host: String, + require_signature: bool, +) -> impl FnOnce(&mut ServiceConfig) +where + D: Dereference + From + 'static, + ::Output: serde::ser::Serialize, + R: for<'a> RepoFactory<'a, D> + 'static, + V: VerifierFactory> + Clone + 'static, +{ + move |service_config: &mut ServiceConfig| { + let verifier = VerifySignature::new( + VerifyMiddleware::>::new(verifier_factory), + config, + ); + let verifier = if require_signature { + verifier + } else { + verifier.optional() + }; + service_config.service( + web::scope("/{object}") + .app_data(web::Data::new(ServeInfo { + local_host, + repo_factory, + })) + .wrap(verifier) + .route("", web::get().to(serve_object_handler::)), + ); + } +} + +async fn serve_object_handler( + req: HttpRequest, + serve_info: web::Data>, +) -> HttpResponse +where + D: Dereference + From + 'static, + ::Output: serde::ser::Serialize, + R: for<'a> RepoFactory<'a, D> + 'static, +{ + let uri = req.uri().to_string(); + let url = format!("{}{}", serve_info.local_host, uri); + let url: Url = match url.parse() { + Ok(url) => url, + Err(_) => return HttpResponse::BadRequest().finish(), + }; + + let d = D::from(url); + let repo = serve_info.repo_factory.repo(); + let res = repo.fetch(&d).await; + + match res { + Ok(Some(object)) => HttpResponse::Ok() + .content_type("application/activity+json") + .json(object), + Ok(None) => HttpResponse::NotFound().finish(), + Err(_) => HttpResponse::InternalServerError().finish(), + } +} + +#[derive(Clone)] +struct VerifyMiddleware { + verifier_factory: V, + _dereference: PhantomData, +} + +impl VerifyMiddleware +where + V: VerifierFactory, + D: Dereference, +{ + fn new(verifier_factory: V) -> Self { + Self { + verifier_factory, + _dereference: PhantomData, + } + } +} + +#[derive( + Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Deserialize, serde::Serialize, +)] +#[serde(transparent)] +#[allow(private_in_public)] +struct ObjectId(apub_core::object_id::ObjectId); + +fn object_id(url: Url) -> ObjectId { + ObjectId(apub_core::object_id::ObjectId::new(url)) +} + +#[derive( + Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Deserialize, serde::Serialize, +)] +#[allow(private_in_public)] +enum PublicKeyType { + PublicKey, +} + +#[derive( + Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Deserialize, serde::Serialize, +)] +enum ActorType { + Person, + Group, +} + +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +#[serde(rename_all = "camelCase")] +struct PublicKey { + id: ObjectId, + owner_id: ObjectId, + public_key_pem: String, +} + +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +#[serde(rename_all = "camelCase")] +#[serde(untagged)] +enum PublicKeyResponse { + PublicKey(PublicKey), + Actor { + id: ObjectId, + public_key: PublicKey, + }, +} + +impl Dereference for ObjectId { + type Output = PublicKeyResponse; + + fn url(&self) -> &Url { + &self.0 + } +} + +async fn verify<'a, V, E>( + verifier: &'a V, + algorithm: Option, + key_id: String, + signature: String, + signing_string: String, +) -> Result +where + V: Verifier<'a, ObjectId>, + V::Error: Send + 'static, + E: From + From + 'static, +{ + match algorithm { + None | Some(Algorithm::Hs2019 | Algorithm::Deprecated(DeprecatedAlgorithm::RsaSha256)) => { + () + } + Some(other) => return Err(VerifyError::Algorithm(other.to_string()).into()), + }; + + let key_id = object_id(key_id.parse().map_err(|_| VerifyError::KeyId(key_id))?); + + let repo = verifier.repo(); + let response = repo + .fetch(&key_id) + .await + .map_err(V::Error::from)? + .ok_or(VerifyError::KeyNotFound)?; + + let public_key = match response { + PublicKeyResponse::Actor { id, public_key } if public_key.owner_id == id => public_key, + PublicKeyResponse::PublicKey(public_key) => public_key, + PublicKeyResponse::Actor { id, .. } => { + return Err(VerifyError::InvalidOwner((*id.0).clone()).into()) + } + }; + + if public_key.id != key_id { + return Err(VerifyError::InvalidKey((*key_id.0).clone()).into()); + } + + let verified = web::block(move || { + let verified = <>>::Verify as Verify>::build( + &public_key.public_key_pem, + )? + .verify(&signing_string, &signature)?; + + Ok(verified) as Result + }) + .await + .map_err(VerifyError::from)??; + + Ok(verified) +} + +impl SignatureVerify for VerifyMiddleware> +where + V: VerifierFactory>, + >>::Verifier: + for<'a> Verifier<'a, ObjectId>, +{ + type Error = V::Error; + type Future = Pin>>>; + + fn signature_verify( + &mut self, + algorithm: Option, + key_id: String, + signature: String, + signing_string: String, + ) -> Self::Future { + let verifier = self.verifier_factory.verifier(); + + Box::pin(async move { + verify::<_, V::Error>(&verifier, algorithm, key_id, signature, signing_string).await + }) + } +} + +impl From for VerifyError { + fn from(_: BlockingError) -> Self { + VerifyError::Canceled + } +} diff --git a/examples/actix-web-example/Cargo.toml b/examples/actix-web-example/Cargo.toml new file mode 100644 index 0000000..0128dd4 --- /dev/null +++ b/examples/actix-web-example/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "actix-web-example" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +actix-web = { version = "4.0.0-beta.11", default-features = false } +apub-actix-web = { version = "0.1.0", path = "../../apub-actix-web/" } +apub-core = { version = "0.1.0", path = "../../apub-core/" } +apub-rustcrypto = { version = "0.1.0", path = "../../apub-rustcrypto/" } +dashmap = "4.0.2" +env_logger = "0.9.0" +example-types = { version = "0.1.0", path = "../example-types/" } +log = "0.4.6" +serde = "1" +serde_json = "1" +thiserror = "1" +url = "2" diff --git a/examples/actix-web-example/src/main.rs b/examples/actix-web-example/src/main.rs new file mode 100644 index 0000000..e534b78 --- /dev/null +++ b/examples/actix-web-example/src/main.rs @@ -0,0 +1,145 @@ +use actix_web::{middleware::Logger, web, App, HttpServer, ResponseError}; +use apub_actix_web::{ + serve_objects, RepoFactory, SignatureConfig, Verifier, VerifierFactory, VerifyError, +}; +use apub_core::deref::{Dereference, Repo}; +use apub_rustcrypto::{RsaVerifier, RustcryptoError}; +use dashmap::DashMap; +use example_types::{object_id, Note, NoteType, ObjectId}; +use std::future::{ready, Ready}; +use url::Url; + +#[derive(Clone, Default)] +struct MemoryRepo { + inner: DashMap, +} + +#[derive(Debug, thiserror::Error)] +enum ServerError { + #[error(transparent)] + Json(#[from] serde_json::Error), + + #[error(transparent)] + Verify(#[from] VerifyError), + + #[error(transparent)] + Rustcrypto(#[from] RustcryptoError), +} + +impl MemoryRepo { + fn insert(&self, id: D, item: &D::Output) -> Result<(), serde_json::Error> + where + D: Dereference, + D::Output: serde::ser::Serialize, + { + let value = serde_json::to_value(item)?; + self.inner.insert(id.url().clone(), value); + Ok(()) + } +} + +impl ResponseError for ServerError {} + +impl<'a, D> Repo<'a, D> for MemoryRepo +where + D: Dereference, + D::Output: 'static, +{ + type Error = serde_json::Error; + type Future = Ready, Self::Error>>; + + fn fetch(&'a self, id: &'a D) -> Self::Future { + if let Some(obj_ref) = self.inner.get(id.url()) { + match serde_json::from_value(obj_ref.clone()) { + Ok(output) => ready(Ok(Some(output))), + Err(e) => ready(Err(e)), + } + } else { + ready(Ok(None)) + } + } +} + +impl<'a, D> Verifier<'a, D> for MemoryRepo +where + D: Dereference, + D::Output: 'static, +{ + type Error = ServerError; + type RepoError = serde_json::Error; + type VerifyError = RustcryptoError; + type Verify = RsaVerifier; + type Repo = MemoryRepo; + + fn repo(&'a self) -> Self::Repo { + self.clone() + } +} + +impl VerifierFactory for MemoryRepo +where + D: Dereference, + D::Output: 'static, +{ + type Error = ServerError; + type VerifyError = ServerError; + type Verifier = MemoryRepo; + + fn verifier(&self) -> Self::Verifier { + self.clone() + } +} + +impl<'a, D> RepoFactory<'a, D> for MemoryRepo +where + D: Dereference, + D::Output: 'static, +{ + type Repo = MemoryRepo; + + fn repo(&'a self) -> Self::Repo { + self.clone() + } +} + +#[actix_web::main] +async fn main() -> Result<(), Box> { + if std::env::var("RUST_LOG").is_ok() { + env_logger::builder().init() + } else { + env_logger::Builder::new() + .filter_level(log::LevelFilter::Info) + .init(); + }; + + let repo = MemoryRepo::default(); + let id = object_id("http://localhost:8008/notes/asdf".parse()?); + repo.insert( + id.clone(), + &Note { + id, + kind: NoteType::Note, + content: String::from("hi"), + }, + )?; + let verifier = repo.clone(); + let config = SignatureConfig::default(); + + HttpServer::new(move || { + App::new() + .wrap(Logger::default()) + .service( + web::scope("/notes").configure(serve_objects::, _, _>( + repo.clone(), + verifier.clone(), + config.clone(), + "http://localhost:8008".to_string(), + false, + )), + ) + }) + .bind("127.0.0.1:8008")? + .run() + .await?; + Ok(()) +}