From 31c7dcbd7da4da03ffb7a44133a6020acfcfdb77 Mon Sep 17 00:00:00 2001 From: Lennart <18233294+lennart-k@users.noreply.github.com> Date: Wed, 13 Sep 2023 12:46:48 +0200 Subject: [PATCH] dav: Migrate WebDav routes into a resource formalism --- crates/dav/Cargo.toml | 1 + crates/dav/src/lib.rs | 152 ++++++++++---- crates/dav/src/propfind.rs | 22 +- crates/dav/src/proptypes.rs | 39 ++++ crates/dav/src/resource.rs | 70 +++++++ crates/dav/src/resources/calendar.rs | 175 ++++++++++++++++ crates/dav/src/resources/event.rs | 46 +++++ crates/dav/src/resources/mod.rs | 4 + crates/dav/src/resources/principal.rs | 114 +++++++++++ crates/dav/src/resources/root.rs | 68 +++++++ crates/dav/src/routes/calendar.rs | 281 +++++++++----------------- crates/dav/src/routes/mod.rs | 2 - crates/dav/src/routes/principal.rs | 146 ------------- crates/dav/src/routes/root.rs | 88 -------- 14 files changed, 741 insertions(+), 467 deletions(-) create mode 100644 crates/dav/src/proptypes.rs create mode 100644 crates/dav/src/resource.rs create mode 100644 crates/dav/src/resources/calendar.rs create mode 100644 crates/dav/src/resources/event.rs create mode 100644 crates/dav/src/resources/mod.rs create mode 100644 crates/dav/src/resources/principal.rs create mode 100644 crates/dav/src/resources/root.rs delete mode 100644 crates/dav/src/routes/principal.rs delete mode 100644 crates/dav/src/routes/root.rs diff --git a/crates/dav/Cargo.toml b/crates/dav/Cargo.toml index 3001ad2..41f5056 100644 --- a/crates/dav/Cargo.toml +++ b/crates/dav/Cargo.toml @@ -21,3 +21,4 @@ rustical_auth = { path = "../auth/" } serde = { version = "1.0.188", features = ["serde_derive", "derive"] } serde_json = "1.0.105" tokio = { version = "1.32.0", features = ["sync", "full"] } +async-trait = "0.1.73" diff --git a/crates/dav/src/lib.rs b/crates/dav/src/lib.rs index 7a5eb37..90cb993 100644 --- a/crates/dav/src/lib.rs +++ b/crates/dav/src/lib.rs @@ -1,9 +1,19 @@ +use actix_web::http::header::ContentType; use actix_web::http::Method; -use actix_web::web::{self, Data}; -use actix_web::{guard, HttpResponse, Responder}; +use actix_web::web::{self, Data, Path}; +use actix_web::{guard, HttpRequest, HttpResponse, Responder}; +use depth_extractor::Depth; use error::Error; -use routes::{calendar, event, principal, root}; -use rustical_auth::CheckAuthentication; +use namespace::Namespace; +use propfind::{generate_multistatus, parse_propfind}; +use quick_xml::events::BytesText; +use resource::{HandlePropfind, Resource}; +use resources::calendar::CalendarResource; +use resources::event::EventResource; +use resources::principal::PrincipalCalendarsResource; +use resources::root::RootResource; +use routes::{calendar, event}; +use rustical_auth::{AuthInfoExtractor, CheckAuthentication}; use rustical_store::calendar::CalendarStore; use std::str::FromStr; use std::sync::Arc; @@ -13,6 +23,9 @@ pub mod depth_extractor; pub mod error; pub mod namespace; mod propfind; +pub mod proptypes; +pub mod resource; +pub mod resources; pub mod routes; pub struct CalDavContext { @@ -32,44 +45,115 @@ pub fn configure_dav( ) { let propfind_method = || Method::from_str("PROPFIND").unwrap(); let report_method = || Method::from_str("REPORT").unwrap(); + let mkcol_method = || Method::from_str("MKCOL").unwrap(); + cfg.app_data(Data::new(CalDavContext { + prefix, + store: store.clone(), + })) + .app_data(Data::from(store.clone())) + .app_data(Data::from(auth)) + .service( + web::resource("{path:.*}") + // Without the guard this service would handle all requests + .guard(guard::Method(Method::OPTIONS)) + .to(options_handler), + ) + .service( + web::resource("").route(web::method(propfind_method()).to(route_new_propfind::< + A, + RootResource, + C, + >)), + ) + .service( + web::resource("/{principal}").route( + web::method(propfind_method()).to(route_new_propfind::< + A, + PrincipalCalendarsResource, + C, + >), + ), + ) + .service( + web::resource("/{principal}/{calendar}") + .route(web::method(report_method()).to(calendar::route_report_calendar::)) + .route( + web::method(propfind_method()).to(route_new_propfind::, C>), + ) + .route(web::method(mkcol_method()).to(calendar::route_mkcol_calendar::)), + ) + .service( + web::resource("/{principal}/{calendar}/{event}") + .route(web::method(propfind_method()).to(route_new_propfind::)) + .route(web::method(Method::DELETE).to(event::delete_event::)) + .route(web::method(Method::GET).to(event::get_event::)) + .route(web::method(Method::PUT).to(event::put_event::)), + ); +} - cfg.app_data(Data::new(CalDavContext { prefix, store })) - .app_data(Data::from(auth)) - .service( - web::resource("{path:.*}") - // Without the guard this service would handle all requests - .guard(guard::Method(Method::OPTIONS)) - .to(options_handler), - ) - .service( - web::resource("") - .route(web::method(propfind_method()).to(root::route_propfind_root::)), - ) - .service( - web::resource("/{principal}").route( - web::method(propfind_method()).to(principal::route_propfind_principal::), - ), - ) - .service( - web::resource("/{principal}/{calendar}") - .route(web::method(report_method()).to(calendar::route_report_calendar::)) - .route(web::method(propfind_method()).to(calendar::route_propfind_calendar::)) - .route(web::method(mkcol_method()).to(calendar::route_mkcol_calendar::)), - ) - .service( - web::resource("/{principal}/{calendar}/{event}") - .route(web::method(Method::DELETE).to(event::delete_event::)) - .route(web::method(Method::GET).to(event::get_event::)) - .route(web::method(Method::PUT).to(event::put_event::)), - ); +async fn route_new_propfind( + path: Path, + body: String, + req: HttpRequest, + context: Data>, + auth: AuthInfoExtractor, + depth: Depth, +) -> Result { + let props = parse_propfind(&body).map_err(|_e| Error::BadRequest)?; + let req_path = req.path().to_string(); + let auth_info = auth.inner; + + let resource = R::acquire_from_request( + req, + auth_info, + path.into_inner(), + context.prefix.to_string(), + ) + .await + .map_err(|_e| Error::InternalError)?; + + let mut responses = vec![resource + .propfind(props.clone()) + .map_err(|_e| Error::InternalError)?]; + + if depth != Depth::Zero { + for member in resource + .get_members() + .await + .map_err(|_e| Error::InternalError)? + { + responses.push( + member + .propfind(props.clone()) + .map_err(|_e| Error::InternalError)?, + ); + } + } + + let output = generate_multistatus( + vec![Namespace::Dav, Namespace::CalDAV, Namespace::ICal], + |writer| { + for response in responses { + writer.write_event(quick_xml::events::Event::Text(BytesText::from_escaped( + response, + )))?; + } + Ok(()) + }, + ) + .map_err(|_e| Error::InternalError)?; + + Ok(HttpResponse::MultiStatus() + .content_type(ContentType::xml()) + .body(output)) } async fn options_handler() -> impl Responder { HttpResponse::Ok() .insert_header(( "Allow", - "OPTIONS, GET, HEAD, POST, PUT, REPORT, PROPFIND, PROPPATCH", + "OPTIONS, GET, HEAD, POST, PUT, REPORT, PROPFIND, PROPPATCH, MKCOL", )) .insert_header(( "DAV", diff --git a/crates/dav/src/propfind.rs b/crates/dav/src/propfind.rs index 164a4a1..ee5700c 100644 --- a/crates/dav/src/propfind.rs +++ b/crates/dav/src/propfind.rs @@ -1,3 +1,5 @@ +use std::io::Write; + use actix_web::http::StatusCode; use anyhow::{anyhow, Result}; use quick_xml::{ @@ -23,17 +25,19 @@ pub fn parse_propfind(body: &str) -> Result> { return Ok(Vec::new()); }; - match prop_node.tag_name().name() { + let props = match prop_node.tag_name().name() { "prop" => Ok(prop_node .children() .map(|node| node.tag_name().name()) .collect()), _ => Err(anyhow!("invalid prop tag")), - } + }; + dbg!(body, &props); + props } -pub fn write_resourcetype( - writer: &mut Writer<&mut Vec>, +pub fn write_resourcetype( + writer: &mut Writer, types: Vec<&str>, ) -> Result<(), quick_xml::Error> { writer @@ -47,8 +51,8 @@ pub fn write_resourcetype( Ok(()) } -pub fn write_invalid_props_response( - writer: &mut Writer<&mut Vec>, +pub fn write_invalid_props_response( + writer: &mut Writer, href: &str, invalid_props: Vec<&str>, ) -> Result<(), quick_xml::Error> { @@ -68,14 +72,14 @@ pub fn write_invalid_props_response( // Writes a propstat response into a multistatus // closure hooks into the element -pub fn write_propstat_response( - writer: &mut Writer<&mut Vec>, +pub fn write_propstat_response( + writer: &mut Writer, href: &str, status: StatusCode, prop_closure: F, ) -> Result<(), quick_xml::Error> where - F: FnOnce(&mut Writer<&mut Vec>) -> Result<(), quick_xml::Error>, + F: FnOnce(&mut Writer) -> Result<(), quick_xml::Error>, { writer .create_element("response") diff --git a/crates/dav/src/proptypes.rs b/crates/dav/src/proptypes.rs new file mode 100644 index 0000000..8757796 --- /dev/null +++ b/crates/dav/src/proptypes.rs @@ -0,0 +1,39 @@ +use std::io::Write; + +use quick_xml::{events::BytesText, Writer}; + +pub fn write_string_prop<'a, W: Write>( + writer: &'a mut Writer, + propname: &'a str, + value: &str, +) -> Result<&'a mut Writer, quick_xml::Error> { + let el = writer.create_element(propname); + if value.is_empty() { + el.write_empty() + } else { + el.write_text_content(BytesText::new(value)) + } +} + +pub fn write_href_prop<'a, W: Write>( + writer: &'a mut Writer, + propname: &'a str, + href: &str, +) -> Result<&'a mut Writer, quick_xml::Error> { + write_hrefs_prop(writer, propname, vec![href]) +} + +pub fn write_hrefs_prop<'a, W: Write>( + writer: &'a mut Writer, + propname: &'a str, + hrefs: Vec<&str>, +) -> Result<&'a mut Writer, quick_xml::Error> { + writer + .create_element(propname) + .write_inner_content(|writer| { + for href in hrefs { + write_string_prop(writer, "href", href)?; + } + Ok(()) + }) +} diff --git a/crates/dav/src/resource.rs b/crates/dav/src/resource.rs new file mode 100644 index 0000000..223b888 --- /dev/null +++ b/crates/dav/src/resource.rs @@ -0,0 +1,70 @@ +use std::io::Write; + +use actix_web::{http::StatusCode, HttpRequest}; +use anyhow::{anyhow, Result}; +use async_trait::async_trait; +use quick_xml::Writer; +use rustical_auth::AuthInfo; + +use crate::propfind::{write_invalid_props_response, write_propstat_response}; + +// A resource is identified by a URI and has properties +// A resource can also be a collection +// A resource cannot be none, only Methods like PROPFIND, GET, REPORT, etc. can be exposed +// A resource exists +#[async_trait(?Send)] +pub trait Resource: Sized { + type MemberType: Resource; + type UriComponents: Sized; // defines how the resource URI maps to parameters, i.e. /{principal}/{calendar} -> (String, String) + + async fn acquire_from_request( + req: HttpRequest, + auth_info: AuthInfo, + uri_components: Self::UriComponents, + prefix: String, + ) -> Result; + + fn get_path(&self) -> &str; + async fn get_members(&self) -> Result>; + + fn list_dead_props() -> Vec<&'static str>; + fn write_prop(&self, writer: &mut Writer, prop: &str) -> Result<()>; +} + +pub trait HandlePropfind { + fn propfind(&self, props: Vec<&str>) -> Result; +} + +impl HandlePropfind for R { + fn propfind(&self, props: Vec<&str>) -> Result { + let mut props = props; + if props.contains(&"allprops") { + if props.len() != 1 { + // allprops MUST be the only queried prop per spec + return Err(anyhow!("allprops MUST be the only quereid prop")); + } + props = R::list_dead_props(); + } + + let mut invalid_props = Vec::<&str>::new(); + + let mut output_buffer = Vec::new(); + let mut writer = Writer::new_with_indent(&mut output_buffer, b' ', 2); + + write_propstat_response(&mut writer, self.get_path(), StatusCode::OK, |writer| { + for prop in props { + // TODO: Fix error types + match self + .write_prop(writer, prop) + .map_err(|_e| quick_xml::Error::TextNotFound) + { + Ok(_) => {} + Err(_) => invalid_props.push(prop), + }; + } + Ok(()) + })?; + write_invalid_props_response(&mut writer, self.get_path(), invalid_props)?; + Ok(std::str::from_utf8(&output_buffer)?.to_string()) + } +} diff --git a/crates/dav/src/resources/calendar.rs b/crates/dav/src/resources/calendar.rs new file mode 100644 index 0000000..fe0a54c --- /dev/null +++ b/crates/dav/src/resources/calendar.rs @@ -0,0 +1,175 @@ +use std::{io::Write, sync::Arc}; + +use actix_web::{web::Data, HttpRequest}; +use anyhow::{anyhow, Result}; +use async_trait::async_trait; +use quick_xml::{events::BytesText, Writer}; +use rustical_auth::AuthInfo; +use rustical_store::calendar::{Calendar, CalendarStore}; +use tokio::sync::RwLock; + +use crate::{ + propfind::write_resourcetype, + proptypes::{write_href_prop, write_string_prop}, + resource::Resource, +}; + +pub struct CalendarResource { + pub cal_store: Arc>, + pub calendar: Calendar, + pub path: String, + pub prefix: String, + pub principal: String, +} + +#[async_trait(?Send)] +impl Resource for CalendarResource { + type MemberType = Self; + type UriComponents = (String, String); // principal, calendar_id + + async fn acquire_from_request( + req: HttpRequest, + _auth_info: AuthInfo, + uri_components: Self::UriComponents, + prefix: String, + ) -> Result { + let cal_store = req + .app_data::>>() + .ok_or(anyhow!("no calendar store in app_data!"))? + .clone() + .into_inner(); + + let (principal, cid) = uri_components; + let calendar = cal_store.read().await.get_calendar(&cid).await?; + Ok(Self { + cal_store, + calendar, + path: req.path().to_string(), + prefix, + principal, + }) + } + + fn get_path(&self) -> &str { + &self.path + } + + async fn get_members(&self) -> Result> { + // As of now the calendar resource has no members + Ok(vec![]) + } + + #[inline] + fn list_dead_props() -> Vec<&'static str> { + vec![ + "resourcetype", + "current-user-principal", + "displayname", + "supported-calendar-component-set", + "supported-calendar-data", + "getcontenttype", + "calendar-description", + "owner", + "calendar-color", + "current-user-privilege-set", + "max-resource-size", + ] + } + fn write_prop(&self, writer: &mut Writer, prop: &str) -> Result<()> { + match prop { + "resourcetype" => write_resourcetype(writer, vec!["C:calendar", "collection"])?, + "current-user-principal" | "owner" => { + write_href_prop( + writer, + prop, + &format!("{}/{}/", self.prefix, self.principal), + )?; + } + "displayname" => { + let el = writer.create_element("displayname"); + if let Some(name) = self.calendar.clone().name { + el.write_text_content(BytesText::new(&name))?; + } else { + el.write_empty()?; + } + } + "calendar-color" => { + let el = writer.create_element("IC:calendar-color"); + if let Some(color) = self.calendar.clone().color { + el.write_text_content(BytesText::new(&color))?; + } else { + el.write_empty()?; + } + } + "calendar-description" => { + let el = writer.create_element("C:calendar-description"); + if let Some(description) = self.calendar.clone().description { + el.write_text_content(BytesText::new(&description))?; + } else { + el.write_empty()?; + } + } + "supported-calendar-component-set" => { + writer + .create_element("C:supported-calendar-component-set") + .write_inner_content(|writer| { + writer + .create_element("C:comp") + .with_attribute(("name", "VEVENT")) + .write_empty()?; + Ok(()) + })?; + } + "supported-calendar-data" => { + writer + .create_element("C:supported-calendar-data") + .write_inner_content(|writer| { + // + writer + .create_element("C:calendar-data") + .with_attributes(vec![ + ("content-type", "text/calendar"), + ("version", "2.0"), + ]) + .write_empty()?; + Ok(()) + })?; + } + "getcontenttype" => { + write_string_prop(writer, "getcontenttype", "text/calendar")?; + } + "max-resource-size" => { + write_string_prop(writer, "max-resource-size", "10000000")?; + } + "current-user-privilege-set" => { + writer + .create_element("current-user-privilege-set") + // These are just hard-coded for now and will possibly change in the future + .write_inner_content(|writer| { + for privilege in [ + "read", + "read-acl", + "write", + "write-acl", + "write-content", + "read-current-user-privilege-set", + "bind", + "unbind", + ] { + writer + .create_element("privilege") + .write_inner_content(|writer| { + writer.create_element(privilege).write_empty()?; + Ok(()) + })?; + } + Ok(()) + })?; + } + _ => { + return Err(anyhow!("invalid prop")); + } + }; + Ok(()) + } +} diff --git a/crates/dav/src/resources/event.rs b/crates/dav/src/resources/event.rs new file mode 100644 index 0000000..dd2201e --- /dev/null +++ b/crates/dav/src/resources/event.rs @@ -0,0 +1,46 @@ +use crate::resource::Resource; +use actix_web::HttpRequest; +use anyhow::{anyhow, Result}; +use async_trait::async_trait; +use rustical_auth::AuthInfo; + +pub struct EventResource { + path: String, +} + +#[async_trait(?Send)] +impl Resource for EventResource { + type UriComponents = (String, String, String); // principal, calendar, event + type MemberType = Self; + + fn get_path(&self) -> &str { + &self.path + } + + async fn get_members(&self) -> Result> { + Ok(vec![]) + } + + async fn acquire_from_request( + req: HttpRequest, + _auth_info: AuthInfo, + _uri_components: Self::UriComponents, + _prefix: String, + ) -> Result { + Ok(Self { + path: req.path().to_string(), + }) + } + + fn write_prop( + &self, + _writer: &mut quick_xml::Writer, + _prop: &str, + ) -> Result<()> { + Err(anyhow!("invalid prop!")) + } + + fn list_dead_props() -> Vec<&'static str> { + vec![] + } +} diff --git a/crates/dav/src/resources/mod.rs b/crates/dav/src/resources/mod.rs new file mode 100644 index 0000000..563b948 --- /dev/null +++ b/crates/dav/src/resources/mod.rs @@ -0,0 +1,4 @@ +pub mod calendar; +pub mod event; +pub mod principal; +pub mod root; diff --git a/crates/dav/src/resources/principal.rs b/crates/dav/src/resources/principal.rs new file mode 100644 index 0000000..71c3a6c --- /dev/null +++ b/crates/dav/src/resources/principal.rs @@ -0,0 +1,114 @@ +use std::sync::Arc; + +use crate::{propfind::write_resourcetype, proptypes::write_href_prop, resource::Resource}; +use actix_web::{web::Data, HttpRequest}; +use anyhow::{anyhow, Result}; +use async_trait::async_trait; +use quick_xml::events::BytesText; +use rustical_auth::AuthInfo; +use rustical_store::calendar::CalendarStore; +use tokio::sync::RwLock; + +use super::calendar::CalendarResource; + +pub struct PrincipalCalendarsResource { + prefix: String, + principal: String, + path: String, + cal_store: Arc>, +} + +#[async_trait(?Send)] +impl Resource for PrincipalCalendarsResource { + type UriComponents = (); + type MemberType = CalendarResource; + + fn get_path(&self) -> &str { + &self.path + } + + async fn get_members(&self) -> Result> { + let calendars = self + .cal_store + .read() + .await + .get_calendars(&self.principal) + .await?; + let mut out = Vec::new(); + for calendar in calendars { + let path = format!("{}/{}", &self.path, &calendar.id); + out.push(CalendarResource { + cal_store: self.cal_store.clone(), + calendar, + path, + prefix: self.prefix.clone(), + principal: self.principal.clone(), + }) + } + Ok(out) + } + + async fn acquire_from_request( + req: HttpRequest, + auth_info: AuthInfo, + _uri_components: Self::UriComponents, + prefix: String, + ) -> Result { + let cal_store = req + .app_data::>>() + .ok_or(anyhow!("no calendar store in app_data!"))? + .clone() + .into_inner(); + Ok(Self { + cal_store, + prefix, + principal: auth_info.user_id, + path: req.path().to_string(), + }) + } + + fn write_prop( + &self, + writer: &mut quick_xml::Writer, + prop: &str, + ) -> Result<()> { + match prop { + "resourcetype" => write_resourcetype(writer, vec!["principal", "collection"])?, + "current-user-principal" | "principal-URL" => { + write_href_prop( + writer, + prop, + &format!("{}/{}/", self.prefix, self.principal), + )?; + } + "calendar-home-set" | "calendar-user-address-set" => { + writer + .create_element(&format!("C:{prop}")) + .write_inner_content(|writer| { + writer + .create_element("href") + .write_text_content(BytesText::new(&format!( + "{}/{}/", + self.prefix, self.principal + )))?; + Ok(()) + })?; + } + "allprops" => {} + _ => { + return Err(anyhow!("invalid prop")); + } + }; + Ok(()) + } + + fn list_dead_props() -> Vec<&'static str> { + vec![ + "resourcetype", + "current-user-principal", + "principal-URL", + "calendar-home-set", + "calendar-user-address-set", + ] + } +} diff --git a/crates/dav/src/resources/root.rs b/crates/dav/src/resources/root.rs new file mode 100644 index 0000000..a7878b2 --- /dev/null +++ b/crates/dav/src/resources/root.rs @@ -0,0 +1,68 @@ +use crate::{propfind::write_resourcetype, resource::Resource}; +use actix_web::HttpRequest; +use anyhow::{anyhow, Result}; +use async_trait::async_trait; +use quick_xml::events::BytesText; +use rustical_auth::AuthInfo; + +pub struct RootResource { + prefix: String, + principal: String, + path: String, +} + +#[async_trait(?Send)] +impl Resource for RootResource { + type UriComponents = (); + type MemberType = Self; + + fn get_path(&self) -> &str { + &self.path + } + + async fn get_members(&self) -> Result> { + Ok(vec![]) + } + + async fn acquire_from_request( + req: HttpRequest, + auth_info: AuthInfo, + _uri_components: Self::UriComponents, + prefix: String, + ) -> Result { + Ok(Self { + prefix, + principal: auth_info.user_id, + path: req.path().to_string(), + }) + } + + fn write_prop( + &self, + writer: &mut quick_xml::Writer, + prop: &str, + ) -> Result<()> { + match prop { + "resourcetype" => write_resourcetype(writer, vec!["collection"])?, + "current-user-principal" => { + writer + .create_element("current-user-principal") + .write_inner_content(|writer| { + writer + .create_element("href") + .write_text_content(BytesText::new(&format!( + "{}/{}", + self.prefix, self.principal + )))?; + Ok(()) + })?; + } + _ => return Err(anyhow!("invalid prop!")), + }; + Ok(()) + } + + fn list_dead_props() -> Vec<&'static str> { + vec!["resourcetype", "current-user-principal"] + } +} diff --git a/crates/dav/src/routes/calendar.rs b/crates/dav/src/routes/calendar.rs index 0d97c75..de65a36 100644 --- a/crates/dav/src/routes/calendar.rs +++ b/crates/dav/src/routes/calendar.rs @@ -1,22 +1,38 @@ -use rustical_auth::{AuthInfoExtractor, CheckAuthentication}; use crate::namespace::Namespace; -use crate::propfind::{ - generate_multistatus, parse_propfind, write_invalid_props_response, write_propstat_response, - write_resourcetype, -}; +use crate::propfind::{generate_multistatus, write_propstat_response}; +use crate::proptypes::write_string_prop; use crate::{CalDavContext, Error}; use actix_web::http::header::ContentType; use actix_web::http::StatusCode; use actix_web::web::{Data, Path}; use actix_web::{HttpRequest, HttpResponse}; use anyhow::Result; -use quick_xml::events::BytesText; -use quick_xml::Writer; use roxmltree::{Node, NodeType}; +use rustical_auth::{AuthInfoExtractor, CheckAuthentication}; use rustical_store::calendar::{Calendar, CalendarStore, Event}; use std::collections::HashSet; use std::ops::Deref; use std::sync::Arc; +use tokio::sync::RwLock; + +async fn _parse_filter(filter_node: &Node<'_, '_>) { + for comp_filter_node in filter_node.children() { + if comp_filter_node.tag_name().name() != "comp-filter" { + dbg!("wtf", comp_filter_node.tag_name().name()); + continue; + } + + for filter in filter_node.children() { + match filter.tag_name().name() { + // {} + _ => { + dbg!("unknown filter", filter.tag_name()); + } + } + } + } +} async fn handle_report_calendar_query( query_node: Node<'_, '_>, @@ -44,19 +60,17 @@ async fn handle_report_calendar_query( for prop in props.deref() { match *prop { "getetag" => { - writer - .create_element("getetag") - .write_text_content(BytesText::new(&event.get_etag()))?; + write_string_prop(writer, "getetag", &event.get_etag())?; } "calendar-data" => { - writer - .create_element("C:calendar-data") - .write_text_content(BytesText::new(event.to_ics()))?; + write_string_prop(writer, "C:calendar-data", event.to_ics())?; } "getcontenttype" => { - writer - .create_element("getcontenttype") - .write_text_content(BytesText::new("text/calendar"))?; + write_string_prop( + writer, + "getcontenttype", + "text/calendar;charset=utf-8", + )?; } prop => { dbg!(prop); @@ -86,10 +100,12 @@ pub async fn route_report_calendar( // TODO: Check authorization let (_principal, cid) = path.into_inner(); - let doc = roxmltree::Document::parse(&body).map_err(|_e| Error::InternalError)?; + let doc = roxmltree::Document::parse(&body).map_err(|_e| Error::BadRequest)?; let query_node = doc.root_element(); let events = context.store.read().await.get_events(&cid).await.unwrap(); + dbg!(&body); + // TODO: implement filtering match query_node.tag_name().name() { "calendar-query" => {} @@ -99,187 +115,76 @@ pub async fn route_report_calendar( handle_report_calendar_query(query_node, request, events).await } -pub fn generate_propfind_calendar_response( - props: Vec<&str>, - principal: &str, - path: &str, - prefix: &str, - calendar: &Calendar, -) -> Result { - let mut props = props; - if props.contains(&"allprops") { - props.extend( - [ - "resourcetype", - "current-user-principal", - "displayname", - "supported-calendar-component-set", - "supported-calendar-data", - "getcontenttype", - "calendar-description", - "owner", - "calendar-color", - "current-user-privilege-set", - "max-resource-size", - ] - .iter(), - ); +pub async fn handle_mkcol_calendar_set( + store: &RwLock, + prop_node: Node<'_, '_>, + cid: String, + owner: String, +) -> Result<()> { + let mut cal = Calendar { + owner, + id: cid.clone(), + ..Default::default() + }; + for prop in prop_node.children() { + match prop.tag_name().name() { + "displayname" => { + cal.name = prop.text().map(|s| s.to_string()); + } + "timezone" => { + cal.timezone = prop.text().map(|s| s.to_string()); + } + "calendar-color" => { + cal.color = prop.text().map(|s| s.to_string()); + } + "calendar-description" => { + cal.description = prop.text().map(|s| s.to_string()); + } + "calendar-timezone" => { + cal.timezone = prop.text().map(|s| s.to_string()); + } + _ => { + println!("unsupported mkcol tag: {}", prop.tag_name().name()) + } + } } - let mut invalid_props = Vec::<&str>::new(); - let mut output_buffer = Vec::new(); - let mut writer = Writer::new_with_indent(&mut output_buffer, b' ', 2); - - write_propstat_response(&mut writer, path, StatusCode::OK, |writer| { - for prop in props { - match prop { - "resourcetype" => write_resourcetype(writer, vec!["C:calendar", "collection"])?, - "current-user-principal" | "owner" => { - writer.create_element(prop).write_inner_content(|writer| { - writer - .create_element("href") - .write_text_content(BytesText::new(&format!( - "{prefix}/{principal}/", - )))?; - Ok(()) - })?; - } - "displayname" => { - let el = writer.create_element("displayname"); - if let Some(name) = calendar.clone().name { - el.write_text_content(BytesText::new(&name))?; - } else { - el.write_empty()?; - } - } - "calendar-color" => { - let el = writer.create_element("IC:calendar-color"); - if let Some(color) = calendar.clone().color { - el.write_text_content(BytesText::new(&color))?; - } else { - el.write_empty()?; - } - } - "calendar-description" => { - let el = writer.create_element("C:calendar-description"); - if let Some(description) = calendar.clone().description { - el.write_text_content(BytesText::new(&description))?; - } else { - el.write_empty()?; - } - } - "supported-calendar-component-set" => { - writer - .create_element("C:supported-calendar-component-set") - .write_inner_content(|writer| { - writer - .create_element("C:comp") - .with_attribute(("name", "VEVENT")) - .write_empty()?; - Ok(()) - })?; - } - "supported-calendar-data" => { - writer - .create_element("C:supported-calendar-data") - .write_inner_content(|writer| { - // - writer - .create_element("C:calendar-data") - .with_attributes(vec![ - ("content-type", "text/calendar"), - ("version", "2.0"), - ]) - .write_empty()?; - Ok(()) - })?; - } - "getcontenttype" => { - writer - .create_element("getcontenttype") - .write_text_content(BytesText::new("text/calendar"))?; - } - "max-resource-size" => { - writer - .create_element("max-resource-size") - .write_text_content(BytesText::new("10000000"))?; - } - "current-user-privilege-set" => { - writer - .create_element("current-user-privilege-set") - // These are just hard-coded for now and will possibly change in the future - .write_inner_content(|writer| { - for privilege in [ - "read", - "read-acl", - "write", - "write-acl", - "write-content", - "read-current-user-privilege-set", - "bind", - "unbind", - ] { - writer.create_element("privilege").write_inner_content( - |writer| { - writer.create_element(privilege).write_empty()?; - Ok(()) - }, - )?; - } - Ok(()) - })?; - } - "allprops" => {} - _ => invalid_props.push(prop), - }; - } - Ok(()) - })?; - - write_invalid_props_response(&mut writer, path, invalid_props)?; - Ok(std::str::from_utf8(&output_buffer)?.to_string()) + store.write().await.insert_calendar(cid, cal).await?; + Ok(()) } -pub async fn route_propfind_calendar( +pub async fn route_mkcol_calendar( path: Path<(String, String)>, body: String, - request: HttpRequest, - context: Data>, auth: AuthInfoExtractor, + context: Data>, ) -> Result { - // TODO: Check authorization let (_principal, cid) = path.into_inner(); - let calendar = context - .store - .read() - .await - .get_calendar(&cid) + let doc = roxmltree::Document::parse(&body).map_err(|_e| Error::BadRequest)?; + let mkcol_node = doc.root_element(); + match mkcol_node.tag_name().name() { + "mkcol" => {} + _ => return Err(Error::BadRequest), + } + + for set_node in mkcol_node.children() { + if set_node.tag_name().name() != "set" { + return Err(Error::BadRequest); + } + let prop_node = set_node.first_element_child().ok_or(Error::BadRequest)?; + if prop_node.tag_name().name() != "prop" { + return Err(Error::BadRequest); + } + handle_mkcol_calendar_set( + &context.store, + prop_node, + cid.clone(), + auth.inner.user_id.clone(), + ) .await .map_err(|_e| Error::InternalError)?; + } - let props = parse_propfind(&body).map_err(|_e| Error::BadRequest)?; - - let responses_string = generate_propfind_calendar_response( - props.clone(), - &auth.inner.user_id, - request.path(), - &context.prefix, - &calendar, - ) - .map_err(|_e| Error::InternalError)?; - - let output = generate_multistatus( - vec![Namespace::Dav, Namespace::CalDAV, Namespace::ICal], - |writer| { - writer.write_event(quick_xml::events::Event::Text(BytesText::from_escaped( - responses_string, - )))?; - Ok(()) - }, - ) - .map_err(|_e| Error::InternalError)?; - - Ok(HttpResponse::MultiStatus() - .content_type(ContentType::xml()) - .body(output)) + println!("{body}"); + Err(Error::InternalError) } diff --git a/crates/dav/src/routes/mod.rs b/crates/dav/src/routes/mod.rs index 563b948..ec0aeac 100644 --- a/crates/dav/src/routes/mod.rs +++ b/crates/dav/src/routes/mod.rs @@ -1,4 +1,2 @@ pub mod calendar; pub mod event; -pub mod principal; -pub mod root; diff --git a/crates/dav/src/routes/principal.rs b/crates/dav/src/routes/principal.rs deleted file mode 100644 index 0f3e228..0000000 --- a/crates/dav/src/routes/principal.rs +++ /dev/null @@ -1,146 +0,0 @@ -use crate::{ - calendar::generate_propfind_calendar_response, - depth_extractor::Depth, - namespace::Namespace, - propfind::{ - generate_multistatus, parse_propfind, write_invalid_props_response, - write_propstat_response, write_resourcetype, - }, - CalDavContext, Error, -}; -use actix_web::{ - http::{header::ContentType, StatusCode}, - web::Data, - HttpRequest, HttpResponse, -}; -use anyhow::Result; -use quick_xml::{ - events::{BytesText, Event}, - Writer, -}; -use rustical_auth::{AuthInfoExtractor, CheckAuthentication}; -use rustical_store::calendar::CalendarStore; - -// Executes the PROPFIND request and returns a XML string to be written into a object. -pub async fn generate_propfind_principal_response( - props: Vec<&str>, - principal: &str, - path: &str, - prefix: &str, -) -> Result { - let mut props = props; - if props.contains(&"allprops") { - props.extend( - [ - "resourcetype", - "current-user-principal", - "principal-URL", - "calendar-home-set", - "calendar-user-address-set", - ] - .iter(), - ); - } - - let mut invalid_props = Vec::<&str>::new(); - - let mut output_buffer = Vec::new(); - let mut writer = Writer::new_with_indent(&mut output_buffer, b' ', 2); - - write_propstat_response(&mut writer, path, StatusCode::OK, |writer| { - let writer = writer; - for prop in props { - match prop { - "resourcetype" => write_resourcetype(writer, vec!["principal", "collection"])?, - "current-user-principal" | "principal-URL" => { - writer.create_element(prop).write_inner_content(|writer| { - writer - .create_element("href") - .write_text_content(BytesText::new(&format!( - "{prefix}/{principal}/", - )))?; - Ok(()) - })?; - } - "calendar-home-set" | "calendar-user-address-set" => { - writer - .create_element(&format!("C:{prop}")) - .write_inner_content(|writer| { - writer - .create_element("href") - .write_text_content(BytesText::new(&format!( - "{prefix}/{principal}/" - )))?; - Ok(()) - })?; - } - "allprops" => {} - _ => invalid_props.push(prop), - }; - } - Ok(()) - })?; - - dbg!(&invalid_props); - write_invalid_props_response(&mut writer, path, invalid_props)?; - Ok(std::str::from_utf8(&output_buffer)?.to_string()) -} - -pub async fn route_propfind_principal( - body: String, - request: HttpRequest, - auth: AuthInfoExtractor, - context: Data>, - depth: Depth, -) -> Result { - let user = &auth.inner.user_id; - let props = parse_propfind(&body).map_err(|_e| Error::BadRequest)?; - - let mut responses = Vec::new(); - // also get calendars: - if depth != Depth::Zero { - let cals = context - .store - .read() - .await - .get_calendars(user) - .await - .map_err(|_e| Error::InternalError)?; - - for cal in cals { - responses.push( - generate_propfind_calendar_response( - props.clone(), - &auth.inner.user_id, - &format!("{}/{}", request.path(), cal.id), - &context.prefix, - &cal, - ) - .map_err(|_e| Error::InternalError)?, - ); - } - } - - responses.push( - generate_propfind_principal_response(props.clone(), user, request.path(), &context.prefix) - .await - .map_err(|_e| Error::InternalError)?, - ); - - let output = generate_multistatus( - vec![Namespace::Dav, Namespace::CalDAV, Namespace::ICal], - |writer| { - for response in responses { - writer.write_event(Event::Text(BytesText::from_escaped(response)))?; - } - Ok(()) - }, - ) - .map_err(|_e| Error::InternalError)?; - - println!("{}", &output); - - Ok(HttpResponse::MultiStatus() - .content_type(ContentType::xml()) - .body(output)) -} diff --git a/crates/dav/src/routes/root.rs b/crates/dav/src/routes/root.rs deleted file mode 100644 index 8553866..0000000 --- a/crates/dav/src/routes/root.rs +++ /dev/null @@ -1,88 +0,0 @@ -use crate::{ - namespace::Namespace, - propfind::{ - generate_multistatus, parse_propfind, write_invalid_props_response, - write_propstat_response, write_resourcetype, - }, - CalDavContext, Error, -}; -use actix_web::{ - http::{header::ContentType, StatusCode}, - web::Data, - HttpRequest, HttpResponse, -}; -use quick_xml::{ - events::{BytesText, Event}, - Writer, -}; -use rustical_auth::{AuthInfoExtractor, CheckAuthentication}; -use rustical_store::calendar::CalendarStore; - -// Executes the PROPFIND request and returns a XML string to be written into a object. -pub async fn generate_propfind_root_response( - props: Vec<&str>, - principal: &str, - path: &str, - prefix: &str, -) -> Result { - let mut props = props; - if props.contains(&"allprops") { - props.extend(["resourcetype", "current-user-principal"].iter()); - } - - let mut invalid_props = Vec::<&str>::new(); - - let mut output_buffer = Vec::new(); - let mut writer = Writer::new_with_indent(&mut output_buffer, b' ', 2); - - write_propstat_response(&mut writer, path, StatusCode::OK, |writer| { - for prop in props { - match prop { - "resourcetype" => write_resourcetype(writer, vec!["collection"])?, - "current-user-principal" => { - writer - .create_element("current-user-principal") - .write_inner_content(|writer| { - writer - .create_element("href") - .write_text_content(BytesText::new(&format!( - "{prefix}/{principal}" - )))?; - Ok(()) - })?; - } - "allprops" => {} - _ => invalid_props.push(prop), - }; - } - Ok(()) - })?; - dbg!(&invalid_props); - write_invalid_props_response(&mut writer, path, invalid_props)?; - Ok(std::str::from_utf8(&output_buffer)?.to_string()) -} - -pub async fn route_propfind_root( - body: String, - request: HttpRequest, - context: Data>, - auth: AuthInfoExtractor, -) -> Result { - let principal = &auth.inner.user_id; - let props = parse_propfind(&body).map_err(|_e| Error::BadRequest)?; - - let responses_string = - generate_propfind_root_response(props.clone(), principal, request.path(), &context.prefix) - .await - .map_err(|_e| Error::InternalError)?; - - let output = generate_multistatus(vec![Namespace::Dav, Namespace::CalDAV], |writer| { - writer.write_event(Event::Text(BytesText::from_escaped(responses_string)))?; - Ok(()) - }) - .map_err(|_e| Error::InternalError)?; - - Ok(HttpResponse::MultiStatus() - .content_type(ContentType::xml()) - .body(output)) -}