Oh gosh oh heck

This commit is contained in:
Aode (lion) 2021-11-17 22:17:36 -06:00
commit cea3fc807e
34 changed files with 1629 additions and 0 deletions

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
/target
Cargo.lock

27
Cargo.toml Normal file
View file

@ -0,0 +1,27 @@
[package]
name = "apub"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
[workspace]
members = [
"apub-awc",
"apub-breaker-session",
"apub-deliver",
"apub-deref",
"apub-deref-client",
"apub-digest",
"apub-objectid",
"apub-openssl",
"apub-reqwest",
"apub-rustcrypto",
"apub-session",
"apub-signer",
"examples/awc-example",
"examples/example-types",
"examples/reqwest-example"
]

21
apub-awc/Cargo.toml Normal file
View file

@ -0,0 +1,21 @@
[package]
name = "apub-awc"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
actix-http = { version = "3.0.0-beta.12", default-features = false }
apub-deliver = { version = "0.1.0", path = "../apub-deliver/" }
apub-digest = { version = "0.1.0", path = "../apub-digest/" }
apub-deref = { version = "0.1.0", path = "../apub-deref/" }
apub-session = { version = "0.1.0", path = "../apub-session/" }
apub-signer = { version = "0.1.0", path = "../apub-signer/" }
awc = { version = "3.0.0-beta.10", default-features = false }
http-signature-normalization-actix = { version = "0.5.0-beta.12", default-features = false, features = ["client", "digest"] }
serde = "1"
serde_json = "1"
thiserror = "1"
tracing-awc = { version = "0.1.0-beta.8", git = "https://git.asonix.dog/asonix/tracing-awc" }
url = "2"

223
apub-awc/src/lib.rs Normal file
View file

