diff --git a/Cargo.lock b/Cargo.lock index e58d0cb..a912de6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -489,6 +489,83 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +[[package]] +name = "axum" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "021e862c184ae977658b36c4500f7feac3221ca5da43e3f25bd04ab6c79a29b5" +dependencies = [ + "axum-core", + "bytes", + "form_urlencoded", + "futures-util", + "http 1.3.1", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68464cd0412f486726fb3373129ef5d2993f90c34bc2bc1c1e9943b2f4fc7ca6" +dependencies = [ + "bytes", + "futures-core", + "http 1.3.1", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-extra" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45bf463831f5131b7d3c756525b305d40f1185b688565648a92e1392ca35713d" +dependencies = [ + "axum", + "axum-core", + "bytes", + "futures-util", + "headers", + "http 1.3.1", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "serde", + "tower", + "tower-layer", + "tower-service", +] + [[package]] name = "backtrace" version = "0.3.75" @@ -1565,6 +1642,7 @@ dependencies = [ "http 1.3.1", "http-body", "httparse", + "httpdate", "itoa", "pin-project-lite", "smallvec", @@ -1974,6 +2052,12 @@ dependencies = [ "regex-automata 0.1.10", ] +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + [[package]] name = "md-5" version = "0.10.6" @@ -3076,6 +3160,7 @@ dependencies = [ "chrono-tz", "derive_more 2.0.1", "futures-util", + "http 1.3.1", "quick-xml", "rustical_dav", "rustical_dav_push", @@ -3103,6 +3188,7 @@ dependencies = [ "chrono", "derive_more 2.0.1", "futures-util", + "http 1.3.1", "quick-xml", "rustical_dav", "rustical_dav_push", @@ -3124,10 +3210,13 @@ version = "0.1.0" dependencies = [ "actix-web", "async-trait", + "axum", + "axum-extra", "derive_more 2.0.1", "futures-util", "headers", "http 0.2.12", + "http 1.3.1", "itertools 0.14.0", "log", "quick-xml", @@ -3135,6 +3224,7 @@ dependencies = [ "serde", "thiserror 2.0.12", "tokio", + "tower", "tracing", "tracing-actix-web", ] @@ -3147,6 +3237,7 @@ dependencies = [ "async-trait", "derive_more 2.0.1", "futures-util", + "http 1.3.1", "itertools 0.14.0", "log", "quick-xml", diff --git a/Cargo.toml b/Cargo.toml index 93251d0..54ebc58 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -86,7 +86,8 @@ sqlx = { version = "0.8", default-features = false, features = [ "migrate", "json", ] } -http = "0.2" # This version is used by actix-web +http_02 = { package = "http", version = "0.2" } # actix-web uses a very outdated version +http = "1.3" headers = "0.4" strum = "0.27" strum_macros = "0.27" diff --git a/crates/caldav/Cargo.toml b/crates/caldav/Cargo.toml index 366a60c..23fabda 100644 --- a/crates/caldav/Cargo.toml +++ b/crates/caldav/Cargo.toml @@ -29,3 +29,4 @@ rustical_xml.workspace = true uuid.workspace = true rustical_dav_push.workspace = true rustical_ical.workspace = true +http.workspace = true diff --git a/crates/caldav/src/calendar/methods/report/mod.rs b/crates/caldav/src/calendar/methods/report/mod.rs index 91a1e6b..e674235 100644 --- a/crates/caldav/src/calendar/methods/report/mod.rs +++ b/crates/caldav/src/calendar/methods/report/mod.rs @@ -6,11 +6,11 @@ use crate::{ }; use actix_web::{ HttpRequest, Responder, - http::StatusCode, web::{Data, Path}, }; use calendar_multiget::{CalendarMultigetRequest, get_objects_calendar_multiget}; use calendar_query::{CalendarQueryRequest, get_objects_calendar_query}; +use http::StatusCode; use rustical_dav::{ resource::{PrincipalUri, Resource}, xml::{ diff --git a/crates/caldav/src/calendar/methods/report/sync_collection.rs b/crates/caldav/src/calendar/methods/report/sync_collection.rs index 38f0731..b1a2b50 100644 --- a/crates/caldav/src/calendar/methods/report/sync_collection.rs +++ b/crates/caldav/src/calendar/methods/report/sync_collection.rs @@ -4,7 +4,7 @@ use crate::{ CalendarObjectPropWrapper, CalendarObjectPropWrapperName, CalendarObjectResource, }, }; -use actix_web::http::StatusCode; +use http::StatusCode; use rustical_dav::{ resource::{PrincipalUri, Resource}, xml::{ diff --git a/crates/caldav/src/calendar/resource.rs b/crates/caldav/src/calendar/resource.rs index ed83e68..e171066 100644 --- a/crates/caldav/src/calendar/resource.rs +++ b/crates/caldav/src/calendar/resource.rs @@ -322,7 +322,7 @@ impl CalendarResourceService { } } -#[async_trait(?Send)] +#[async_trait] impl ResourceService for CalendarResourceService { type MemberType = CalendarObjectResource; type PathComponents = (String, String); // principal, calendar_id @@ -331,6 +331,8 @@ impl ResourceService for CalendarResourc type Principal = User; type PrincipalUri = CalDavPrincipalUri; + const DAV_HEADER: &str = "1, 3, access-control, calendar-access"; + async fn get_resource( &self, (principal, cal_id): &Self::PathComponents, diff --git a/crates/caldav/src/calendar_object/resource.rs b/crates/caldav/src/calendar_object/resource.rs index fdffd1f..29a8a04 100644 --- a/crates/caldav/src/calendar_object/resource.rs +++ b/crates/caldav/src/calendar_object/resource.rs @@ -137,7 +137,7 @@ pub struct CalendarObjectPathComponents { pub object_id: String, } -#[async_trait(?Send)] +#[async_trait] impl ResourceService for CalendarObjectResourceService { type PathComponents = CalendarObjectPathComponents; type Resource = CalendarObjectResource; @@ -146,6 +146,8 @@ impl ResourceService for CalendarObjectResourceService { type Principal = User; type PrincipalUri = CalDavPrincipalUri; + const DAV_HEADER: &str = "1, 3, access-control, calendar-access"; + async fn get_resource( &self, CalendarObjectPathComponents { diff --git a/crates/caldav/src/calendar_set/mod.rs b/crates/caldav/src/calendar_set/mod.rs index 4ba4ba9..d15ea14 100644 --- a/crates/caldav/src/calendar_set/mod.rs +++ b/crates/caldav/src/calendar_set/mod.rs @@ -77,7 +77,7 @@ impl CalendarSetResourceService { } } -#[async_trait(?Send)] +#[async_trait] impl ResourceService for CalendarSetResourceService { type PathComponents = (String,); type MemberType = CalendarResource; @@ -86,6 +86,8 @@ impl ResourceService for CalendarSetReso type Principal = User; type PrincipalUri = CalDavPrincipalUri; + const DAV_HEADER: &str = "1, 3, access-control, extended-mkcol"; + async fn get_resource( &self, (principal,): &Self::PathComponents, diff --git a/crates/caldav/src/error.rs b/crates/caldav/src/error.rs index 71116e2..00bac42 100644 --- a/crates/caldav/src/error.rs +++ b/crates/caldav/src/error.rs @@ -71,7 +71,8 @@ impl actix_web::ResponseError for Error { _ => StatusCode::INTERNAL_SERVER_ERROR, }, Error::ChronoParseError(_) => StatusCode::INTERNAL_SERVER_ERROR, - Error::DavError(err) => err.status_code(), + Error::DavError(err) => StatusCode::try_from(err.status_code().as_u16()) + .expect("Just converting between versions"), Error::Unauthorized => StatusCode::UNAUTHORIZED, Error::XmlDecodeError(_) => StatusCode::BAD_REQUEST, Error::NotImplemented => StatusCode::INTERNAL_SERVER_ERROR, diff --git a/crates/caldav/src/principal/mod.rs b/crates/caldav/src/principal/mod.rs index 41e94e7..57987dc 100644 --- a/crates/caldav/src/principal/mod.rs +++ b/crates/caldav/src/principal/mod.rs @@ -145,7 +145,7 @@ impl ResourceService for PrincipalResourceService { @@ -156,6 +156,8 @@ impl ResourceService for AddressObjectResourceService { type PathComponents = AddressObjectPathComponents; type Resource = AddressObjectResource; @@ -114,6 +114,8 @@ impl ResourceService for AddressObjectResourceService type Principal = User; type PrincipalUri = CardDavPrincipalUri; + const DAV_HEADER: &str = "1, 3, access-control, addressbook"; + async fn get_resource( &self, AddressObjectPathComponents { diff --git a/crates/carddav/src/addressbook/methods/report/addressbook_multiget.rs b/crates/carddav/src/addressbook/methods/report/addressbook_multiget.rs index 666e2ae..9dbee21 100644 --- a/crates/carddav/src/addressbook/methods/report/addressbook_multiget.rs +++ b/crates/carddav/src/addressbook/methods/report/addressbook_multiget.rs @@ -4,10 +4,9 @@ use crate::{ AddressObjectPropWrapper, AddressObjectPropWrapperName, AddressObjectResource, }, }; -use actix_web::{ - dev::{Path, ResourceDef}, - http::StatusCode, -}; +use actix_web::dev::{Path, ResourceDef}; + +use http::StatusCode; use rustical_dav::{ resource::{PrincipalUri, Resource}, xml::{MultistatusElement, PropfindType, multistatus::ResponseElement}, diff --git a/crates/carddav/src/addressbook/methods/report/sync_collection.rs b/crates/carddav/src/addressbook/methods/report/sync_collection.rs index c2b3922..49dfe7d 100644 --- a/crates/carddav/src/addressbook/methods/report/sync_collection.rs +++ b/crates/carddav/src/addressbook/methods/report/sync_collection.rs @@ -4,7 +4,7 @@ use crate::{ AddressObjectPropWrapper, AddressObjectPropWrapperName, AddressObjectResource, }, }; -use actix_web::http::StatusCode; +use http::StatusCode; use rustical_dav::{ resource::{PrincipalUri, Resource}, xml::{ diff --git a/crates/carddav/src/addressbook/resource.rs b/crates/carddav/src/addressbook/resource.rs index f74bd33..04976d9 100644 --- a/crates/carddav/src/addressbook/resource.rs +++ b/crates/carddav/src/addressbook/resource.rs @@ -188,7 +188,7 @@ impl Resource for AddressbookResource { } } -#[async_trait(?Send)] +#[async_trait] impl ResourceService for AddressbookResourceService { @@ -199,6 +199,8 @@ impl ResourceService type Principal = User; type PrincipalUri = CardDavPrincipalUri; + const DAV_HEADER: &str = "1, 3, access-control, addressbook"; + async fn get_resource( &self, (principal, addressbook_id): &Self::PathComponents, diff --git a/crates/carddav/src/error.rs b/crates/carddav/src/error.rs index c60689e..07beb2b 100644 --- a/crates/carddav/src/error.rs +++ b/crates/carddav/src/error.rs @@ -38,7 +38,8 @@ impl actix_web::ResponseError for Error { _ => StatusCode::INTERNAL_SERVER_ERROR, }, Error::ChronoParseError(_) => StatusCode::INTERNAL_SERVER_ERROR, - Error::DavError(err) => err.status_code(), + Error::DavError(err) => StatusCode::try_from(err.status_code().as_u16()) + .expect("Just converting between versions"), Error::Unauthorized => StatusCode::UNAUTHORIZED, Error::XmlDecodeError(_) => StatusCode::BAD_REQUEST, Error::NotImplemented => StatusCode::INTERNAL_SERVER_ERROR, diff --git a/crates/carddav/src/principal/mod.rs b/crates/carddav/src/principal/mod.rs index 92364c0..fb09d2e 100644 --- a/crates/carddav/src/principal/mod.rs +++ b/crates/carddav/src/principal/mod.rs @@ -140,7 +140,7 @@ impl Resource for PrincipalResource { } } -#[async_trait(?Send)] +#[async_trait] impl ResourceService for PrincipalResourceService { @@ -151,6 +151,8 @@ impl Reso type Principal = User; type PrincipalUri = CardDavPrincipalUri; + const DAV_HEADER: &str = "1, 3, access-control, addressbook"; + async fn get_resource( &self, (principal,): &Self::PathComponents, diff --git a/crates/dav/Cargo.toml b/crates/dav/Cargo.toml index a72cf0e..675ba40 100644 --- a/crates/dav/Cargo.toml +++ b/crates/dav/Cargo.toml @@ -7,9 +7,16 @@ repository.workspace = true publish = false [features] -actix = ["dep:actix-web", "dep:tracing-actix-web"] +actix = ["dep:actix-web", "dep:tracing-actix-web", "dep:http_02"] +axum = ["dep:axum", "dep:axum-extra", "dep:tower"] [dependencies] +axum = { version = "0.8", optional = true } +axum-extra = { version = "0.10", optional = true, features = ["typed-header"] } +tower = { version = "0.5", optional = true } + +http_02 = { workspace = true, optional = true } + rustical_xml.workspace = true async-trait.workspace = true futures-util.workspace = true diff --git a/crates/dav/src/error.rs b/crates/dav/src/error.rs index e7b8e6c..e1d7b11 100644 --- a/crates/dav/src/error.rs +++ b/crates/dav/src/error.rs @@ -55,17 +55,40 @@ impl Error { #[cfg(feature = "actix")] impl actix_web::error::ResponseError for Error { - fn status_code(&self) -> StatusCode { + fn status_code(&self) -> actix_web::http::StatusCode { self.status_code() + .as_u16() + .try_into() + .expect("Just converting between versions") } fn error_response(&self) -> actix_web::HttpResponse { + use actix_web::ResponseError; + error!("Error: {self}"); match self { - Error::Unauthorized => actix_web::HttpResponse::build(self.status_code()) + Error::Unauthorized => actix_web::HttpResponse::build(ResponseError::status_code(self)) .append_header(("WWW-Authenticate", "Basic")) .body(self.to_string()), - _ => actix_web::HttpResponse::build(self.status_code()).body(self.to_string()), + _ => actix_web::HttpResponse::build(ResponseError::status_code(self)) + .body(self.to_string()), } } } + +#[cfg(feature = "axum")] +impl axum::response::IntoResponse for Error { + fn into_response(self) -> axum::response::Response { + use axum::body::Body; + + let mut resp = axum::response::Response::builder().status(self.status_code()); + if matches!(&self, &Error::Unauthorized) { + resp.headers_mut() + .expect("This must always work") + .insert("WWW-Authenticate", "Basic".parse().unwrap()); + } + + resp.body(Body::new(self.to_string())) + .expect("This should always work") + } +} diff --git a/crates/dav/src/header/depth.rs b/crates/dav/src/header/depth.rs index 35ad1e2..4b47ede 100644 --- a/crates/dav/src/header/depth.rs +++ b/crates/dav/src/header/depth.rs @@ -1,7 +1,8 @@ #[cfg(feature = "actix")] use actix_web::{HttpRequest, ResponseError}; +#[cfg(feature = "axum")] +use axum::{body::Body, extract::FromRequestParts, response::IntoResponse}; use futures_util::future::{Ready, err, ok}; -use http::StatusCode; use rustical_xml::{ValueDeserialize, ValueSerialize, XmlError}; use thiserror::Error; @@ -12,7 +13,17 @@ pub struct InvalidDepthHeader; #[cfg(feature = "actix")] impl ResponseError for InvalidDepthHeader { fn status_code(&self) -> actix_web::http::StatusCode { - StatusCode::BAD_REQUEST + http_02::StatusCode::BAD_REQUEST + } +} + +#[cfg(feature = "axum")] +impl IntoResponse for InvalidDepthHeader { + fn into_response(self) -> axum::response::Response { + axum::response::Response::builder() + .status(axum::http::StatusCode::BAD_REQUEST) + .body(Body::empty()) + .expect("this always works") } } @@ -81,3 +92,19 @@ impl actix_web::FromRequest for Depth { Self::extract(req) } } + +#[cfg(feature = "axum")] +impl FromRequestParts for Depth { + type Rejection = InvalidDepthHeader; + + async fn from_request_parts( + parts: &mut axum::http::request::Parts, + state: &S, + ) -> Result { + if let Some(depth_header) = parts.headers.get("Depth") { + depth_header.as_bytes().try_into() + } else { + Ok(Self::Zero) + } + } +} diff --git a/crates/dav/src/lib.rs b/crates/dav/src/lib.rs index 4bbfc08..b40fc83 100644 --- a/crates/dav/src/lib.rs +++ b/crates/dav/src/lib.rs @@ -9,6 +9,6 @@ pub mod xml; pub use error::Error; -pub trait Principal: std::fmt::Debug + Clone + 'static { +pub trait Principal: std::fmt::Debug + Clone + Send + Sync + 'static { fn get_id(&self) -> &str; } diff --git a/crates/dav/src/resource/axum_methods.rs b/crates/dav/src/resource/axum_methods.rs new file mode 100644 index 0000000..b9c3883 --- /dev/null +++ b/crates/dav/src/resource/axum_methods.rs @@ -0,0 +1,94 @@ +use axum::body::Body; +use futures_util::future::BoxFuture; +use headers::Allow; +use http::{Method, Request, Response}; +use std::{convert::Infallible, str::FromStr, sync::Arc}; + +pub type MethodFunction = + fn(Arc, Request) -> BoxFuture<'static, Result, Infallible>>; + +pub trait AxumMethods: Sized + Send + Sync + 'static { + #[inline] + fn report() -> Option> { + None + } + + #[inline] + fn get() -> Option> { + None + } + + #[inline] + fn head() -> Option> { + None + } + + #[inline] + fn post() -> Option> { + None + } + + #[inline] + fn mkcol() -> Option> { + None + } + + #[inline] + fn mkcalendar() -> Option> { + None + } + + #[inline] + fn copy() -> Option> { + None + } + + #[inline] + fn mv() -> Option> { + None + } + + #[inline] + fn put() -> Option> { + None + } + + #[inline] + fn allow_header() -> Allow { + let mut allow = vec![ + Method::from_str("PROPFIND").unwrap(), + Method::from_str("PROPPATCH").unwrap(), + Method::DELETE, + Method::OPTIONS, + ]; + if Self::report().is_some() { + allow.push(Method::from_str("REPORT").unwrap()); + } + if Self::get().is_some() { + allow.push(Method::GET); + } + if Self::head().is_some() { + allow.push(Method::HEAD); + } + if Self::post().is_some() { + allow.push(Method::POST); + } + if Self::mkcol().is_some() { + allow.push(Method::from_str("MKCOL").unwrap()); + } + if Self::mkcalendar().is_some() { + allow.push(Method::from_str("MKCALENDAR").unwrap()); + } + if Self::copy().is_some() { + allow.push(Method::from_str("COPY").unwrap()); + } + if Self::mv().is_some() { + allow.push(Method::from_str("MOVE").unwrap()); + } + if Self::put().is_some() { + allow.push(Method::PUT); + } + + allow.into_iter().collect() + } +} diff --git a/crates/dav/src/resource/axum_service.rs b/crates/dav/src/resource/axum_service.rs new file mode 100644 index 0000000..a361778 --- /dev/null +++ b/crates/dav/src/resource/axum_service.rs @@ -0,0 +1,120 @@ +use super::methods::{axum_route_propfind, axum_route_proppatch}; +use crate::resource::{ResourceService, axum_methods::AxumMethods}; +use axum::{ + body::Body, + handler::Handler, + http::{Request, Response}, + response::IntoResponse, +}; +use futures_util::future::BoxFuture; +use headers::HeaderMapExt; +use http::{HeaderValue, StatusCode}; +use std::{convert::Infallible, sync::Arc}; +use tower::Service; + +#[derive(Clone)] +pub struct AxumService { + resource_service: Arc, +} + +impl AxumService { + pub fn new(resource_service: Arc) -> Self { + Self { resource_service } + } +} + +impl Service> + for AxumService +where + RS::Error: IntoResponse + Send + Sync + 'static, +{ + type Error = Infallible; + type Response = Response; + type Future = BoxFuture<'static, Result>; + + #[inline] + fn poll_ready( + &mut self, + _cx: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + Ok(()).into() + } + + #[inline] + fn call(&mut self, req: Request) -> Self::Future { + use crate::resource::methods::axum_route_delete; + let mut propfind_service = + Handler::with_state(axum_route_propfind::, self.resource_service.clone()); + let mut proppatch_service = + Handler::with_state(axum_route_proppatch::, self.resource_service.clone()); + let mut delete_service = + Handler::with_state(axum_route_delete::, self.resource_service.clone()); + let mut options_service = Handler::with_state(route_options::, ()); + match req.method().as_str() { + "PROPFIND" => return Box::pin(Service::call(&mut propfind_service, req)), + "PROPPATCH" => return Box::pin(Service::call(&mut proppatch_service, req)), + "DELETE" => return Box::pin(Service::call(&mut delete_service, req)), + "OPTIONS" => return Box::pin(Service::call(&mut options_service, req)), + "REPORT" => { + if let Some(svc) = RS::report() { + return svc(self.resource_service.clone(), req); + } + } + "GET" => { + if let Some(svc) = RS::get() { + return svc(self.resource_service.clone(), req); + } + } + "HEAD" => { + if let Some(svc) = RS::head() { + return svc(self.resource_service.clone(), req); + } + } + "POST" => { + if let Some(svc) = RS::post() { + return svc(self.resource_service.clone(), req); + } + } + "MKCOL" => { + if let Some(svc) = RS::mkcol() { + return svc(self.resource_service.clone(), req); + } + } + "MKCALENDAR" => { + if let Some(svc) = RS::mkcalendar() { + return svc(self.resource_service.clone(), req); + } + } + "COPY" => { + if let Some(svc) = RS::copy() { + return svc(self.resource_service.clone(), req); + } + } + "MOVE" => { + if let Some(svc) = RS::mv() { + return svc(self.resource_service.clone(), req); + } + } + "PUT" => { + if let Some(svc) = RS::put() { + return svc(self.resource_service.clone(), req); + } + } + _ => {} + }; + Box::pin(async move { + Ok(Response::builder() + .status(StatusCode::METHOD_NOT_ALLOWED) + .body(Body::from("Method not allowed")) + .unwrap()) + }) + } +} + +async fn route_options() -> Response { + let mut resp = Response::builder().status(StatusCode::OK); + let headers = resp.headers_mut().unwrap(); + headers.insert("DAV", HeaderValue::from_static(RS::DAV_HEADER)); + headers.typed_insert(RS::allow_header()); + resp.body(Body::empty()).unwrap() +} diff --git a/crates/dav/src/resource/methods/delete.rs b/crates/dav/src/resource/methods/delete.rs index 3f6de3f..b743bf6 100644 --- a/crates/dav/src/resource/methods/delete.rs +++ b/crates/dav/src/resource/methods/delete.rs @@ -2,9 +2,17 @@ use crate::Error; use crate::privileges::UserPrivilege; use crate::resource::Resource; use crate::resource::ResourceService; +#[cfg(feature = "axum")] +use axum::extract::{Extension, Path, State}; +#[cfg(feature = "axum")] +use axum_extra::TypedHeader; use headers::Header; use headers::{HeaderValue, IfMatch, IfNoneMatch}; +#[cfg(feature = "axum")] +use http::HeaderMap; use itertools::Itertools; +#[cfg(feature = "axum")] +use std::sync::Arc; use tracing::instrument; #[cfg(feature = "actix")] @@ -27,12 +35,12 @@ pub async fn actix_route_delete( // while actix-web still uses http==0.2 let if_match = req .headers() - .get_all(http::header::IF_MATCH) + .get_all(http_02::header::IF_MATCH) .map(|val_02| HeaderValue::from_bytes(val_02.as_bytes()).unwrap()) .collect_vec(); let if_none_match = req .headers() - .get_all(http::header::IF_NONE_MATCH) + .get_all(http_02::header::IF_NONE_MATCH) .map(|val_02| HeaderValue::from_bytes(val_02.as_bytes()).unwrap()) .collect_vec(); @@ -49,7 +57,7 @@ pub async fn actix_route_delete( route_delete( &path.into_inner(), - principal, + &principal, resource_service.as_ref(), no_trash, if_match, @@ -60,9 +68,33 @@ pub async fn actix_route_delete( Ok(actix_web::HttpResponse::Ok().body("")) } +#[cfg(feature = "axum")] +pub(crate) async fn axum_route_delete( + Path(path): Path, + State(resource_service): State>, + Extension(principal): Extension, + if_match: Option>, + if_none_match: Option>, + header_map: HeaderMap, +) -> Result<(), R::Error> { + let no_trash = header_map + .get("X-No-Trashbin") + .map(|val| matches!(val.to_str(), Ok("1"))) + .unwrap_or(false); + route_delete( + &path, + &principal, + resource_service.as_ref(), + no_trash, + if_match.map(|hdr| hdr.0), + if_none_match.map(|hdr| hdr.0), + ) + .await +} + pub async fn route_delete( path_components: &R::PathComponents, - principal: R::Principal, + principal: &R::Principal, resource_service: &R, no_trash: bool, if_match: Option, @@ -70,7 +102,7 @@ pub async fn route_delete( ) -> Result<(), R::Error> { let resource = resource_service.get_resource(path_components).await?; - let privileges = resource.get_user_privileges(&principal)?; + let privileges = resource.get_user_privileges(principal)?; if !privileges.has(&UserPrivilege::Write) { return Err(Error::Unauthorized.into()); } diff --git a/crates/dav/src/resource/methods/mod.rs b/crates/dav/src/resource/methods/mod.rs index eea604d..9a45420 100644 --- a/crates/dav/src/resource/methods/mod.rs +++ b/crates/dav/src/resource/methods/mod.rs @@ -2,15 +2,17 @@ mod delete; mod propfind; mod proppatch; -pub(crate) use delete::route_delete; -pub(crate) use propfind::route_propfind; -pub(crate) use proppatch::route_proppatch; - #[cfg(feature = "actix")] pub(crate) use delete::actix_route_delete; +#[cfg(feature = "axum")] +pub(crate) use delete::axum_route_delete; #[cfg(feature = "actix")] pub(crate) use propfind::actix_route_propfind; +#[cfg(feature = "axum")] +pub(crate) use propfind::axum_route_propfind; #[cfg(feature = "actix")] pub(crate) use proppatch::actix_route_proppatch; +#[cfg(feature = "axum")] +pub(crate) use proppatch::axum_route_proppatch; diff --git a/crates/dav/src/resource/methods/propfind.rs b/crates/dav/src/resource/methods/propfind.rs index 89c5d5e..cbc1ad0 100644 --- a/crates/dav/src/resource/methods/propfind.rs +++ b/crates/dav/src/resource/methods/propfind.rs @@ -7,8 +7,11 @@ use crate::resource::ResourceService; use crate::xml::MultistatusElement; use crate::xml::PropfindElement; use crate::xml::PropfindType; +#[cfg(feature = "axum")] +use axum::extract::{Extension, OriginalUri, Path, State}; use rustical_xml::PropName; use rustical_xml::XmlDocument; +use std::sync::Arc; use tracing::instrument; #[cfg(feature = "actix")] @@ -30,21 +33,46 @@ pub(crate) async fn actix_route_propfind( route_propfind( &path.into_inner(), req.path(), - body, - user, - depth, + &body, + &user, + &depth, resource_service.as_ref(), puri.as_ref(), ) .await } +#[cfg(feature = "axum")] +pub(crate) async fn axum_route_propfind( + Path(path): Path, + State(resource_service): State>, + depth: Depth, + Extension(principal): Extension, + uri: OriginalUri, + Extension(puri): Extension, + body: String, +) -> Result< + MultistatusElement<::Prop, ::Prop>, + R::Error, +> { + route_propfind::( + &path, + uri.path(), + &body, + &principal, + &depth, + resource_service.as_ref(), + &puri, + ) + .await +} + pub(crate) async fn route_propfind( path_components: &R::PathComponents, path: &str, - body: String, - user: R::Principal, - depth: Depth, + body: &str, + principal: &R::Principal, + depth: &Depth, resource_service: &R, puri: &impl PrincipalUri, ) -> Result< @@ -52,7 +80,7 @@ pub(crate) async fn route_propfind( R::Error, > { let resource = resource_service.get_resource(path_components).await?; - let privileges = resource.get_user_privileges(&user)?; + let privileges = resource.get_user_privileges(principal)?; if !privileges.has(&UserPrivilege::Read) { return Err(Error::Unauthorized.into()); } @@ -60,7 +88,7 @@ pub(crate) async fn route_propfind( // A request body is optional. If empty we MUST return all props let propfind_self: PropfindElement<<::Prop as PropName>::Names> = if !body.is_empty() { - PropfindElement::parse_str(&body).map_err(Error::XmlError)? + PropfindElement::parse_str(body).map_err(Error::XmlError)? } else { PropfindElement { prop: PropfindType::Allprop, @@ -68,7 +96,7 @@ pub(crate) async fn route_propfind( }; let propfind_member: PropfindElement<<::Prop as PropName>::Names> = if !body.is_empty() { - PropfindElement::parse_str(&body).map_err(Error::XmlError)? + PropfindElement::parse_str(body).map_err(Error::XmlError)? } else { PropfindElement { prop: PropfindType::Allprop, @@ -76,18 +104,18 @@ pub(crate) async fn route_propfind( }; let mut member_responses = Vec::new(); - if depth != Depth::Zero { + if depth != &Depth::Zero { for (subpath, member) in resource_service.get_members(path_components).await? { member_responses.push(member.propfind_typed( &format!("{}/{}", path.trim_end_matches('/'), subpath), &propfind_member.prop, puri, - &user, + principal, )?); } } - let response = resource.propfind_typed(path, &propfind_self.prop, puri, &user)?; + let response = resource.propfind_typed(path, &propfind_self.prop, puri, &principal)?; Ok(MultistatusElement { responses: vec![response], diff --git a/crates/dav/src/resource/methods/proppatch.rs b/crates/dav/src/resource/methods/proppatch.rs index c158c15..c3935c0 100644 --- a/crates/dav/src/resource/methods/proppatch.rs +++ b/crates/dav/src/resource/methods/proppatch.rs @@ -1,7 +1,10 @@ use crate::Error; use crate::privileges::UserPrivilege; +use std::sync::Arc; use crate::resource::Resource; use crate::resource::ResourceService; +#[cfg(feature = "axum")] +use axum::extract::{Extension, OriginalUri, Path, State}; use crate::xml::MultistatusElement; use crate::xml::TagList; use crate::xml::multistatus::{PropstatElement, PropstatWrapper, ResponseElement}; @@ -74,8 +77,30 @@ pub(crate) async fn actix_route_proppatch( route_proppatch( &path.into_inner(), req.path(), - body, - principal, + &body, + &principal, + resource_service.as_ref(), + ) + .await +} + + +#[cfg(feature = "axum")] +pub(crate) async fn axum_route_proppatch( + Path(path): Path, + State(resource_service): State>, + Extension(principal): Extension, + uri: OriginalUri, + body: String, +) -> Result< + MultistatusElement, + R::Error, +> { + route_proppatch( + &path, + uri.path(), + &body, + &principal, resource_service.as_ref(), ) .await @@ -84,8 +109,8 @@ pub(crate) async fn actix_route_proppatch( pub(crate) async fn route_proppatch( path_components: &R::PathComponents, path: &str, - body: String, - principal: R::Principal, + body: &str, + principal: &R::Principal, resource_service: &R, ) -> Result, R::Error> { let href = path.to_owned(); diff --git a/crates/dav/src/resource/mod.rs b/crates/dav/src/resource/mod.rs index 9e81e21..2773e6f 100644 --- a/crates/dav/src/resource/mod.rs +++ b/crates/dav/src/resource/mod.rs @@ -12,12 +12,19 @@ use rustical_xml::{EnumVariants, NamespaceOwned, PropName, XmlDeserialize, XmlSe use std::collections::HashSet; use std::str::FromStr; +#[cfg(feature = "axum")] +mod axum_methods; +#[cfg(feature = "axum")] +mod axum_service; mod methods; mod principal_uri; mod resource_service; +#[cfg(feature = "axum")] +pub use axum_methods::AxumMethods; +#[cfg(feature = "axum")] +pub use axum_service::AxumService; pub use principal_uri::PrincipalUri; -pub use resource_service::*; pub trait ResourceProp: XmlSerialize + XmlDeserialize {} impl ResourceProp for T {} @@ -25,8 +32,8 @@ impl ResourceProp for T {} pub trait ResourcePropName: FromStr {} impl ResourcePropName for T {} -pub trait Resource: Clone + 'static { - type Prop: ResourceProp + PartialEq + Clone + EnumVariants + PropName; +pub trait Resource: Clone + Send + 'static { + type Prop: ResourceProp + PartialEq + Clone + EnumVariants + PropName + Send; type Error: From; type Principal: Principal; diff --git a/crates/dav/src/resource/principal_uri.rs b/crates/dav/src/resource/principal_uri.rs index 2cb5a74..fe3c1b2 100644 --- a/crates/dav/src/resource/principal_uri.rs +++ b/crates/dav/src/resource/principal_uri.rs @@ -1,3 +1,3 @@ -pub trait PrincipalUri: 'static { +pub trait PrincipalUri: 'static + Clone + Send + Sync { fn principal_uri(&self, principal: &str) -> String; } diff --git a/crates/dav/src/resource/resource_service.rs b/crates/dav/src/resource/resource_service.rs index d3e3848..45ea74f 100644 --- a/crates/dav/src/resource/resource_service.rs +++ b/crates/dav/src/resource/resource_service.rs @@ -2,28 +2,37 @@ use super::methods::{actix_route_delete, actix_route_propfind, actix_route_proppatch}; use super::{PrincipalUri, Resource}; use crate::Principal; +#[cfg(feature = "axum")] +use crate::resource::{AxumMethods, AxumService}; #[cfg(feature = "actix")] use actix_web::{http::Method, web, web::Data}; use async_trait::async_trait; use serde::Deserialize; -use std::str::FromStr; +use std::{str::FromStr, sync::Arc}; -#[async_trait(?Send)] -pub trait ResourceService: Sized + 'static { +#[async_trait] +pub trait ResourceService: Sized + Send + Sync + 'static { + type PathComponents: for<'de> Deserialize<'de> + Sized + Send + Sync + Clone + 'static; // defines how the resource URI maps to parameters, i.e. /{principal}/{calendar} -> (String, String) type MemberType: Resource; - type PathComponents: for<'de> Deserialize<'de> + Sized + Clone + 'static; // defines how the resource URI maps to parameters, i.e. /{principal}/{calendar} -> (String, String) type Resource: Resource; - type Error: From; + type Error: From + Send; type Principal: Principal; type PrincipalUri: PrincipalUri; + const DAV_HEADER: &'static str; + async fn get_members( &self, - _path_components: &Self::PathComponents, + _path: &Self::PathComponents, ) -> Result, Self::Error> { Ok(vec![]) } + async fn test_get_members(&self, _path: &Self::PathComponents) -> Result { + // ) -> Result, Self::Error> { + Ok("asd".to_string()) + } + async fn get_resource( &self, _path: &Self::PathComponents, @@ -69,4 +78,12 @@ pub trait ResourceService: Sized + 'static { where Self::Error: actix_web::ResponseError, Self::Principal: actix_web::FromRequest; + + #[cfg(feature = "axum")] + fn axum_service(self) -> AxumService + where + Self: Clone + Send + Sync + AxumMethods, + { + AxumService::new(Arc::new(self)) + } } diff --git a/crates/dav/src/resources/root.rs b/crates/dav/src/resources/root.rs index c410cdf..d900c4e 100644 --- a/crates/dav/src/resources/root.rs +++ b/crates/dav/src/resources/root.rs @@ -58,7 +58,7 @@ impl } } -#[async_trait(?Send)] +#[async_trait] impl + Clone, P: Principal, PURI: PrincipalUri> ResourceService for RootResourceService { @@ -69,6 +69,8 @@ impl + Clone, P: Principal, PURI: PrincipalU type Principal = P; type PrincipalUri = PURI; + const DAV_HEADER: &str = "1, 3, access-control"; + async fn get_resource(&self, _: &()) -> Result { Ok(RootResource::::default()) } diff --git a/crates/dav/src/xml/multistatus.rs b/crates/dav/src/xml/multistatus.rs index 69fdd64..665b455 100644 --- a/crates/dav/src/xml/multistatus.rs +++ b/crates/dav/src/xml/multistatus.rs @@ -124,3 +124,25 @@ impl Responder for MultistatusElement axum::response::IntoResponse + for MultistatusElement +{ + fn into_response(self) -> axum::response::Response { + use axum::body::Body; + use http::header; + + let mut output: Vec<_> = b"\n".into(); + let mut writer = quick_xml::Writer::new_with_indent(&mut output, b' ', 4); + if let Err(err) = self.serialize_root(&mut writer) { + return crate::Error::from(err).into_response(); + } + + let mut resp = axum::response::Response::builder().status(StatusCode::MULTI_STATUS); + resp.headers_mut() + .unwrap() + .insert(header::CONTENT_TYPE, "application/xml".try_into().unwrap()); + resp.body(Body::from(output)).unwrap() + } +} diff --git a/crates/dav_push/Cargo.toml b/crates/dav_push/Cargo.toml index 9752236..31eb391 100644 --- a/crates/dav_push/Cargo.toml +++ b/crates/dav_push/Cargo.toml @@ -23,3 +23,4 @@ reqwest.workspace = true tokio.workspace = true rustical_dav.workspace = true rustical_store.workspace = true +http.workspace = true diff --git a/crates/dav_push/src/notifier.rs b/crates/dav_push/src/notifier.rs index 17de1c4..99e455d 100644 --- a/crates/dav_push/src/notifier.rs +++ b/crates/dav_push/src/notifier.rs @@ -1,4 +1,4 @@ -use actix_web::http::StatusCode; +use http::StatusCode; use reqwest::{ Method, Request, header::{self, HeaderName, HeaderValue}, diff --git a/crates/frontend/src/lib.rs b/crates/frontend/src/lib.rs index 81d2839..8678f8b 100644 --- a/crates/frontend/src/lib.rs +++ b/crates/frontend/src/lib.rs @@ -335,7 +335,7 @@ pub fn configure_frontend(Arc); -#[async_trait(?Send)] +#[async_trait] impl UserStore for OidcUserStore { type Error = rustical_store::Error; diff --git a/crates/oidc/src/user_store.rs b/crates/oidc/src/user_store.rs index 8a380e5..40d5485 100644 --- a/crates/oidc/src/user_store.rs +++ b/crates/oidc/src/user_store.rs @@ -1,7 +1,7 @@ use actix_web::ResponseError; use async_trait::async_trait; -#[async_trait(?Send)] +#[async_trait] pub trait UserStore: 'static { type Error: ResponseError; diff --git a/crates/store/src/auth/mod.rs b/crates/store/src/auth/mod.rs index 9a345cb..5cf647b 100644 --- a/crates/store/src/auth/mod.rs +++ b/crates/store/src/auth/mod.rs @@ -4,7 +4,7 @@ use crate::error::Error; use async_trait::async_trait; #[async_trait] -pub trait AuthenticationProvider: 'static { +pub trait AuthenticationProvider: Send + Sync + 'static { async fn get_principals(&self) -> Result, crate::Error>; async fn get_principal(&self, id: &str) -> Result, crate::Error>; async fn remove_principal(&self, id: &str) -> Result<(), crate::Error>; diff --git a/crates/xml/src/lib.rs b/crates/xml/src/lib.rs index 725b5ca..6195fc7 100644 --- a/crates/xml/src/lib.rs +++ b/crates/xml/src/lib.rs @@ -41,6 +41,8 @@ pub trait EnumVariants { pub trait PropName: Sized { type Names: Into<(Option>, &'static str)> + Clone + + Send + + Sync + From + FromStr + Hash