Add object serving for actix-web
This commit is contained in:
parent
365fdcfc75
commit
07fefa5b13
|
@ -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",
|
||||
|
|
14
apub-actix-web/Cargo.toml
Normal file
14
apub-actix-web/Cargo.toml
Normal file
|
@ -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"] }
|
308
apub-actix-web/src/lib.rs
Normal file
308
apub-actix-web/src/lib.rs
Normal file
|
@ -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<D: Dereference> {
|
||||
type Error: ResponseError + From<VerifyError> + From<Self::VerifyError> + '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<Self::RepoError> + From<Self::VerifyError> + Send + 'static;
|
||||
type RepoError;
|
||||
type VerifyError;
|
||||
type Verify: Verify<Error = Self::VerifyError>;
|
||||
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<R> {
|
||||
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::<ObjectId<Activity>, _, _>(
|
||||
/// repo_factory,
|
||||
/// verfier_factory,
|
||||
/// SignatureConfig::default(),
|
||||
/// "https://my_server.com".to_string(),
|
||||
/// true,
|
||||
/// ))
|
||||
/// );
|
||||
/// ```
|
||||
pub fn serve_objects<D, R, V>(
|
||||
repo_factory: R,
|
||||
verifier_factory: V,
|
||||
config: SignatureConfig,
|
||||
local_host: String,
|
||||
require_signature: bool,
|
||||
) -> impl FnOnce(&mut ServiceConfig)
|
||||
where
|
||||
D: Dereference + From<Url> + 'static,
|
||||
<D as Dereference>::Output: serde::ser::Serialize,
|
||||
R: for<'a> RepoFactory<'a, D> + 'static,
|
||||
V: VerifierFactory<ObjectId<PublicKeyType>> + Clone + 'static,
|
||||
{
|
||||
move |service_config: &mut ServiceConfig| {
|
||||
let verifier = VerifySignature::new(
|
||||
VerifyMiddleware::<V, ObjectId<PublicKeyType>>::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::<D, R>)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async fn serve_object_handler<D, R>(
|
||||
req: HttpRequest,
|
||||
serve_info: web::Data<ServeInfo<R>>,
|
||||
) -> HttpResponse
|
||||
where
|
||||
D: Dereference + From<Url> + 'static,
|
||||
<D as Dereference>::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<V, D> {
|
||||
verifier_factory: V,
|
||||
_dereference: PhantomData<D>,
|
||||
}
|
||||
|
||||
impl<V, D> VerifyMiddleware<V, D>
|
||||
where
|
||||
V: VerifierFactory<D>,
|
||||
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<Kind>(apub_core::object_id::ObjectId<Kind>);
|
||||
|
||||
fn object_id<Kind>(url: Url) -> ObjectId<Kind> {
|
||||
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<PublicKeyType>,
|
||||
owner_id: ObjectId<ActorType>,
|
||||
public_key_pem: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[serde(untagged)]
|
||||
enum PublicKeyResponse {
|
||||
PublicKey(PublicKey),
|
||||
Actor {
|
||||
id: ObjectId<ActorType>,
|
||||
public_key: PublicKey,
|
||||
},
|
||||
}
|
||||
|
||||
impl Dereference for ObjectId<PublicKeyType> {
|
||||
type Output = PublicKeyResponse;
|
||||
|
||||
fn url(&self) -> &Url {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
async fn verify<'a, V, E>(
|
||||
verifier: &'a V,
|
||||
algorithm: Option<Algorithm>,
|
||||
key_id: String,
|
||||
signature: String,
|
||||
signing_string: String,
|
||||
) -> Result<bool, E>
|
||||
where
|
||||
V: Verifier<'a, ObjectId<PublicKeyType>>,
|
||||
V::Error: Send + 'static,
|
||||
E: From<VerifyError> + From<V::Error> + '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 = <<V as Verifier<'a, ObjectId<PublicKeyType>>>::Verify as Verify>::build(
|
||||
&public_key.public_key_pem,
|
||||
)?
|
||||
.verify(&signing_string, &signature)?;
|
||||
|
||||
Ok(verified) as Result<bool, V::Error>
|
||||
})
|
||||
.await
|
||||
.map_err(VerifyError::from)??;
|
||||
|
||||
Ok(verified)
|
||||
}
|
||||
|
||||
impl<V> SignatureVerify for VerifyMiddleware<V, ObjectId<PublicKeyType>>
|
||||
where
|
||||
V: VerifierFactory<ObjectId<PublicKeyType>>,
|
||||
<V as VerifierFactory<ObjectId<PublicKeyType>>>::Verifier:
|
||||
for<'a> Verifier<'a, ObjectId<PublicKeyType>>,
|
||||
{
|
||||
type Error = V::Error;
|
||||
type Future = Pin<Box<dyn Future<Output = Result<bool, Self::Error>>>>;
|
||||
|
||||
fn signature_verify(
|
||||
&mut self,
|
||||
algorithm: Option<Algorithm>,
|
||||
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<BlockingError> for VerifyError {
|
||||
fn from(_: BlockingError) -> Self {
|
||||
VerifyError::Canceled
|
||||
}
|
||||
}
|
20
examples/actix-web-example/Cargo.toml
Normal file
20
examples/actix-web-example/Cargo.toml
Normal file
|
@ -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"
|
145
examples/actix-web-example/src/main.rs
Normal file
145
examples/actix-web-example/src/main.rs
Normal file
|
@ -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<Url, serde_json::Value>,
|
||||
}
|
||||
|
||||
#[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<D>(&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<Result<Option<D::Output>, 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<D> VerifierFactory<D> 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<dyn std::error::Error>> {
|
||||
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::<ObjectId<NoteType>, _, _>(
|
||||
repo.clone(),
|
||||
verifier.clone(),
|
||||
config.clone(),
|
||||
"http://localhost:8008".to_string(),
|
||||
false,
|
||||
)),
|
||||
)
|
||||
})
|
||||
.bind("127.0.0.1:8008")?
|
||||
.run()
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
Loading…
Reference in a new issue