@ -0,0 +1,223 @@
use actix_http::error::BlockingError;
use apub_deref::{Dereference, Repo};
use apub_digest::{Digest, DigestFactory};
use apub_session::Session;
use apub_signer::{Sign, SignFactory};
use awc::{http::header::HttpDate, Client};
use http_signature_normalization_actix::{
digest::DigestName,
prelude::{Config, DigestCreate, InvalidHeaderValue, PrepareSignError, Sign as _, SignExt},
};
use std::{future::Future, pin::Pin, time::SystemTime};
use tracing_awc::Propagate;
use url::Url;
pub struct AwcClient<'a, Session, Crypto> {
client: &'a Client,
session: &'a Session,
config: Config,
crypto: Crypto,
}
#[derive(Debug, thiserror::Error)]
pub enum SignatureError<E: std::error::Error + Send> {
#[error(transparent)]
Header(#[from] InvalidHeaderValue),
#[error(transparent)]
Sign(#[from] PrepareSignError),
#[error(transparent)]
Blocking(#[from] BlockingError),
#[error(transparent)]
Signer(E),
}
#[derive(Debug, thiserror::Error)]
pub enum AwcError<E: std::error::Error + Send> {
#[error("Session indicated request should not procede")]
Session,
#[error(transparent)]
Request(#[from] awc::error::SendRequestError),
#[error(transparent)]
Response(#[from] awc::error::JsonPayloadError),
#[error(transparent)]
Json(#[from] serde_json::Error),
#[error("Invalid response code: {0}")]
Status(u16),
#[error(transparent)]
SignatureError(#[from] SignatureError<E>),
}
type SignError<S> = <<S as SignFactory>::Signer as Sign>::Error;
struct DigestWrapper<D>(D);
impl<D> DigestName for DigestWrapper<D>
where
D: Digest,
{
const NAME: &'static str = D::NAME;
}
impl<D> DigestCreate for DigestWrapper<D>
where
D: Digest + Clone,
{
fn compute(&mut self, input: &[u8]) -> String {
self.0.clone().digest(input)
}
}
impl<'a, CurrentSession, Crypto> AwcClient<'a, CurrentSession, Crypto>
where
CurrentSession: Session,
Crypto: SignFactory + 'static,
{
pub fn new(
client: &'a Client,
session: &'a CurrentSession,
config: Config,
crypto: Crypto,
) -> Self {
Self {
client,
session,
config,
crypto,
}
}
async fn do_fetch<Id: Dereference>(
&self,
id: &Id,
) -> Result<Option<<Id as Dereference>::Output>, AwcError<SignError<Crypto>>> {
let mut response = self
.client
.get(id.url().as_str())
.insert_header(("Accept", "application/activity+json"))
.insert_header(("Date", HttpDate::from(SystemTime::now())))
.signature(self.config.clone(), self.crypto.key_id(), {
let sign = self.crypto.signer();
move |signing_string| sign.sign(signing_string).map_err(SignatureError::Signer)
})
.await?
.propagate()
.send()
.await?;
Ok(Some(response.json().await?))
}
async fn do_deliver<T>(
&self,
inbox: &Url,
activity: T,
) -> Result<(), AwcError<SignError<Crypto>>>
where
Crypto: DigestFactory,
<Crypto as DigestFactory>::Digest: Clone,
T: serde::ser::Serialize,
{
let activity_string = serde_json::to_string(&activity)?;
let (req, body) = self
.client
.post(inbox.as_str())
.content_type("application/activity+json")
.insert_header(("Accept", "application/activity+json"))
.insert_header(("Date", HttpDate::from(SystemTime::now())))
.signature_with_digest(
self.config.clone(),
self.crypto.key_id(),
DigestWrapper(self.crypto.digest()),
activity_string,
{
let sign = self.crypto.signer();
move |signing_string| sign.sign(signing_string).map_err(SignatureError::Signer)
},
)
.await?
.split();
let response = req.propagate().send_body(body).await?;
if !response.status().is_success() {
return Err(AwcError::Status(response.status().as_u16()));
}
Ok(())
}
}
impl<'a, Id, CurrentSession, Crypto> Repo<'a, Id> for AwcClient<'a, CurrentSession, Crypto>
where
Id: Dereference + 'a,
CurrentSession: Session,
Crypto: SignFactory + 'static,
{
type Error = AwcError<SignError<Crypto>>;
type Future = Pin<
Box<dyn Future<Output = Result<Option<<Id as Dereference>::Output>, Self::Error>> + 'a>,
>;
fn fetch(&'a self, id: &'a Id) -> Self::Future {
Box::pin(async move {
if !self.session.should_procede(id.url()) {
return Err(AwcError::Session);
}
match self.do_fetch(id).await {
Ok(opt) => {
self.session.mark_success(id.url());
Ok(opt)
}
Err(e) => {
self.session.mark_failure(id.url());
Err(e)
}
}
})
}
}
impl<'a, CurrentSession, Crypto> apub_deliver::Client<'a> for AwcClient<'a, CurrentSession, Crypto>
where
CurrentSession: Session,
Crypto: DigestFactory + SignFactory + 'static,
<Crypto as DigestFactory>::Digest: Clone,
{
type Error = AwcError<SignError<Crypto>>;
type Future = Pin<Box<dyn Future<Output = Result<(), Self::Error>> + 'a>>;
fn deliver<T: serde::ser::Serialize + 'a>(
&'a self,
inbox: &'a Url,
activity: T,
) -> Self::Future {
Box::pin(async move {
if !self.session.should_procede(inbox) {
return Err(AwcError::Session);
}
match self.do_deliver(inbox, activity).await {
Ok(()) => {
self.session.mark_success(inbox);
Ok(())
}
Err(e) => {
self.session.mark_failure(inbox);
Err(e)
}
}
})
}
}

View file

@ -0,0 +1,11 @@
[package]
name = "apub-breaker-session"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
apub-session = { version = "0.1.0", path = "../apub-session/" }
dashmap = "4.0.2"
url = "2"

View file

@ -0,0 +1,82 @@
use apub_session::Session;
use dashmap::DashMap;
use std::{
sync::Arc,
time::{Duration, Instant},
};
use url::{Host, Url};
#[derive(Debug)]
struct Breaker {
failure_count: usize,
broken_at: Instant,
}
#[derive(Clone, Debug)]
pub struct BreakerSession {
limit: usize,
breaker_duration: Duration,
hosts: Arc<DashMap<(Host<String>, Option<u16>), Breaker>>,
}
impl BreakerSession {
pub fn limit(limit: usize, breaker_duration: Duration) -> Self {
Self {
limit,
breaker_duration,
hosts: Arc::new(DashMap::new()),
}
}
}
impl Session for BreakerSession {
fn should_procede(&self, url: &Url) -> bool {
if let Some(host) = url.host() {
let key = (host.to_owned(), url.port());
let mut breaker = self.hosts.entry(key).or_default();
if breaker.failure_count < self.limit {
return true;
}
if Instant::now() > breaker.broken_at + self.breaker_duration {
breaker.failure_count = 0;
}
false
} else {
true
}
}
fn mark_success(&self, url: &Url) {
if let Some(host) = url.host() {
let key = (host.to_owned(), url.port());
let mut breaker = self.hosts.entry(key).or_default();
breaker.failure_count = 0;
}
}
fn mark_failure(&self, url: &Url) {
if let Some(host) = url.host() {
let key = (host.to_owned(), url.port());
let mut breaker = self.hosts.entry(key).or_default();
breaker.failure_count += 1;
if breaker.failure_count >= self.limit {
breaker.broken_at = Instant::now();
}
}
}
}
impl Default for Breaker {
fn default() -> Self {
Self {
failure_count: 0,
broken_at: Instant::now(),
}
}
}

10
apub-deliver/Cargo.toml Normal file
View file

@ -0,0 +1,10 @@
[package]
name = "apub-deliver"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
serde = "1"
url = "2"

13
apub-deliver/src/lib.rs Normal file
View file

@ -0,0 +1,13 @@
use std::future::Future;
use url::Url;
pub trait Client<'a> {
type Error: std::error::Error + 'static;
type Future: Future<Output = Result<(), Self::Error>> + 'a;
fn deliver<T: serde::ser::Serialize + 'a>(
&'a self,
inbox: &'a Url,
activity: T,
) -> Self::Future;
}

View file

@ -0,0 +1,12 @@
[package]
name = "apub-deref-client"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
apub-deref = { version = "0.1.0", path = "../apub-deref/" }
pin-project-lite = "0.2.7"
thiserror = "1"
url = "2"

View file

@ -0,0 +1,141 @@
use apub_deref::{Dereference, Repo};
use std::{
future::Future,
pin::Pin,
task::{Context, Poll},
};
pub struct Client<Local, Http> {
local_domain: String,
local: Local,
http: Http,
}
#[derive(Clone, Debug, thiserror::Error)]
pub enum Error<Left, Right> {
#[error("{0}")]
Left(#[source] Left),
#[error("{0}")]
Right(#[source] Right),
}
type ClientError<'a, Id, Local, Http> =
Error<<Local as Repo<'a, Id>>::Error, <Http as Repo<'a, Id>>::Error>;
impl<Local, Http> Client<Local, Http> {
pub fn new(local_domain: String, local: Local, http: Http) -> Self {
Client {
local_domain,
local,
http,
}
}
pub async fn dereference<'a, Id>(
&'a self,
id: &'a Id,
) -> Result<Option<<Id as Dereference>::Output>, ClientError<'a, Id, Local, Http>>
where
Id: Dereference,
Local: Repo<'a, Id> + 'a,
Http: Repo<'a, Id> + 'a,
{
self.fetch(id).await
}
}
impl<'a, Id, Local, Http> Repo<'a, Id> for Client<Local, Http>
where
Id: Dereference + 'a,
Local: Repo<'a, Id> + 'a,
Http: Repo<'a, Id> + 'a,
{
type Error = ClientError<'a, Id, Local, Http>;
type Future = DereferenceFuture<'a, Id, Local, Http>;
fn fetch(&'a self, id: &'a Id) -> Self::Future {
DereferenceFuture {
id,
client: self,
state: DereferenceFutureInner::Pending,
}
}
}
pin_project_lite::pin_project! {
#[project = DereferenceFutureInnerProj]
#[project_replace = DereferenceFutureInnerProjReplace]
enum DereferenceFutureInner<'a, Id: Dereference, Local, Http>
where
Local: Repo<'a, Id>,
Http: Repo<'a, Id>,
{
Pending,
Local {
#[pin]
future: <Local as Repo<'a, Id>>::Future,
},
Http {
#[pin]
future: <Http as Repo<'a, Id>>::Future,
}
}
}
pin_project_lite::pin_project! {
pub struct DereferenceFuture<'a, Id: Dereference, Local, Http>
where
Local: Repo<'a, Id>,
Http: Repo<'a, Id>,
{
id: &'a Id,
client: &'a Client<Local, Http>,
#[pin]
state: DereferenceFutureInner<'a, Id, Local, Http>,
}
}
impl<'a, Id, Local, Http> Future for DereferenceFuture<'a, Id, Local, Http>
where
Id: Dereference + 'a,
Local: Repo<'a, Id>,
Http: Repo<'a, Id>,
{
type Output = Result<Option<<Id as Dereference>::Output>, ClientError<'a, Id, Local, Http>>;
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let mut this = self.as_mut().project();
match this.state.as_mut().project() {
DereferenceFutureInnerProj::Pending => {
this.state.project_replace(DereferenceFutureInner::Local {
future: this.client.local.fetch(this.id),
});
self.poll(cx)
}
DereferenceFutureInnerProj::Local { future } => match future.poll(cx) {
Poll::Ready(Ok(None))
if this.id.url().domain() == Some(&this.client.local_domain) =>
{
Poll::Ready(Ok(None))
}
Poll::Ready(Ok(None)) => {
this.state.project_replace(DereferenceFutureInner::Http {
future: this.client.http.fetch(this.id),
});
self.poll(cx)
}
Poll::Ready(Ok(opt)) => Poll::Ready(Ok(opt)),
Poll::Ready(Err(e)) => Poll::Ready(Err(Error::Left(e))),
Poll::Pending => Poll::Pending,
},
DereferenceFutureInnerProj::Http { future } => {
future.poll(cx).map(|res| res.map_err(Error::Right))
}
}
}
}

2
apub-deref/.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
/target
Cargo.lock

10
apub-deref/Cargo.toml Normal file
View file

@ -0,0 +1,10 @@
[package]
name = "apub-deref"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
serde = "1"
url = "2"

15
apub-deref/src/lib.rs Normal file
View file

@ -0,0 +1,15 @@
use std::future::Future;
use url::Url;
pub trait Repo<'a, D: Dereference> {
type Error: std::error::Error + 'static;
type Future: Future<Output = Result<Option<<D as Dereference>::Output>, Self::Error>> + 'a;
fn fetch(&'a self, id: &'a D) -> Self::Future;
}
pub trait Dereference {
type Output: serde::de::DeserializeOwned;
fn url(&self) -> &Url;
}

8
apub-digest/Cargo.toml Normal file
View file

@ -0,0 +1,8 @@
[package]
name = "apub-digest"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]

11
apub-digest/src/lib.rs Normal file
View file

@ -0,0 +1,11 @@
pub trait Digest {
const NAME: &'static str;
fn digest(self, input: &[u8]) -> String;
}
pub trait DigestFactory {
type Digest: Digest + Send;
fn digest(&self) -> Self::Digest;
}

10
apub-objectid/Cargo.toml Normal file
View file

@ -0,0 +1,10 @@
[package]
name = "apub-objectid"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
serde = "1"
url = { version = "2", features = ["serde"] }

59
apub-objectid/src/lib.rs Normal file
View file

@ -0,0 +1,59 @@
use std::{
fmt::Display,
marker::PhantomData,
ops::{Deref, DerefMut},
};
use url::Url;
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct ObjectId<Kind> {
url: Url,
_kind: PhantomData<fn() -> Kind>,
}
impl<Kind> ObjectId<Kind> {
pub fn new(url: Url) -> Self {
Self {
url,
_kind: PhantomData,
}
}
}
impl<Kind> Deref for ObjectId<Kind> {
type Target = Url;
fn deref(&self) -> &Self::Target {
&self.url
}
}
impl<Kind> DerefMut for ObjectId<Kind> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.url
}
}
impl<Kind> Display for ObjectId<Kind> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.url.fmt(f)
}
}
impl<Kind> serde::ser::Serialize for ObjectId<Kind> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
self.url.serialize(serializer)
}
}
impl<'de, Kind> serde::de::Deserialize<'de> for ObjectId<Kind> {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
Url::deserialize(deserializer).map(ObjectId::new)
}
}

