/* * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC * * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ #![warn(clippy::large_futures)] pub mod calendar; pub mod card; pub mod common; pub mod file; pub mod principal; pub mod request; use dav_proto::schema::{ request::DavPropertyValue, response::{Condition, List, Prop, PropStat, ResponseDescription, Status}, }; use groupware::{DavResourceName, RFC_3986}; use hyper::{Method, StatusCode}; use std::borrow::Cow; use store::ahash::AHashMap; pub(crate) type Result = std::result::Result; #[derive(Debug, Clone, Copy)] pub enum DavMethod { GET, PUT, POST, DELETE, HEAD, PATCH, PROPFIND, PROPPATCH, REPORT, MKCOL, MKCALENDAR, COPY, MOVE, LOCK, UNLOCK, OPTIONS, ACL, } impl From for trc::WebDavEvent { fn from(value: DavMethod) -> Self { match value { DavMethod::GET => trc::WebDavEvent::Get, DavMethod::PUT => trc::WebDavEvent::Put, DavMethod::POST => trc::WebDavEvent::Post, DavMethod::DELETE => trc::WebDavEvent::Delete, DavMethod::HEAD => trc::WebDavEvent::Head, DavMethod::PATCH => trc::WebDavEvent::Patch, DavMethod::PROPFIND => trc::WebDavEvent::Propfind, DavMethod::PROPPATCH => trc::WebDavEvent::Proppatch, DavMethod::REPORT => trc::WebDavEvent::Report, DavMethod::MKCOL => trc::WebDavEvent::Mkcol, DavMethod::MKCALENDAR => trc::WebDavEvent::Mkcalendar, DavMethod::COPY => trc::WebDavEvent::Copy, DavMethod::MOVE => trc::WebDavEvent::Move, DavMethod::LOCK => trc::WebDavEvent::Lock, DavMethod::UNLOCK => trc::WebDavEvent::Unlock, DavMethod::OPTIONS => trc::WebDavEvent::Options, DavMethod::ACL => trc::WebDavEvent::Acl, } } } pub(crate) enum DavError { Parse(dav_proto::parser::Error), Internal(trc::Error), Condition(DavErrorCondition), Code(StatusCode), } struct DavErrorCondition { pub code: StatusCode, pub condition: Condition, pub details: Option, } impl From for DavError { fn from(value: DavErrorCondition) -> Self { DavError::Condition(value) } } impl From for DavErrorCondition { fn from(value: Condition) -> Self { DavErrorCondition { code: StatusCode::CONFLICT, condition: value, details: None, } } } impl DavErrorCondition { pub fn new(code: StatusCode, condition: impl Into) -> Self { DavErrorCondition { code, condition: condition.into(), details: None, } } pub fn with_details(mut self, details: impl Into) -> Self { self.details = Some(details.into()); self } } impl DavMethod { pub fn parse(method: &Method) -> Option { match *method { Method::GET => Some(DavMethod::GET), Method::PUT => Some(DavMethod::PUT), Method::DELETE => Some(DavMethod::DELETE), Method::OPTIONS => Some(DavMethod::OPTIONS), Method::POST => Some(DavMethod::POST), Method::PATCH => Some(DavMethod::PATCH), Method::HEAD => Some(DavMethod::HEAD), _ => { hashify::tiny_map!(method.as_str().as_bytes(), "PROPFIND" => DavMethod::PROPFIND, "PROPPATCH" => DavMethod::PROPPATCH, "REPORT" => DavMethod::REPORT, "MKCOL" => DavMethod::MKCOL, "MKCALENDAR" => DavMethod::MKCALENDAR, "COPY" => DavMethod::COPY, "MOVE" => DavMethod::MOVE, "LOCK" => DavMethod::LOCK, "UNLOCK" => DavMethod::UNLOCK, "ACL" => DavMethod::ACL ) } } } #[inline] pub fn has_body(self) -> bool { matches!( self, DavMethod::PUT | DavMethod::POST | DavMethod::PATCH | DavMethod::PROPPATCH | DavMethod::PROPFIND | DavMethod::REPORT | DavMethod::LOCK | DavMethod::ACL | DavMethod::MKCALENDAR ) } } #[derive(Debug, Default)] pub struct PropStatBuilder { propstats: AHashMap<(StatusCode, Option, Option), Vec>, } impl PropStatBuilder { pub fn insert_ok(&mut self, prop: impl Into) -> &mut Self { self.propstats .entry((StatusCode::OK, None, None)) .or_default() .push(prop.into()); self } pub fn insert_with_status( &mut self, prop: impl Into, status: StatusCode, ) -> &mut Self { self.propstats .entry((status, None, None)) .or_default() .push(prop.into()); self } pub fn insert_error_with_description( &mut self, prop: impl Into, status: StatusCode, description: impl Into, ) -> &mut Self { self.propstats .entry((status, None, Some(description.into()))) .or_default() .push(prop.into()); self } pub fn insert_precondition_failed( &mut self, prop: impl Into, status: StatusCode, condition: impl Into, ) -> &mut Self { self.propstats .entry((status, Some(condition.into()), None)) .or_default() .push(prop.into()); self } pub fn insert_precondition_failed_with_description( &mut self, prop: impl Into, status: StatusCode, condition: impl Into, description: impl Into, ) -> &mut Self { self.propstats .entry((status, Some(condition.into()), Some(description.into()))) .or_default() .push(prop.into()); self } pub fn build(self) -> Vec { self.propstats .into_iter() .map(|((status, condition, description), props)| PropStat { prop: Prop(List(props)), status: Status(status), error: condition, response_description: description.map(ResponseDescription), }) .collect() } } // Workaround for Apple bug with missing percent encoding in paths pub(crate) fn fix_percent_encoding(path: &'_ str) -> Cow<'_, str> { let (parent, name) = if let Some((parent, name)) = path.rsplit_once('/') { (Some(parent), name) } else { (None, path) }; for &ch in name.as_bytes() { if !matches!(ch, b'0'..=b'9' | b'A'..=b'Z' | b'a'..=b'z' | b'-' | b'.' | b'_' | b'~' | b'%') { let name = percent_encoding::percent_encode(name.as_bytes(), RFC_3986); return if let Some(parent) = parent { Cow::Owned(format!("{parent}/{name}")) } else { Cow::Owned(name.to_string()) }; } } path.into() }