260 lines
7.3 KiB
Rust
260 lines
7.3 KiB
Rust
/*
|
|
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
|
|
*
|
|
* 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<T> = std::result::Result<T, DavError>;
|
|
|
|
#[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<DavMethod> 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<String>,
|
|
}
|
|
|
|
impl From<DavErrorCondition> for DavError {
|
|
fn from(value: DavErrorCondition) -> Self {
|
|
DavError::Condition(value)
|
|
}
|
|
}
|
|
|
|
impl From<Condition> 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<Condition>) -> Self {
|
|
DavErrorCondition {
|
|
code,
|
|
condition: condition.into(),
|
|
details: None,
|
|
}
|
|
}
|
|
|
|
pub fn with_details(mut self, details: impl Into<String>) -> Self {
|
|
self.details = Some(details.into());
|
|
self
|
|
}
|
|
}
|
|
|
|
impl DavMethod {
|
|
pub fn parse(method: &Method) -> Option<Self> {
|
|
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<Condition>, Option<String>), Vec<DavPropertyValue>>,
|
|
}
|
|
|
|
impl PropStatBuilder {
|
|
pub fn insert_ok(&mut self, prop: impl Into<DavPropertyValue>) -> &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<DavPropertyValue>,
|
|
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<DavPropertyValue>,
|
|
status: StatusCode,
|
|
description: impl Into<String>,
|
|
) -> &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<DavPropertyValue>,
|
|
status: StatusCode,
|
|
condition: impl Into<Condition>,
|
|
) -> &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<DavPropertyValue>,
|
|
status: StatusCode,
|
|
condition: impl Into<Condition>,
|
|
description: impl Into<String>,
|
|
) -> &mut Self {
|
|
self.propstats
|
|
.entry((status, Some(condition.into()), Some(description.into())))
|
|
.or_default()
|
|
.push(prop.into());
|
|
self
|
|
}
|
|
|
|
pub fn build(self) -> Vec<PropStat> {
|
|
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()
|
|
}
|