11
apub-openssl/Cargo.toml Normal file
View file

@ -0,0 +1,11 @@
[package]
name = "apub-openssl"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
apub-digest = { version = "0.1.0", path = "../apub-digest/" }
apub-signer = { version = "0.1.0", path = "../apub-signer/" }
openssl = "0.10.36"

103
apub-openssl/src/lib.rs Normal file
View file

@ -0,0 +1,103 @@
use openssl::{
error::ErrorStack,
hash::MessageDigest,
pkey::{PKey, Private},
sha::Sha256,
sign::Signer,
};
use std::fmt::Debug;
#[derive(Clone)]
pub struct OpenSslDigest {
digest: Sha256,
}
pub struct OpenSslSigner {
private_key: PKey<Private>,
}
pub struct OpenSsl {
key_id: String,
private_key: PKey<Private>,
}
impl OpenSsl {
pub fn new(key_id: String, private_key: PKey<Private>) -> Self {
Self {
key_id,
private_key,
}
}
}
impl<'a> apub_digest::Digest for OpenSslDigest {
const NAME: &'static str = "SHA-256";
fn digest(mut self, input: &[u8]) -> String {
self.digest.update(input);
let bytes = self.digest.finish();
openssl::base64::encode_block(&bytes)
}
}
impl apub_signer::Sign for OpenSslSigner {
type Error = ErrorStack;
fn sign(&self, signing_string: &str) -> Result<String, Self::Error> {
let mut signer = Signer::new(MessageDigest::sha256(), &self.private_key)?;
signer.update(signing_string.as_bytes())?;
Ok(openssl::base64::encode_block(&signer.sign_to_vec()?))
}
}
impl apub_digest::DigestFactory for OpenSsl {
type Digest = OpenSslDigest;
fn digest(&self) -> Self::Digest {
OpenSslDigest {
digest: Sha256::new(),
}
}
}
impl apub_signer::SignFactory for OpenSsl {
type Signer = OpenSslSigner;
type KeyId = String;
fn key_id(&self) -> Self::KeyId {
self.key_id.clone()
}
fn signer(&self) -> Self::Signer {
OpenSslSigner {
private_key: self.private_key.clone(),
}
}
}
impl Debug for OpenSslDigest {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("OpenSslDigest")
.field("digest", &"Sha256")
.finish()
}
}
impl Debug for OpenSslSigner {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("OpenSslSigner")
.field("private_key", &"hidden")
.finish()
}
}
impl Debug for OpenSsl {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("OpenSsl")
.field("key_id", &self.key_id)
.field("private_key", &"hidden")
.finish()
}
}

