refactor!: remove actix-web feature (#1)
* refactor!: remove `actix-web` feature * chore: remove codeowners * chore(ci): add check action * fix(ci/check): remove nightly toolchain * fix(ci): allow dead code * refactor(examples): remove actix-web
This commit is contained in:
parent
83a156394e
commit
03569d5a56
10 changed files with 30 additions and 306 deletions
1
.github/CODEOWNERS
vendored
1
.github/CODEOWNERS
vendored
|
|
@ -1 +0,0 @@
|
|||
* @Nutomic @dessalines
|
||||
23
.github/workflows/check.yml
vendored
Normal file
23
.github/workflows/check.yml
vendored
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
name: check
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
# https://github.com/Mozilla-Actions/sccache-action#rust-code
|
||||
RUSTC_WRAPPER: "sccache"
|
||||
SCCACHE_GHA_ENABLED: "true"
|
||||
|
||||
jobs:
|
||||
check:
|
||||
runs-on: ubuntu-latest
|
||||
name: check
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: mozilla-actions/sccache-action@v0.0.3
|
||||
# - run: cargo fmt -- --check
|
||||
- run: cargo clippy --all-targets --all-features
|
||||
- run: cargo test --all-features --no-fail-fast
|
||||
- run: cargo run --example local_federation axum
|
||||
|
|
@ -9,8 +9,7 @@ repository = "https://github.com/LemmyNet/activitypub-federation-rust"
|
|||
documentation = "https://docs.rs/activitypub_federation/"
|
||||
|
||||
[features]
|
||||
default = ["actix-web", "axum"]
|
||||
actix-web = ["dep:actix-web"]
|
||||
default = ["axum"]
|
||||
axum = ["dep:axum", "dep:tower", "dep:hyper", "dep:http-body-util"]
|
||||
diesel = ["dep:diesel"]
|
||||
|
||||
|
|
@ -82,9 +81,6 @@ diesel = { version = "2.2.1", features = [
|
|||
futures = "0.3.30"
|
||||
moka = { version = "0.12.8", features = ["future"] }
|
||||
|
||||
# Actix-web
|
||||
actix-web = { version = "4.8.0", default-features = false, optional = true }
|
||||
|
||||
# Axum
|
||||
axum = { version = "0.6.20", features = [
|
||||
"json",
|
||||
|
|
|
|||
|
|
@ -63,7 +63,6 @@ impl UrlVerifier for MyUrlVerifier {
|
|||
|
||||
pub enum Webserver {
|
||||
Axum,
|
||||
ActixWeb,
|
||||
}
|
||||
|
||||
impl FromStr for Webserver {
|
||||
|
|
@ -72,7 +71,6 @@ impl FromStr for Webserver {
|
|||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
Ok(match s {
|
||||
"axum" => Webserver::Axum,
|
||||
"actix-web" => Webserver::ActixWeb,
|
||||
_ => panic!("Invalid webserver parameter, must be either `axum` or `actix-web`"),
|
||||
})
|
||||
}
|
||||
|
|
@ -84,7 +82,7 @@ pub fn listen(
|
|||
) -> Result<(), Error> {
|
||||
match webserver {
|
||||
Webserver::Axum => crate::axum::http::listen(config)?,
|
||||
Webserver::ActixWeb => crate::actix_web::http::listen(config)?,
|
||||
// Webserver::ActixWeb => crate::actix_web::http::listen(config)?,
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,8 +10,6 @@ use std::{env::args, str::FromStr};
|
|||
use tracing::log::{info, LevelFilter};
|
||||
|
||||
mod activities;
|
||||
#[cfg(feature = "actix-web")]
|
||||
mod actix_web;
|
||||
#[cfg(feature = "axum")]
|
||||
mod axum;
|
||||
mod error;
|
||||
|
|
|
|||
|
|
@ -1,182 +0,0 @@
|
|||
//! Handles incoming activities, verifying HTTP signatures and other checks
|
||||
|
||||
use crate::{
|
||||
config::Data,
|
||||
error::Error,
|
||||
http_signatures::{verify_body_hash, verify_signature},
|
||||
parse_received_activity,
|
||||
traits::{ActivityHandler, Actor, Object},
|
||||
};
|
||||
use actix_web::{web::Bytes, HttpRequest, HttpResponse};
|
||||
use serde::de::DeserializeOwned;
|
||||
use tracing::debug;
|
||||
|
||||
/// Handles incoming activities, verifying HTTP signatures and other checks
|
||||
///
|
||||
/// After successful validation, activities are passed to respective [trait@ActivityHandler].
|
||||
pub async fn receive_activity<Activity, ActorT, Datatype>(
|
||||
request: HttpRequest,
|
||||
body: Bytes,
|
||||
data: &Data<Datatype>,
|
||||
) -> Result<HttpResponse, <Activity as ActivityHandler>::Error>
|
||||
where
|
||||
Activity: ActivityHandler<DataType = Datatype> + DeserializeOwned + Send + 'static,
|
||||
ActorT: Object<DataType = Datatype> + Actor + Send + 'static,
|
||||
for<'de2> <ActorT as Object>::Kind: serde::Deserialize<'de2>,
|
||||
<Activity as ActivityHandler>::Error: From<Error> + From<<ActorT as Object>::Error>,
|
||||
<ActorT as Object>::Error: From<Error>,
|
||||
Datatype: Clone,
|
||||
{
|
||||
verify_body_hash(request.headers().get("Digest"), &body)?;
|
||||
|
||||
let (activity, actor) = parse_received_activity::<Activity, ActorT, _>(&body, data).await?;
|
||||
|
||||
verify_signature(
|
||||
request.headers(),
|
||||
request.method(),
|
||||
request.uri(),
|
||||
actor.public_key_pem(),
|
||||
)?;
|
||||
|
||||
debug!("Receiving activity {}", activity.id().to_string());
|
||||
activity.verify(data).await?;
|
||||
activity.receive(data).await?;
|
||||
Ok(HttpResponse::Ok().finish())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[allow(clippy::unwrap_used)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::{
|
||||
activity_sending::generate_request_headers,
|
||||
config::FederationConfig,
|
||||
fetch::object_id::ObjectId,
|
||||
http_signatures::sign_request,
|
||||
traits::tests::{DbConnection, DbUser, Follow, DB_USER_KEYPAIR},
|
||||
};
|
||||
use actix_web::test::TestRequest;
|
||||
use reqwest::Client;
|
||||
use reqwest_middleware::ClientWithMiddleware;
|
||||
use serde_json::json;
|
||||
use url::Url;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_receive_activity() {
|
||||
let (body, incoming_request, config) = setup_receive_test().await;
|
||||
receive_activity::<Follow, DbUser, DbConnection>(
|
||||
incoming_request.to_http_request(),
|
||||
body,
|
||||
&config.to_request_data(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_receive_activity_invalid_body_signature() {
|
||||
let (_, incoming_request, config) = setup_receive_test().await;
|
||||
let err = receive_activity::<Follow, DbUser, DbConnection>(
|
||||
incoming_request.to_http_request(),
|
||||
"invalid".into(),
|
||||
&config.to_request_data(),
|
||||
)
|
||||
.await
|
||||
.err()
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(&err, &Error::ActivityBodyDigestInvalid)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_receive_activity_invalid_path() {
|
||||
let (body, incoming_request, config) = setup_receive_test().await;
|
||||
let incoming_request = incoming_request.uri("/wrong");
|
||||
let err = receive_activity::<Follow, DbUser, DbConnection>(
|
||||
incoming_request.to_http_request(),
|
||||
body,
|
||||
&config.to_request_data(),
|
||||
)
|
||||
.await
|
||||
.err()
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(&err, &Error::ActivitySignatureInvalid)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_receive_unparseable_activity() {
|
||||
let (_, _, config) = setup_receive_test().await;
|
||||
|
||||
let actor = Url::parse("http://ds9.lemmy.ml/u/lemmy_alpha").unwrap();
|
||||
let id = "http://localhost:123/1";
|
||||
let activity = json!({
|
||||
"actor": actor.as_str(),
|
||||
"to": ["https://www.w3.org/ns/activitystreams#Public"],
|
||||
"object": "http://ds9.lemmy.ml/post/1",
|
||||
"cc": ["http://enterprise.lemmy.ml/c/main"],
|
||||
"type": "Delete",
|
||||
"id": id
|
||||
}
|
||||
);
|
||||
let body: Bytes = serde_json::to_vec(&activity).unwrap().into();
|
||||
let incoming_request = construct_request(&body, &actor).await;
|
||||
|
||||
// intentionally cause a parse error by using wrong type for deser
|
||||
let res = receive_activity::<Follow, DbUser, DbConnection>(
|
||||
incoming_request.to_http_request(),
|
||||
body,
|
||||
&config.to_request_data(),
|
||||
)
|
||||
.await;
|
||||
|
||||
match res {
|
||||
Err(Error::ParseReceivedActivity(_, url)) => {
|
||||
assert_eq!(id, url.expect("has url").as_str());
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn construct_request(body: &Bytes, actor: &Url) -> TestRequest {
|
||||
let inbox = "https://example.com/inbox";
|
||||
let headers = generate_request_headers(&Url::parse(inbox).unwrap());
|
||||
let request_builder = ClientWithMiddleware::from(Client::default())
|
||||
.post(inbox)
|
||||
.headers(headers);
|
||||
let outgoing_request = sign_request(
|
||||
request_builder,
|
||||
actor,
|
||||
body.clone(),
|
||||
DB_USER_KEYPAIR.private_key().unwrap(),
|
||||
false,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let mut incoming_request = TestRequest::post().uri(outgoing_request.url().path());
|
||||
for h in outgoing_request.headers() {
|
||||
incoming_request = incoming_request.append_header(h);
|
||||
}
|
||||
incoming_request
|
||||
}
|
||||
|
||||
async fn setup_receive_test() -> (Bytes, TestRequest, FederationConfig<DbConnection>) {
|
||||
let activity = Follow {
|
||||
actor: ObjectId::parse("http://localhost:123").unwrap(),
|
||||
object: ObjectId::parse("http://localhost:124").unwrap(),
|
||||
kind: Default::default(),
|
||||
id: "http://localhost:123/1".try_into().unwrap(),
|
||||
};
|
||||
let body: Bytes = serde_json::to_vec(&activity).unwrap().into();
|
||||
let incoming_request = construct_request(&body, activity.actor.inner()).await;
|
||||
|
||||
let config = FederationConfig::builder()
|
||||
.domain("localhost:8002")
|
||||
.app_data(DbConnection)
|
||||
.debug(true)
|
||||
.build()
|
||||
.await
|
||||
.unwrap();
|
||||
(body, incoming_request, config)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,76 +0,0 @@
|
|||
use crate::config::{Data, FederationConfig, FederationMiddleware};
|
||||
use actix_web::{
|
||||
dev::{forward_ready, Payload, Service, ServiceRequest, ServiceResponse, Transform},
|
||||
Error,
|
||||
FromRequest,
|
||||
HttpMessage,
|
||||
HttpRequest,
|
||||
};
|
||||
use std::future::{ready, Ready};
|
||||
|
||||
impl<S, B, T> Transform<S, ServiceRequest> for FederationMiddleware<T>
|
||||
where
|
||||
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
|
||||
S::Future: 'static,
|
||||
B: 'static,
|
||||
T: Clone + Sync + 'static,
|
||||
{
|
||||
type Response = ServiceResponse<B>;
|
||||
type Error = Error;
|
||||
type Transform = FederationService<S, T>;
|
||||
type InitError = ();
|
||||
type Future = Ready<Result<Self::Transform, Self::InitError>>;
|
||||
|
||||
fn new_transform(&self, service: S) -> Self::Future {
|
||||
ready(Ok(FederationService {
|
||||
service,
|
||||
config: self.0.clone(),
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
/// Passes [FederationConfig] to HTTP handlers, converting it to [Data] in the process
|
||||
#[doc(hidden)]
|
||||
pub struct FederationService<S, T: Clone>
|
||||
where
|
||||
S: Service<ServiceRequest, Error = Error>,
|
||||
S::Future: 'static,
|
||||
T: Sync,
|
||||
{
|
||||
service: S,
|
||||
config: FederationConfig<T>,
|
||||
}
|
||||
|
||||
impl<S, B, T> Service<ServiceRequest> for FederationService<S, T>
|
||||
where
|
||||
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
|
||||
S::Future: 'static,
|
||||
B: 'static,
|
||||
T: Clone + Sync + 'static,
|
||||
{
|
||||
type Response = ServiceResponse<B>;
|
||||
type Error = Error;
|
||||
type Future = S::Future;
|
||||
|
||||
forward_ready!(service);
|
||||
|
||||
fn call(&self, req: ServiceRequest) -> Self::Future {
|
||||
req.extensions_mut().insert(self.config.clone());
|
||||
|
||||
self.service.call(req)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Clone + 'static> FromRequest for Data<T> {
|
||||
type Error = Error;
|
||||
type Future = Ready<Result<Self, Self::Error>>;
|
||||
|
||||
fn from_request(req: &HttpRequest, _payload: &mut Payload) -> Self::Future {
|
||||
ready(match req.extensions().get::<FederationConfig<T>>() {
|
||||
Some(c) => Ok(c.to_request_data()),
|
||||
None => Err(actix_web::error::ErrorBadRequest(
|
||||
"Missing extension, did you register FederationMiddleware?",
|
||||
)),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
//! Utilities for using this library with actix-web framework
|
||||
|
||||
pub mod inbox;
|
||||
#[doc(hidden)]
|
||||
pub mod middleware;
|
||||
|
||||
use crate::{
|
||||
config::Data,
|
||||
error::Error,
|
||||
http_signatures::{self, verify_body_hash},
|
||||
traits::{Actor, Object},
|
||||
};
|
||||
use actix_web::{web::Bytes, HttpRequest};
|
||||
use serde::Deserialize;
|
||||
|
||||
/// Checks whether the request is signed by an actor of type A, and returns
|
||||
/// the actor in question if a valid signature is found.
|
||||
pub async fn signing_actor<A>(
|
||||
request: &HttpRequest,
|
||||
body: Option<Bytes>,
|
||||
data: &Data<<A as Object>::DataType>,
|
||||
) -> Result<A, <A as Object>::Error>
|
||||
where
|
||||
A: Object + Actor,
|
||||
<A as Object>::Error: From<Error>,
|
||||
for<'de2> <A as Object>::Kind: Deserialize<'de2>,
|
||||
{
|
||||
verify_body_hash(request.headers().get("Digest"), &body.unwrap_or_default())?;
|
||||
|
||||
http_signatures::signing_actor(request.headers(), request.method(), request.uri(), data).await
|
||||
}
|
||||
|
|
@ -2,7 +2,6 @@
|
|||
//!
|
||||
//! Signature creation and verification is handled internally in the library. See
|
||||
//! [send_activity](crate::activity_sending::SendActivityTask::sign_and_send) and
|
||||
//! [receive_activity (actix-web)](crate::actix_web::inbox::receive_activity) /
|
||||
//! [receive_activity (axum)](crate::axum::inbox::receive_activity).
|
||||
|
||||
use crate::{
|
||||
|
|
@ -24,9 +23,7 @@ use reqwest::Request;
|
|||
use reqwest_middleware::RequestBuilder;
|
||||
use rsa::{
|
||||
pkcs8::{DecodePublicKey, EncodePrivateKey, EncodePublicKey, LineEnding},
|
||||
Pkcs1v15Sign,
|
||||
RsaPrivateKey,
|
||||
RsaPublicKey,
|
||||
Pkcs1v15Sign, RsaPrivateKey, RsaPublicKey,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use sha2::{Digest, Sha256};
|
||||
|
|
@ -139,6 +136,7 @@ where
|
|||
/// from any actor of type A, and returns that actor if a valid signature is found.
|
||||
/// This function will return an `Err` variant when no signature is found
|
||||
/// or if the signature could not be verified.
|
||||
#[allow(dead_code)]
|
||||
pub(crate) async fn signing_actor<'a, A, H>(
|
||||
headers: H,
|
||||
method: &Method,
|
||||
|
|
@ -230,10 +228,12 @@ struct DigestPart {
|
|||
#[allow(dead_code)]
|
||||
pub algorithm: String,
|
||||
/// The hashsum
|
||||
#[allow(dead_code)]
|
||||
pub digest: String,
|
||||
}
|
||||
|
||||
impl DigestPart {
|
||||
#[allow(dead_code)]
|
||||
fn try_from_header(h: &HeaderValue) -> Option<Vec<DigestPart>> {
|
||||
let h = h.to_str().ok()?.split(';').next()?;
|
||||
let v: Vec<_> = h
|
||||
|
|
@ -258,6 +258,7 @@ impl DigestPart {
|
|||
}
|
||||
|
||||
/// Verify body of an inbox request against the hash provided in `Digest` header.
|
||||
#[allow(dead_code)]
|
||||
pub(crate) fn verify_body_hash(
|
||||
digest_header: Option<&HeaderValue>,
|
||||
body: &[u8],
|
||||
|
|
|
|||
|
|
@ -12,8 +12,6 @@
|
|||
|
||||
pub mod activity_queue;
|
||||
pub mod activity_sending;
|
||||
#[cfg(feature = "actix-web")]
|
||||
pub mod actix_web;
|
||||
#[cfg(feature = "axum")]
|
||||
pub mod axum;
|
||||
pub mod config;
|
||||
|
|
|
|||
Loading…
Reference in a new issue