From 0f3c3d5bebd4c9996a4c30d7a665f2265697e5fd Mon Sep 17 00:00:00 2001 From: Lennart <18233294+lennart-k@users.noreply.github.com> Date: Wed, 29 May 2024 13:57:23 +0200 Subject: [PATCH] Refactor all the propfind business --- crates/caldav/src/lib.rs | 10 +- crates/dav/src/propfind.rs | 178 ++++++++++++++------------------- crates/dav/src/resource.rs | 32 ++++-- crates/dav/src/xml_snippets.rs | 16 --- 4 files changed, 103 insertions(+), 133 deletions(-) diff --git a/crates/caldav/src/lib.rs b/crates/caldav/src/lib.rs index 2d619e7..7a627fc 100644 --- a/crates/caldav/src/lib.rs +++ b/crates/caldav/src/lib.rs @@ -7,7 +7,7 @@ use principal::PrincipalResource; use root::RootResource; use rustical_auth::CheckAuthentication; use rustical_dav::error::Error; -use rustical_dav::propfind::{handle_propfind, ServicePrefix}; +use rustical_dav::propfind::{route_propfind, ServicePrefix}; use rustical_store::calendar::CalendarStore; use std::str::FromStr; use std::sync::Arc; @@ -48,15 +48,15 @@ pub fn configure_dav( .guard(guard::Method(Method::OPTIONS)) .to(options_handler), ) - .service(web::resource("").route(propfind_method().to(handle_propfind::))) + .service(web::resource("").route(propfind_method().to(route_propfind::))) .service( web::resource("/{principal}") - .route(propfind_method().to(handle_propfind::>)), + .route(propfind_method().to(route_propfind::>)), ) .service( web::resource("/{principal}/{calendar}") .route(report_method().to(calendar::methods::route_report_calendar::)) - .route(propfind_method().to(handle_propfind::>)) + .route(propfind_method().to(route_propfind::>)) .route( mkcalendar_method().to(calendar::methods::mkcalendar::route_mkcol_calendar::), ) @@ -67,7 +67,7 @@ pub fn configure_dav( ) .service( web::resource("/{principal}/{calendar}/{event}") - .route(propfind_method().to(handle_propfind::>)) + .route(propfind_method().to(route_propfind::>)) .route(web::method(Method::DELETE).to(event::methods::delete_event::)) .route(web::method(Method::GET).to(event::methods::get_event::)) .route(web::method(Method::PUT).to(event::methods::put_event::)), diff --git a/crates/dav/src/propfind.rs b/crates/dav/src/propfind.rs index e565673..d22c5b0 100644 --- a/crates/dav/src/propfind.rs +++ b/crates/dav/src/propfind.rs @@ -2,124 +2,45 @@ use crate::depth_extractor::Depth; use crate::namespace::Namespace; use crate::resource::HandlePropfind; use crate::resource::ResourceService; -use crate::xml_snippets::generate_multistatus; +use crate::xml::tag_list::TagList; use actix_web::http::header::ContentType; -use actix_web::http::StatusCode; use actix_web::web::{Data, Path}; use actix_web::{HttpRequest, HttpResponse}; -use anyhow::{anyhow, Result}; +use anyhow::Result; use rustical_auth::{AuthInfoExtractor, CheckAuthentication}; +use serde::Deserialize; use serde::Serialize; -use thiserror::Error; // This is not the final place for this struct pub struct ServicePrefix(pub String); -#[derive(Debug, Error)] -pub enum Error { - #[error("invalid propfind request: {0}")] - InvalidPropfind(&'static str), - #[error("input is not valid xml")] - ParsingError(#[from] roxmltree::Error), - #[error(transparent)] - Other(#[from] anyhow::Error), +#[derive(Deserialize, Clone, Debug)] +#[serde(rename_all = "kebab-case")] +struct PropElement { + #[serde(flatten)] + prop: TagList, } -impl actix_web::error::ResponseError for Error { - fn status_code(&self) -> actix_web::http::StatusCode { - match self { - Self::InvalidPropfind(_) => StatusCode::BAD_REQUEST, - Self::ParsingError(_) => StatusCode::BAD_REQUEST, - Self::Other(_) => StatusCode::INTERNAL_SERVER_ERROR, - } - } - - fn error_response(&self) -> HttpResponse { - HttpResponse::build(self.status_code()).body(self.to_string()) - } +#[derive(Deserialize, Clone, Debug)] +#[serde(rename_all = "kebab-case")] +enum PropfindType { + Propname, + Allprop, + Prop(PropElement), } -pub fn parse_propfind(body: &str) -> Result, Error> { - if body.is_empty() { - // if body is empty, allprops must be returned (RFC 4918) - return Ok(vec!["allprops"]); - } - let doc = roxmltree::Document::parse(body)?; - - let propfind_node = doc.root_element(); - if propfind_node.tag_name().name() != "propfind" { - return Err(Error::InvalidPropfind("root tag is not ")); - } - - let prop_node = if let Some(el) = propfind_node.first_element_child() { - el - } else { - return Ok(Vec::new()); - }; - - match prop_node.tag_name().name() { - "prop" => Ok(prop_node - .children() - .filter(|node| node.is_element()) - .map(|node| node.tag_name().name()) - .collect()), - _ => Err(Error::InvalidPropfind( - "invalid tag in , expected ", - )), - } -} - -pub async fn handle_propfind( - path: Path, - body: String, - req: HttpRequest, - prefix: Data, - auth: AuthInfoExtractor, - depth: Depth, -) -> Result { - // TODO: fix errors - let props = parse_propfind(&body).map_err(|_e| anyhow!("propfind parsing error"))?; - let auth_info = auth.inner; - let prefix = prefix.0.to_owned(); - let path_components = path.into_inner(); - - let resource_service = R::new(req, auth_info.clone(), path_components.clone()).await?; - - let resource = resource_service.get_file().await?; - let response = resource.propfind(&prefix, props.clone()).await?; - let mut member_responses = Vec::new(); - - if depth != Depth::Zero { - for member in resource_service.get_members(auth_info).await? { - member_responses.push(member.propfind(&prefix, props.clone()).await?); - } - } - - let output = generate_multistatus( - vec![Namespace::Dav, Namespace::CalDAV, Namespace::ICal], - |writer| { - writer - .write_serializable("response", &response) - .map_err(|_e| quick_xml::Error::TextNotFound)?; - for response in member_responses { - writer - .write_serializable("response", &response) - .map_err(|_e| quick_xml::Error::TextNotFound)?; - } - Ok(()) - }, - )?; - - Ok(HttpResponse::MultiStatus() - .content_type(ContentType::xml()) - .body(output)) +#[derive(Deserialize, Clone, Debug)] +#[serde(rename_all = "kebab-case")] +struct PropfindElement { + #[serde(rename = "$value")] + prop: PropfindType, } #[derive(Serialize)] +#[serde(rename = "multistatus")] struct MultistatusElement { - #[serde(rename = "$value")] - responses: Vec, - #[serde(rename = "$value")] + response: T1, + #[serde(rename = "response")] member_responses: Vec, #[serde(rename = "@xmlns")] ns_dav: &'static str, @@ -128,3 +49,58 @@ struct MultistatusElement { #[serde(rename = "@xmlns:IC")] ns_ical: &'static str, } + +pub async fn route_propfind( + path: Path, + body: String, + req: HttpRequest, + prefix: Data, + auth: AuthInfoExtractor, + depth: Depth, +) -> Result { + let auth_info = auth.inner; + let prefix = prefix.0.to_owned(); + let path_components = path.into_inner(); + + let resource_service = R::new(req, auth_info.clone(), path_components.clone()).await?; + + let propfind: PropfindElement = quick_xml::de::from_str(&body).unwrap(); + let props = match propfind.prop { + PropfindType::Allprop => { + vec!["allprop".to_owned()] + } + PropfindType::Propname => { + // TODO: Implement + return Err(crate::error::Error::InternalError); + } + PropfindType::Prop(PropElement { prop: prop_tags }) => prop_tags.into(), + }; + let props: Vec<&str> = props.iter().map(String::as_str).collect(); + + let mut member_responses = Vec::new(); + if depth != Depth::Zero { + for member in resource_service.get_members(auth_info).await? { + member_responses.push(member.propfind(&prefix, props.clone()).await?); + } + } + + let resource = resource_service.get_file().await?; + let response = resource.propfind(&prefix, props).await?; + + let mut output = String::new(); + let mut ser = quick_xml::se::Serializer::new(&mut output); + ser.indent(' ', 4); + MultistatusElement { + response, + member_responses, + ns_dav: Namespace::Dav.as_str(), + ns_caldav: Namespace::CalDAV.as_str(), + ns_ical: Namespace::ICal.as_str(), + } + .serialize(ser) + .unwrap(); + + Ok(HttpResponse::MultiStatus() + .content_type(ContentType::xml()) + .body(output)) +} diff --git a/crates/dav/src/resource.rs b/crates/dav/src/resource.rs index 32e61af..d0d353d 100644 --- a/crates/dav/src/resource.rs +++ b/crates/dav/src/resource.rs @@ -1,4 +1,4 @@ -use crate::{error::Error, xml_snippets::TagList}; +use crate::{error::Error, xml::tag_list::TagList}; use actix_web::{http::StatusCode, HttpRequest}; use anyhow::{anyhow, Result}; use async_trait::async_trait; @@ -10,7 +10,7 @@ use strum::VariantNames; #[async_trait(?Send)] pub trait Resource { - type PropType: FromStr + VariantNames + Into<&'static str> + Clone; + type PropType: FromStr + VariantNames + Clone; type PropResponse: Serialize; fn list_dead_props() -> &'static [&'static str] { @@ -82,12 +82,19 @@ impl HandlePropfind for R { prefix: &str, props: Vec<&str>, ) -> Result>, TagList>> { - // TODO: implement propname - let mut props = props.into_iter().unique().collect_vec(); - if props.contains(&"allprops") { + let mut props = props; + if props.contains(&"propname") { if props.len() != 1 { - // allprops MUST be the only queried prop per spec - return Err(anyhow!("allprops MUST be the only queried prop")); + // propname MUST be the only queried prop per spec + return Err(anyhow!("propname MUST be the only queried prop")); + } + // TODO: implement propname + props = R::list_dead_props().into(); + } + if props.contains(&"allprop") { + if props.len() != 1 { + // allprop MUST be the only queried prop per spec + return Err(anyhow!("allprop MUST be the only queried prop")); } props = R::list_dead_props().into(); } @@ -103,17 +110,20 @@ impl HandlePropfind for R { } } - let mut propstats = Vec::new(); - propstats.push(PropstatType::Normal(PropstatElement { + let mut propstats = vec![PropstatType::Normal(PropstatElement { status: format!("HTTP/1.1 {}", StatusCode::OK), prop: PropWrapper { prop: prop_responses, }, - })); + })]; if !invalid_props.is_empty() { propstats.push(PropstatType::NotFound(PropstatElement { status: format!("HTTP/1.1 {}", StatusCode::NOT_FOUND), - prop: TagList(invalid_props.iter().map(|&s| s.to_owned()).collect()), + prop: invalid_props + .into_iter() + .map(|s| s.to_owned()) + .collect_vec() + .into(), })); } Ok(PropstatResponseElement { diff --git a/crates/dav/src/xml_snippets.rs b/crates/dav/src/xml_snippets.rs index 4f3612d..89f19e5 100644 --- a/crates/dav/src/xml_snippets.rs +++ b/crates/dav/src/xml_snippets.rs @@ -1,6 +1,5 @@ use anyhow::Result; use quick_xml::{events::attributes::Attribute, Writer}; -use serde::ser::SerializeMap; use serde::Serialize; #[derive(Serialize)] @@ -16,21 +15,6 @@ impl HrefElement { #[derive(Serialize)] pub struct TextNode(pub Option); -pub struct TagList(pub Vec); - -impl Serialize for TagList { - fn serialize(&self, serializer: S) -> std::prelude::v1::Result - where - S: serde::Serializer, - { - let mut el = serializer.serialize_map(Some(self.0.len()))?; - for tag in &self.0 { - el.serialize_entry(&tag, &())?; - } - el.end() - } -} - pub fn generate_multistatus<'a, F, A>(namespaces: A, closure: F) -> Result where F: FnOnce(&mut Writer<&mut Vec>) -> Result<(), quick_xml::Error>,