20
apub-reqwest/Cargo.toml Normal file
View file

@ -0,0 +1,20 @@
[package]
name = "apub-reqwest"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
apub-deliver = { version = "0.1.0", path = "../apub-deliver/" }
apub-deref = { version = "0.1.0", path = "../apub-deref/" }
apub-digest = { version = "0.1.0", path = "../apub-digest/" }
apub-session = { version = "0.1.0", path = "../apub-session/" }
apub-signer = { version = "0.1.0", path = "../apub-signer/" }
http-signature-normalization-reqwest = { version = "0.2.0", default-features = false, features = ["digest"] }
httpdate = "1.0.2"
reqwest = { version = "0.11.6", default-features = false, features = ["json"] }
serde = "1"
serde_json = "1"
thiserror = "1"
url = "2"

214
apub-reqwest/src/lib.rs Normal file
View file

@ -0,0 +1,214 @@
use apub_deref::{Dereference, Repo};
use apub_digest::{Digest, DigestFactory};
use apub_session::Session;
use apub_signer::{Sign, SignFactory};
use http_signature_normalization_reqwest::{
digest::{DigestCreate, SignExt},
prelude::{Config, Sign as _, SignError},
};
use reqwest::{
header::{ACCEPT, CONTENT_TYPE, DATE},
Client,
};
use std::{future::Future, pin::Pin, time::SystemTime};
use url::Url;
pub struct ReqwestClient<'a, Session, Crypto> {
client: &'a Client,
session: &'a Session,
config: &'a Config,
crypto: Crypto,
}
#[derive(Debug, thiserror::Error)]
pub enum SignatureError<E: std::error::Error + Send> {
#[error(transparent)]
Sign(#[from] SignError),
#[error(transparent)]
Reqweset(#[from] reqwest::Error),
#[error(transparent)]
Signer(E),
}
#[derive(Debug, thiserror::Error)]
pub enum ReqwestError<E: std::error::Error + Send> {
#[error("Session indicated request should not procede")]
Session,
#[error(transparent)]
Reqweset(#[from] reqwest::Error),
#[error(transparent)]
Json(#[from] serde_json::Error),
#[error("Invalid response code: {0}")]
Status(u16),
#[error(transparent)]
SignatureError(#[from] SignatureError<E>),
}
type SignTraitError<S> = <<S as SignFactory>::Signer as Sign>::Error;
struct DigestWrapper<D>(D);
impl<D> DigestCreate for DigestWrapper<D>
where
D: Digest + Clone,
{
const NAME: &'static str = D::NAME;
fn compute(&mut self, input: &[u8]) -> String {
self.0.clone().digest(input)
}
}
impl<'a, CurrentSession, Crypto> ReqwestClient<'a, CurrentSession, Crypto>
where
CurrentSession: Session,
Crypto: SignFactory + 'static,
{
pub fn new(
client: &'a Client,
session: &'a CurrentSession,
config: &'a Config,
crypto: Crypto,
) -> Self {
Self {
client,
session,
config,
crypto,
}
}
async fn do_fetch<Id>(
&self,
id: &Id,
) -> Result<Option<<Id as Dereference>::Output>, ReqwestError<SignTraitError<Crypto>>>
where
Id: Dereference,
{
let response = self
.client
.get(id.url().as_str())
.header(ACCEPT, "application/activity+json")
.header(DATE, httpdate::fmt_http_date(SystemTime::now()))
.signature(self.config, self.crypto.key_id(), {
let sign = self.crypto.signer();
move |signing_string| sign.sign(signing_string).map_err(SignatureError::Signer)
})?
.send()
.await?;
Ok(Some(response.json().await?))
}
async fn do_deliver<T: serde::ser::Serialize>(
&self,
url: &Url,
activity: T,
) -> Result<(), ReqwestError<SignTraitError<Crypto>>>
where
Crypto: DigestFactory,
<Crypto as DigestFactory>::Digest: Clone,
{
let activity_string = serde_json::to_string(&activity)?;
let response = self
.client
.post(url.as_str())
.header(CONTENT_TYPE, "application/activity+json")
.header(ACCEPT, "application/activity+json")
.header(DATE, httpdate::fmt_http_date(SystemTime::now()))
.signature_with_digest(
self.config.clone(),
self.crypto.key_id(),
DigestWrapper(self.crypto.digest()),
activity_string,
{
let sign = self.crypto.signer();
move |signing_string| sign.sign(signing_string).map_err(SignatureError::Signer)
},
)
.await?;
if !response.status().is_success() {
return Err(ReqwestError::Status(response.status().as_u16()));
}
Ok(())
}
}
impl<'a, Id, CurrentSession, Crypto> Repo<'a, Id> for ReqwestClient<'a, CurrentSession, Crypto>
where
Id: Dereference + Send + Sync,
<Id as Dereference>::Output: 'static,
CurrentSession: Session + Send + Sync,
Crypto: SignFactory + Send + Sync + 'static,
<Crypto as SignFactory>::KeyId: Send,
{
type Error = ReqwestError<SignTraitError<Crypto>>;
type Future = Pin<
Box<
dyn Future<Output = Result<Option<<Id as Dereference>::Output>, Self::Error>>
+ Send
+ 'a,
>,
>;
fn fetch(&'a self, id: &'a Id) -> Self::Future {
Box::pin(async move {
if !self.session.should_procede(id.url()) {
return Err(ReqwestError::Session);
}
match self.do_fetch(id).await {
Ok(opt) => {
self.session.mark_success(id.url());
Ok(opt)
}
Err(e) => {
self.session.mark_failure(id.url());
Err(e)
}
}
})
}
}
impl<'a, CurrentSession, Crypto> apub_deliver::Client<'a>
for ReqwestClient<'a, CurrentSession, Crypto>
where
CurrentSession: Session + Send + Sync,
Crypto: DigestFactory + SignFactory + Send + Sync + 'static,
<Crypto as SignFactory>::KeyId: Send,
<Crypto as DigestFactory>::Digest: Clone,
{
type Error = ReqwestError<SignTraitError<Crypto>>;
type Future = Pin<Box<dyn Future<Output = Result<(), Self::Error>> + 'a>>;
fn deliver<T: serde::ser::Serialize + 'a>(&'a self, url: &'a Url, activity: T) -> Self::Future {
Box::pin(async move {
if !self.session.should_procede(url) {
return Err(ReqwestError::Session);
}
match self.do_deliver(url, activity).await {
Ok(opt) => {
self.session.mark_success(url);
Ok(opt)
}
Err(e) => {
self.session.mark_failure(url);
Err(e)
}
}
})
}
}

View file

@ -0,0 +1,13 @@
[package]
name = "apub-rustcrypto"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
base64 = "0.13.0"
rsa = "0.5.0"
sha2 = "0.9.0"
apub-digest = { version = "0.1.0", path = "../apub-digest/" }
apub-signer = { version = "0.1.0", path = "../apub-signer" }

View file

@ -0,0 +1,94 @@
use rsa::{hash::Hash, PaddingScheme, RsaPrivateKey};
use sha2::{Digest, Sha256};
use std::fmt::Debug;
#[derive(Debug, Clone)]
pub struct Sha256Digest {
digest: Sha256,
}
pub struct RsaSigner {
private_key: RsaPrivateKey,
}
pub struct Rustcrypto {
key_id: String,
private_key: RsaPrivateKey,
}
impl Rustcrypto {
pub fn new(key_id: String, private_key: RsaPrivateKey) -> Self {
Self {
key_id,
private_key,
}
}
}
impl apub_digest::Digest for Sha256Digest {
const NAME: &'static str = "SHA-256";
fn digest(mut self, input: &[u8]) -> String {
self.digest.update(input);
let bytes = self.digest.finalize();
base64::encode(&bytes)
}
}
impl apub_signer::Sign for RsaSigner {
type Error = rsa::errors::Error;
fn sign(&self, signing_string: &str) -> Result<String, Self::Error> {
let hashed = Sha256::digest(signing_string.as_bytes());
let bytes = self.private_key.sign(
PaddingScheme::PKCS1v15Sign {
hash: Some(Hash::SHA2_256),
},
&hashed,
)?;
Ok(base64::encode(bytes))
}
}
impl apub_digest::DigestFactory for Rustcrypto {
type Digest = Sha256Digest;
fn digest(&self) -> Self::Digest {
Sha256Digest {
digest: Sha256::new(),
}
}
}
impl apub_signer::SignFactory for Rustcrypto {
type Signer = RsaSigner;
type KeyId = String;
fn key_id(&self) -> Self::KeyId {
self.key_id.clone()
}
fn signer(&self) -> Self::Signer {
RsaSigner {
private_key: self.private_key.clone(),
}
}
}
impl Debug for RsaSigner {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("RsaSigner")
.field("private_key", &"hidden")
.finish()
}
}
impl Debug for Rustcrypto {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Rustcrypto")
.field("key_id", &self.key_id)
.field("private_key", &"hidden")
.finish()
}
}

9
apub-session/Cargo.toml Normal file
View file

@ -0,0 +1,9 @@
[package]
name = "apub-session"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
url = "2"

277
apub-session/src/lib.rs Normal file
View file

@ -0,0 +1,277 @@
use std::{
rc::Rc,
sync::{
atomic::{AtomicUsize, Ordering},
Arc,
},
};
use url::Url;
pub trait Session {
fn should_procede(&self, url: &Url) -> bool;
fn mark_success(&self, url: &Url);
fn mark_failure(&self, url: &Url);
}
pub struct RequestCountSession {
count: AtomicUsize,
max_requests: usize,
}
impl RequestCountSession {
pub fn max(max_requests: usize) -> Self {
Self {
count: AtomicUsize::new(0),
max_requests,
}
}
}
impl Session for RequestCountSession {
fn should_procede(&self, _: &Url) -> bool {
self.count.load(Ordering::Acquire) < self.max_requests
}
fn mark_success(&self, _: &Url) {
self.count.fetch_add(1, Ordering::AcqRel);
}
fn mark_failure(&self, _: &Url) {
self.count.fetch_add(1, Ordering::AcqRel);
}
}
impl<T> Session for Arc<T>
where
T: Session,
{
fn should_procede(&self, url: &Url) -> bool {
T::should_procede(&self, url)
}
fn mark_success(&self, url: &Url) {
T::mark_success(&self, url)
}
fn mark_failure(&self, url: &Url) {
T::mark_failure(&self, url)
}
}
impl<T> Session for Rc<T>
where
T: Session,
{
fn should_procede(&self, url: &Url) -> bool {
T::should_procede(&self, url)
}
fn mark_success(&self, url: &Url) {
T::mark_success(&self, url)
}
fn mark_failure(&self, url: &Url) {
T::mark_failure(&self, url)
}
}
impl<T> Session for Box<T>
where
T: Session,
{
fn should_procede(&self, url: &Url) -> bool {
T::should_procede(&self, url)
}
fn mark_success(&self, url: &Url) {
T::mark_success(&self, url)
}
fn mark_failure(&self, url: &Url) {
T::mark_failure(&self, url)
}
}
impl<T, U> Session for (T, U)
where
T: Session,
U: Session,
{
fn should_procede(&self, url: &Url) -> bool {
self.0.should_procede(url) && self.1.should_procede(url)
}
fn mark_success(&self, url: &Url) {
self.0.mark_success(url);
self.1.mark_success(url);
}
fn mark_failure(&self, url: &Url) {
self.0.mark_failure(url);
self.1.mark_failure(url);
}
}
impl<T, U, V> Session for (T, U, V)
where
T: Session,
U: Session,
V: Session,
{
fn should_procede(&self, url: &Url) -> bool {
self.0.should_procede(url) && self.1.should_procede(url) && self.2.should_procede(url)
}
fn mark_success(&self, url: &Url) {
self.0.mark_success(url);
self.1.mark_success(url);
self.2.mark_success(url);
}
fn mark_failure(&self, url: &Url) {
self.0.mark_failure(url);
self.1.mark_failure(url);
self.2.mark_failure(url);
}
}
impl<T, U, V, W> Session for (T, U, V, W)
where
T: Session,
U: Session,
V: Session,
W: Session,
{
fn should_procede(&self, url: &Url) -> bool {
self.0.should_procede(url)
&& self.1.should_procede(url)
&& self.2.should_procede(url)
&& self.3.should_procede(url)
}
fn mark_success(&self, url: &Url) {
self.0.mark_success(url);
self.1.mark_success(url);
self.2.mark_success(url);
self.3.mark_success(url);
}
fn mark_failure(&self, url: &Url) {
self.0.mark_failure(url);
self.1.mark_failure(url);
self.2.mark_failure(url);
self.3.mark_failure(url);
}
}
impl<T, U, V, W, X> Session for (T, U, V, W, X)
where
T: Session,
U: Session,
V: Session,
W: Session,
X: Session,
{
fn should_procede(&self, url: &Url) -> bool {
self.0.should_procede(url)
&& self.1.should_procede(url)
&& self.2.should_procede(url)
&& self.3.should_procede(url)
&& self.4.should_procede(url)
}
fn mark_success(&self, url: &Url) {
self.0.mark_success(url);
self.1.mark_success(url);
self.2.mark_success(url);
self.3.mark_success(url);
self.4.mark_success(url);
}
fn mark_failure(&self, url: &Url) {
self.0.mark_failure(url);
self.1.mark_failure(url);
self.2.mark_failure(url);
self.3.mark_failure(url);
self.4.mark_failure(url);
}
}
impl<T, U, V, W, X, Y> Session for (T, U, V, W, X, Y)
where
T: Session,
U: Session,
V: Session,
W: Session,
X: Session,
Y: Session,
{
fn should_procede(&self, url: &Url) -> bool {
self.0.should_procede(url)
&& self.1.should_procede(url)
&& self.2.should_procede(url)
&& self.3.should_procede(url)
&& self.4.should_procede(url)
&& self.5.should_procede(url)
}
fn mark_success(&self, url: &Url) {
self.0.mark_success(url);
self.1.mark_success(url);
self.2.mark_success(url);
self.3.mark_success(url);
self.4.mark_success(url);
self.5.mark_success(url);
}
fn mark_failure(&self, url: &Url) {
self.0.mark_failure(url);
self.1.mark_failure(url);
self.2.mark_failure(url);
self.3.mark_failure(url);
self.4.mark_failure(url);
self.5.mark_failure(url);
}
}
impl<T, U, V, W, X, Y, Z> Session for (T, U, V, W, X, Y, Z)
where
T: Session,
U: Session,
V: Session,
W: Session,
X: Session,
Y: Session,
Z: Session,
{
fn should_procede(&self, url: &Url) -> bool {
self.0.should_procede(url)
&& self.1.should_procede(url)
&& self.2.should_procede(url)
&& self.3.should_procede(url)
&& self.4.should_procede(url)
&& self.5.should_procede(url)
&& self.6.should_procede(url)
}
fn mark_success(&self, url: &Url) {
self.0.mark_success(url);
self.1.mark_success(url);
self.2.mark_success(url);
self.3.mark_success(url);
self.4.mark_success(url);
self.5.mark_success(url);
self.6.mark_success(url);
}
fn mark_failure(&self, url: &Url) {
self.0.mark_failure(url);
self.1.mark_failure(url);
self.2.mark_failure(url);
self.3.mark_failure(url);
self.4.mark_failure(url);
self.5.mark_failure(url);
self.6.mark_failure(url);
}
}

8
apub-signer/Cargo.toml Normal file
View file

@ -0,0 +1,8 @@
[package]
name = "apub-signer"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]

15
apub-signer/src/lib.rs Normal file
View file

@ -0,0 +1,15 @@
use std::fmt::Display;
pub trait Sign {
type Error: std::error::Error + Send;
fn sign(&self, signing_string: &str) -> Result<String, Self::Error>;
}
pub trait SignFactory {
type Signer: Sign + Send + 'static;
type KeyId: Display + 'static;
fn key_id(&self) -> Self::KeyId;
fn signer(&self) -> Self::Signer;
}

View file

@ -0,0 +1,19 @@
[package]
name = "awc-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-rt = "2.4.0"
apub-awc = { version = "0.1.0", path = "../../apub-awc/" }
apub-breaker-session = { version = "0.1.0", path = "../../apub-breaker-session/" }
apub-session = { version = "0.1.0", path = "../../apub-session/" }
apub-rustcrypto = { version = "0.1.0", path = "../../apub-rustcrypto/" }
awc = { version = "3.0.0-beta.10", default-features = false, features = ["rustls"] }
example-types = { version = "0.1.0", path = "../example-types/" }
http-signature-normalization-actix = { version = "0.5.0-beta.12", default-features = false, features = ["client"] }
rand = "0.8.4"
rsa = "0.5.0"
serde = { version = "1", features = ["derive"] }

View file

@ -0,0 +1,35 @@
use apub_awc::AwcClient;
use apub_breaker_session::BreakerSession;
use apub_rustcrypto::Rustcrypto;
use apub_session::RequestCountSession;
use example_types::{object_id, NoteType, ObjectId};
use http_signature_normalization_actix::Config;
use rsa::RsaPrivateKey;
use std::time::Duration;
#[actix_rt::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let private_key = RsaPrivateKey::new(&mut rand::thread_rng(), 1024)?;
let crypto = Rustcrypto::new("key-id".to_string(), private_key);
let config = Config::default();
let breakers = BreakerSession::limit(10, Duration::from_secs(60 * 60));
let session = (RequestCountSession::max(30), breakers);
let client = awc::Client::new();
let awc_client = AwcClient::new(&client, &session, config, crypto);
let id: ObjectId<NoteType> =
object_id("https://masto.asonix.dog/users/asonix/statuses/107289461429162429".parse()?);
if let Some(note) = id.dereference(&awc_client).await? {
println!("id: {}, content: {}", note.id, note.content);
let inbox = "https://masto.asonix.dog/inbox".parse()?;
note.deliver(&inbox, &awc_client).await?;
}
Ok(())
}

