From 66eac8ec7a420f20fa01bb77ccc25d964a404af3 Mon Sep 17 00:00:00 2001 From: Quentin Dufour Date: Sat, 6 Apr 2024 13:05:57 +0200 Subject: [PATCH] Refactor DAV --- aero-proto/src/dav.rs | 269 +++++++++++++++++++++++------------------- 1 file changed, 146 insertions(+), 123 deletions(-) diff --git a/aero-proto/src/dav.rs b/aero-proto/src/dav.rs index 98767a1..424d4be 100644 --- a/aero-proto/src/dav.rs +++ b/aero-proto/src/dav.rs @@ -240,6 +240,7 @@ async fn router(user: std::sync::Arc, req: Request) -> Result return Ok(Response::builder() @@ -259,8 +260,8 @@ async fn router(user: std::sync::Arc, req: Request) -> Result { todo!(); }, - "PROPFIND" => propfind(user, req, node).await, - "REPORT" => report(user, req, node).await, + "PROPFIND" => response.propfind().await, + "REPORT" => response.report().await, _ => return Ok(Response::builder() .status(501) .body(text_body("HTTP Method not implemented"))?), @@ -282,123 +283,7 @@ const ALLPROP: [dav::PropertyRequest; 10] = [ // ---------- Building objects -async fn propfind(user: std::sync::Arc, req: Request, base_node: Box) -> Result>> { - let depth = depth(&req); - if matches!(depth, dav::Depth::Infinity) { - return Ok(Response::builder() - .status(501) - .body(text_body("Depth: Infinity not implemented"))?) - } - - let status = hyper::StatusCode::from_u16(207)?; - - // A client may choose not to submit a request body. An empty PROPFIND - // request body MUST be treated as if it were an 'allprop' request. - // @FIXME here we handle any invalid data as an allprop, an empty request is thus correctly - // handled, but corrupted requests are also silently handled as allprop. - let propfind = deserialize::>(req).await.unwrap_or_else(|_| dav::PropFind::::AllProp(None)); - tracing::debug!(recv=?propfind, "inferred propfind request"); - - // Collect nodes as PROPFIND is not limited at the targeted node - let mut nodes = vec![]; - if matches!(depth, dav::Depth::One | dav::Depth::Infinity) { - nodes.extend(base_node.children(&user).await); - } - nodes.push(base_node); - - // Expand properties request - let propname = match propfind { - dav::PropFind::PropName => None, - dav::PropFind::AllProp(None) => Some(dav::PropName(ALLPROP.to_vec())), - dav::PropFind::AllProp(Some(dav::Include(mut include))) => { - include.extend_from_slice(&ALLPROP); - Some(dav::PropName(include)) - }, - dav::PropFind::Prop(inner) => Some(inner), - }; - - // Not Found is currently impossible considering the way we designed this function - let not_found = vec![]; - serialize(status, multistatus(&user, nodes, not_found, propname)) -} - - -async fn report(user: std::sync::Arc, req: Request, node: Box) -> Result>> { - let status = hyper::StatusCode::from_u16(207)?; - - let report = match deserialize::>(req).await { - Ok(v) => v, - Err(e) => { - tracing::error!(err=?e, "unable to decode REPORT body"); - return Ok(Response::builder() - .status(400) - .body(text_body("Bad request"))?) - } - }; - - // Multiget is really like a propfind where Depth: 0|1|Infinity is replaced by an arbitrary - // list of URLs - let multiget = match report { - cal::Report::Multiget(m) => m, - _ => return Ok(Response::builder() - .status(501) - .body(text_body("Not implemented"))?), - }; - - // Getting the list of nodes - let (mut ok_node, mut not_found) = (Vec::new(), Vec::new()); - for h in multiget.href.into_iter() { - let maybe_collected_node = match Path::new(h.0.as_str()) { - Ok(Path::Abs(p)) => RootNode{}.fetch(&user, p.as_slice()).await.or(Err(h)), - Ok(Path::Rel(p)) => node.fetch(&user, p.as_slice()).await.or(Err(h)), - Err(_) => Err(h), - }; - - match maybe_collected_node { - Ok(v) => ok_node.push(v), - Err(h) => not_found.push(h), - }; - } - - // Getting props - let props = match multiget.selector { - None | Some(cal::CalendarSelector::AllProp) => Some(dav::PropName(ALLPROP.to_vec())), - Some(cal::CalendarSelector::PropName) => None, - Some(cal::CalendarSelector::Prop(inner)) => Some(inner), - }; - - serialize(status, multistatus(&user, ok_node, not_found, props)) -} - -fn multistatus(user: &ArcUser, nodes: Vec>, not_found: Vec, props: Option>) -> dav::Multistatus { - // Collect properties on existing objects - let mut responses: Vec> = match props { - Some(props) => nodes.into_iter().map(|n| n.response_props(user, props.clone())).collect(), - None => nodes.into_iter().map(|n| n.response_propname(user)).collect(), - }; - - // Register not found objects only if relevant - if !not_found.is_empty() { - responses.push(dav::Response { - status_or_propstat: dav::StatusOrPropstat::Status( - not_found, - dav::Status(hyper::StatusCode::NOT_FOUND), - ), - error: None, - location: None, - responsedescription: None, - }); - } - - // Build response - dav::Multistatus:: { - responses, - responsedescription: None, - } -} - // ---- HTTP DAV Binding - use futures::stream::TryStreamExt; use http_body_util::BodyStream; use http_body_util::StreamBody; @@ -473,20 +358,27 @@ async fn deserialize>(req: Request) -> Result { //--- use futures::{future, future::BoxFuture, future::FutureExt}; -trait DavNode: Send { - // ------- specialized logic - // recurence - // @FIXME not satisfied by BoxFutures but I have no better idea currently +/// A DAV node should implement the following methods +/// @FIXME not satisfied by BoxFutures but I have no better idea currently +trait DavNode: Send { + // recurence, filesystem hierarchy + /// This node direct children fn children<'a>(&self, user: &'a ArcUser) -> BoxFuture<'a, Vec>>; + /// Recursively fetch a child (progress inside the filesystem hierarchy) fn fetch<'a>(&self, user: &'a ArcUser, path: &'a [&str]) -> BoxFuture<'a, Result>>; // node properties + /// Get the path fn path(&self, user: &ArcUser) -> String; + /// Get the supported WebDAV properties fn supported_properties(&self, user: &ArcUser) -> dav::PropName; + /// Get the values for the given properties fn properties(&self, user: &ArcUser, prop: dav::PropName) -> Vec>; - // --- shared + //@FIXME maybe add etag, maybe add a way to set content + + /// Utility function to get a propname response from a node fn response_propname(&self, user: &ArcUser) -> dav::Response { dav::Response { status_or_propstat: dav::StatusOrPropstat::PropStat( @@ -506,6 +398,7 @@ trait DavNode: Send { } } + /// Utility function to get a prop response from a node & a list of propname fn response_props(&self, user: &ArcUser, props: dav::PropName) -> dav::Response { let mut prop_desc = vec![]; let (found, not_found): (Vec<_>, Vec<_>) = self.properties(user, props).into_iter().partition(|v| matches!(v, dav::AnyProperty::Value(_))); @@ -542,6 +435,136 @@ trait DavNode: Send { } } +struct DavResponse { + node: Box, + user: std::sync::Arc, + req: Request, +} +impl DavResponse { + // --- Public API --- + + /// REPORT has been first described in the "Versioning Extension" of WebDAV + /// It allows more complex queries compared to PROPFIND + /// + /// Note: current implementation is not generic at all, it is heavily tied to CalDAV. + /// A rewrite would be required to make it more generic (with the extension system that has + /// been introduced in aero-dav) + async fn report(self) -> Result>> { + let status = hyper::StatusCode::from_u16(207)?; + + let report = match deserialize::>(self.req).await { + Ok(v) => v, + Err(e) => { + tracing::error!(err=?e, "unable to decode REPORT body"); + return Ok(Response::builder() + .status(400) + .body(text_body("Bad request"))?) + } + }; + + // Multiget is really like a propfind where Depth: 0|1|Infinity is replaced by an arbitrary + // list of URLs + let multiget = match report { + cal::Report::Multiget(m) => m, + _ => return Ok(Response::builder() + .status(501) + .body(text_body("Not implemented"))?), + }; + + // Getting the list of nodes + let (mut ok_node, mut not_found) = (Vec::new(), Vec::new()); + for h in multiget.href.into_iter() { + let maybe_collected_node = match Path::new(h.0.as_str()) { + Ok(Path::Abs(p)) => RootNode{}.fetch(&self.user, p.as_slice()).await.or(Err(h)), + Ok(Path::Rel(p)) => self.node.fetch(&self.user, p.as_slice()).await.or(Err(h)), + Err(_) => Err(h), + }; + + match maybe_collected_node { + Ok(v) => ok_node.push(v), + Err(h) => not_found.push(h), + }; + } + + // Getting props + let props = match multiget.selector { + None | Some(cal::CalendarSelector::AllProp) => Some(dav::PropName(ALLPROP.to_vec())), + Some(cal::CalendarSelector::PropName) => None, + Some(cal::CalendarSelector::Prop(inner)) => Some(inner), + }; + + serialize(status, Self::multistatus(&self.user, ok_node, not_found, props)) + } + + /// PROPFIND is the standard way to fetch WebDAV properties + async fn propfind(self) -> Result>> { + let depth = depth(&self.req); + if matches!(depth, dav::Depth::Infinity) { + return Ok(Response::builder() + .status(501) + .body(text_body("Depth: Infinity not implemented"))?) + } + + let status = hyper::StatusCode::from_u16(207)?; + + // A client may choose not to submit a request body. An empty PROPFIND + // request body MUST be treated as if it were an 'allprop' request. + // @FIXME here we handle any invalid data as an allprop, an empty request is thus correctly + // handled, but corrupted requests are also silently handled as allprop. + let propfind = deserialize::>(self.req).await.unwrap_or_else(|_| dav::PropFind::::AllProp(None)); + tracing::debug!(recv=?propfind, "inferred propfind request"); + + // Collect nodes as PROPFIND is not limited to the targeted node + let mut nodes = vec![]; + if matches!(depth, dav::Depth::One | dav::Depth::Infinity) { + nodes.extend(self.node.children(&self.user).await); + } + nodes.push(self.node); + + // Expand properties request + let propname = match propfind { + dav::PropFind::PropName => None, + dav::PropFind::AllProp(None) => Some(dav::PropName(ALLPROP.to_vec())), + dav::PropFind::AllProp(Some(dav::Include(mut include))) => { + include.extend_from_slice(&ALLPROP); + Some(dav::PropName(include)) + }, + dav::PropFind::Prop(inner) => Some(inner), + }; + + // Not Found is currently impossible considering the way we designed this function + let not_found = vec![]; + serialize(status, Self::multistatus(&self.user, nodes, not_found, propname)) + } + + // --- Internal functions --- + /// Utility function to build a multistatus response from + /// a list of DavNodes + fn multistatus(user: &ArcUser, nodes: Vec>, not_found: Vec, props: Option>) -> dav::Multistatus { + // Collect properties on existing objects + let mut responses: Vec> = match props { + Some(props) => nodes.into_iter().map(|n| n.response_props(user, props.clone())).collect(), + None => nodes.into_iter().map(|n| n.response_propname(user)).collect(), + }; + + // Register not found objects only if relevant + if !not_found.is_empty() { + responses.push(dav::Response { + status_or_propstat: dav::StatusOrPropstat::Status(not_found, dav::Status(hyper::StatusCode::NOT_FOUND)), + error: None, + location: None, + responsedescription: None, + }); + } + + // Build response + dav::Multistatus:: { + responses, + responsedescription: None, + } + } +} + #[derive(Clone)] struct RootNode {} impl DavNode for RootNode {