From fb6a379f43ff579dbc224fb52180ba3a6d6cde5c Mon Sep 17 00:00:00 2001 From: Quentin Dufour Date: Tue, 19 Mar 2024 17:36:32 +0100 Subject: [PATCH] Working thunderbird autodetect --- aero-dav/src/acldecoder.rs | 68 +++++++++++++++++++++++++++ aero-dav/src/aclencoder.rs | 71 ++++++++++++++++++++++++++++ aero-dav/src/acltypes.rs | 38 ++++++++++++++- aero-dav/src/caltypes.rs | 1 - aero-dav/src/lib.rs | 8 +++- aero-dav/src/realization.rs | 94 ++++++++++++++++++++++++++++++++++++- aero-dav/src/xml.rs | 2 +- aero-proto/src/dav.rs | 59 +++++++++++++---------- 8 files changed, 309 insertions(+), 32 deletions(-) create mode 100644 aero-dav/src/acldecoder.rs create mode 100644 aero-dav/src/aclencoder.rs diff --git a/aero-dav/src/acldecoder.rs b/aero-dav/src/acldecoder.rs new file mode 100644 index 0000000..67dfb0b --- /dev/null +++ b/aero-dav/src/acldecoder.rs @@ -0,0 +1,68 @@ +use super::acltypes::*; +use super::types as dav; +use super::xml::{QRead, Reader, IRead, DAV_URN}; +use super::error::ParsingError; + +impl QRead for Property { + async fn qread(xml: &mut Reader) -> Result { + if xml.maybe_open_start(DAV_URN, "owner").await?.is_some() { + let href = xml.find().await?; + xml.close().await?; + return Ok(Self::Owner(href)) + } + if xml.maybe_open_start(DAV_URN, "current-user-principal").await?.is_some() { + let user = xml.find().await?; + xml.close().await?; + return Ok(Self::CurrentUserPrincipal(user)) + } + if xml.maybe_open_start(DAV_URN, "current-user-privilege-set").await?.is_some() { + xml.close().await?; + return Ok(Self::CurrentUserPrivilegeSet(vec![])) + } + + Err(ParsingError::Recoverable) + } +} + +impl QRead for PropertyRequest { + async fn qread(xml: &mut Reader) -> Result { + if xml.maybe_open(DAV_URN, "owner").await?.is_some() { + xml.close().await?; + return Ok(Self::Owner) + } + + if xml.maybe_open(DAV_URN, "current-user-principal").await?.is_some() { + xml.close().await?; + return Ok(Self::CurrentUserPrincipal) + } + + if xml.maybe_open(DAV_URN, "current-user-privilege-set").await?.is_some() { + xml.close().await?; + return Ok(Self::CurrentUserPrivilegeSet) + } + + Err(ParsingError::Recoverable) + } +} + +impl QRead for ResourceType { + async fn qread(xml: &mut Reader) -> Result { + if xml.maybe_open(DAV_URN, "principal").await?.is_some() { + xml.close().await?; + return Ok(Self::Principal) + } + Err(ParsingError::Recoverable) + } +} + +// ----- +impl QRead for User { + async fn qread(xml: &mut Reader) -> Result { + if xml.maybe_open(DAV_URN, "unauthenticated").await?.is_some() { + xml.close().await?; + return Ok(Self::Unauthenticated) + } + + dav::Href::qread(xml).await.map(Self::Authenticated) + } +} diff --git a/aero-dav/src/aclencoder.rs b/aero-dav/src/aclencoder.rs new file mode 100644 index 0000000..2fa4707 --- /dev/null +++ b/aero-dav/src/aclencoder.rs @@ -0,0 +1,71 @@ +use quick_xml::Error as QError; +use quick_xml::events::Event; + +use super::acltypes::*; +use super::xml::{QWrite, Writer, IWrite}; +use super::error::ParsingError; + +impl QWrite for Property { + async fn qwrite(&self, xml: &mut Writer) -> Result<(), QError> { + match self { + Self::Owner(href) => { + let start = xml.create_dav_element("owner"); + let end = start.to_end(); + xml.q.write_event_async(Event::Start(start.clone())).await?; + href.qwrite(xml).await?; + xml.q.write_event_async(Event::End(end)).await + }, + Self::CurrentUserPrincipal(user) => { + let start = xml.create_dav_element("current-user-principal"); + let end = start.to_end(); + xml.q.write_event_async(Event::Start(start.clone())).await?; + user.qwrite(xml).await?; + xml.q.write_event_async(Event::End(end)).await + }, + Self::CurrentUserPrivilegeSet(_) => { + let empty_tag = xml.create_dav_element("current-user-privilege-set"); + xml.q.write_event_async(Event::Empty(empty_tag)).await + }, + } + } +} + +impl QWrite for PropertyRequest { + async fn qwrite(&self, xml: &mut Writer) -> Result<(), QError> { + let mut atom = async |c| { + let empty_tag = xml.create_dav_element(c); + xml.q.write_event_async(Event::Empty(empty_tag)).await + }; + + match self { + Self::Owner => atom("owner").await, + Self::CurrentUserPrincipal => atom("current-user-principal").await, + Self::CurrentUserPrivilegeSet => atom("current-user-privilege-set").await, + } + } +} + +impl QWrite for ResourceType { + async fn qwrite(&self, xml: &mut Writer) -> Result<(), QError> { + match self { + Self::Principal => { + let empty_tag = xml.create_dav_element("principal"); + xml.q.write_event_async(Event::Empty(empty_tag)).await + } + } + } +} + +// ----- + +impl QWrite for User { + async fn qwrite(&self, xml: &mut Writer) -> Result<(), QError> { + match self { + Self::Unauthenticated => { + let tag = xml.create_dav_element("unauthenticated"); + xml.q.write_event_async(Event::Empty(tag)).await + }, + Self::Authenticated(href) => href.qwrite(xml).await, + } + } +} diff --git a/aero-dav/src/acltypes.rs b/aero-dav/src/acltypes.rs index f356813..d5be413 100644 --- a/aero-dav/src/acltypes.rs +++ b/aero-dav/src/acltypes.rs @@ -1,4 +1,40 @@ -//@FIXME required for a full DAV implementation +use super::types as dav; + +//RFC covered: RFC3744 (ACL core) + RFC5397 (ACL Current Principal Extension) + + +//@FIXME required for a full CalDAV implementation // See section 6. of the CalDAV RFC // It seems mainly required for free-busy that I will not implement now. // It can also be used for discovering main calendar, not sure it is used. +// Note: it is used by Thunderbird + + +#[derive(Debug, PartialEq, Clone)] +pub enum PropertyRequest { + Owner, + CurrentUserPrincipal, + CurrentUserPrivilegeSet, +} + +#[derive(Debug, PartialEq, Clone)] +pub enum Property { + Owner(dav::Href), + CurrentUserPrincipal(User), + CurrentUserPrivilegeSet(Vec), +} + +#[derive(Debug, PartialEq, Clone)] +pub enum ResourceType { + Principal, +} + +/// Not implemented, it's a placeholder +#[derive(Debug, PartialEq, Clone)] +pub struct Privilege(()); + +#[derive(Debug, PartialEq, Clone)] +pub enum User { + Unauthenticated, + Authenticated(dav::Href), +} diff --git a/aero-dav/src/caltypes.rs b/aero-dav/src/caltypes.rs index aa056d4..602498c 100644 --- a/aero-dav/src/caltypes.rs +++ b/aero-dav/src/caltypes.rs @@ -2,7 +2,6 @@ use chrono::{DateTime,Utc}; use super::types as dav; -use super::xml; pub const ICAL_DATETIME_FMT: &str = "%Y%m%dT%H%M%SZ"; diff --git a/aero-dav/src/lib.rs b/aero-dav/src/lib.rs index 0ca8243..009951a 100644 --- a/aero-dav/src/lib.rs +++ b/aero-dav/src/lib.rs @@ -16,8 +16,12 @@ pub mod caltypes; pub mod calencoder; pub mod caldecoder; -// wip -mod acltypes; +// acl (wip) +pub mod acltypes; +pub mod aclencoder; +pub mod acldecoder; + +// versioning (wip) mod versioningtypes; // final type diff --git a/aero-dav/src/realization.rs b/aero-dav/src/realization.rs index 7bec729..bfed4d7 100644 --- a/aero-dav/src/realization.rs +++ b/aero-dav/src/realization.rs @@ -1,5 +1,6 @@ use super::types as dav; use super::caltypes as cal; +use super::acltypes as acl; use super::xml; use super::error; @@ -11,8 +12,8 @@ impl xml::QRead for Disabled { } } impl xml::QWrite for Disabled { - fn qwrite(&self, _xml: &mut xml::Writer) -> impl futures::Future> + Send { - async { unreachable!(); } + async fn qwrite(&self, _xml: &mut xml::Writer) -> Result<(), quick_xml::Error> { + unreachable!() } } @@ -40,3 +41,92 @@ impl dav::Extension for Calendar type ResourceType = cal::ResourceType; } +// ACL +#[derive(Debug, PartialEq, Clone)] +pub struct Acl {} +impl dav::Extension for Acl +{ + type Error = Disabled; + type Property = acl::Property; + type PropertyRequest = acl::PropertyRequest; + type ResourceType = acl::ResourceType; +} + +// All merged +#[derive(Debug, PartialEq, Clone)] +pub struct All {} +impl dav::Extension for All { + type Error = cal::Violation; + type Property = Property; + type PropertyRequest = PropertyRequest; + type ResourceType = ResourceType; +} + +#[derive(Debug, PartialEq, Clone)] +pub enum Property { + Cal(cal::Property), + Acl(acl::Property), +} +impl xml::QRead for Property { + async fn qread(xml: &mut xml::Reader) -> Result { + match cal::Property::qread(xml).await { + Err(error::ParsingError::Recoverable) => (), + otherwise => return otherwise.map(Property::Cal), + } + acl::Property::qread(xml).await.map(Property::Acl) + } +} +impl xml::QWrite for Property { + async fn qwrite(&self, xml: &mut xml::Writer) -> Result<(), quick_xml::Error> { + match self { + Self::Cal(c) => c.qwrite(xml).await, + Self::Acl(a) => a.qwrite(xml).await, + } + } +} + +#[derive(Debug, PartialEq, Clone)] +pub enum PropertyRequest { + Cal(cal::PropertyRequest), + Acl(acl::PropertyRequest), +} +impl xml::QRead for PropertyRequest { + async fn qread(xml: &mut xml::Reader) -> Result { + match cal::PropertyRequest::qread(xml).await { + Err(error::ParsingError::Recoverable) => (), + otherwise => return otherwise.map(PropertyRequest::Cal), + } + acl::PropertyRequest::qread(xml).await.map(PropertyRequest::Acl) + } +} +impl xml::QWrite for PropertyRequest { + async fn qwrite(&self, xml: &mut xml::Writer) -> Result<(), quick_xml::Error> { + match self { + Self::Cal(c) => c.qwrite(xml).await, + Self::Acl(a) => a.qwrite(xml).await, + } + } +} + +#[derive(Debug, PartialEq, Clone)] +pub enum ResourceType { + Cal(cal::ResourceType), + Acl(acl::ResourceType), +} +impl xml::QRead for ResourceType { + async fn qread(xml: &mut xml::Reader) -> Result { + match cal::ResourceType::qread(xml).await { + Err(error::ParsingError::Recoverable) => (), + otherwise => return otherwise.map(ResourceType::Cal), + } + acl::ResourceType::qread(xml).await.map(ResourceType::Acl) + } +} +impl xml::QWrite for ResourceType { + async fn qwrite(&self, xml: &mut xml::Writer) -> Result<(), quick_xml::Error> { + match self { + Self::Cal(c) => c.qwrite(xml).await, + Self::Acl(a) => a.qwrite(xml).await, + } + } +} diff --git a/aero-dav/src/xml.rs b/aero-dav/src/xml.rs index 26f54cc..020ee6c 100644 --- a/aero-dav/src/xml.rs +++ b/aero-dav/src/xml.rs @@ -264,7 +264,7 @@ impl Reader { _ => return Err(ParsingError::Recoverable), }; - //println!("open tag {:?}", evt); + //println!("open start tag {:?}", evt); self.parents.push(evt.clone()); Ok(evt) } diff --git a/aero-proto/src/dav.rs b/aero-proto/src/dav.rs index c8e534e..608aef1 100644 --- a/aero-proto/src/dav.rs +++ b/aero-proto/src/dav.rs @@ -23,7 +23,8 @@ use aero_user::login::ArcLoginProvider; use aero_collections::user::User; use aero_dav::types as dav; use aero_dav::caltypes as cal; -use aero_dav::realization::Calendar; +use aero_dav::acltypes as acl; +use aero_dav::realization::{All, self as all}; use aero_dav::xml as dxml; pub struct Server { @@ -244,7 +245,7 @@ async fn router(user: std::sync::Arc, req: Request) -> Result /// -const ALLPROP: [dav::PropertyRequest; 10] = [ +const ALLPROP: [dav::PropertyRequest; 10] = [ dav::PropertyRequest::CreationDate, dav::PropertyRequest::DisplayName, dav::PropertyRequest::GetContentLanguage, @@ -265,7 +266,7 @@ async fn propfind(user: std::sync::Arc, req: Request, node: Box< // 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)); + let propfind = deserialize::>(req).await.unwrap_or_else(|_| dav::PropFind::::AllProp(None)); tracing::debug!(recv=?propfind, "inferred propfind request"); if matches!(propfind, dav::PropFind::PropName) { @@ -370,19 +371,19 @@ trait DavNode: Send { // node properties fn path(&self, user: &ArcUser) -> String; - fn supported_properties(&self, user: &ArcUser) -> dav::PropName; - fn properties(&self, user: &ArcUser, prop: dav::PropName) -> Vec>; + fn supported_properties(&self, user: &ArcUser) -> dav::PropName; + fn properties(&self, user: &ArcUser, prop: dav::PropName) -> Vec>; // ----- common /// building DAV responses - fn multistatus_name(&self, user: &ArcUser, depth: dav::Depth) -> dav::Multistatus { + fn multistatus_name(&self, user: &ArcUser, depth: dav::Depth) -> dav::Multistatus { let mut names = vec![(self.path(user), self.supported_properties(user))]; if matches!(depth, dav::Depth::One | dav::Depth::Infinity) { names.extend(self.children(user).iter().map(|c| (c.path(user), c.supported_properties(user)))); } - dav::Multistatus:: { + dav::Multistatus:: { responses: names.into_iter().map(|(url, names)| dav::Response { status_or_propstat: dav::StatusOrPropstat::PropStat( dav::Href(url), @@ -401,7 +402,7 @@ trait DavNode: Send { } } - fn multistatus_val(&self, user: &ArcUser, props: dav::PropName, depth: dav::Depth) -> dav::Multistatus { + fn multistatus_val(&self, user: &ArcUser, props: dav::PropName, depth: dav::Depth) -> dav::Multistatus { // Collect properties let mut values = vec![(self.path(user), self.properties(user, props.clone()))]; if matches!(depth, dav::Depth::One | dav::Depth::Infinity) { @@ -426,7 +427,7 @@ trait DavNode: Send { }).collect(); // Build response - dav::Multistatus:: { + dav::Multistatus:: { responses: values.into_iter().map(|(url, propdesc)| dav::Response { status_or_propstat: dav::StatusOrPropstat::PropStat( dav::Href(url), @@ -468,18 +469,23 @@ impl DavNode for RootNode { fn children(&self, user: &ArcUser) -> Vec> { vec![Box::new(HomeNode { })] } - fn supported_properties(&self, user: &ArcUser) -> dav::PropName { + fn supported_properties(&self, user: &ArcUser) -> dav::PropName { dav::PropName(vec![ dav::PropertyRequest::DisplayName, dav::PropertyRequest::ResourceType, dav::PropertyRequest::GetContentType, + dav::PropertyRequest::Extension(all::PropertyRequest::Acl(acl::PropertyRequest::CurrentUserPrincipal)), ]) } - fn properties(&self, _user: &ArcUser, prop: dav::PropName) -> Vec> { + fn properties(&self, user: &ArcUser, prop: dav::PropName) -> Vec> { prop.0.into_iter().map(|n| match n { dav::PropertyRequest::DisplayName => dav::AnyProperty::Value(dav::Property::DisplayName("DAV Root".to_string())), - dav::PropertyRequest::ResourceType => dav::AnyProperty::Value(dav::Property::ResourceType(vec![dav::ResourceType::Collection])), + dav::PropertyRequest::ResourceType => dav::AnyProperty::Value(dav::Property::ResourceType(vec![ + dav::ResourceType::Collection, + ])), dav::PropertyRequest::GetContentType => dav::AnyProperty::Value(dav::Property::GetContentType("httpd/unix-directory".into())), + dav::PropertyRequest::Extension(all::PropertyRequest::Acl(acl::PropertyRequest::CurrentUserPrincipal)) => + dav::AnyProperty::Value(dav::Property::Extension(all::Property::Acl(acl::Property::CurrentUserPrincipal(acl::User::Authenticated(dav::Href(HomeNode{}.path(user))))))), v => dav::AnyProperty::Request(v), }).collect() } @@ -507,21 +513,24 @@ impl DavNode for HomeNode { fn children(&self, user: &ArcUser) -> Vec> { vec![Box::new(CalendarListNode { })] } - fn supported_properties(&self, user: &ArcUser) -> dav::PropName { + fn supported_properties(&self, user: &ArcUser) -> dav::PropName { dav::PropName(vec![ dav::PropertyRequest::DisplayName, dav::PropertyRequest::ResourceType, dav::PropertyRequest::GetContentType, - dav::PropertyRequest::Extension(cal::PropertyRequest::CalendarHomeSet), + dav::PropertyRequest::Extension(all::PropertyRequest::Cal(cal::PropertyRequest::CalendarHomeSet)), ]) } - fn properties(&self, user: &ArcUser, prop: dav::PropName) -> Vec> { + fn properties(&self, user: &ArcUser, prop: dav::PropName) -> Vec> { prop.0.into_iter().map(|n| match n { dav::PropertyRequest::DisplayName => dav::AnyProperty::Value(dav::Property::DisplayName(format!("{} home", user.username))), - dav::PropertyRequest::ResourceType => dav::AnyProperty::Value(dav::Property::ResourceType(vec![dav::ResourceType::Collection])), + dav::PropertyRequest::ResourceType => dav::AnyProperty::Value(dav::Property::ResourceType(vec![ + dav::ResourceType::Collection, + dav::ResourceType::Extension(all::ResourceType::Acl(acl::ResourceType::Principal)), + ])), dav::PropertyRequest::GetContentType => dav::AnyProperty::Value(dav::Property::GetContentType("httpd/unix-directory".into())), - dav::PropertyRequest::Extension(cal::PropertyRequest::CalendarHomeSet) => - dav::AnyProperty::Value(dav::Property::Extension(cal::Property::CalendarHomeSet(dav::Href(CalendarListNode{}.path(user))))), + dav::PropertyRequest::Extension(all::PropertyRequest::Cal(cal::PropertyRequest::CalendarHomeSet)) => + dav::AnyProperty::Value(dav::Property::Extension(all::Property::Cal(cal::Property::CalendarHomeSet(dav::Href(CalendarListNode{}.path(user)))))), v => dav::AnyProperty::Request(v), }).collect() } @@ -550,14 +559,14 @@ impl DavNode for CalendarListNode { fn children(&self, user: &ArcUser) -> Vec> { vec![Box::new(CalendarNode { name: "personal".into() })] } - fn supported_properties(&self, user: &ArcUser) -> dav::PropName { + fn supported_properties(&self, user: &ArcUser) -> dav::PropName { dav::PropName(vec![ dav::PropertyRequest::DisplayName, dav::PropertyRequest::ResourceType, dav::PropertyRequest::GetContentType, ]) } - fn properties(&self, user: &ArcUser, prop: dav::PropName) -> Vec> { + fn properties(&self, user: &ArcUser, prop: dav::PropName) -> Vec> { prop.0.into_iter().map(|n| match n { dav::PropertyRequest::DisplayName => dav::AnyProperty::Value(dav::Property::DisplayName(format!("{} calendars", user.username))), dav::PropertyRequest::ResourceType => dav::AnyProperty::Value(dav::Property::ResourceType(vec![dav::ResourceType::Collection])), @@ -595,19 +604,19 @@ impl DavNode for CalendarNode { fn children(&self, user: &ArcUser) -> Vec> { vec![Box::new(EventNode { calendar: self.name.to_string(), event_file: "something.ics".into() })] } - fn supported_properties(&self, user: &ArcUser) -> dav::PropName { + fn supported_properties(&self, user: &ArcUser) -> dav::PropName { dav::PropName(vec![ dav::PropertyRequest::DisplayName, dav::PropertyRequest::ResourceType, dav::PropertyRequest::GetContentType, ]) } - fn properties(&self, _user: &ArcUser, prop: dav::PropName) -> Vec> { + fn properties(&self, _user: &ArcUser, prop: dav::PropName) -> Vec> { prop.0.into_iter().map(|n| match n { dav::PropertyRequest::DisplayName => dav::AnyProperty::Value(dav::Property::DisplayName(format!("{} calendar", self.name))), dav::PropertyRequest::ResourceType => dav::AnyProperty::Value(dav::Property::ResourceType(vec![ dav::ResourceType::Collection, - dav::ResourceType::Extension(cal::ResourceType::Calendar), + dav::ResourceType::Extension(all::ResourceType::Cal(cal::ResourceType::Calendar)), ])), //dav::PropertyRequest::GetContentType => dav::AnyProperty::Value(dav::Property::GetContentType("httpd/unix-directory".into())), //@FIXME seems wrong but seems to be what Thunderbird expects... @@ -637,13 +646,13 @@ impl DavNode for EventNode { fn children(&self, user: &ArcUser) -> Vec> { vec![] } - fn supported_properties(&self, user: &ArcUser) -> dav::PropName { + fn supported_properties(&self, user: &ArcUser) -> dav::PropName { dav::PropName(vec![ dav::PropertyRequest::DisplayName, dav::PropertyRequest::ResourceType, ]) } - fn properties(&self, _user: &ArcUser, prop: dav::PropName) -> Vec> { + fn properties(&self, _user: &ArcUser, prop: dav::PropName) -> Vec> { prop.0.into_iter().map(|n| match n { dav::PropertyRequest::DisplayName => dav::AnyProperty::Value(dav::Property::DisplayName(format!("{} event", self.event_file))), dav::PropertyRequest::ResourceType => dav::AnyProperty::Value(dav::Property::ResourceType(vec![])),