View file

@ -0,0 +1,13 @@
[package]
name = "example-types"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
apub-deliver = { version = "0.1.0", path = "../../apub-deliver/" }
apub-deref = { version = "0.1.0", path = "../../apub-deref/" }
apub-objectid = { version = "0.1.0", path = "../../apub-objectid/" }
serde = { version = "1", features = ["derive"] }
url = { version = "2", features = ["serde"] }

View file

@ -0,0 +1,89 @@
use apub_deliver::Client;
use apub_deref::{Dereference, Repo};
use std::{
fmt::Display,
ops::{Deref, DerefMut},
};
use url::Url;
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
#[serde(transparent)]
pub struct ObjectId<Kind>(apub_objectid::ObjectId<Kind>);
pub fn object_id<Kind>(id: Url) -> ObjectId<Kind>
where
ObjectId<Kind>: Dereference,
{
ObjectId(apub_objectid::ObjectId::new(id))
}
#[derive(Debug, serde::Deserialize, serde::Serialize)]
pub enum NoteType {
Note,
}
#[derive(Debug, serde::Deserialize, serde::Serialize)]
pub struct Note {
pub id: ObjectId<NoteType>,
#[serde(rename = "type")]
pub kind: NoteType,
pub content: String,
}
impl<Kind> Deref for ObjectId<Kind> {
type Target = Url;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl<Kind> DerefMut for ObjectId<Kind> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl<Kind> Display for ObjectId<Kind> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.0.fmt(f)
}
}
impl Dereference for ObjectId<NoteType> {
type Output = Note;
fn url(&self) -> &Url {
&self
}
}
impl<Kind> ObjectId<Kind>
where
Self: Dereference,
{
pub async fn dereference<'a, R>(
&'a self,
repo: &'a R,
) -> Result<Option<<Self as Dereference>::Output>, Box<dyn std::error::Error>>
where
R: Repo<'a, Self>,
{
Ok(repo.fetch(self).await?)
}
}
impl Note {
pub async fn deliver<'a, C>(
&'a self,
inbox: &'a Url,
client: &'a C,
) -> Result<(), Box<dyn std::error::Error>>
where
C: Client<'a>,
{
Ok(client.deliver(inbox, self).await?)
}
}

