Add object serving for actix-web

This commit is contained in:
Aode (lion) 2021-11-19 14:52:31 -06:00
parent 365fdcfc75
commit 07fefa5b13
5 changed files with 489 additions and 0 deletions

View file

@ -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
View 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
View 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
}
}

View 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"

View 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(())
}