mirror of
https://github.com/lennart-k/rustical.git
synced 2025-12-13 22:52:22 +00:00
Work on axum support
This commit is contained in:
@@ -29,3 +29,4 @@ rustical_xml.workspace = true
|
||||
uuid.workspace = true
|
||||
rustical_dav_push.workspace = true
|
||||
rustical_ical.workspace = true
|
||||
http.workspace = true
|
||||
|
||||
@@ -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::{
|
||||
|
||||
@@ -4,7 +4,7 @@ use crate::{
|
||||
CalendarObjectPropWrapper, CalendarObjectPropWrapperName, CalendarObjectResource,
|
||||
},
|
||||
};
|
||||
use actix_web::http::StatusCode;
|
||||
use http::StatusCode;
|
||||
use rustical_dav::{
|
||||
resource::{PrincipalUri, Resource},
|
||||
xml::{
|
||||
|
||||
@@ -322,7 +322,7 @@ impl<C: CalendarStore, S: SubscriptionStore> CalendarResourceService<C, S> {
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait(?Send)]
|
||||
#[async_trait]
|
||||
impl<C: CalendarStore, S: SubscriptionStore> ResourceService for CalendarResourceService<C, S> {
|
||||
type MemberType = CalendarObjectResource;
|
||||
type PathComponents = (String, String); // principal, calendar_id
|
||||
@@ -331,6 +331,8 @@ impl<C: CalendarStore, S: SubscriptionStore> 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,
|
||||
|
||||
@@ -137,7 +137,7 @@ pub struct CalendarObjectPathComponents {
|
||||
pub object_id: String,
|
||||
}
|
||||
|
||||
#[async_trait(?Send)]
|
||||
#[async_trait]
|
||||
impl<C: CalendarStore> ResourceService for CalendarObjectResourceService<C> {
|
||||
type PathComponents = CalendarObjectPathComponents;
|
||||
type Resource = CalendarObjectResource;
|
||||
@@ -146,6 +146,8 @@ impl<C: CalendarStore> ResourceService for CalendarObjectResourceService<C> {
|
||||
type Principal = User;
|
||||
type PrincipalUri = CalDavPrincipalUri;
|
||||
|
||||
const DAV_HEADER: &str = "1, 3, access-control, calendar-access";
|
||||
|
||||
async fn get_resource(
|
||||
&self,
|
||||
CalendarObjectPathComponents {
|
||||
|
||||
@@ -77,7 +77,7 @@ impl<C: CalendarStore, S: SubscriptionStore> CalendarSetResourceService<C, S> {
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait(?Send)]
|
||||
#[async_trait]
|
||||
impl<C: CalendarStore, S: SubscriptionStore> ResourceService for CalendarSetResourceService<C, S> {
|
||||
type PathComponents = (String,);
|
||||
type MemberType = CalendarResource;
|
||||
@@ -86,6 +86,8 @@ impl<C: CalendarStore, S: SubscriptionStore> 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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -145,7 +145,7 @@ impl<AP: AuthenticationProvider, S: SubscriptionStore, CS: CalendarStore, BS: Ca
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait(?Send)]
|
||||
#[async_trait]
|
||||
impl<AP: AuthenticationProvider, S: SubscriptionStore, CS: CalendarStore, BS: CalendarStore>
|
||||
ResourceService for PrincipalResourceService<AP, S, CS, BS>
|
||||
{
|
||||
@@ -156,6 +156,8 @@ impl<AP: AuthenticationProvider, S: SubscriptionStore, CS: CalendarStore, BS: Ca
|
||||
type Principal = User;
|
||||
type PrincipalUri = CalDavPrincipalUri;
|
||||
|
||||
const DAV_HEADER: &str = "1, 3, access-control";
|
||||
|
||||
async fn get_resource(
|
||||
&self,
|
||||
(principal,): &Self::PathComponents,
|
||||
|
||||
@@ -27,3 +27,4 @@ rustical_xml.workspace = true
|
||||
uuid.workspace = true
|
||||
rustical_dav_push.workspace = true
|
||||
rustical_ical.workspace = true
|
||||
http.workspace = true
|
||||
|
||||
@@ -105,7 +105,7 @@ pub struct AddressObjectPathComponents {
|
||||
pub object_id: String,
|
||||
}
|
||||
|
||||
#[async_trait(?Send)]
|
||||
#[async_trait]
|
||||
impl<AS: AddressbookStore> ResourceService for AddressObjectResourceService<AS> {
|
||||
type PathComponents = AddressObjectPathComponents;
|
||||
type Resource = AddressObjectResource;
|
||||
@@ -114,6 +114,8 @@ impl<AS: AddressbookStore> ResourceService for AddressObjectResourceService<AS>
|
||||
type Principal = User;
|
||||
type PrincipalUri = CardDavPrincipalUri;
|
||||
|
||||
const DAV_HEADER: &str = "1, 3, access-control, addressbook";
|
||||
|
||||
async fn get_resource(
|
||||
&self,
|
||||
AddressObjectPathComponents {
|
||||
|
||||
@@ -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},
|
||||
|
||||
@@ -4,7 +4,7 @@ use crate::{
|
||||
AddressObjectPropWrapper, AddressObjectPropWrapperName, AddressObjectResource,
|
||||
},
|
||||
};
|
||||
use actix_web::http::StatusCode;
|
||||
use http::StatusCode;
|
||||
use rustical_dav::{
|
||||
resource::{PrincipalUri, Resource},
|
||||
xml::{
|
||||
|
||||
@@ -188,7 +188,7 @@ impl Resource for AddressbookResource {
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait(?Send)]
|
||||
#[async_trait]
|
||||
impl<AS: AddressbookStore, S: SubscriptionStore> ResourceService
|
||||
for AddressbookResourceService<AS, S>
|
||||
{
|
||||
@@ -199,6 +199,8 @@ impl<AS: AddressbookStore, S: SubscriptionStore> 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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -140,7 +140,7 @@ impl Resource for PrincipalResource {
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait(?Send)]
|
||||
#[async_trait]
|
||||
impl<A: AddressbookStore, AP: AuthenticationProvider, S: SubscriptionStore> ResourceService
|
||||
for PrincipalResourceService<A, AP, S>
|
||||
{
|
||||
@@ -151,6 +151,8 @@ impl<A: AddressbookStore, AP: AuthenticationProvider, S: SubscriptionStore> Reso
|
||||
type Principal = User;
|
||||
type PrincipalUri = CardDavPrincipalUri;
|
||||
|
||||
const DAV_HEADER: &str = "1, 3, access-control, addressbook";
|
||||
|
||||
async fn get_resource(
|
||||
&self,
|
||||
(principal,): &Self::PathComponents,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<S: Send + Sync> FromRequestParts<S> for Depth {
|
||||
type Rejection = InvalidDepthHeader;
|
||||
|
||||
async fn from_request_parts(
|
||||
parts: &mut axum::http::request::Parts,
|
||||
state: &S,
|
||||
) -> Result<Self, Self::Rejection> {
|
||||
if let Some(depth_header) = parts.headers.get("Depth") {
|
||||
depth_header.as_bytes().try_into()
|
||||
} else {
|
||||
Ok(Self::Zero)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
94
crates/dav/src/resource/axum_methods.rs
Normal file
94
crates/dav/src/resource/axum_methods.rs
Normal file
@@ -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<Service> =
|
||||
fn(Arc<Service>, Request<Body>) -> BoxFuture<'static, Result<Response<Body>, Infallible>>;
|
||||
|
||||
pub trait AxumMethods: Sized + Send + Sync + 'static {
|
||||
#[inline]
|
||||
fn report() -> Option<MethodFunction<Self>> {
|
||||
None
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn get() -> Option<MethodFunction<Self>> {
|
||||
None
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn head() -> Option<MethodFunction<Self>> {
|
||||
None
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn post() -> Option<MethodFunction<Self>> {
|
||||
None
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn mkcol() -> Option<MethodFunction<Self>> {
|
||||
None
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn mkcalendar() -> Option<MethodFunction<Self>> {
|
||||
None
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn copy() -> Option<MethodFunction<Self>> {
|
||||
None
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn mv() -> Option<MethodFunction<Self>> {
|
||||
None
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn put() -> Option<MethodFunction<Self>> {
|
||||
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()
|
||||
}
|
||||
}
|
||||
120
crates/dav/src/resource/axum_service.rs
Normal file
120
crates/dav/src/resource/axum_service.rs
Normal file
@@ -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<RS: ResourceService + AxumMethods> {
|
||||
resource_service: Arc<RS>,
|
||||
}
|
||||
|
||||
impl<RS: ResourceService + AxumMethods> AxumService<RS> {
|
||||
pub fn new(resource_service: Arc<RS>) -> Self {
|
||||
Self { resource_service }
|
||||
}
|
||||
}
|
||||
|
||||
impl<RS: ResourceService + AxumMethods + Clone + Send + Sync> Service<Request<Body>>
|
||||
for AxumService<RS>
|
||||
where
|
||||
RS::Error: IntoResponse + Send + Sync + 'static,
|
||||
{
|
||||
type Error = Infallible;
|
||||
type Response = Response<Body>;
|
||||
type Future = BoxFuture<'static, Result<Self::Response, Self::Error>>;
|
||||
|
||||
#[inline]
|
||||
fn poll_ready(
|
||||
&mut self,
|
||||
_cx: &mut std::task::Context<'_>,
|
||||
) -> std::task::Poll<Result<(), Self::Error>> {
|
||||
Ok(()).into()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn call(&mut self, req: Request<Body>) -> Self::Future {
|
||||
use crate::resource::methods::axum_route_delete;
|
||||
let mut propfind_service =
|
||||
Handler::with_state(axum_route_propfind::<RS>, self.resource_service.clone());
|
||||
let mut proppatch_service =
|
||||
Handler::with_state(axum_route_proppatch::<RS>, self.resource_service.clone());
|
||||
let mut delete_service =
|
||||
Handler::with_state(axum_route_delete::<RS>, self.resource_service.clone());
|
||||
let mut options_service = Handler::with_state(route_options::<RS>, ());
|
||||
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<RS: ResourceService + AxumMethods>() -> Response<Body> {
|
||||
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()
|
||||
}
|
||||
@@ -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<R: ResourceService>(
|
||||
// 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<R: ResourceService>(
|
||||
|
||||
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<R: ResourceService>(
|
||||
Ok(actix_web::HttpResponse::Ok().body(""))
|
||||
}
|
||||
|
||||
#[cfg(feature = "axum")]
|
||||
pub(crate) async fn axum_route_delete<R: ResourceService>(
|
||||
Path(path): Path<R::PathComponents>,
|
||||
State(resource_service): State<Arc<R>>,
|
||||
Extension(principal): Extension<R::Principal>,
|
||||
if_match: Option<TypedHeader<IfMatch>>,
|
||||
if_none_match: Option<TypedHeader<IfNoneMatch>>,
|
||||
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<R: ResourceService>(
|
||||
path_components: &R::PathComponents,
|
||||
principal: R::Principal,
|
||||
principal: &R::Principal,
|
||||
resource_service: &R,
|
||||
no_trash: bool,
|
||||
if_match: Option<IfMatch>,
|
||||
@@ -70,7 +102,7 @@ pub async fn route_delete<R: ResourceService>(
|
||||
) -> 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());
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<R: ResourceService>(
|
||||
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<R: ResourceService>(
|
||||
Path(path): Path<R::PathComponents>,
|
||||
State(resource_service): State<Arc<R>>,
|
||||
depth: Depth,
|
||||
Extension(principal): Extension<R::Principal>,
|
||||
uri: OriginalUri,
|
||||
Extension(puri): Extension<R::PrincipalUri>,
|
||||
body: String,
|
||||
) -> Result<
|
||||
MultistatusElement<<R::Resource as Resource>::Prop, <R::MemberType as Resource>::Prop>,
|
||||
R::Error,
|
||||
> {
|
||||
route_propfind::<R>(
|
||||
&path,
|
||||
uri.path(),
|
||||
&body,
|
||||
&principal,
|
||||
&depth,
|
||||
resource_service.as_ref(),
|
||||
&puri,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn route_propfind<R: ResourceService>(
|
||||
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: ResourceService>(
|
||||
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<R: ResourceService>(
|
||||
// A request body is optional. If empty we MUST return all props
|
||||
let propfind_self: PropfindElement<<<R::Resource as Resource>::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<R: ResourceService>(
|
||||
};
|
||||
let propfind_member: PropfindElement<<<R::MemberType as Resource>::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<R: ResourceService>(
|
||||
};
|
||||
|
||||
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],
|
||||
|
||||
@@ -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<R: ResourceService>(
|
||||
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<R: ResourceService>(
|
||||
Path(path): Path<R::PathComponents>,
|
||||
State(resource_service): State<Arc<R>>,
|
||||
Extension(principal): Extension<R::Principal>,
|
||||
uri: OriginalUri,
|
||||
body: String,
|
||||
) -> Result<
|
||||
MultistatusElement<String, String>,
|
||||
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<R: ResourceService>(
|
||||
pub(crate) async fn route_proppatch<R: ResourceService>(
|
||||
path_components: &R::PathComponents,
|
||||
path: &str,
|
||||
body: String,
|
||||
principal: R::Principal,
|
||||
body: &str,
|
||||
principal: &R::Principal,
|
||||
resource_service: &R,
|
||||
) -> Result<MultistatusElement<String, String>, R::Error> {
|
||||
let href = path.to_owned();
|
||||
|
||||
@@ -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<T: XmlSerialize + XmlDeserialize> ResourceProp for T {}
|
||||
@@ -25,8 +32,8 @@ impl<T: XmlSerialize + XmlDeserialize> ResourceProp for T {}
|
||||
pub trait ResourcePropName: FromStr {}
|
||||
impl<T: FromStr> 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<crate::Error>;
|
||||
type Principal: Principal;
|
||||
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
pub trait PrincipalUri: 'static {
|
||||
pub trait PrincipalUri: 'static + Clone + Send + Sync {
|
||||
fn principal_uri(&self, principal: &str) -> String;
|
||||
}
|
||||
|
||||
@@ -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<Error = Self::Error, Principal = Self::Principal>;
|
||||
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<Error = Self::Error, Principal = Self::Principal>;
|
||||
type Error: From<crate::Error>;
|
||||
type Error: From<crate::Error> + 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<Vec<(String, Self::MemberType)>, Self::Error> {
|
||||
Ok(vec![])
|
||||
}
|
||||
|
||||
async fn test_get_members(&self, _path: &Self::PathComponents) -> Result<String, Self::Error> {
|
||||
// ) -> Result<Vec<Self::MemberType>, 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<Self>
|
||||
where
|
||||
Self: Clone + Send + Sync + AxumMethods,
|
||||
{
|
||||
AxumService::new(Arc::new(self))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ impl<PRS: ResourceService + Clone, P: Principal, PURI: PrincipalUri>
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait(?Send)]
|
||||
#[async_trait]
|
||||
impl<PRS: ResourceService<Principal = P> + Clone, P: Principal, PURI: PrincipalUri> ResourceService
|
||||
for RootResourceService<PRS, P, PURI>
|
||||
{
|
||||
@@ -69,6 +69,8 @@ impl<PRS: ResourceService<Principal = P> + 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<Self::Resource, Self::Error> {
|
||||
Ok(RootResource::<PRS::Resource, P>::default())
|
||||
}
|
||||
|
||||
@@ -124,3 +124,25 @@ impl<T1: XmlSerialize, T2: XmlSerialize> Responder for MultistatusElement<T1, T2
|
||||
.body(String::from_utf8(output).unwrap())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "axum")]
|
||||
impl<T1: XmlSerialize, T2: XmlSerialize> axum::response::IntoResponse
|
||||
for MultistatusElement<T1, T2>
|
||||
{
|
||||
fn into_response(self) -> axum::response::Response {
|
||||
use axum::body::Body;
|
||||
use http::header;
|
||||
|
||||
let mut output: Vec<_> = b"<?xml version=\"1.0\" encoding=\"utf-8\"?>\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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,3 +23,4 @@ reqwest.workspace = true
|
||||
tokio.workspace = true
|
||||
rustical_dav.workspace = true
|
||||
rustical_store.workspace = true
|
||||
http.workspace = true
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use actix_web::http::StatusCode;
|
||||
use http::StatusCode;
|
||||
use reqwest::{
|
||||
Method, Request,
|
||||
header::{self, HeaderName, HeaderValue},
|
||||
|
||||
@@ -335,7 +335,7 @@ pub fn configure_frontend<AP: AuthenticationProvider, CS: CalendarStore, AS: Add
|
||||
|
||||
struct OidcUserStore<AP: AuthenticationProvider>(Arc<AP>);
|
||||
|
||||
#[async_trait(?Send)]
|
||||
#[async_trait]
|
||||
impl<AP: AuthenticationProvider> UserStore for OidcUserStore<AP> {
|
||||
type Error = rustical_store::Error;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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<Vec<User>, crate::Error>;
|
||||
async fn get_principal(&self, id: &str) -> Result<Option<User>, crate::Error>;
|
||||
async fn remove_principal(&self, id: &str) -> Result<(), crate::Error>;
|
||||
|
||||
@@ -41,6 +41,8 @@ pub trait EnumVariants {
|
||||
pub trait PropName: Sized {
|
||||
type Names: Into<(Option<Namespace<'static>>, &'static str)>
|
||||
+ Clone
|
||||
+ Send
|
||||
+ Sync
|
||||
+ From<Self>
|
||||
+ FromStr<Err: std::fmt::Debug>
|
||||
+ Hash
|
||||
|
||||
Reference in New Issue
Block a user