View file

@ -0,0 +1,18 @@
[package]
name = "reqwest-example"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
apub-breaker-session = { version = "0.1.0", path = "../../apub-breaker-session/" }
apub-session = { version = "0.1.0", path = "../../apub-session/" }
apub-reqwest = { version = "0.1.0", path = "../../apub-reqwest/" }
apub-openssl = { version = "0.1.0", path = "../../apub-openssl/" }
example-types = { version = "0.1.0", path = "../example-types/" }
http-signature-normalization-reqwest = "0.2.0"
openssl = "0.10.13"
reqwest = "0.11.6"
serde = { version = "1", features = ["derive"] }
tokio = { version = "1.14.0", features = ["full"] }

View file

@ -0,0 +1,34 @@
use apub_breaker_session::BreakerSession;
use apub_openssl::OpenSsl;
use apub_reqwest::ReqwestClient;
use apub_session::RequestCountSession;
use example_types::{object_id, NoteType, ObjectId};
use http_signature_normalization_reqwest::Config;
use openssl::{pkey::PKey, rsa::Rsa};
use std::time::Duration;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let private_key = PKey::from_rsa(Rsa::generate(1024)?)?;
let crypto = OpenSsl::new("key-id".to_string(), private_key);
let config = Config::default();
let breakers = BreakerSession::limit(10, Duration::from_secs(60 * 60));
let session = (RequestCountSession::max(30), breakers);
let client = reqwest::Client::new();
let reqwest_client = ReqwestClient::new(&client, &session, &config, crypto);
let id: ObjectId<NoteType> =
object_id("https://masto.asonix.dog/users/asonix/statuses/107289461429162429".parse()?);
if let Some(note) = id.dereference(&reqwest_client).await? {
println!("id: {}, content: {}", note.id, note.content);
let inbox = "https://masto.asonix.dog/inbox".parse()?;
note.deliver(&inbox, &reqwest_client).await?;
}
Ok(())
}

0
src/lib.rs Normal file
View file