Import upstream v0.16.22, stripped

Upstream commit: 474dd0229cb20cf513036619781ed97bd8073c3f
Enterprise-only files removed or emptied: 63
Enterprise-only snippets removed: 117 in 50 files
Dangling module declarations removed: 5
Cargo edits turning enterprise off: 14
Verification: clean
Enterprise feature gates left for rebuilt features: 19 in 18 files

Produced by tools/fork/strip.py. The full report is in docs/fork/strip-reports/ on main.
This commit is contained in:
2026-09-18 10:21:56 -07:00
commit 7dae9b29fd
1650 changed files with 485521 additions and 0 deletions
+33
View File
@@ -0,0 +1,33 @@
[package]
name = "dav"
version = "0.16.22"
edition = "2024"
[dependencies]
dav-proto = { path = "../dav-proto" }
common = { path = "../common" }
store = { path = "../store" }
utils = { path = "../utils" }
groupware = { path = "../groupware" }
directory = { path = "../directory" }
registry = { path = "../registry" }
http_proto = { path = "../http-proto" }
types = { path = "../types" }
trc = { path = "../trc" }
calcard = { version = "0.3", features = ["rkyv"] }
hashify = { version = "0.2" }
hyper = { version = "1.11.1", features = ["server", "http1", "http2"] }
percent-encoding = "2.3.2"
rkyv = { version = "0.8.18", features = ["little_endian"] }
compact_str = "0.10.0"
chrono = "0.4.45"
[dev-dependencies]
[features]
test_mode = []
dev_mode = []
enterprise = []
[lints]
workspace = true
File diff suppressed because it is too large Load Diff
+248
View File
@@ -0,0 +1,248 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::{
DavError, DavMethod,
common::{
ETag,
lock::{LockRequestHandler, ResourceState},
uri::DavUriResource,
},
};
use common::{Server, auth::AccessToken, sharing::EffectiveAcl};
use dav_proto::RequestHeaders;
use groupware::{
DestroyArchive,
cache::GroupwareCache,
calendar::{Calendar, CalendarEvent},
};
use http_proto::HttpResponse;
use hyper::StatusCode;
use registry::schema::enums::Permission;
use store::write::{BatchBuilder, ValueClass};
use store::{
ValueKey,
write::{AlignedBytes, Archive},
};
use trc::AddContext;
use types::{
acl::Acl,
collection::{Collection, SyncCollection},
field::PrincipalField,
};
pub(crate) trait CalendarDeleteRequestHandler: Sync + Send {
fn handle_calendar_delete_request(
&self,
access_token: &AccessToken,
headers: &RequestHeaders<'_>,
) -> impl Future<Output = crate::Result<HttpResponse>> + Send;
}
impl CalendarDeleteRequestHandler for Server {
async fn handle_calendar_delete_request(
&self,
access_token: &AccessToken,
headers: &RequestHeaders<'_>,
) -> crate::Result<HttpResponse> {
// Validate URI
let resource = self
.validate_uri(access_token, headers.uri)
.await?
.into_owned_uri()?;
let account_id = resource.account_id;
let delete_path = resource
.resource
.filter(|r| !r.is_empty())
.ok_or(DavError::Code(StatusCode::FORBIDDEN))?;
let resources = self
.fetch_dav_resources(
access_token.account_id(),
account_id,
SyncCollection::Calendar,
)
.await
.caused_by(trc::location!())?;
// Check resource type
let delete_resource = resources
.by_path(delete_path)
.ok_or(DavError::Code(StatusCode::NOT_FOUND))?;
let document_id = delete_resource.document_id();
let account_info = self
.scheduling_account_info(access_token.account_id(), account_id)
.await?;
let send_itip = self.core.groupware.itip_enabled
&& !headers.no_schedule_reply
&& !account_info.addresses().is_empty()
&& access_token.has_permission(Permission::CalendarSchedulingSend);
// Fetch entry
let mut batch = BatchBuilder::new();
if delete_resource.is_container() {
// Deleting the default calendar is not allowed
#[cfg(not(any(feature = "dev_mode", feature = "test_mode")))]
if self
.core
.groupware
.default_calendar_name
.as_ref()
.is_some_and(|name| name == delete_path)
{
return Err(DavError::Condition(crate::DavErrorCondition::new(
StatusCode::FORBIDDEN,
dav_proto::schema::response::CalCondition::DefaultCalendarNeeded,
)));
}
let calendar_ = self
.store()
.get_value::<Archive<AlignedBytes>>(ValueKey::archive(
account_id,
Collection::Calendar,
document_id,
))
.await
.caused_by(trc::location!())?
.ok_or(DavError::Code(StatusCode::NOT_FOUND))?;
let calendar = calendar_
.to_unarchived::<Calendar>()
.caused_by(trc::location!())?;
// Validate ACL
if !access_token.is_member(account_id)
&& !calendar
.inner
.acls
.effective_acl(access_token)
.contains_all([Acl::Delete, Acl::RemoveItems].into_iter())
{
return Err(DavError::Code(StatusCode::FORBIDDEN));
}
// Validate headers
self.validate_headers(
access_token,
headers,
vec![ResourceState {
account_id,
collection: Collection::Calendar,
document_id: document_id.into(),
etag: calendar.etag().into(),
path: delete_path,
..Default::default()
}],
Default::default(),
DavMethod::DELETE,
)
.await?;
// Delete calendar and events
DestroyArchive(calendar)
.delete_with_events(
self,
&account_info,
account_id,
document_id,
resources
.subtree(delete_path)
.filter(|r| !r.is_container())
.map(|r| r.document_id())
.collect::<Vec<_>>(),
resources.format_resource(delete_resource).into(),
send_itip,
&mut batch,
)
.await
.caused_by(trc::location!())?;
// Reset default calendar id
let default_calendar_id = self
.store()
.get_value::<u32>(ValueKey {
account_id,
collection: Collection::Principal.into(),
document_id: 0,
class: ValueClass::Property(PrincipalField::DefaultCalendarId.into()),
})
.await
.caused_by(trc::location!())?;
if default_calendar_id.is_some_and(|id| id == document_id) {
batch
.with_account_id(account_id)
.with_collection(Collection::Principal)
.with_document(0)
.clear(PrincipalField::DefaultCalendarId);
}
} else {
// Validate ACL
let calendar_id = delete_resource.parent_id().unwrap();
if !access_token.is_member(account_id)
&& !resources.has_access_to_container(access_token, calendar_id, Acl::RemoveItems)
{
return Err(DavError::Code(StatusCode::FORBIDDEN));
}
let event_ = self
.store()
.get_value::<Archive<AlignedBytes>>(ValueKey::archive(
account_id,
Collection::CalendarEvent,
document_id,
))
.await
.caused_by(trc::location!())?
.ok_or(DavError::Code(StatusCode::NOT_FOUND))?;
// Validate headers
self.validate_headers(
access_token,
headers,
vec![ResourceState {
account_id,
collection: Collection::CalendarEvent,
document_id: document_id.into(),
etag: event_.etag().into(),
path: delete_path,
..Default::default()
}],
Default::default(),
DavMethod::DELETE,
)
.await?;
// Validate schedule tag
let event = event_
.to_unarchived::<CalendarEvent>()
.caused_by(trc::location!())?;
if headers.if_schedule_tag.is_some()
&& event.inner.schedule_tag.as_ref().map(|t| t.to_native())
!= headers.if_schedule_tag
{
return Err(DavError::Code(StatusCode::PRECONDITION_FAILED));
}
// Delete event
DestroyArchive(event)
.delete(
&account_info,
account_id,
document_id,
calendar_id,
resources.format_resource(delete_resource).into(),
send_itip,
&mut batch,
)
.caused_by(trc::location!())?;
}
self.commit_batch(batch).await.caused_by(trc::location!())?;
self.notify_task_queue();
Ok(HttpResponse::new(StatusCode::NO_CONTENT))
}
}
+407
View File
@@ -0,0 +1,407 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use super::query::CalendarQueryHandler;
use crate::{DavError, calendar::query::is_resource_in_time_range, common::uri::DavUriResource};
use calcard::{
common::{PartialDateTime, timezone::Tz},
icalendar::{
ArchivedICalendarComponentType, ArchivedICalendarEntry, ArchivedICalendarParameterName,
ArchivedICalendarParameterValue, ArchivedICalendarProperty, ArchivedICalendarStatus,
ArchivedICalendarValue, ICalendar, ICalendarComponent, ICalendarComponentType,
ICalendarEntry, ICalendarFreeBusyType, ICalendarParameter, ICalendarPeriod,
ICalendarProperty, ICalendarTransparency, ICalendarValue,
},
};
use common::{DavResourcePath, DavResources, PROD_ID, Server, auth::AccessToken};
use dav_proto::{RequestHeaders, schema::request::FreeBusyQuery};
use groupware::{cache::GroupwareCache, calendar::CalendarEvent};
use http_proto::HttpResponse;
use hyper::StatusCode;
use std::str::FromStr;
use store::{
ValueKey,
write::{AlignedBytes, Archive},
};
use store::{
ahash::AHashMap,
write::{now, serialize::rkyv_deserialize},
};
use trc::AddContext;
use types::{
TimeRange,
acl::Acl,
collection::{Collection, SyncCollection},
};
pub(crate) trait CalendarFreebusyRequestHandler: Sync + Send {
fn handle_calendar_freebusy_request(
&self,
access_token: &AccessToken,
headers: &RequestHeaders<'_>,
request: FreeBusyQuery,
) -> impl Future<Output = crate::Result<HttpResponse>> + Send;
fn build_freebusy_object(
&self,
access_token: &AccessToken,
request: FreeBusyQuery,
resources: &DavResources,
account_id: u32,
resource: DavResourcePath<'_>,
) -> impl Future<Output = crate::Result<ICalendar>> + Send;
}
impl CalendarFreebusyRequestHandler for Server {
async fn handle_calendar_freebusy_request(
&self,
access_token: &AccessToken,
headers: &RequestHeaders<'_>,
request: FreeBusyQuery,
) -> crate::Result<HttpResponse> {
// Validate URI
let resource_ = self
.validate_uri(access_token, headers.uri)
.await?
.into_owned_uri()?;
let account_id = resource_.account_id;
let resources = self
.fetch_dav_resources(
access_token.account_id(),
account_id,
SyncCollection::Calendar,
)
.await
.caused_by(trc::location!())?;
let resource = resources
.by_path(
resource_
.resource
.ok_or(DavError::Code(StatusCode::METHOD_NOT_ALLOWED))?,
)
.ok_or(DavError::Code(StatusCode::NOT_FOUND))?;
if !resource.is_container() {
return Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED));
}
self.build_freebusy_object(access_token, request, &resources, account_id, resource)
.await
.map(|ical| {
HttpResponse::new(StatusCode::OK)
.with_content_type("text/calendar; charset=utf-8")
.with_text_body(ical.to_string())
})
}
async fn build_freebusy_object(
&self,
access_token: &AccessToken,
request: FreeBusyQuery,
resources: &DavResources,
account_id: u32,
resource: DavResourcePath<'_>,
) -> crate::Result<ICalendar> {
// Obtain shared ids
let shared_ids = if !access_token.is_member(account_id) {
resources
.shared_items(
access_token,
[Acl::ReadItems, Acl::SchedulingReadFreeBusy],
false,
)
.into()
} else {
None
};
// Build FreeBusy component
let default_tz = resource
.resource
.calendar_preferences(account_id)
.map(|p| p.tz)
.unwrap_or(Tz::UTC);
let mut entries = Vec::with_capacity(6);
if let Some(range) = request.range {
entries.push(ICalendarEntry {
name: ICalendarProperty::Dtstart,
params: vec![],
values: vec![ICalendarValue::PartialDateTime(Box::new(
PartialDateTime::from_utc_timestamp(range.start),
))],
});
entries.push(ICalendarEntry {
name: ICalendarProperty::Dtend,
params: vec![],
values: vec![ICalendarValue::PartialDateTime(Box::new(
PartialDateTime::from_utc_timestamp(range.end),
))],
});
entries.push(ICalendarEntry {
name: ICalendarProperty::Dtstamp,
params: vec![],
values: vec![ICalendarValue::PartialDateTime(Box::new(
PartialDateTime::from_utc_timestamp(now() as i64),
))],
});
let document_ids = resources
.children(resource.document_id())
.filter(|resource| {
shared_ids
.as_ref()
.is_none_or(|ids| ids.contains(resource.document_id()))
&& is_resource_in_time_range(resource.resource, &range)
})
.map(|resource| resource.document_id())
.collect::<Vec<_>>();
let mut fb_entries: AHashMap<ICalendarFreeBusyType, Vec<(i64, i64)>> =
AHashMap::with_capacity(document_ids.len());
let max_instances = self.core.groupware.max_ical_instances;
let mut total_instances: usize = 0;
for document_id in document_ids {
let Some(archive) = self
.store()
.get_value::<Archive<AlignedBytes>>(ValueKey::archive(
account_id,
Collection::CalendarEvent,
document_id,
))
.await
.caused_by(trc::location!())?
else {
continue;
};
let event = archive
.unarchive::<CalendarEvent>()
.caused_by(trc::location!())?;
/*
Only VEVENT components without a TRANSP property or with the TRANSP
property set to OPAQUE, and VFREEBUSY components SHOULD be considered
in generating the free busy time information.
*/
let mut components = event
.data
.event
.components
.iter()
.enumerate()
.filter(|(_, comp)| {
(matches!(comp.component_type, ArchivedICalendarComponentType::VEvent)
&& comp
.transparency()
.is_none_or(|t| t == &ICalendarTransparency::Opaque))
|| matches!(
comp.component_type,
ArchivedICalendarComponentType::VFreebusy
)
})
.peekable();
if components.peek().is_none() {
continue;
}
let events =
CalendarQueryHandler::new(event, Some(range), default_tz).into_expanded_times();
if events.is_empty() {
continue;
}
total_instances = total_instances.saturating_add(events.len());
if total_instances > max_instances {
return Err(DavError::Code(StatusCode::PAYLOAD_TOO_LARGE));
}
for (component_id, component) in components {
let component_id = component_id as u32;
match component.component_type {
ArchivedICalendarComponentType::VEvent => {
let fbtype = match component.status() {
Some(ArchivedICalendarStatus::Cancelled) => continue,
Some(ArchivedICalendarStatus::Tentative) => {
ICalendarFreeBusyType::BusyTentative
}
_ => ICalendarFreeBusyType::Busy,
};
let mut events_in_range = Vec::new();
for event in &events {
if event.comp_id == component_id
&& range.is_in_range(false, event.start, event.end)
{
events_in_range.push((event.start, event.end));
}
}
if !events_in_range.is_empty() {
fb_entries
.entry(fbtype)
.or_default()
.extend(events_in_range);
}
}
ArchivedICalendarComponentType::VFreebusy => {
for entry in component.entries.iter() {
if matches!(entry.name, ArchivedICalendarProperty::Freebusy) {
let mut fb_in_range =
freebusy_in_range_utc(entry, &range, default_tz).peekable();
if fb_in_range.peek().is_some() {
let fb_type = entry
.params
.iter()
.find_map(|param| {
if let (
ArchivedICalendarParameterName::Fbtype,
ArchivedICalendarParameterValue::Fbtype(param),
) = (&param.name, &param.value)
{
rkyv_deserialize(param).ok()
} else {
None
}
})
.unwrap_or(ICalendarFreeBusyType::Busy);
fb_entries.entry(fb_type).or_default().extend(fb_in_range);
}
}
}
}
_ => {}
}
}
}
for (fbtype, events_in_range) in fb_entries {
entries.push(ICalendarEntry {
name: ICalendarProperty::Freebusy,
params: vec![ICalendarParameter::fbtype(fbtype)],
values: merge_intervals(events_in_range),
});
}
}
// Build ICalendar
Ok(ICalendar {
components: vec![
ICalendarComponent {
component_type: ICalendarComponentType::VCalendar,
entries: vec![
ICalendarEntry {
name: ICalendarProperty::Version,
params: vec![],
values: vec![ICalendarValue::Text("2.0".to_string())],
},
ICalendarEntry {
name: ICalendarProperty::Prodid,
params: vec![],
values: vec![ICalendarValue::Text(PROD_ID.to_string())],
},
],
component_ids: vec![1],
},
ICalendarComponent {
component_type: ICalendarComponentType::VFreebusy,
entries,
component_ids: vec![],
},
],
})
}
}
fn merge_intervals(mut intervals: Vec<(i64, i64)>) -> Vec<ICalendarValue> {
if intervals.len() > 1 {
intervals.sort_unstable_by_key(|a| a.0);
let mut unique_intervals = Vec::new();
let mut start_time = intervals[0].0;
let mut end_time = intervals[0].1;
for &(curr_start, curr_end) in intervals.iter().skip(1) {
if curr_start <= end_time {
end_time = end_time.max(curr_end);
} else {
unique_intervals.push(build_ical_value(start_time, end_time));
start_time = curr_start;
end_time = curr_end;
}
}
unique_intervals.push(build_ical_value(start_time, end_time));
unique_intervals
} else {
intervals
.into_iter()
.map(|(start, end)| build_ical_value(start, end))
.collect()
}
}
fn build_ical_value(from: i64, to: i64) -> ICalendarValue {
ICalendarValue::Period(ICalendarPeriod::Range {
start: PartialDateTime::from_utc_timestamp(from),
end: PartialDateTime::from_utc_timestamp(to),
})
}
pub(crate) fn freebusy_in_range(
entry: &ArchivedICalendarEntry,
range: &TimeRange,
default_tz: Tz,
) -> impl Iterator<Item = ICalendarValue> {
let tz = entry
.tz_id()
.and_then(|tz_id| Tz::from_str(tz_id).ok())
.unwrap_or(default_tz);
entry.values.iter().filter_map(move |value| {
if let ArchivedICalendarValue::Period(period) = &value {
period.time_range(tz).and_then(|(start, end)| {
let start = start.timestamp();
let end = end.timestamp();
if range.is_in_range(false, start, end) {
rkyv_deserialize(value).ok()
} else {
None
}
})
} else {
None
}
})
}
fn freebusy_in_range_utc(
entry: &ArchivedICalendarEntry,
range: &TimeRange,
default_tz: Tz,
) -> impl Iterator<Item = (i64, i64)> {
let tz = entry
.tz_id()
.and_then(|tz_id| Tz::from_str(tz_id).ok())
.unwrap_or(default_tz);
entry.values.iter().filter_map(move |value| {
if let ArchivedICalendarValue::Period(period) = &value {
period.time_range(tz).and_then(|(start, end)| {
let start = start.timestamp();
let end = end.timestamp();
if range.is_in_range(false, start, end) {
Some((start, end))
} else {
None
}
})
} else {
None
}
})
}
+130
View File
@@ -0,0 +1,130 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::{
DavError, DavMethod,
common::{
ETag,
lock::{LockRequestHandler, ResourceState},
uri::DavUriResource,
},
};
use common::{Server, auth::AccessToken};
use dav_proto::{RequestHeaders, schema::property::Rfc1123DateTime};
use groupware::{cache::GroupwareCache, calendar::CalendarEvent};
use http_proto::HttpResponse;
use hyper::StatusCode;
use store::{
ValueKey,
write::{AlignedBytes, Archive},
};
use trc::AddContext;
use types::{
acl::Acl,
collection::{Collection, SyncCollection},
};
pub(crate) trait CalendarGetRequestHandler: Sync + Send {
fn handle_calendar_get_request(
&self,
access_token: &AccessToken,
headers: &RequestHeaders<'_>,
is_head: bool,
) -> impl Future<Output = crate::Result<HttpResponse>> + Send;
}
impl CalendarGetRequestHandler for Server {
async fn handle_calendar_get_request(
&self,
access_token: &AccessToken,
headers: &RequestHeaders<'_>,
is_head: bool,
) -> crate::Result<HttpResponse> {
// Validate URI
let resource_ = self
.validate_uri(access_token, headers.uri)
.await?
.into_owned_uri()?;
let account_id = resource_.account_id;
let resources = self
.fetch_dav_resources(
access_token.account_id(),
account_id,
SyncCollection::Calendar,
)
.await
.caused_by(trc::location!())?;
let resource = resources
.by_path(
resource_
.resource
.ok_or(DavError::Code(StatusCode::METHOD_NOT_ALLOWED))?,
)
.ok_or(DavError::Code(StatusCode::NOT_FOUND))?;
if resource.is_container() {
return Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED));
}
// Validate ACL
if !access_token.is_member(account_id)
&& !resources.has_access_to_container(
access_token,
resource.parent_id().unwrap(),
Acl::ReadItems,
)
{
return Err(DavError::Code(StatusCode::FORBIDDEN));
}
// Fetch event
let event_ = self
.store()
.get_value::<Archive<AlignedBytes>>(ValueKey::archive(
account_id,
Collection::CalendarEvent,
resource.document_id(),
))
.await
.caused_by(trc::location!())?
.ok_or(DavError::Code(StatusCode::NOT_FOUND))?;
let event = event_
.unarchive::<CalendarEvent>()
.caused_by(trc::location!())?;
// Validate headers
let etag = event_.etag();
let schedule_tag = event.schedule_tag.as_ref().map(|tag| tag.to_native());
self.validate_headers(
access_token,
headers,
vec![ResourceState {
account_id,
collection: Collection::CalendarEvent,
document_id: resource.document_id().into(),
etag: etag.clone().into(),
path: resource_.resource.unwrap(),
..Default::default()
}],
Default::default(),
DavMethod::GET,
)
.await?;
let response = HttpResponse::new(StatusCode::OK)
.with_content_type("text/calendar; charset=utf-8")
.with_etag(etag)
.with_schedule_tag_opt(schedule_tag)
.with_last_modified(Rfc1123DateTime::new(i64::from(event.modified)).to_string());
let ical = event.data.event.to_string();
if !is_head {
Ok(response.with_binary_body(ical))
} else {
Ok(response.with_content_length(ical.len()))
}
}
}
+156
View File
@@ -0,0 +1,156 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use super::proppatch::CalendarPropPatchRequestHandler;
use crate::{
DavError, DavMethod, PropStatBuilder,
common::{
ExtractETag,
lock::{LockRequestHandler, ResourceState},
uri::DavUriResource,
},
};
use common::{Server, auth::AccessToken};
use dav_proto::{
RequestHeaders, Return,
schema::{Namespace, request::MkCol, response::MkColResponse},
};
use groupware::{
cache::GroupwareCache,
calendar::{Calendar, CalendarPreferences},
};
use http_proto::HttpResponse;
use hyper::StatusCode;
use store::write::BatchBuilder;
use trc::AddContext;
use types::collection::{Collection, SyncCollection};
pub(crate) trait CalendarMkColRequestHandler: Sync + Send {
fn handle_calendar_mkcol_request(
&self,
access_token: &AccessToken,
headers: &RequestHeaders<'_>,
request: Option<MkCol>,
) -> impl Future<Output = crate::Result<HttpResponse>> + Send;
}
impl CalendarMkColRequestHandler for Server {
async fn handle_calendar_mkcol_request(
&self,
access_token: &AccessToken,
headers: &RequestHeaders<'_>,
request: Option<MkCol>,
) -> crate::Result<HttpResponse> {
// Validate URI
let resource = self
.validate_uri(access_token, headers.uri)
.await?
.into_owned_uri()?;
let account_id = resource.account_id;
let name = resource
.resource
.ok_or(DavError::Code(StatusCode::FORBIDDEN))?;
if !access_token.is_member(account_id) {
return Err(DavError::Code(StatusCode::FORBIDDEN));
} else if name.contains('/')
|| self
.fetch_dav_resources(
access_token.account_id(),
account_id,
SyncCollection::Calendar,
)
.await
.caused_by(trc::location!())?
.by_path(name)
.is_some()
{
return Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED));
}
// Validate headers
self.validate_headers(
access_token,
headers,
vec![ResourceState {
account_id,
collection: resource.collection,
document_id: Some(u32::MAX),
path: name,
..Default::default()
}],
Default::default(),
DavMethod::MKCOL,
)
.await?;
// Build file container
let mut calendar = Calendar {
name: name.to_string(),
preferences: vec![CalendarPreferences {
account_id,
name: name.to_string(),
..Default::default()
}],
..Default::default()
};
// Apply MKCOL properties
let mut return_prop_stat = None;
let mut is_mkcalendar = false;
if let Some(mkcol) = request {
let mut prop_stat = PropStatBuilder::default();
is_mkcalendar = mkcol.is_mkcalendar;
if !self.apply_calendar_properties(
access_token.personal_id(account_id, Collection::Calendar),
&mut calendar,
false,
mkcol.props,
&mut prop_stat,
) {
return Ok(HttpResponse::new(StatusCode::FORBIDDEN).with_xml_body(
MkColResponse::new(prop_stat.build())
.with_namespace(Namespace::CalDav)
.with_mkcalendar(is_mkcalendar)
.to_string(),
));
}
if headers.ret != Return::Minimal {
return_prop_stat = Some(prop_stat);
}
}
// Prepare write batch
let mut batch = BatchBuilder::new();
let document_id = self
.store()
.assign_document_ids(account_id, Collection::Calendar, 1)
.await
.caused_by(trc::location!())?;
calendar
.insert(
access_token.account_tenant_ids(),
account_id,
document_id,
&mut batch,
)
.caused_by(trc::location!())?;
let etag = batch.etag();
self.commit_batch(batch).await.caused_by(trc::location!())?;
if let Some(prop_stat) = return_prop_stat {
Ok(HttpResponse::new(StatusCode::CREATED)
.with_xml_body(
MkColResponse::new(prop_stat.build())
.with_namespace(Namespace::CalDav)
.with_mkcalendar(is_mkcalendar)
.to_string(),
)
.with_etag_opt(etag))
} else {
Ok(HttpResponse::new(StatusCode::CREATED).with_etag_opt(etag))
}
}
}
+149
View File
@@ -0,0 +1,149 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
pub mod copy_move;
pub mod delete;
pub mod freebusy;
pub mod get;
pub mod mkcol;
pub mod proppatch;
pub mod query;
pub mod scheduling;
pub mod update;
use crate::{DavError, DavErrorCondition};
use common::{DavResources, Server};
use dav_proto::schema::{
property::{CalDavProperty, CalendarData, DavProperty, WebDavProperty},
response::CalCondition,
};
use groupware::scheduling::ItipError;
use hyper::StatusCode;
use trc::AddContext;
use types::{collection::Collection, field::CalendarEventField};
pub(crate) static CALENDAR_CONTAINER_PROPS: [DavProperty; 31] = [
DavProperty::WebDav(WebDavProperty::CreationDate),
DavProperty::WebDav(WebDavProperty::DisplayName),
DavProperty::WebDav(WebDavProperty::GetETag),
DavProperty::WebDav(WebDavProperty::GetLastModified),
DavProperty::WebDav(WebDavProperty::ResourceType),
DavProperty::WebDav(WebDavProperty::LockDiscovery),
DavProperty::WebDav(WebDavProperty::SupportedLock),
DavProperty::WebDav(WebDavProperty::CurrentUserPrincipal),
DavProperty::WebDav(WebDavProperty::SyncToken),
DavProperty::WebDav(WebDavProperty::Owner),
DavProperty::WebDav(WebDavProperty::SupportedPrivilegeSet),
DavProperty::WebDav(WebDavProperty::CurrentUserPrivilegeSet),
DavProperty::WebDav(WebDavProperty::Acl),
DavProperty::WebDav(WebDavProperty::AclRestrictions),
DavProperty::WebDav(WebDavProperty::InheritedAclSet),
DavProperty::WebDav(WebDavProperty::PrincipalCollectionSet),
DavProperty::WebDav(WebDavProperty::SupportedReportSet),
DavProperty::WebDav(WebDavProperty::QuotaAvailableBytes),
DavProperty::WebDav(WebDavProperty::QuotaUsedBytes),
DavProperty::CalDav(CalDavProperty::CalendarDescription),
DavProperty::CalDav(CalDavProperty::SupportedCalendarData),
DavProperty::CalDav(CalDavProperty::SupportedCollationSet),
DavProperty::CalDav(CalDavProperty::SupportedCalendarComponentSet),
DavProperty::CalDav(CalDavProperty::CalendarTimezone),
DavProperty::CalDav(CalDavProperty::MaxResourceSize),
DavProperty::CalDav(CalDavProperty::MinDateTime),
DavProperty::CalDav(CalDavProperty::MaxDateTime),
DavProperty::CalDav(CalDavProperty::MaxInstances),
DavProperty::CalDav(CalDavProperty::MaxAttendeesPerInstance),
DavProperty::CalDav(CalDavProperty::TimezoneServiceSet),
DavProperty::CalDav(CalDavProperty::TimezoneId),
];
pub(crate) static CALENDAR_ITEM_PROPS: [DavProperty; 20] = [
DavProperty::WebDav(WebDavProperty::CreationDate),
DavProperty::WebDav(WebDavProperty::DisplayName),
DavProperty::WebDav(WebDavProperty::GetETag),
DavProperty::WebDav(WebDavProperty::GetLastModified),
DavProperty::WebDav(WebDavProperty::ResourceType),
DavProperty::WebDav(WebDavProperty::LockDiscovery),
DavProperty::WebDav(WebDavProperty::SupportedLock),
DavProperty::WebDav(WebDavProperty::CurrentUserPrincipal),
DavProperty::WebDav(WebDavProperty::SyncToken),
DavProperty::WebDav(WebDavProperty::Owner),
DavProperty::WebDav(WebDavProperty::SupportedPrivilegeSet),
DavProperty::WebDav(WebDavProperty::CurrentUserPrivilegeSet),
DavProperty::WebDav(WebDavProperty::Acl),
DavProperty::WebDav(WebDavProperty::AclRestrictions),
DavProperty::WebDav(WebDavProperty::InheritedAclSet),
DavProperty::WebDav(WebDavProperty::PrincipalCollectionSet),
DavProperty::WebDav(WebDavProperty::GetContentLanguage),
DavProperty::WebDav(WebDavProperty::GetContentLength),
DavProperty::WebDav(WebDavProperty::GetContentType),
DavProperty::CalDav(CalDavProperty::CalendarData(CalendarData {
properties: vec![],
expand: None,
limit_recurrence: None,
limit_freebusy: None,
})),
];
pub(crate) async fn assert_is_unique_uid(
server: &Server,
resources: &DavResources,
account_id: u32,
calendar_id: u32,
uid: Option<&str>,
) -> crate::Result<()> {
if let Some(uid) = uid {
let hits = server
.document_ids_matching(
account_id,
Collection::CalendarEvent,
CalendarEventField::Uid,
uid.as_bytes(),
)
.await
.caused_by(trc::location!())?;
if !hits.is_empty() {
for path in resources.children(calendar_id) {
if hits.contains(path.document_id()) {
return Err(DavError::Condition(DavErrorCondition::new(
StatusCode::PRECONDITION_FAILED,
CalCondition::NoUidConflict(resources.format_resource(path).into()),
)));
}
}
}
}
Ok(())
}
pub(crate) trait ItipPrecondition {
fn failed_precondition(&self) -> Option<CalCondition>;
}
impl ItipPrecondition for ItipError {
fn failed_precondition(&self) -> Option<CalCondition> {
match self {
ItipError::MultipleOrganizer => Some(CalCondition::SameOrganizerInAllComponents),
ItipError::OrganizerIsLocalAddress
| ItipError::SenderIsNotParticipant(_)
| ItipError::OrganizerMismatch => Some(CalCondition::ValidOrganizer),
ItipError::CannotModifyProperty(_)
| ItipError::CannotModifyInstance
| ItipError::CannotModifyAddress => Some(CalCondition::AllowedAttendeeObjectChange),
ItipError::MissingUid
| ItipError::MultipleUid
| ItipError::MultipleObjectTypes
| ItipError::MultipleObjectInstances
| ItipError::MissingMethod
| ItipError::InvalidComponentType
| ItipError::OutOfSequence
| ItipError::UnknownParticipant(_)
| ItipError::UnsupportedMethod(_) => Some(CalCondition::ValidSchedulingMessage),
_ => None,
}
}
}
+565
View File
@@ -0,0 +1,565 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::{
DavError, DavMethod, PropStatBuilder,
common::{
ETag, ExtractETag,
lock::{LockRequestHandler, ResourceState},
uri::DavUriResource,
},
};
use calcard::common::timezone::Tz;
use common::{Server, auth::AccessToken};
use dav_proto::{
RequestHeaders, Return,
schema::{
Namespace,
property::{CalDavProperty, DavProperty, DavValue, ResourceType, WebDavProperty},
request::{DavPropertyValue, PropertyUpdate},
response::{BaseCondition, CalCondition, MultiStatus, Response},
},
};
use groupware::{
cache::GroupwareCache,
calendar::{Calendar, CalendarEvent, SupportedComponent, Timezone},
};
use http_proto::HttpResponse;
use hyper::StatusCode;
use std::str::FromStr;
use store::write::BatchBuilder;
use store::{
ValueKey,
write::{AlignedBytes, Archive},
};
use trc::AddContext;
use types::{
acl::Acl,
collection::{Collection, SyncCollection},
};
use utils::map::bitmap::Bitmap;
pub(crate) trait CalendarPropPatchRequestHandler: Sync + Send {
fn handle_calendar_proppatch_request(
&self,
access_token: &AccessToken,
headers: &RequestHeaders<'_>,
request: PropertyUpdate,
) -> impl Future<Output = crate::Result<HttpResponse>> + Send;
fn apply_calendar_properties(
&self,
personal_id: u32,
calendar: &mut Calendar,
is_update: bool,
properties: Vec<DavPropertyValue>,
items: &mut PropStatBuilder,
) -> bool;
fn apply_event_properties(
&self,
event: &mut CalendarEvent,
is_update: bool,
properties: Vec<DavPropertyValue>,
items: &mut PropStatBuilder,
) -> bool;
}
impl CalendarPropPatchRequestHandler for Server {
async fn handle_calendar_proppatch_request(
&self,
access_token: &AccessToken,
headers: &RequestHeaders<'_>,
mut request: PropertyUpdate,
) -> crate::Result<HttpResponse> {
// Validate URI
let resource_ = self
.validate_uri(access_token, headers.uri)
.await?
.into_owned_uri()?;
let uri = headers.uri;
let account_id = resource_.account_id;
let resources = self
.fetch_dav_resources(
access_token.account_id(),
account_id,
SyncCollection::Calendar,
)
.await
.caused_by(trc::location!())?;
let resource = resource_
.resource
.and_then(|r| resources.by_path(r))
.ok_or(DavError::Code(StatusCode::NOT_FOUND))?;
let document_id = resource.document_id();
let collection = if resource.is_container() {
Collection::Calendar
} else {
Collection::CalendarEvent
};
if !request.has_changes() {
return Ok(HttpResponse::new(StatusCode::NO_CONTENT));
}
// Verify ACL
if !access_token.is_member(account_id) {
let (acl, document_id) = if resource.is_container() {
(Acl::Modify, resource.document_id())
} else {
(Acl::ModifyItems, resource.parent_id().unwrap())
};
if !resources.has_access_to_container(access_token, document_id, acl) {
return Err(DavError::Code(StatusCode::FORBIDDEN));
}
}
// Fetch archive
let archive = self
.store()
.get_value::<Archive<AlignedBytes>>(ValueKey::archive(
account_id,
collection,
document_id,
))
.await
.caused_by(trc::location!())?
.ok_or(DavError::Code(StatusCode::NOT_FOUND))?;
// Validate headers
self.validate_headers(
access_token,
headers,
vec![ResourceState {
account_id,
collection,
document_id: document_id.into(),
etag: archive.etag().into(),
path: resource_.resource.unwrap(),
..Default::default()
}],
Default::default(),
DavMethod::PROPPATCH,
)
.await?;
let is_success;
let mut batch = BatchBuilder::new();
let mut items = PropStatBuilder::default();
let etag = if resource.is_container() {
// Deserialize
let calendar = archive
.to_unarchived::<Calendar>()
.caused_by(trc::location!())?;
let mut new_calendar = archive
.deserialize::<Calendar>()
.caused_by(trc::location!())?;
let personal_id = access_token.personal_id(account_id, Collection::Calendar);
// Remove properties
if !request.set_first && !request.remove.is_empty() {
remove_calendar_properties(
personal_id,
&mut new_calendar,
std::mem::take(&mut request.remove),
&mut items,
);
}
// Set properties
is_success = self.apply_calendar_properties(
personal_id,
&mut new_calendar,
true,
request.set,
&mut items,
);
// Remove properties
if is_success && !request.remove.is_empty() {
remove_calendar_properties(
personal_id,
&mut new_calendar,
request.remove,
&mut items,
);
}
if is_success {
new_calendar
.update(
access_token.account_tenant_ids(),
calendar,
account_id,
document_id,
&mut batch,
)
.caused_by(trc::location!())?
.etag()
} else {
calendar.etag().into()
}
} else {
// Deserialize
let event = archive
.to_unarchived::<CalendarEvent>()
.caused_by(trc::location!())?;
let mut new_event = archive
.deserialize::<CalendarEvent>()
.caused_by(trc::location!())?;
// Remove properties
if !request.set_first && !request.remove.is_empty() {
remove_event_properties(
&mut new_event,
std::mem::take(&mut request.remove),
&mut items,
);
}
// Set properties
is_success = self.apply_event_properties(&mut new_event, true, request.set, &mut items);
// Remove properties
if is_success && !request.remove.is_empty() {
remove_event_properties(&mut new_event, request.remove, &mut items);
}
if is_success {
new_event
.update(
access_token.account_tenant_ids(),
event,
account_id,
document_id,
&mut batch,
)
.caused_by(trc::location!())?
.etag()
} else {
event.etag().into()
}
};
if is_success {
self.commit_batch(batch).await.caused_by(trc::location!())?;
}
if headers.ret != Return::Minimal || !is_success {
Ok(HttpResponse::new(StatusCode::MULTI_STATUS)
.with_xml_body(
MultiStatus::new(vec![Response::new_propstat(uri, items.build())])
.with_namespace(Namespace::CalDav)
.to_string(),
)
.with_etag_opt(etag))
} else {
Ok(HttpResponse::new(StatusCode::NO_CONTENT).with_etag_opt(etag))
}
}
fn apply_calendar_properties(
&self,
personal_id: u32,
calendar: &mut Calendar,
is_update: bool,
properties: Vec<DavPropertyValue>,
items: &mut PropStatBuilder,
) -> bool {
let mut has_errors = false;
for property in properties {
match (&property.property, property.value) {
(DavProperty::WebDav(WebDavProperty::DisplayName), DavValue::String(name)) => {
if name.len() <= self.core.groupware.live_property_size {
calendar.preferences_mut(personal_id).name = name;
items.insert_ok(property.property);
} else {
items.insert_error_with_description(
property.property,
StatusCode::INSUFFICIENT_STORAGE,
"Property value is too long",
);
has_errors = true;
}
}
(
DavProperty::CalDav(CalDavProperty::CalendarDescription),
DavValue::String(name),
) => {
if name.len() <= self.core.groupware.live_property_size {
calendar.preferences_mut(personal_id).description = Some(name);
items.insert_ok(property.property);
} else {
items.insert_error_with_description(
property.property,
StatusCode::INSUFFICIENT_STORAGE,
"Property value is too long",
);
has_errors = true;
}
}
(
DavProperty::CalDav(CalDavProperty::CalendarTimezone),
DavValue::ICalendar(ical),
) => {
if ical.size() > self.core.groupware.max_ical_size {
items.insert_error_with_description(
property.property,
StatusCode::INSUFFICIENT_STORAGE,
"Property value is too long",
);
has_errors = true;
} else if !ical.is_timezone() {
items.insert_precondition_failed_with_description(
property.property,
StatusCode::PRECONDITION_FAILED,
CalCondition::ValidCalendarData,
"Invalid calendar timezone",
);
has_errors = true;
} else {
calendar.preferences_mut(personal_id).time_zone = Timezone::Custom(ical);
items.insert_ok(property.property);
}
}
(DavProperty::CalDav(CalDavProperty::TimezoneId), DavValue::String(tz_id)) => {
if let Ok(tz) = Tz::from_str(&tz_id) {
calendar.preferences_mut(personal_id).time_zone =
Timezone::IANA(tz.as_id());
items.insert_ok(property.property);
} else {
items.insert_precondition_failed_with_description(
property.property,
StatusCode::PRECONDITION_FAILED,
CalCondition::ValidTimezone,
"Invalid timezone ID",
);
has_errors = true;
}
}
(DavProperty::WebDav(WebDavProperty::CreationDate), DavValue::Timestamp(dt)) => {
calendar.created = dt;
items.insert_ok(property.property);
}
(
DavProperty::WebDav(WebDavProperty::ResourceType),
DavValue::ResourceTypes(types),
) => {
if !types
.0
.iter()
.all(|rt| matches!(rt, ResourceType::Collection | ResourceType::Calendar))
{
items.insert_precondition_failed(
property.property,
StatusCode::FORBIDDEN,
BaseCondition::ValidResourceType,
);
has_errors = true;
} else {
items.insert_ok(property.property);
}
}
(
DavProperty::CalDav(CalDavProperty::SupportedCalendarComponentSet),
DavValue::Components(components),
) => {
if !is_update {
calendar.supported_components = Bitmap::<SupportedComponent>::from_iter(
components
.0
.into_iter()
.map(|v| SupportedComponent::from(v.0)),
)
.into_inner();
if calendar.supported_components != 0 {
items.insert_ok(property.property);
} else {
items.insert_precondition_failed_with_description(
property.property,
StatusCode::PRECONDITION_FAILED,
CalCondition::SupportedCalendarComponent,
"At least one supported component must be specified",
);
has_errors = true;
}
} else {
items.insert_precondition_failed_with_description(
property.property,
StatusCode::PRECONDITION_FAILED,
CalCondition::SupportedCalendarComponent,
"Property cannot be modified",
);
has_errors = true;
}
}
(DavProperty::DeadProperty(dead), DavValue::DeadProperty(values))
if self.core.groupware.dead_property_size.is_some() =>
{
if is_update {
calendar.dead_properties.remove_element(dead);
}
if calendar.dead_properties.size() + values.size() + dead.size()
< self.core.groupware.dead_property_size.unwrap()
{
calendar.dead_properties.add_element(dead.clone(), values.0);
items.insert_ok(property.property);
} else {
items.insert_error_with_description(
property.property,
StatusCode::INSUFFICIENT_STORAGE,
"Property value is too long",
);
has_errors = true;
}
}
(_, DavValue::Null) => {
items.insert_ok(property.property);
}
_ => {
items.insert_error_with_description(
property.property,
StatusCode::CONFLICT,
"Property cannot be modified",
);
has_errors = true;
}
}
}
!has_errors
}
fn apply_event_properties(
&self,
event: &mut CalendarEvent,
is_update: bool,
properties: Vec<DavPropertyValue>,
items: &mut PropStatBuilder,
) -> bool {
let mut has_errors = false;
for property in properties {
match (&property.property, property.value) {
(DavProperty::WebDav(WebDavProperty::DisplayName), DavValue::String(name)) => {
if name.len() <= self.core.groupware.live_property_size {
event.display_name = Some(name);
items.insert_ok(property.property);
} else {
items.insert_error_with_description(
property.property,
StatusCode::INSUFFICIENT_STORAGE,
"Property value is too long",
);
has_errors = true;
}
}
(DavProperty::WebDav(WebDavProperty::CreationDate), DavValue::Timestamp(dt)) => {
event.created = dt;
items.insert_ok(property.property);
}
(DavProperty::DeadProperty(dead), DavValue::DeadProperty(values))
if self.core.groupware.dead_property_size.is_some() =>
{
if is_update {
event.dead_properties.remove_element(dead);
}
if event.dead_properties.size() + values.size() + dead.size()
< self.core.groupware.dead_property_size.unwrap()
{
event.dead_properties.add_element(dead.clone(), values.0);
items.insert_ok(property.property);
} else {
items.insert_error_with_description(
property.property,
StatusCode::INSUFFICIENT_STORAGE,
"Property value is too long",
);
has_errors = true;
}
}
(_, DavValue::Null) => {
items.insert_ok(property.property);
}
_ => {
items.insert_error_with_description(
property.property,
StatusCode::CONFLICT,
"Property cannot be modified",
);
has_errors = true;
}
}
}
!has_errors
}
}
fn remove_event_properties(
event: &mut CalendarEvent,
properties: Vec<DavProperty>,
items: &mut PropStatBuilder,
) {
for property in properties {
match &property {
DavProperty::WebDav(WebDavProperty::DisplayName) => {
event.display_name = None;
items.insert_with_status(property, StatusCode::NO_CONTENT);
}
DavProperty::DeadProperty(dead) => {
event.dead_properties.remove_element(dead);
items.insert_with_status(property, StatusCode::NO_CONTENT);
}
_ => {
items.insert_error_with_description(
property,
StatusCode::CONFLICT,
"Property cannot be deleted",
);
}
}
}
}
fn remove_calendar_properties(
personal_id: u32,
calendar: &mut Calendar,
properties: Vec<DavProperty>,
items: &mut PropStatBuilder,
) {
for property in properties {
match &property {
DavProperty::CalDav(CalDavProperty::CalendarDescription) => {
calendar.preferences_mut(personal_id).description = None;
items.insert_with_status(property, StatusCode::NO_CONTENT);
}
DavProperty::CalDav(CalDavProperty::CalendarTimezone)
| DavProperty::CalDav(CalDavProperty::TimezoneId) => {
calendar.preferences_mut(personal_id).time_zone = Timezone::Default;
items.insert_with_status(property, StatusCode::NO_CONTENT);
}
DavProperty::DeadProperty(dead) => {
calendar.dead_properties.remove_element(dead);
items.insert_with_status(property, StatusCode::NO_CONTENT);
}
_ => {
items.insert_error_with_description(
property,
StatusCode::CONFLICT,
"Property cannot be deleted",
);
}
}
}
}
+657
View File
@@ -0,0 +1,657 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use super::freebusy::freebusy_in_range;
use crate::{
DavError,
common::{
CalendarFilter, DavQuery,
propfind::{PropFindItem, PropFindRequestHandler},
uri::DavUriResource,
},
};
use calcard::{
common::{PartialDateTime, timezone::Tz},
icalendar::{
ArchivedICalendar, ArchivedICalendarComponent, ArchivedICalendarEntry,
ArchivedICalendarParameter, ArchivedICalendarProperty, ArchivedICalendarValue,
ICalendarComponentType, ICalendarEntry, ICalendarParameterName, ICalendarProperty,
ICalendarValue,
},
};
use common::{DavResource, Server, auth::AccessToken};
use dav_proto::{
RequestHeaders,
schema::{
property::{CalDavProperty, CalendarData, DavProperty},
request::{CalendarQuery, Filter, FilterOp, PropFind, Timezone},
response::MultiStatus,
},
};
use groupware::{
cache::GroupwareCache,
calendar::{ArchivedCalendarEvent, expand::CalendarEventExpansion},
};
use http_proto::HttpResponse;
use hyper::StatusCode;
use std::{fmt::Write, slice::Iter, str::FromStr};
use store::{
ahash::{AHashMap, AHashSet},
write::serialize::rkyv_deserialize,
};
use trc::AddContext;
use types::{TimeRange, acl::Acl, collection::SyncCollection};
pub(crate) trait CalendarQueryRequestHandler: Sync + Send {
fn handle_calendar_query_request(
&self,
access_token: &AccessToken,
headers: &RequestHeaders<'_>,
request: CalendarQuery,
) -> impl Future<Output = crate::Result<HttpResponse>> + Send;
}
impl CalendarQueryRequestHandler for Server {
async fn handle_calendar_query_request(
&self,
access_token: &AccessToken,
headers: &RequestHeaders<'_>,
request: CalendarQuery,
) -> crate::Result<HttpResponse> {
// Validate URI
let resource_ = self
.validate_uri(access_token, headers.uri)
.await?
.into_owned_uri()?;
let account_id = resource_.account_id;
let resources = self
.fetch_dav_resources(
access_token.account_id(),
account_id,
SyncCollection::Calendar,
)
.await
.caused_by(trc::location!())?;
let Some(resource) = resources.by_path(
resource_
.resource
.ok_or(DavError::Code(StatusCode::METHOD_NOT_ALLOWED))?,
) else {
return Ok(HttpResponse::new(StatusCode::MULTI_STATUS)
.with_xml_body(MultiStatus::not_found(headers.uri).to_string()));
};
if !resource.is_container() {
return Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED));
}
// Obtain shared ids
let shared_ids = if !access_token.is_member(account_id) {
resources
.shared_items(access_token, [Acl::ReadItems], false)
.into()
} else {
None
};
// Pre-filter by date range
let filter_range = extract_filter_range(&request);
// Obtain document ids in folder
let mut items = Vec::with_capacity(16);
for resource in resources.children(resource.document_id()) {
if shared_ids
.as_ref()
.is_none_or(|ids| ids.contains(resource.document_id()))
&& filter_range
.as_ref()
.is_none_or(|range| is_resource_in_time_range(resource.resource, range))
{
items.push(PropFindItem::new(
resources.format_resource(resource),
account_id,
resource,
));
}
}
// Extract the time range from the request
let max_time_range = extract_data_range(&request.properties, filter_range);
self.handle_dav_query(
access_token,
DavQuery::calendar_query(request, max_time_range, items, headers),
)
.await
}
}
pub(crate) fn is_resource_in_time_range(resource: &DavResource, filter: &TimeRange) -> bool {
// Check whether the resource has a time range and if it overlaps with the filter
if let Some((start, end)) = resource.event_time_range() {
((filter.start < end) || (filter.start <= start))
&& (filter.end > start || filter.end >= end)
} else {
// If the resource does not have a time range, it is not in the range
false
}
}
fn extract_filter_range(query: &CalendarQuery) -> Option<TimeRange> {
let mut range = TimeRange {
start: i64::MAX,
end: i64::MIN,
};
for filter in &query.filters {
let op = match filter {
Filter::Component { op, .. } => op,
Filter::Property { op, .. } => op,
Filter::Parameter { op, .. } => op,
_ => continue,
};
if let FilterOp::TimeRange(date_range) = op {
if date_range.start < range.start {
range.start = date_range.start;
}
if date_range.end > range.end {
range.end = date_range.end;
}
}
}
if range.start != i64::MAX {
Some(range)
} else {
None
}
}
fn extract_data_range(propfind: &PropFind, filter_range: Option<TimeRange>) -> Option<TimeRange> {
let props = match propfind {
PropFind::AllProp(props) | PropFind::Prop(props) => props,
PropFind::PropName => &[][..],
};
for prop in props {
if let DavProperty::CalDav(CalDavProperty::CalendarData(data)) = prop {
let mut range = filter_range.unwrap_or(TimeRange {
start: i64::MAX,
end: i64::MIN,
});
for data_range in [&data.expand, &data.limit_recurrence, &data.limit_freebusy]
.into_iter()
.flatten()
{
if data_range.start < range.start {
range.start = data_range.start;
}
if data_range.end > range.end {
range.end = data_range.end;
}
}
return if range.start != i64::MAX {
Some(range)
} else {
None
};
}
}
filter_range
}
pub fn try_parse_tz(tz: &Timezone) -> Option<Tz> {
match tz {
Timezone::Name(value) | Timezone::Id(value) => Tz::from_str(value).ok(),
Timezone::None => None,
}
}
pub(crate) struct CalendarQueryHandler {
default_tz: Tz,
expanded_times: Vec<CalendarEventExpansion>,
}
impl CalendarQueryHandler {
pub fn new(
event: &ArchivedCalendarEvent,
max_time_range: Option<TimeRange>,
default_tz: Tz,
) -> Self {
Self {
default_tz,
expanded_times: max_time_range
.map(|max_time_range| {
event
.data
.expand(default_tz, max_time_range)
.unwrap_or_else(|| {
trc::event!(
Calendar(trc::CalendarEvent::RuleExpansionError),
Reason = "chrono error",
Details = event.data.event.to_string(),
);
vec![]
})
})
.unwrap_or_default(),
}
}
pub fn filter(&mut self, event: &ArchivedCalendarEvent, filters: &CalendarFilter) -> bool {
let ical = &event.data.event;
let mut is_all = true;
let mut matches_one = false;
for filter in filters {
match filter {
Filter::AnyOf => {
is_all = false;
}
Filter::AllOf => {
is_all = true;
}
Filter::Property { prop, op, comp } => {
let mut properties = find_components(ical, comp)
.flat_map(|(_, comp)| find_properties(comp, prop))
.peekable();
let result = if properties.peek().is_some() {
properties.any(|entry| {
match op {
FilterOp::Exists => true,
FilterOp::Undefined => false,
FilterOp::TextMatch(text_match) => {
let mut matched_any = false;
for value in entry.values.iter() {
if let Some(text) = value.as_text()
&& text_match.matches(text)
{
matched_any = true;
break;
}
}
matched_any
}
FilterOp::TimeRange(range) => {
if let Some(ArchivedICalendarValue::PartialDateTime(date)) =
entry.values.first()
{
let tz = entry
.tz_id()
.and_then(|tz_id| Tz::from_str(tz_id).ok())
.unwrap_or(self.default_tz);
if let Some(date) = date
.to_date_time()
.and_then(|date| date.to_date_time_with_tz(tz))
{
let timestamp = date.timestamp();
// RFC4791#9.9: start <= DTSTART AND end > DTSTART
range.start <= timestamp && range.end > timestamp
} else {
false
}
} else {
false
}
}
}
})
} else {
matches!(op, FilterOp::Undefined)
};
if result {
matches_one = true;
} else if is_all {
return false;
}
}
Filter::Parameter {
prop,
param,
op,
comp,
} => {
let mut parameters = find_components(ical, comp)
.flat_map(|(_, comp)| {
find_properties(comp, prop)
.filter_map(|entry| find_parameter(entry, param))
})
.peekable();
let result = if parameters.peek().is_some() {
parameters.any(|entry| match op {
FilterOp::Exists => true,
FilterOp::Undefined => false,
FilterOp::TextMatch(text_match) => {
if let Some(text) = entry.value.as_text() {
text_match.matches(text)
} else {
false
}
}
FilterOp::TimeRange(_) => false,
})
} else {
matches!(op, FilterOp::Undefined)
};
if result {
matches_one = true;
} else if is_all {
return false;
}
}
Filter::Component { comp, op } => {
let result = match op {
FilterOp::Exists => find_components(ical, comp).next().is_some(),
FilterOp::Undefined => find_components(ical, comp).next().is_none(),
FilterOp::TimeRange(range) => {
if !matches!(comp.last(), Some(ICalendarComponentType::VAlarm)) {
let matching_comp_ids = find_components(ical, comp)
.map(|(id, comp)| (id as u32, &comp.component_type))
.collect::<AHashMap<_, _>>();
!matching_comp_ids.is_empty()
&& self.expanded_times.iter().any(|event| {
matching_comp_ids.get(&event.comp_id).is_some_and(|ct| {
range.is_in_range(
ct == &&ICalendarComponentType::VTodo,
event.start,
event.end,
)
})
})
} else {
let matching_comp_ids = event
.data
.alarms
.iter()
.map(|alarm| alarm.parent_id.to_native() as u32)
.collect::<AHashSet<_>>();
!matching_comp_ids.is_empty()
&& self.expanded_times.iter().any(|time| {
matching_comp_ids.contains(&time.comp_id)
&& event.data.alarms.iter().any(|alarm| {
alarm.parent_id.to_native() as u32 == time.comp_id
&& alarm
.delta
.to_timestamp(
time.start,
time.end,
self.default_tz,
)
.is_some_and(|timestamp| {
range.is_in_range(
false, timestamp, timestamp,
)
})
})
})
}
}
FilterOp::TextMatch(_) => false,
};
if result {
matches_one = true;
} else if is_all {
return false;
}
}
}
}
is_all || matches_one
}
pub fn serialize_ical(
&mut self,
event: &ArchivedCalendarEvent,
data: &CalendarData,
instances_limit: &mut usize,
) -> Option<String> {
let mut out = String::with_capacity(event.size.to_native() as usize);
let _v = [0.into()];
let mut component_iter: Iter<'_, rkyv::rend::u32_le> = _v.iter();
let mut component_stack: Vec<(&ArchivedICalendarComponent, Iter<'_, rkyv::rend::u32_le>)> =
Vec::with_capacity(4);
if data.expand.is_some() {
self.expanded_times.sort_unstable_by_key(|a| a.start);
}
loop {
if let Some(component_id) = component_iter.next() {
let component_id = component_id.to_native();
let component = event
.data
.event
.components
.get(component_id as usize)
.unwrap();
// Limit recurrence override
if let Some(limit_recurrence) = &data.limit_recurrence
&& component.is_recurrence_override()
&& !self.expanded_times.iter().any(|event| {
event.comp_id == component_id
&& limit_recurrence.is_in_range(
component.component_type == ICalendarComponentType::VTodo,
event.start,
event.end,
)
})
{
continue;
}
// Limit freebusy
if let Some(limit_recurrence) = &data.limit_freebusy
&& component.component_type == ICalendarComponentType::VFreebusy
&& !self.expanded_times.iter().any(|event| {
event.comp_id == component_id
&& limit_recurrence.is_in_range(false, event.start, event.end)
})
{
continue;
}
// Filter entries
let mut entries = component
.entries
.iter()
.filter_map(|entry| {
if data.properties.is_empty()
|| component.component_type == ICalendarComponentType::VCalendar
{
Some((entry, true))
} else {
data.properties
.iter()
.find(|prop| {
prop.component.as_ref().is_none_or(|comp| {
comp == &component.component_type
|| component_stack.iter().any(|(parent_comp, _)| {
comp == &parent_comp.component_type
})
}) && prop.name.as_ref().is_none_or(|name| name == &entry.name)
})
.map(|prop| (entry, !prop.no_value))
}
})
.peekable();
// Expand recurrences
let component_name = component.component_type.as_str();
if let Some(expand) = &data
.expand
.filter(|_| component.component_type.has_time_ranges())
{
let is_recurrent = component.is_recurrent();
let is_recurrent_or_override =
is_recurrent || component.is_recurrence_override();
let is_todo = component.component_type == ICalendarComponentType::VTodo;
let mut has_duration = false;
let entries = entries
.filter(|(entry, _)| match &entry.name {
ArchivedICalendarProperty::Dtstart
| ArchivedICalendarProperty::Dtend
| ArchivedICalendarProperty::Exdate
| ArchivedICalendarProperty::Exrule
| ArchivedICalendarProperty::Rdate
| ArchivedICalendarProperty::Rrule
| ArchivedICalendarProperty::RecurrenceId => false,
ArchivedICalendarProperty::Due
| ArchivedICalendarProperty::Completed
| ArchivedICalendarProperty::Created => is_recurrent,
ArchivedICalendarProperty::Duration => {
has_duration = true;
true
}
_ => true,
})
.collect::<Vec<_>>();
for event in &self.expanded_times {
if event.comp_id == component_id
&& (!is_recurrent_or_override
|| expand.is_in_range(is_todo, event.start, event.end))
{
if *instances_limit > 0 {
*instances_limit -= 1;
} else {
return None;
}
let _ = write!(&mut out, "BEGIN:{component_name}\r\n");
// Write DTSTART, DTEND and RECURRENCE-ID
let mut entry = ICalendarEntry {
name: ICalendarProperty::Dtstart,
params: vec![],
values: vec![ICalendarValue::PartialDateTime(Box::new(
PartialDateTime::from_utc_timestamp(event.start),
))],
};
let _ = entry.write_to(&mut out);
if is_recurrent_or_override {
entry.name = ICalendarProperty::RecurrenceId;
let _ = entry.write_to(&mut out);
}
if !has_duration {
entry.name = ICalendarProperty::Dtend;
entry.values = vec![ICalendarValue::PartialDateTime(Box::new(
PartialDateTime::from_utc_timestamp(event.end),
))];
let _ = entry.write_to(&mut out);
}
// Write other component entries
for (entry, with_value) in &entries {
let _ = entry.write_to(&mut out, *with_value);
}
let _ = write!(&mut out, "END:{component_name}\r\n");
}
}
} else if entries.peek().is_some()
|| (component.component_type == ICalendarComponentType::VCalendar
&& !component.component_ids.is_empty())
{
let _ = write!(&mut out, "BEGIN:{component_name}\r\n");
match data.limit_freebusy {
Some(range)
if component.component_type == ICalendarComponentType::VFreebusy =>
{
// Filter freebusy
for (entry, with_value) in entries {
if matches!(entry.name, ArchivedICalendarProperty::Freebusy) {
let mut fb_in_range =
freebusy_in_range(entry, &range, self.default_tz)
.peekable();
if fb_in_range.peek().is_none() {
continue;
} else {
let _ = ICalendarEntry {
name: ICalendarProperty::Freebusy,
params: rkyv_deserialize(&entry.params)
.ok()
.unwrap_or_default(),
values: fb_in_range.collect(),
}
.write_to(&mut out);
}
} else {
let _ = entry.write_to(&mut out, with_value);
}
}
}
_ => {
for (entry, with_value) in entries {
let _ = entry.write_to(&mut out, with_value);
}
}
}
if !component.component_ids.is_empty() {
component_stack.push((component, component_iter));
component_iter = component.component_ids.iter();
} else if component.component_ids.is_empty() {
let _ = write!(&mut out, "END:{component_name}\r\n");
}
}
} else if let Some((component, iter)) = component_stack.pop() {
let _ = write!(&mut out, "END:{}\r\n", component.component_type.as_str());
component_iter = iter;
} else {
break;
}
}
Some(out)
}
pub fn into_expanded_times(self) -> Vec<CalendarEventExpansion> {
self.expanded_times
}
}
#[inline(always)]
fn find_components<'x>(
ical: &'x ArchivedICalendar,
comp: &[ICalendarComponentType],
) -> impl Iterator<Item = (usize, &'x ArchivedICalendarComponent)> {
// TODO: Properly expand the component type path
let comp = comp.last().unwrap_or(&ICalendarComponentType::VCalendar);
ical.components
.iter()
.enumerate()
.filter(move |(_, entry)| {
comp == &ICalendarComponentType::VCalendar || &entry.component_type == comp
})
}
#[inline(always)]
fn find_properties<'x>(
comp: &'x ArchivedICalendarComponent,
prop: &ICalendarProperty,
) -> impl Iterator<Item = &'x ArchivedICalendarEntry> {
comp.entries.iter().filter(move |entry| &entry.name == prop)
}
#[inline(always)]
fn find_parameter<'x>(
entry: &'x ArchivedICalendarEntry,
name: &ICalendarParameterName,
) -> Option<&'x ArchivedICalendarParameter> {
entry.params.iter().find(|param| param.name == *name)
}
+438
View File
@@ -0,0 +1,438 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::{
DavError, DavErrorCondition, DavMethod,
calendar::freebusy::CalendarFreebusyRequestHandler,
common::{
ETag,
lock::{LockRequestHandler, ResourceState},
uri::DavUriResource,
},
};
use calcard::{
Entry, Parser,
icalendar::{
ICalendarComponentType, ICalendarEntry, ICalendarMethod, ICalendarProperty, ICalendarValue,
Uri,
},
};
use common::{Server, auth::AccessToken};
use dav_proto::{
RequestHeaders,
schema::{
property::Rfc1123DateTime,
request::FreeBusyQuery,
response::{CalCondition, Href, ScheduleResponse, ScheduleResponseItem},
},
};
use groupware::{
DestroyArchive, cache::GroupwareCache, calendar::CalendarEventNotification, strip_mailto_scheme,
};
use http_proto::HttpResponse;
use hyper::StatusCode;
use store::{
ValueKey,
write::{AlignedBytes, Archive},
};
use store::{ahash::AHashMap, write::BatchBuilder};
use trc::AddContext;
use types::collection::{Collection, SyncCollection};
use utils::sanitize_email;
pub(crate) trait CalendarEventNotificationHandler: Sync + Send {
fn handle_scheduling_get_request(
&self,
access_token: &AccessToken,
headers: &RequestHeaders<'_>,
is_head: bool,
) -> impl Future<Output = crate::Result<HttpResponse>> + Send;
fn handle_scheduling_delete_request(
&self,
access_token: &AccessToken,
headers: &RequestHeaders<'_>,
) -> impl Future<Output = crate::Result<HttpResponse>> + Send;
fn handle_scheduling_post_request(
&self,
access_token: &AccessToken,
headers: &RequestHeaders<'_>,
bytes: Vec<u8>,
) -> impl Future<Output = crate::Result<HttpResponse>> + Send;
}
impl CalendarEventNotificationHandler for Server {
async fn handle_scheduling_get_request(
&self,
access_token: &AccessToken,
headers: &RequestHeaders<'_>,
is_head: bool,
) -> crate::Result<HttpResponse> {
// Validate URI
let resource_ = self
.validate_uri(access_token, headers.uri)
.await?
.into_owned_uri()?;
let account_id = resource_.account_id;
let resources = self
.fetch_dav_resources(
access_token.account_id(),
account_id,
SyncCollection::CalendarEventNotification,
)
.await
.caused_by(trc::location!())?;
let resource = resources
.by_path(
resource_
.resource
.ok_or(DavError::Code(StatusCode::METHOD_NOT_ALLOWED))?,
)
.ok_or(DavError::Code(StatusCode::NOT_FOUND))?;
if resource.is_container() {
return Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED));
}
// Validate ACL
if !access_token.is_member(account_id) {
return Err(DavError::Code(StatusCode::FORBIDDEN));
}
// Fetch event
let event_ = self
.store()
.get_value::<Archive<AlignedBytes>>(ValueKey::archive(
account_id,
Collection::CalendarEventNotification,
resource.document_id(),
))
.await
.caused_by(trc::location!())?
.ok_or(DavError::Code(StatusCode::NOT_FOUND))?;
let event = event_
.unarchive::<CalendarEventNotification>()
.caused_by(trc::location!())?;
// Validate headers
let etag = event_.etag();
self.validate_headers(
access_token,
headers,
vec![ResourceState {
account_id,
collection: Collection::CalendarEventNotification,
document_id: resource.document_id().into(),
etag: etag.clone().into(),
path: resource_.resource.unwrap(),
..Default::default()
}],
Default::default(),
DavMethod::GET,
)
.await?;
let response = HttpResponse::new(StatusCode::OK)
.with_content_type("text/calendar; charset=utf-8")
.with_etag(etag)
.with_last_modified(Rfc1123DateTime::new(i64::from(event.modified)).to_string());
let ical = event.event.to_string();
if !is_head {
Ok(response.with_binary_body(ical))
} else {
Ok(response.with_content_length(ical.len()))
}
}
async fn handle_scheduling_delete_request(
&self,
access_token: &AccessToken,
headers: &RequestHeaders<'_>,
) -> crate::Result<HttpResponse> {
// Validate URI
let resource = self
.validate_uri(access_token, headers.uri)
.await?
.into_owned_uri()?;
let account_id = resource.account_id;
let delete_path = resource
.resource
.filter(|r| !r.is_empty())
.ok_or(DavError::Code(StatusCode::FORBIDDEN))?;
let resources = self
.fetch_dav_resources(
access_token.account_id(),
account_id,
SyncCollection::CalendarEventNotification,
)
.await
.caused_by(trc::location!())?;
// Check resource type
let resource = resources
.by_path(delete_path)
.ok_or(DavError::Code(StatusCode::NOT_FOUND))?;
if resource.is_container() {
return Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED));
}
// Validate ACL
if !access_token.is_member(account_id) {
return Err(DavError::Code(StatusCode::FORBIDDEN));
}
let document_id = resource.document_id();
let event_ = self
.store()
.get_value::<Archive<AlignedBytes>>(ValueKey::archive(
account_id,
Collection::CalendarEventNotification,
document_id,
))
.await
.caused_by(trc::location!())?
.ok_or(DavError::Code(StatusCode::NOT_FOUND))?;
// Validate headers
self.validate_headers(
access_token,
headers,
vec![ResourceState {
account_id,
collection: Collection::CalendarEventNotification,
document_id: document_id.into(),
etag: event_.etag().into(),
path: delete_path,
..Default::default()
}],
Default::default(),
DavMethod::DELETE,
)
.await?;
let event = event_
.to_unarchived::<CalendarEventNotification>()
.caused_by(trc::location!())?;
// Delete event
let mut batch = BatchBuilder::new();
DestroyArchive(event)
.delete(
access_token.account_tenant_ids(),
account_id,
document_id,
&mut batch,
)
.caused_by(trc::location!())?;
self.commit_batch(batch).await.caused_by(trc::location!())?;
Ok(HttpResponse::new(StatusCode::NO_CONTENT))
}
async fn handle_scheduling_post_request(
&self,
access_token: &AccessToken,
headers: &RequestHeaders<'_>,
bytes: Vec<u8>,
) -> crate::Result<HttpResponse> {
// Validate URI
let resource = self
.validate_uri(access_token, headers.uri)
.await?
.into_owned_uri()?;
if resource.resource.is_none_or(|r| r != "outbox") {
return Err(DavError::Code(StatusCode::FORBIDDEN));
}
// Parse iTIP message
if bytes.len() > self.core.groupware.max_ical_size {
return Err(DavError::Condition(DavErrorCondition::new(
StatusCode::PRECONDITION_FAILED,
CalCondition::MaxResourceSize(self.core.groupware.max_ical_size as u32),
)));
}
let itip_raw = std::str::from_utf8(&bytes).map_err(|_| {
DavError::Condition(
DavErrorCondition::new(
StatusCode::BAD_REQUEST,
CalCondition::ValidSchedulingMessage,
)
.with_details("Invalid UTF-8 in iCalendar data"),
)
})?;
let itip = match Parser::new(itip_raw).entry() {
Entry::ICalendar(ical) if ical.components.len() > 1 => ical,
_ => {
return Err(DavError::Condition(
DavErrorCondition::new(
StatusCode::BAD_REQUEST,
CalCondition::ValidSchedulingMessage,
)
.with_details("Failed to parse iCalendar data"),
));
}
};
// Parse request
let mut from_date = None;
let mut to_date = None;
let mut organizer = None;
let mut attendees = AHashMap::new();
let mut uid = None;
let tz_resolver = itip.build_tz_resolver();
let mut found_freebusy = false;
for component in &itip.components {
if component.component_type != ICalendarComponentType::VFreebusy {
continue;
} else if !found_freebusy {
found_freebusy = true;
} else {
return Err(DavError::Condition(
DavErrorCondition::new(
StatusCode::BAD_REQUEST,
CalCondition::ValidSchedulingMessage,
)
.with_details("Multiple VFREEBUSY components found"),
));
}
for entry in &component.entries {
let tz_id = entry.tz_id();
match (&entry.name, entry.values.first()) {
(ICalendarProperty::Dtstart, Some(ICalendarValue::PartialDateTime(dt))) => {
from_date = dt.to_date_time_with_tz(tz_resolver.resolve_or_default(tz_id));
}
(ICalendarProperty::Dtend, Some(ICalendarValue::PartialDateTime(dt))) => {
to_date = dt.to_date_time_with_tz(tz_resolver.resolve_or_default(tz_id));
}
(ICalendarProperty::Uid, Some(ICalendarValue::Text(_))) => {
uid = Some(entry);
}
(
ICalendarProperty::Organizer,
Some(ICalendarValue::Text(_) | ICalendarValue::Uri(Uri::Location(_))),
) => {
organizer = Some(entry);
}
(
ICalendarProperty::Attendee,
Some(
ICalendarValue::Text(value) | ICalendarValue::Uri(Uri::Location(value)),
),
) => {
if let Some(email) = sanitize_email(strip_mailto_scheme(value.as_str())) {
attendees.insert(email, entry);
}
}
_ => {}
}
}
}
let (Some(from_date), Some(to_date)) = (from_date, to_date) else {
return Err(DavError::Condition(
DavErrorCondition::new(
StatusCode::BAD_REQUEST,
CalCondition::ValidSchedulingMessage,
)
.with_details("Missing DTSTART or DTEND in VFREEBUSY component"),
));
};
let Some(organizer) = organizer else {
return Err(DavError::Condition(
DavErrorCondition::new(
StatusCode::BAD_REQUEST,
CalCondition::ValidSchedulingMessage,
)
.with_details("Missing ORGANIZER in VFREEBUSY component"),
));
};
if attendees.is_empty() {
return Err(DavError::Condition(
DavErrorCondition::new(
StatusCode::BAD_REQUEST,
CalCondition::ValidSchedulingMessage,
)
.with_details("Missing ATTENDEE in VFREEBUSY component"),
));
}
let mut response = ScheduleResponse::default();
for (email, attendee) in attendees {
if let Some(account_id) = self
.account_id_from_email(&email, true)
.await
.caused_by(trc::location!())?
{
let resources = self
.fetch_dav_resources(
access_token.account_id(),
account_id,
SyncCollection::Calendar,
)
.await
.caused_by(trc::location!())?;
if let Some(resource) = self
.core
.groupware
.default_calendar_name
.as_ref()
.and_then(|name| resources.by_path(name))
{
let mut free_busy = self
.build_freebusy_object(
access_token,
FreeBusyQuery::new(from_date.timestamp(), to_date.timestamp()),
&resources,
account_id,
resource,
)
.await?;
// Add iTIP method
free_busy.components[0].entries.push(ICalendarEntry {
name: ICalendarProperty::Method,
params: vec![],
values: vec![ICalendarValue::Method(ICalendarMethod::Reply)],
});
// Add properties
let component = &mut free_busy.components[1];
component.entries.push(organizer.clone());
component.entries.push(attendee.clone());
if let Some(uid) = uid {
component.entries.push(uid.clone());
}
response.items.0.push(ScheduleResponseItem {
recipient: Href(format!("mailto:{email}")),
request_status: "2.0;Success".into(),
calendar_data: Some(free_busy.to_string()),
});
} else {
response.items.0.push(ScheduleResponseItem {
recipient: Href(format!("mailto:{email}")),
request_status: "3.7;Default calendar not found".into(),
calendar_data: None,
});
}
} else {
response.items.0.push(ScheduleResponseItem {
recipient: Href(format!("mailto:{email}")),
request_status: "3.7;Invalid calendar user or insufficient permissions".into(),
calendar_data: None,
});
}
}
Ok(HttpResponse::new(StatusCode::OK).with_xml_body(response.to_string()))
}
}
+536
View File
@@ -0,0 +1,536 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use super::assert_is_unique_uid;
use crate::{
DavError, DavErrorCondition, DavMethod,
calendar::ItipPrecondition,
common::{
ETag, ExtractETag,
lock::{LockRequestHandler, ResourceState},
uri::DavUriResource,
},
file::DavFileResource,
fix_percent_encoding,
};
use calcard::{
Entry, Parser,
common::timezone::Tz,
icalendar::{ICalendar, ICalendarComponentType},
};
use common::{DavName, Server, auth::AccessToken};
use dav_proto::{
RequestHeaders, Return,
schema::{property::Rfc1123DateTime, response::CalCondition},
};
use groupware::{
cache::GroupwareCache,
calendar::{CalendarEvent, CalendarEventData, itip::ItipSendStatus},
scheduling::{
ItipMessages, event_create::itip_create, event_update::itip_update,
itip::itip_set_unreachable_status,
},
};
use http_proto::HttpResponse;
use hyper::StatusCode;
use std::collections::HashSet;
use store::write::{BatchBuilder, now};
use store::{
ValueKey,
write::{AlignedBytes, Archive},
};
use trc::AddContext;
use types::{
acl::Acl,
collection::{Collection, SyncCollection},
};
pub(crate) trait CalendarUpdateRequestHandler: Sync + Send {
fn handle_calendar_update_request(
&self,
access_token: &AccessToken,
headers: &RequestHeaders<'_>,
bytes: Vec<u8>,
is_patch: bool,
) -> impl Future<Output = crate::Result<HttpResponse>> + Send;
}
impl CalendarUpdateRequestHandler for Server {
async fn handle_calendar_update_request(
&self,
access_token: &AccessToken,
headers: &RequestHeaders<'_>,
bytes: Vec<u8>,
_is_patch: bool,
) -> crate::Result<HttpResponse> {
// Validate URI
let resource = self
.validate_uri(access_token, headers.uri)
.await?
.into_owned_uri()?;
let account_id = resource.account_id;
let resources = self
.fetch_dav_resources(
access_token.account_id(),
account_id,
SyncCollection::Calendar,
)
.await
.caused_by(trc::location!())?;
let resource_name = fix_percent_encoding(
resource
.resource
.ok_or(DavError::Code(StatusCode::CONFLICT))?,
);
if bytes.len() > self.core.groupware.max_ical_size {
return Err(DavError::Condition(DavErrorCondition::new(
StatusCode::PRECONDITION_FAILED,
CalCondition::MaxResourceSize(self.core.groupware.max_ical_size as u32),
)));
}
let ical_raw = std::str::from_utf8(&bytes).map_err(|_| {
DavError::Condition(
DavErrorCondition::new(
StatusCode::PRECONDITION_FAILED,
CalCondition::SupportedCalendarData,
)
.with_details("Invalid UTF-8 in iCalendar data"),
)
})?;
let ical = match Parser::new(ical_raw).entry() {
Entry::ICalendar(ical) => ical,
_ => {
return Err(DavError::Condition(
DavErrorCondition::new(
StatusCode::PRECONDITION_FAILED,
CalCondition::SupportedCalendarData,
)
.with_details("Failed to parse iCalendar data"),
));
}
};
let account_info = self
.scheduling_account_info(access_token.account_id(), account_id)
.await
.caused_by(trc::location!())?;
if let Some(resource) = resources.by_path(resource_name.as_ref()) {
if resource.is_container() {
return Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED));
}
// Validate ACL
let parent_id = resource.parent_id().unwrap();
let document_id = resource.document_id();
if !access_token.is_member(account_id)
&& !resources.has_access_to_container(access_token, parent_id, Acl::ModifyItems)
{
return Err(DavError::Code(StatusCode::FORBIDDEN));
}
// Update
let event_ = self
.store()
.get_value::<Archive<AlignedBytes>>(ValueKey::archive(
account_id,
Collection::CalendarEvent,
document_id,
))
.await
.caused_by(trc::location!())?
.ok_or(DavError::Code(StatusCode::NOT_FOUND))?;
let event = event_
.to_unarchived::<CalendarEvent>()
.caused_by(trc::location!())?;
// Validate headers
match self
.validate_headers(
access_token,
headers,
vec![ResourceState {
account_id,
collection: Collection::CalendarEvent,
document_id: Some(document_id),
etag: event.etag().into(),
path: resource_name.as_ref(),
..Default::default()
}],
Default::default(),
DavMethod::PUT,
)
.await
{
Ok(_) => {}
Err(DavError::Code(StatusCode::PRECONDITION_FAILED))
if headers.ret == Return::Representation =>
{
return Ok(HttpResponse::new(StatusCode::PRECONDITION_FAILED)
.with_content_type("text/calendar; charset=utf-8")
.with_etag(event.etag())
.with_last_modified(
Rfc1123DateTime::new(i64::from(event.inner.modified)).to_string(),
)
.with_header("Preference-Applied", "return=representation")
.with_binary_body(event.inner.data.event.to_string()));
}
Err(e) => return Err(e),
}
if ical == event.inner.data.event {
// No changes, return existing event
return Ok(HttpResponse::new(StatusCode::NO_CONTENT));
}
// Validate iCal
if event.inner.data.event.uids().next().unwrap_or_default() != validate_ical(&ical)? {
return Err(DavError::Condition(DavErrorCondition::new(
StatusCode::PRECONDITION_FAILED,
CalCondition::NoUidConflict(resources.format_resource(resource).into()),
)));
}
// Validate schedule tag
if headers.if_schedule_tag.is_some()
&& event.inner.schedule_tag.as_ref().map(|t| t.to_native())
!= headers.if_schedule_tag
{
return Err(DavError::Code(StatusCode::PRECONDITION_FAILED));
}
// Obtain previous alarm
let now = now() as i64;
let prev_email_alarm = event.inner.data.next_alarm(now, Tz::Floating);
// Build event
let mut next_email_alarm = None;
let mut new_event = event
.deserialize::<CalendarEvent>()
.caused_by(trc::location!())?;
let old_ical = new_event.data.event;
new_event.size = bytes.len() as u32;
new_event.data = CalendarEventData::new(
ical,
Tz::Floating,
self.core.groupware.max_ical_instances,
&mut next_email_alarm,
);
// Scheduling
let mut itip_messages = None;
let itip_status = ItipSendStatus::resolve(
self,
access_token,
&account_info,
new_event.data.event_range_end(),
);
if itip_status.is_send() {
let result = if new_event.schedule_tag.is_some() {
itip_update(
&mut new_event.data.event,
&old_ical,
account_info.addresses(),
)
} else {
itip_create(&mut new_event.data.event, account_info.addresses())
};
match result {
Ok(messages) => {
let mut is_organizer = false;
if messages
.iter()
.map(|r| {
is_organizer = r.from_organizer;
r.to.len()
})
.sum::<usize>()
< self.core.groupware.itip_outbound_max_recipients
{
// Only update schedule tag if the user is the organizer
if is_organizer {
if let Some(schedule_tag) = &mut new_event.schedule_tag {
*schedule_tag += 1;
} else {
new_event.schedule_tag = Some(1);
}
}
itip_messages = Some(ItipMessages::new(messages));
} else {
return Err(DavError::Condition(DavErrorCondition::new(
StatusCode::PRECONDITION_FAILED,
CalCondition::MaxAttendeesPerInstance,
)));
}
}
Err(err) => {
if let Some(failed_precondition) = err.failed_precondition() {
return Err(DavError::Condition(
DavErrorCondition::new(
StatusCode::PRECONDITION_FAILED,
failed_precondition,
)
.with_details(err.to_string()),
));
}
trc::event!(
Calendar(trc::CalendarEvent::ItipMessageError),
AccountId = account_id,
DocumentId = document_id,
Reason = err.to_string(),
);
// Event changed, but there are no iTIP messages to send
if let Some(schedule_tag) = &mut new_event.schedule_tag {
*schedule_tag += 1;
}
}
}
itip_set_unreachable_status(&mut new_event.data.event, account_info.addresses());
} else if let Some(reason) = itip_status.reason() {
trc::event!(
Calendar(trc::CalendarEvent::ItipMessageError),
AccountId = account_id,
DocumentId = document_id,
Reason = reason,
);
}
// Validate quota
let extra_bytes =
(bytes.len() as u64).saturating_sub(u32::from(event.inner.size) as u64);
if extra_bytes > 0 {
self.has_available_quota(self.account(account_id).await?.as_ref(), extra_bytes)
.await?;
}
// Prepare write batch
let mut batch = BatchBuilder::new();
let schedule_tag = new_event.schedule_tag;
let etag = new_event
.update(
access_token.account_tenant_ids(),
event,
account_id,
document_id,
&mut batch,
)
.caused_by(trc::location!())?
.etag();
if prev_email_alarm != next_email_alarm {
if let Some(prev_alarm) = prev_email_alarm {
prev_alarm.delete_task(&mut batch);
}
if let Some(next_alarm) = next_email_alarm {
next_alarm.write_task(&mut batch);
}
}
if let Some(itip_messages) = itip_messages {
itip_messages
.queue(&mut batch)
.caused_by(trc::location!())?;
}
self.commit_batch(batch).await.caused_by(trc::location!())?;
self.notify_task_queue();
Ok(HttpResponse::new(StatusCode::NO_CONTENT)
.with_etag_opt(etag)
.with_schedule_tag_opt(schedule_tag))
} else if let Some((Some(parent), name)) = resources.map_parent(resource_name.as_ref()) {
if !parent.is_container() {
return Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED));
}
// Validate ACL
if !access_token.is_member(account_id)
&& !resources.has_access_to_container(
access_token,
parent.document_id(),
Acl::AddItems,
)
{
return Err(DavError::Code(StatusCode::FORBIDDEN));
}
// Validate headers
self.validate_headers(
access_token,
headers,
vec![ResourceState {
account_id,
collection: resource.collection,
document_id: Some(u32::MAX),
path: resource_name.as_ref(),
..Default::default()
}],
Default::default(),
DavMethod::PUT,
)
.await?;
// Validate ical object
assert_is_unique_uid(
self,
&resources,
account_id,
parent.document_id(),
validate_ical(&ical)?.into(),
)
.await?;
// Build event
let mut next_email_alarm = None;
let mut event = CalendarEvent {
names: vec![DavName {
name: name.to_string(),
parent_id: parent.document_id(),
}],
data: CalendarEventData::new(
ical,
Tz::Floating,
self.core.groupware.max_ical_instances,
&mut next_email_alarm,
),
size: bytes.len() as u32,
..Default::default()
};
// Scheduling
let mut itip_messages = None;
let itip_status = ItipSendStatus::resolve(
self,
access_token,
&account_info,
event.data.event_range_end(),
);
if itip_status.is_send() {
match itip_create(&mut event.data.event, account_info.addresses()) {
Ok(messages) => {
if messages.iter().map(|r| r.to.len()).sum::<usize>()
< self.core.groupware.itip_outbound_max_recipients
{
event.schedule_tag = Some(1);
itip_messages = Some(ItipMessages::new(messages));
} else {
return Err(DavError::Condition(DavErrorCondition::new(
StatusCode::PRECONDITION_FAILED,
CalCondition::MaxAttendeesPerInstance,
)));
}
}
Err(err) => {
if let Some(failed_precondition) = err.failed_precondition() {
return Err(DavError::Condition(
DavErrorCondition::new(
StatusCode::PRECONDITION_FAILED,
failed_precondition,
)
.with_details(err.to_string()),
));
}
trc::event!(
Calendar(trc::CalendarEvent::ItipMessageError),
AccountId = account_id,
Reason = err.to_string(),
);
}
}
itip_set_unreachable_status(&mut event.data.event, account_info.addresses());
} else if let Some(reason) = itip_status.reason() {
trc::event!(
Calendar(trc::CalendarEvent::ItipMessageError),
AccountId = account_id,
Reason = reason,
);
}
// Validate quota
if !bytes.is_empty() {
self.has_available_quota(
self.account(account_id).await?.as_ref(),
bytes.len() as u64,
)
.await?;
}
// Prepare write batch
let mut batch = BatchBuilder::new();
let document_id = self
.store()
.assign_document_ids(account_id, Collection::CalendarEvent, 1)
.await
.caused_by(trc::location!())?;
let schedule_tag = event.schedule_tag;
let etag = event
.insert(
access_token.account_tenant_ids(),
account_id,
document_id,
next_email_alarm,
&mut batch,
)
.caused_by(trc::location!())?
.etag();
if let Some(itip_messages) = itip_messages {
itip_messages
.queue(&mut batch)
.caused_by(trc::location!())?;
}
self.commit_batch(batch).await.caused_by(trc::location!())?;
self.notify_task_queue();
Ok(HttpResponse::new(StatusCode::CREATED)
.with_etag_opt(etag)
.with_schedule_tag_opt(schedule_tag))
} else {
Err(DavError::Code(StatusCode::CONFLICT))?
}
}
}
fn validate_ical(ical: &ICalendar) -> crate::Result<&str> {
// Validate UIDs
let mut uids = HashSet::with_capacity(1);
// Validate component types
let mut types: [u8; 5] = [0; 5];
for comp in &ical.components {
*(match comp.component_type {
ICalendarComponentType::VEvent => &mut types[0],
ICalendarComponentType::VTodo => &mut types[1],
ICalendarComponentType::VJournal => &mut types[2],
ICalendarComponentType::VFreebusy => &mut types[3],
ICalendarComponentType::VAvailability => &mut types[4],
_ => {
continue;
}
}) += 1;
if let Some(uid) = comp.uid() {
uids.insert(uid);
}
}
if uids.len() == 1 && types.iter().filter(|&&v| v == 0).count() == 4 {
Ok(uids.iter().next().unwrap())
} else {
Err(DavError::Condition(
DavErrorCondition::new(
StatusCode::PRECONDITION_FAILED,
CalCondition::ValidCalendarObjectResource,
)
.with_details("iCalendar must contain exactly one UID and same component types"),
))
}
}
File diff suppressed because it is too large Load Diff
+220
View File
@@ -0,0 +1,220 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::{
DavError, DavMethod,
common::{
ETag,
lock::{LockRequestHandler, ResourceState},
uri::DavUriResource,
},
};
use common::{Server, auth::AccessToken, sharing::EffectiveAcl};
use dav_proto::RequestHeaders;
use groupware::{
DestroyArchive,
cache::GroupwareCache,
contact::{AddressBook, ContactCard},
};
use http_proto::HttpResponse;
use hyper::StatusCode;
use store::write::{BatchBuilder, ValueClass};
use store::{
ValueKey,
write::{AlignedBytes, Archive},
};
use trc::AddContext;
use types::{
acl::Acl,
collection::{Collection, SyncCollection},
field::PrincipalField,
};
pub(crate) trait CardDeleteRequestHandler: Sync + Send {
fn handle_card_delete_request(
&self,
access_token: &AccessToken,
headers: &RequestHeaders<'_>,
) -> impl Future<Output = crate::Result<HttpResponse>> + Send;
}
impl CardDeleteRequestHandler for Server {
async fn handle_card_delete_request(
&self,
access_token: &AccessToken,
headers: &RequestHeaders<'_>,
) -> crate::Result<HttpResponse> {
// Validate URI
let resource = self
.validate_uri(access_token, headers.uri)
.await?
.into_owned_uri()?;
let account_id = resource.account_id;
let delete_path = resource
.resource
.filter(|r| !r.is_empty())
.ok_or(DavError::Code(StatusCode::FORBIDDEN))?;
let resources = self
.fetch_dav_resources(
access_token.account_id(),
account_id,
SyncCollection::AddressBook,
)
.await
.caused_by(trc::location!())?;
// Check resource type
let delete_resource = resources
.by_path(delete_path)
.ok_or(DavError::Code(StatusCode::NOT_FOUND))?;
let document_id = delete_resource.document_id();
// Fetch entry
let mut batch = BatchBuilder::new();
if delete_resource.is_container() {
let book_ = self
.store()
.get_value::<Archive<AlignedBytes>>(ValueKey::archive(
account_id,
Collection::AddressBook,
document_id,
))
.await
.caused_by(trc::location!())?
.ok_or(DavError::Code(StatusCode::NOT_FOUND))?;
let book = book_
.to_unarchived::<AddressBook>()
.caused_by(trc::location!())?;
// Validate ACL
if !access_token.is_member(account_id)
&& !book
.inner
.acls
.effective_acl(access_token)
.contains_all([Acl::Delete, Acl::RemoveItems].into_iter())
{
return Err(DavError::Code(StatusCode::FORBIDDEN));
}
// Validate headers
self.validate_headers(
access_token,
headers,
vec![ResourceState {
account_id,
collection: Collection::AddressBook,
document_id: document_id.into(),
etag: book.etag().into(),
path: delete_path,
..Default::default()
}],
Default::default(),
DavMethod::DELETE,
)
.await?;
// Delete addressbook and cards
DestroyArchive(book)
.delete_with_cards(
self,
access_token.account_tenant_ids(),
account_id,
document_id,
resources
.subtree(delete_path)
.filter(|r| !r.is_container())
.map(|r| r.document_id())
.collect::<Vec<_>>(),
resources.format_resource(delete_resource).into(),
&mut batch,
)
.await
.caused_by(trc::location!())?;
// Reset default address book id
let default_book_id = self
.store()
.get_value::<u32>(ValueKey {
account_id,
collection: Collection::Principal.into(),
document_id: 0,
class: ValueClass::Property(PrincipalField::DefaultAddressBookId.into()),
})
.await
.caused_by(trc::location!())?;
if default_book_id.is_some_and(|id| id == document_id) {
batch
.with_account_id(account_id)
.with_collection(Collection::Principal)
.with_document(0)
.clear(PrincipalField::DefaultAddressBookId);
}
} else {
// Validate ACL
let addressbook_id = delete_resource.parent_id().unwrap();
if !access_token.is_member(account_id)
&& !resources.has_access_to_container(
access_token,
addressbook_id,
Acl::RemoveItems,
)
{
return Err(DavError::Code(StatusCode::FORBIDDEN));
}
let card_ = self
.store()
.get_value::<Archive<AlignedBytes>>(ValueKey::archive(
account_id,
Collection::ContactCard,
document_id,
))
.await
.caused_by(trc::location!())?
.ok_or(DavError::Code(StatusCode::NOT_FOUND))?;
// Validate headers
self.validate_headers(
access_token,
headers,
vec![ResourceState {
account_id,
collection: Collection::ContactCard,
document_id: document_id.into(),
etag: card_.etag().into(),
path: delete_path,
..Default::default()
}],
Default::default(),
DavMethod::DELETE,
)
.await?;
// Delete card
DestroyArchive(
card_
.to_unarchived::<ContactCard>()
.caused_by(trc::location!())?,
)
.delete(
access_token.account_tenant_ids(),
account_id,
document_id,
addressbook_id,
resources.format_resource(delete_resource).into(),
&mut batch,
)
.caused_by(trc::location!())?;
}
self.commit_batch(batch).await.caused_by(trc::location!())?;
self.notify_task_queue();
Ok(HttpResponse::new(StatusCode::NO_CONTENT))
}
}
+134
View File
@@ -0,0 +1,134 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::{
DavError, DavMethod,
common::{
ETag,
lock::{LockRequestHandler, ResourceState},
uri::DavUriResource,
},
};
use common::{Server, auth::AccessToken};
use dav_proto::{RequestHeaders, schema::property::Rfc1123DateTime};
use groupware::{cache::GroupwareCache, contact::ContactCard};
use http_proto::HttpResponse;
use hyper::StatusCode;
use store::{
ValueKey,
write::{AlignedBytes, Archive},
};
use trc::AddContext;
use types::{
acl::Acl,
collection::{Collection, SyncCollection},
};
pub(crate) trait CardGetRequestHandler: Sync + Send {
fn handle_card_get_request(
&self,
access_token: &AccessToken,
headers: &RequestHeaders<'_>,
is_head: bool,
) -> impl Future<Output = crate::Result<HttpResponse>> + Send;
}
impl CardGetRequestHandler for Server {
async fn handle_card_get_request(
&self,
access_token: &AccessToken,
headers: &RequestHeaders<'_>,
is_head: bool,
) -> crate::Result<HttpResponse> {
// Validate URI
let resource_ = self
.validate_uri(access_token, headers.uri)
.await?
.into_owned_uri()?;
let account_id = resource_.account_id;
let resources = self
.fetch_dav_resources(
access_token.account_id(),
account_id,
SyncCollection::AddressBook,
)
.await
.caused_by(trc::location!())?;
let resource = resources
.by_path(
resource_
.resource
.ok_or(DavError::Code(StatusCode::METHOD_NOT_ALLOWED))?,
)
.ok_or(DavError::Code(StatusCode::NOT_FOUND))?;
if resource.is_container() {
return Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED));
}
// Validate ACL
if !access_token.is_member(account_id)
&& !resources.has_access_to_container(
access_token,
resource.parent_id().unwrap(),
Acl::ReadItems,
)
{
return Err(DavError::Code(StatusCode::FORBIDDEN));
}
// Fetch card
let card_ = self
.store()
.get_value::<Archive<AlignedBytes>>(ValueKey::archive(
account_id,
Collection::ContactCard,
resource.document_id(),
))
.await
.caused_by(trc::location!())?
.ok_or(DavError::Code(StatusCode::NOT_FOUND))?;
let card = card_
.unarchive::<ContactCard>()
.caused_by(trc::location!())?;
// Validate headers
let etag = card_.etag();
self.validate_headers(
access_token,
headers,
vec![ResourceState {
account_id,
collection: Collection::ContactCard,
document_id: resource.document_id().into(),
etag: etag.clone().into(),
path: resource_.resource.unwrap(),
..Default::default()
}],
Default::default(),
DavMethod::GET,
)
.await?;
let response = HttpResponse::new(StatusCode::OK)
.with_content_type("text/vcard; charset=utf-8")
.with_etag(etag)
.with_last_modified(Rfc1123DateTime::new(i64::from(card.modified)).to_string());
let mut vcard = String::with_capacity(128);
let _ = card.card.write_to(
&mut vcard,
headers
.vcard_version
.unwrap_or(self.core.groupware.vcard_version),
);
if !is_head {
Ok(response.with_binary_body(vcard))
} else {
Ok(response.with_content_length(vcard.len()))
}
}
}
+151
View File
@@ -0,0 +1,151 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use super::proppatch::CardPropPatchRequestHandler;
use crate::{
DavError, DavMethod, PropStatBuilder,
common::{
ExtractETag,
lock::{LockRequestHandler, ResourceState},
uri::DavUriResource,
},
};
use common::{Server, auth::AccessToken};
use dav_proto::{
RequestHeaders, Return,
schema::{Namespace, request::MkCol, response::MkColResponse},
};
use groupware::{
cache::GroupwareCache,
contact::{AddressBook, AddressBookPreferences},
};
use http_proto::HttpResponse;
use hyper::StatusCode;
use store::write::BatchBuilder;
use trc::AddContext;
use types::collection::{Collection, SyncCollection};
pub(crate) trait CardMkColRequestHandler: Sync + Send {
fn handle_card_mkcol_request(
&self,
access_token: &AccessToken,
headers: &RequestHeaders<'_>,
request: Option<MkCol>,
) -> impl Future<Output = crate::Result<HttpResponse>> + Send;
}
impl CardMkColRequestHandler for Server {
async fn handle_card_mkcol_request(
&self,
access_token: &AccessToken,
headers: &RequestHeaders<'_>,
request: Option<MkCol>,
) -> crate::Result<HttpResponse> {
// Validate URI
let resource = self
.validate_uri(access_token, headers.uri)
.await?
.into_owned_uri()?;
let account_id = resource.account_id;
let name = resource
.resource
.ok_or(DavError::Code(StatusCode::FORBIDDEN))?;
if !access_token.is_member(account_id) {
return Err(DavError::Code(StatusCode::FORBIDDEN));
} else if name.contains('/')
|| self
.fetch_dav_resources(
access_token.account_id(),
account_id,
SyncCollection::AddressBook,
)
.await
.caused_by(trc::location!())?
.by_path(name)
.is_some()
{
return Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED));
}
// Validate headers
self.validate_headers(
access_token,
headers,
vec![ResourceState {
account_id,
collection: resource.collection,
document_id: Some(u32::MAX),
path: name,
..Default::default()
}],
Default::default(),
DavMethod::MKCOL,
)
.await?;
// Build file container
let mut book = AddressBook {
name: name.to_string(),
preferences: vec![AddressBookPreferences {
account_id,
name: "Address Book".to_string(),
..Default::default()
}],
..Default::default()
};
// Apply MKCOL properties
let mut return_prop_stat = None;
if let Some(mkcol) = request {
let mut prop_stat = PropStatBuilder::default();
if !self.apply_addressbook_properties(
access_token.personal_id(account_id, Collection::AddressBook),
&mut book,
false,
mkcol.props,
&mut prop_stat,
) {
return Ok(HttpResponse::new(StatusCode::FORBIDDEN).with_xml_body(
MkColResponse::new(prop_stat.build())
.with_namespace(Namespace::CardDav)
.to_string(),
));
}
if headers.ret != Return::Minimal {
return_prop_stat = Some(prop_stat);
}
}
// Prepare write batch
let mut batch = BatchBuilder::new();
let document_id = self
.store()
.assign_document_ids(account_id, Collection::AddressBook, 1)
.await
.caused_by(trc::location!())?;
book.insert(
access_token.account_tenant_ids(),
account_id,
document_id,
&mut batch,
)
.caused_by(trc::location!())?;
let etag = batch.etag();
self.commit_batch(batch).await.caused_by(trc::location!())?;
if let Some(prop_stat) = return_prop_stat {
Ok(HttpResponse::new(StatusCode::CREATED)
.with_xml_body(
MkColResponse::new(prop_stat.build())
.with_namespace(Namespace::CardDav)
.to_string(),
)
.with_etag_opt(etag))
} else {
Ok(HttpResponse::new(StatusCode::CREATED).with_etag_opt(etag))
}
}
}
+107
View File
@@ -0,0 +1,107 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::{DavError, DavErrorCondition};
use common::{DavResources, Server};
use dav_proto::schema::{
property::{CardDavProperty, DavProperty, WebDavProperty},
response::CardCondition,
};
use hyper::StatusCode;
use trc::AddContext;
use types::{collection::Collection, field::ContactField};
pub mod copy_move;
pub mod delete;
pub mod get;
pub mod mkcol;
pub mod proppatch;
pub mod query;
pub mod update;
pub(crate) static CARD_CONTAINER_PROPS: [DavProperty; 23] = [
DavProperty::WebDav(WebDavProperty::CreationDate),
DavProperty::WebDav(WebDavProperty::DisplayName),
DavProperty::WebDav(WebDavProperty::GetETag),
DavProperty::WebDav(WebDavProperty::GetLastModified),
DavProperty::WebDav(WebDavProperty::ResourceType),
DavProperty::WebDav(WebDavProperty::LockDiscovery),
DavProperty::WebDav(WebDavProperty::SupportedLock),
DavProperty::WebDav(WebDavProperty::CurrentUserPrincipal),
DavProperty::WebDav(WebDavProperty::SyncToken),
DavProperty::WebDav(WebDavProperty::Owner),
DavProperty::WebDav(WebDavProperty::SupportedPrivilegeSet),
DavProperty::WebDav(WebDavProperty::CurrentUserPrivilegeSet),
DavProperty::WebDav(WebDavProperty::Acl),
DavProperty::WebDav(WebDavProperty::AclRestrictions),
DavProperty::WebDav(WebDavProperty::InheritedAclSet),
DavProperty::WebDav(WebDavProperty::PrincipalCollectionSet),
DavProperty::WebDav(WebDavProperty::SupportedReportSet),
DavProperty::WebDav(WebDavProperty::QuotaAvailableBytes),
DavProperty::WebDav(WebDavProperty::QuotaUsedBytes),
DavProperty::CardDav(CardDavProperty::AddressbookDescription),
DavProperty::CardDav(CardDavProperty::SupportedAddressData),
DavProperty::CardDav(CardDavProperty::SupportedCollationSet),
DavProperty::CardDav(CardDavProperty::MaxResourceSize),
];
pub(crate) static CARD_ITEM_PROPS: [DavProperty; 20] = [
DavProperty::WebDav(WebDavProperty::CreationDate),
DavProperty::WebDav(WebDavProperty::DisplayName),
DavProperty::WebDav(WebDavProperty::GetETag),
DavProperty::WebDav(WebDavProperty::GetLastModified),
DavProperty::WebDav(WebDavProperty::ResourceType),
DavProperty::WebDav(WebDavProperty::LockDiscovery),
DavProperty::WebDav(WebDavProperty::SupportedLock),
DavProperty::WebDav(WebDavProperty::CurrentUserPrincipal),
DavProperty::WebDav(WebDavProperty::SyncToken),
DavProperty::WebDav(WebDavProperty::Owner),
DavProperty::WebDav(WebDavProperty::SupportedPrivilegeSet),
DavProperty::WebDav(WebDavProperty::CurrentUserPrivilegeSet),
DavProperty::WebDav(WebDavProperty::Acl),
DavProperty::WebDav(WebDavProperty::AclRestrictions),
DavProperty::WebDav(WebDavProperty::InheritedAclSet),
DavProperty::WebDav(WebDavProperty::PrincipalCollectionSet),
DavProperty::WebDav(WebDavProperty::GetContentLanguage),
DavProperty::WebDav(WebDavProperty::GetContentLength),
DavProperty::WebDav(WebDavProperty::GetContentType),
DavProperty::CardDav(CardDavProperty::AddressData {
properties: Vec::new(),
version: None,
}),
];
pub(crate) async fn assert_is_unique_uid(
server: &Server,
resources: &DavResources,
account_id: u32,
addressbook_id: u32,
uid: Option<&str>,
) -> crate::Result<()> {
if let Some(uid) = uid {
let hits = server
.document_ids_matching(
account_id,
Collection::ContactCard,
ContactField::Uid,
uid.as_bytes(),
)
.await
.caused_by(trc::location!())?;
if !hits.is_empty() {
for path in resources.children(addressbook_id) {
if hits.contains(path.document_id()) {
return Err(DavError::Condition(DavErrorCondition::new(
StatusCode::PRECONDITION_FAILED,
CardCondition::NoUidConflict(resources.format_resource(path).into()),
)));
}
}
}
}
Ok(())
}
+488
View File
@@ -0,0 +1,488 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::{
DavError, DavMethod, PropStatBuilder,
common::{
ETag, ExtractETag,
lock::{LockRequestHandler, ResourceState},
uri::DavUriResource,
},
};
use common::{Server, auth::AccessToken};
use dav_proto::{
RequestHeaders, Return,
schema::{
Namespace,
property::{CardDavProperty, DavProperty, DavValue, ResourceType, WebDavProperty},
request::{DavPropertyValue, PropertyUpdate},
response::{BaseCondition, MultiStatus, Response},
},
};
use groupware::{
cache::GroupwareCache,
contact::{AddressBook, ContactCard},
};
use http_proto::HttpResponse;
use hyper::StatusCode;
use store::write::BatchBuilder;
use store::{
ValueKey,
write::{AlignedBytes, Archive},
};
use trc::AddContext;
use types::{
acl::Acl,
collection::{Collection, SyncCollection},
};
pub(crate) trait CardPropPatchRequestHandler: Sync + Send {
fn handle_card_proppatch_request(
&self,
access_token: &AccessToken,
headers: &RequestHeaders<'_>,
request: PropertyUpdate,
) -> impl Future<Output = crate::Result<HttpResponse>> + Send;
fn apply_addressbook_properties(
&self,
personal_id: u32,
address_book: &mut AddressBook,
is_update: bool,
properties: Vec<DavPropertyValue>,
items: &mut PropStatBuilder,
) -> bool;
fn apply_card_properties(
&self,
card: &mut ContactCard,
is_update: bool,
properties: Vec<DavPropertyValue>,
items: &mut PropStatBuilder,
) -> bool;
}
impl CardPropPatchRequestHandler for Server {
async fn handle_card_proppatch_request(
&self,
access_token: &AccessToken,
headers: &RequestHeaders<'_>,
mut request: PropertyUpdate,
) -> crate::Result<HttpResponse> {
// Validate URI
let resource_ = self
.validate_uri(access_token, headers.uri)
.await?
.into_owned_uri()?;
let uri = headers.uri;
let account_id = resource_.account_id;
let resources = self
.fetch_dav_resources(
access_token.account_id(),
account_id,
SyncCollection::AddressBook,
)
.await
.caused_by(trc::location!())?;
let resource = resource_
.resource
.and_then(|r| resources.by_path(r))
.ok_or(DavError::Code(StatusCode::NOT_FOUND))?;
let document_id = resource.document_id();
let collection = if resource.is_container() {
Collection::AddressBook
} else {
Collection::ContactCard
};
if !request.has_changes() {
return Ok(HttpResponse::new(StatusCode::NO_CONTENT));
}
// Verify ACL
if !access_token.is_member(account_id) {
let (acl, document_id) = if resource.is_container() {
(Acl::Modify, resource.document_id())
} else {
(Acl::ModifyItems, resource.parent_id().unwrap())
};
if !resources.has_access_to_container(access_token, document_id, acl) {
return Err(DavError::Code(StatusCode::FORBIDDEN));
}
}
// Fetch archive
let archive = self
.store()
.get_value::<Archive<AlignedBytes>>(ValueKey::archive(
account_id,
collection,
document_id,
))
.await
.caused_by(trc::location!())?
.ok_or(DavError::Code(StatusCode::NOT_FOUND))?;
// Validate headers
self.validate_headers(
access_token,
headers,
vec![ResourceState {
account_id,
collection,
document_id: document_id.into(),
etag: archive.etag().into(),
path: resource_.resource.unwrap(),
..Default::default()
}],
Default::default(),
DavMethod::PROPPATCH,
)
.await?;
let is_success;
let mut batch = BatchBuilder::new();
let mut items = PropStatBuilder::default();
let etag = if resource.is_container() {
// Deserialize
let book = archive
.to_unarchived::<AddressBook>()
.caused_by(trc::location!())?;
let mut new_book = archive
.deserialize::<AddressBook>()
.caused_by(trc::location!())?;
let personal_id = access_token.personal_id(account_id, Collection::AddressBook);
// Remove properties
if !request.set_first && !request.remove.is_empty() {
remove_addressbook_properties(
personal_id,
&mut new_book,
std::mem::take(&mut request.remove),
&mut items,
);
}
// Set properties
is_success = self.apply_addressbook_properties(
personal_id,
&mut new_book,
true,
request.set,
&mut items,
);
// Remove properties
if is_success && !request.remove.is_empty() {
remove_addressbook_properties(
personal_id,
&mut new_book,
request.remove,
&mut items,
);
}
if is_success {
new_book
.update(
access_token.account_tenant_ids(),
book,
account_id,
document_id,
&mut batch,
)
.caused_by(trc::location!())?
.etag()
} else {
book.etag().into()
}
} else {
// Deserialize
let card = archive
.to_unarchived::<ContactCard>()
.caused_by(trc::location!())?;
let mut new_card = archive
.deserialize::<ContactCard>()
.caused_by(trc::location!())?;
// Remove properties
if !request.set_first && !request.remove.is_empty() {
remove_card_properties(
&mut new_card,
std::mem::take(&mut request.remove),
&mut items,
);
}
// Set properties
is_success = self.apply_card_properties(&mut new_card, true, request.set, &mut items);
// Remove properties
if is_success && !request.remove.is_empty() {
remove_card_properties(&mut new_card, request.remove, &mut items);
}
if is_success {
new_card
.update(
access_token.account_tenant_ids(),
card,
account_id,
document_id,
&mut batch,
)
.caused_by(trc::location!())?
.etag()
} else {
card.etag().into()
}
};
if is_success {
self.commit_batch(batch).await.caused_by(trc::location!())?;
}
if headers.ret != Return::Minimal || !is_success {
Ok(HttpResponse::new(StatusCode::MULTI_STATUS)
.with_xml_body(
MultiStatus::new(vec![Response::new_propstat(uri, items.build())])
.with_namespace(Namespace::CardDav)
.to_string(),
)
.with_etag_opt(etag))
} else {
Ok(HttpResponse::new(StatusCode::NO_CONTENT).with_etag_opt(etag))
}
}
fn apply_addressbook_properties(
&self,
personal_id: u32,
address_book: &mut AddressBook,
is_update: bool,
properties: Vec<DavPropertyValue>,
items: &mut PropStatBuilder,
) -> bool {
let mut has_errors = false;
for property in properties {
match (&property.property, property.value) {
(DavProperty::WebDav(WebDavProperty::DisplayName), DavValue::String(name)) => {
if name.len() <= self.core.groupware.live_property_size {
address_book.preferences_mut(personal_id).name = name;
items.insert_ok(property.property);
} else {
items.insert_error_with_description(
property.property,
StatusCode::INSUFFICIENT_STORAGE,
"Property value is too long",
);
has_errors = true;
}
}
(
DavProperty::CardDav(CardDavProperty::AddressbookDescription),
DavValue::String(name),
) => {
if name.len() <= self.core.groupware.live_property_size {
address_book.preferences_mut(personal_id).description = Some(name);
items.insert_ok(property.property);
} else {
items.insert_error_with_description(
property.property,
StatusCode::INSUFFICIENT_STORAGE,
"Property value is too long",
);
has_errors = true;
}
}
(DavProperty::WebDav(WebDavProperty::CreationDate), DavValue::Timestamp(dt)) => {
address_book.created = dt;
items.insert_ok(property.property);
}
(
DavProperty::WebDav(WebDavProperty::ResourceType),
DavValue::ResourceTypes(types),
) => {
if !types.0.iter().all(|rt| {
matches!(rt, ResourceType::Collection | ResourceType::AddressBook)
}) {
items.insert_precondition_failed(
property.property,
StatusCode::FORBIDDEN,
BaseCondition::ValidResourceType,
);
has_errors = true;
} else {
items.insert_ok(property.property);
}
}
(DavProperty::DeadProperty(dead), DavValue::DeadProperty(values))
if self.core.groupware.dead_property_size.is_some() =>
{
if is_update {
address_book.dead_properties.remove_element(dead);
}
if address_book.dead_properties.size() + values.size() + dead.size()
< self.core.groupware.dead_property_size.unwrap()
{
address_book
.dead_properties
.add_element(dead.clone(), values.0);
items.insert_ok(property.property);
} else {
items.insert_error_with_description(
property.property,
StatusCode::INSUFFICIENT_STORAGE,
"Property value is too long",
);
has_errors = true;
}
}
(_, DavValue::Null) => {
items.insert_ok(property.property);
}
_ => {
items.insert_error_with_description(
property.property,
StatusCode::CONFLICT,
"Property cannot be modified",
);
has_errors = true;
}
}
}
!has_errors
}
fn apply_card_properties(
&self,
card: &mut ContactCard,
is_update: bool,
properties: Vec<DavPropertyValue>,
items: &mut PropStatBuilder,
) -> bool {
let mut has_errors = false;
for property in properties {
match (&property.property, property.value) {
(DavProperty::WebDav(WebDavProperty::DisplayName), DavValue::String(name)) => {
if name.len() <= self.core.groupware.live_property_size {
card.display_name = Some(name);
items.insert_ok(property.property);
} else {
items.insert_error_with_description(
property.property,
StatusCode::INSUFFICIENT_STORAGE,
"Property value is too long",
);
has_errors = true;
}
}
(DavProperty::WebDav(WebDavProperty::CreationDate), DavValue::Timestamp(dt)) => {
card.created = dt;
items.insert_ok(property.property);
}
(DavProperty::DeadProperty(dead), DavValue::DeadProperty(values))
if self.core.groupware.dead_property_size.is_some() =>
{
if is_update {
card.dead_properties.remove_element(dead);
}
if card.dead_properties.size() + values.size() + dead.size()
< self.core.groupware.dead_property_size.unwrap()
{
card.dead_properties.add_element(dead.clone(), values.0);
items.insert_ok(property.property);
} else {
items.insert_error_with_description(
property.property,
StatusCode::INSUFFICIENT_STORAGE,
"Property value is too long",
);
has_errors = true;
}
}
(_, DavValue::Null) => {
items.insert_ok(property.property);
}
_ => {
items.insert_error_with_description(
property.property,
StatusCode::CONFLICT,
"Property cannot be modified",
);
has_errors = true;
}
}
}
!has_errors
}
}
fn remove_card_properties(
card: &mut ContactCard,
properties: Vec<DavProperty>,
items: &mut PropStatBuilder,
) {
for property in properties {
match &property {
DavProperty::WebDav(WebDavProperty::DisplayName) => {
card.display_name = None;
items.insert_with_status(property, StatusCode::NO_CONTENT);
}
DavProperty::DeadProperty(dead) => {
card.dead_properties.remove_element(dead);
items.insert_with_status(property, StatusCode::NO_CONTENT);
}
_ => {
items.insert_error_with_description(
property,
StatusCode::CONFLICT,
"Property cannot be deleted",
);
}
}
}
}
fn remove_addressbook_properties(
personal_id: u32,
book: &mut AddressBook,
properties: Vec<DavProperty>,
items: &mut PropStatBuilder,
) {
for property in properties {
match &property {
DavProperty::CardDav(CardDavProperty::AddressbookDescription) => {
book.preferences_mut(personal_id).description = None;
items.insert_with_status(property, StatusCode::NO_CONTENT);
}
DavProperty::WebDav(WebDavProperty::DisplayName) => {
book.preferences_mut(personal_id).name.clear();
items.insert_with_status(property, StatusCode::NO_CONTENT);
}
DavProperty::DeadProperty(dead) => {
book.dead_properties.remove_element(dead);
items.insert_with_status(property, StatusCode::NO_CONTENT);
}
_ => {
items.insert_error_with_description(
property,
StatusCode::CONFLICT,
"Property cannot be deleted",
);
}
}
}
}
+235
View File
@@ -0,0 +1,235 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::{
DavError,
common::{
AddressbookFilter, DavQuery,
propfind::{PropFindItem, PropFindRequestHandler},
uri::DavUriResource,
},
};
use calcard::vcard::{
ArchivedVCard, ArchivedVCardEntry, ArchivedVCardParameter, VCardParameterName, VCardProperty,
VCardVersion,
};
use common::{Server, auth::AccessToken};
use dav_proto::{
RequestHeaders,
schema::{
property::CardDavPropertyName,
request::{AddressbookQuery, Filter, FilterOp, VCardPropertyWithGroup},
response::MultiStatus,
},
};
use groupware::cache::GroupwareCache;
use http_proto::HttpResponse;
use hyper::StatusCode;
use std::fmt::Write;
use trc::AddContext;
use types::{acl::Acl, collection::SyncCollection};
pub(crate) trait CardQueryRequestHandler: Sync + Send {
fn handle_card_query_request(
&self,
access_token: &AccessToken,
headers: &RequestHeaders<'_>,
request: AddressbookQuery,
) -> impl Future<Output = crate::Result<HttpResponse>> + Send;
}
impl CardQueryRequestHandler for Server {
async fn handle_card_query_request(
&self,
access_token: &AccessToken,
headers: &RequestHeaders<'_>,
request: AddressbookQuery,
) -> crate::Result<HttpResponse> {
// Validate URI
let resource_ = self
.validate_uri(access_token, headers.uri)
.await?
.into_owned_uri()?;
let account_id = resource_.account_id;
let resources = self
.fetch_dav_resources(
access_token.account_id(),
account_id,
SyncCollection::AddressBook,
)
.await
.caused_by(trc::location!())?;
let Some(resource) = resources.by_path(
resource_
.resource
.ok_or(DavError::Code(StatusCode::METHOD_NOT_ALLOWED))?,
) else {
return Ok(HttpResponse::new(StatusCode::MULTI_STATUS)
.with_xml_body(MultiStatus::not_found(headers.uri).to_string()));
};
if !resource.is_container() {
return Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED));
}
// Obtain shared ids
let shared_ids = if !access_token.is_member(account_id) {
resources
.shared_items(access_token, [Acl::ReadItems], false)
.into()
} else {
None
};
// Obtain document ids in folder
let mut items = Vec::with_capacity(16);
for resource in resources.children(resource.document_id()) {
if shared_ids
.as_ref()
.is_none_or(|ids| ids.contains(resource.document_id()))
{
items.push(PropFindItem::new(
resources.format_resource(resource),
account_id,
resource,
));
}
}
self.handle_dav_query(
access_token,
DavQuery::addressbook_query(request, items, headers),
)
.await
}
}
pub(crate) fn vcard_query(card: &ArchivedVCard, filters: &AddressbookFilter) -> bool {
let mut is_all = true;
let mut matches_one = false;
for filter in filters {
match filter {
Filter::AnyOf => {
is_all = false;
}
Filter::AllOf => {
is_all = true;
}
Filter::Property { prop, op, .. } => {
let mut properties = find_properties(card, prop).peekable();
let result = if properties.peek().is_some() {
properties.any(|entry| match op {
FilterOp::Exists => true,
FilterOp::Undefined => false,
FilterOp::TextMatch(text_match) => {
let mut matched_any = false;
for value in entry.values.iter() {
if let Some(text) = value.as_text()
&& text_match.matches(text)
{
matched_any = true;
break;
}
}
matched_any
}
FilterOp::TimeRange(_) => false,
})
} else {
matches!(op, FilterOp::Undefined)
};
if result {
matches_one = true;
} else if is_all {
return false;
}
}
Filter::Parameter {
prop, param, op, ..
} => {
let mut properties = find_properties(card, prop)
.filter_map(|entry| find_parameter(entry, param))
.peekable();
let result = if properties.peek().is_some() {
properties.any(|entry| match op {
FilterOp::Exists => true,
FilterOp::Undefined => false,
FilterOp::TextMatch(text_match) => {
if let Some(text) = entry.value.as_text() {
text_match.matches(text)
} else {
false
}
}
FilterOp::TimeRange(_) => false,
})
} else {
matches!(op, FilterOp::Undefined)
};
if result {
matches_one = true;
} else if is_all {
return false;
}
}
Filter::Component { .. } => {}
}
}
is_all || matches_one
}
#[inline(always)]
fn find_properties<'x>(
card: &'x ArchivedVCard,
prop: &VCardPropertyWithGroup,
) -> impl Iterator<Item = &'x ArchivedVCardEntry> {
card.entries
.iter()
.filter(move |entry| entry.name == prop.name && entry.group == prop.group)
}
#[inline(always)]
fn find_parameter<'x>(
entry: &'x ArchivedVCardEntry,
name: &VCardParameterName,
) -> Option<&'x ArchivedVCardParameter> {
entry.params.iter().find(|param| param.name == *name)
}
pub(crate) fn serialize_vcard_with_props(
card: &ArchivedVCard,
props: &[CardDavPropertyName],
version: VCardVersion,
) -> String {
let mut vcard = String::with_capacity(128);
if !props.is_empty() {
let _ = write!(&mut vcard, "BEGIN:VCARD\r\n");
let is_v4 = matches!(version, VCardVersion::V4_0);
for entry in card.entries.iter() {
for item in props {
if entry.name == item.name && entry.group == item.group {
if item.name != VCardProperty::Version {
let _ = entry.write_to(&mut vcard, !item.no_value, is_v4);
} else {
let _ = write!(&mut vcard, "VERSION:{version}\r\n");
}
break;
}
}
}
let _ = write!(&mut vcard, "END:VCARD\r\n");
} else {
let _ = card.write_to(&mut vcard, version);
}
vcard
}
+305
View File
@@ -0,0 +1,305 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use super::assert_is_unique_uid;
use crate::{
DavError, DavErrorCondition, DavMethod,
common::{
ETag, ExtractETag,
lock::{LockRequestHandler, ResourceState},
uri::DavUriResource,
},
file::DavFileResource,
fix_percent_encoding,
};
use calcard::{Entry, Parser};
use common::{DavName, Server, auth::AccessToken};
use dav_proto::{
RequestHeaders, Return,
schema::{property::Rfc1123DateTime, response::CardCondition},
};
use groupware::{cache::GroupwareCache, contact::ContactCard};
use http_proto::HttpResponse;
use hyper::StatusCode;
use store::write::BatchBuilder;
use store::{
ValueKey,
write::{AlignedBytes, Archive},
};
use trc::AddContext;
use types::{
acl::Acl,
collection::{Collection, SyncCollection},
};
pub(crate) trait CardUpdateRequestHandler: Sync + Send {
fn handle_card_update_request(
&self,
access_token: &AccessToken,
headers: &RequestHeaders<'_>,
bytes: Vec<u8>,
is_patch: bool,
) -> impl Future<Output = crate::Result<HttpResponse>> + Send;
}
impl CardUpdateRequestHandler for Server {
async fn handle_card_update_request(
&self,
access_token: &AccessToken,
headers: &RequestHeaders<'_>,
bytes: Vec<u8>,
_is_patch: bool,
) -> crate::Result<HttpResponse> {
// Validate URI
let resource = self
.validate_uri(access_token, headers.uri)
.await?
.into_owned_uri()?;
let account_id = resource.account_id;
let resources = self
.fetch_dav_resources(
access_token.account_id(),
account_id,
SyncCollection::AddressBook,
)
.await
.caused_by(trc::location!())?;
let resource_name = fix_percent_encoding(
resource
.resource
.ok_or(DavError::Code(StatusCode::CONFLICT))?,
);
if bytes.len() > self.core.groupware.max_vcard_size {
return Err(DavError::Condition(DavErrorCondition::new(
StatusCode::PRECONDITION_FAILED,
CardCondition::MaxResourceSize(self.core.groupware.max_vcard_size as u32),
)));
}
let vcard_raw = std::str::from_utf8(&bytes).map_err(|_| {
DavError::Condition(
DavErrorCondition::new(
StatusCode::PRECONDITION_FAILED,
CardCondition::SupportedAddressData,
)
.with_details("The request body is not valid UTF-8."),
)
})?;
let vcard = match Parser::new(vcard_raw).strict().entry() {
Entry::VCard(vcard) => vcard,
_ => {
return Err(DavError::Condition(
DavErrorCondition::new(
StatusCode::PRECONDITION_FAILED,
CardCondition::SupportedAddressData,
)
.with_details("Failed to parse vCard data."),
));
}
};
if let Some(resource) = resources.by_path(resource_name.as_ref()) {
if resource.is_container() {
return Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED));
}
// Validate ACL
let parent_id = resource.parent_id().unwrap();
let document_id = resource.document_id();
if !access_token.is_member(account_id)
&& !resources.has_access_to_container(access_token, parent_id, Acl::ModifyItems)
{
return Err(DavError::Code(StatusCode::FORBIDDEN));
}
// Update
let card_ = self
.store()
.get_value::<Archive<AlignedBytes>>(ValueKey::archive(
account_id,
Collection::ContactCard,
document_id,
))
.await
.caused_by(trc::location!())?
.ok_or(DavError::Code(StatusCode::NOT_FOUND))?;
let card = card_
.to_unarchived::<ContactCard>()
.caused_by(trc::location!())?;
// Validate headers
match self
.validate_headers(
access_token,
headers,
vec![ResourceState {
account_id,
collection: Collection::ContactCard,
document_id: Some(document_id),
etag: card.etag().into(),
path: resource_name.as_ref(),
..Default::default()
}],
Default::default(),
DavMethod::PUT,
)
.await
{
Ok(_) => {}
Err(DavError::Code(StatusCode::PRECONDITION_FAILED))
if headers.ret == Return::Representation =>
{
let mut vcard = String::with_capacity(128);
let _ = card.inner.card.write_to(
&mut vcard,
headers
.vcard_version
.unwrap_or(self.core.groupware.vcard_version),
);
return Ok(HttpResponse::new(StatusCode::PRECONDITION_FAILED)
.with_content_type("text/vcard; charset=utf-8")
.with_etag(card.etag())
.with_last_modified(
Rfc1123DateTime::new(i64::from(card.inner.modified)).to_string(),
)
.with_header("Preference-Applied", "return=representation")
.with_binary_body(vcard));
}
Err(e) => return Err(e),
}
// Validate UID
match (card.inner.card.uid(), vcard.uid()) {
(Some(old_uid), Some(new_uid)) if old_uid == new_uid => {}
(None, None) | (None, Some(_)) => {}
_ => {
return Err(DavError::Condition(DavErrorCondition::new(
StatusCode::PRECONDITION_FAILED,
CardCondition::NoUidConflict(resources.format_resource(resource).into()),
)));
}
}
// Validate quota
let extra_bytes =
(bytes.len() as u64).saturating_sub(u32::from(card.inner.size) as u64);
if extra_bytes > 0 {
self.has_available_quota(self.account(account_id).await?.as_ref(), extra_bytes)
.await?;
}
// Build node
let mut new_card = card
.deserialize::<ContactCard>()
.caused_by(trc::location!())?;
new_card.size = bytes.len() as u32;
new_card.card = vcard;
// Prepare write batch
let mut batch = BatchBuilder::new();
let etag = new_card
.update(
access_token.account_tenant_ids(),
card,
account_id,
document_id,
&mut batch,
)
.caused_by(trc::location!())?
.etag();
self.commit_batch(batch).await.caused_by(trc::location!())?;
self.notify_task_queue();
Ok(HttpResponse::new(StatusCode::NO_CONTENT).with_etag_opt(etag))
} else if let Some((Some(parent), name)) = resources.map_parent(resource_name.as_ref()) {
if !parent.is_container() {
return Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED));
}
// Validate ACL
if !access_token.is_member(account_id)
&& !resources.has_access_to_container(
access_token,
parent.document_id(),
Acl::AddItems,
)
{
return Err(DavError::Code(StatusCode::FORBIDDEN));
}
// Validate headers
self.validate_headers(
access_token,
headers,
vec![ResourceState {
account_id,
collection: resource.collection,
document_id: Some(u32::MAX),
path: resource_name.as_ref(),
..Default::default()
}],
Default::default(),
DavMethod::PUT,
)
.await?;
// Validate UID
assert_is_unique_uid(
self,
&resources,
account_id,
parent.document_id(),
vcard.uid(),
)
.await?;
// Validate quota
if !bytes.is_empty() {
self.has_available_quota(
self.account(account_id).await?.as_ref(),
bytes.len() as u64,
)
.await?;
}
// Build node
let card = ContactCard {
names: vec![DavName {
name: name.to_string(),
parent_id: parent.document_id(),
}],
card: vcard,
size: bytes.len() as u32,
..Default::default()
};
// Prepare write batch
let mut batch = BatchBuilder::new();
let document_id = self
.store()
.assign_document_ids(account_id, Collection::ContactCard, 1)
.await
.caused_by(trc::location!())?;
let etag = card
.insert(
access_token.account_tenant_ids(),
account_id,
document_id,
&mut batch,
)
.caused_by(trc::location!())?
.etag();
self.commit_batch(batch).await.caused_by(trc::location!())?;
self.notify_task_queue();
Ok(HttpResponse::new(StatusCode::CREATED).with_etag_opt(etag))
} else {
Err(DavError::Code(StatusCode::CONFLICT))?
}
}
}
+582
View File
@@ -0,0 +1,582 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use super::ArchivedResource;
use crate::{
DavError, DavErrorCondition, DavResourceName, common::uri::DavUriResource,
principal::propfind::PrincipalPropFind,
};
use common::{DavResources, Server, auth::AccessToken, sharing::EffectiveAcl};
use dav_proto::{
RequestHeaders,
schema::{
property::{DavProperty, Privilege, WebDavProperty},
request::{AclPrincipalPropSet, PropFind},
response::{Ace, BaseCondition, GrantDeny, Href, MultiStatus, Principal},
},
};
use groupware::RFC_3986;
use groupware::{cache::GroupwareCache, calendar::Calendar, contact::AddressBook, file::FileNode};
use http_proto::HttpResponse;
use hyper::StatusCode;
use rkyv::vec::ArchivedVec;
use store::{
ValueKey,
write::{AlignedBytes, Archive},
};
use store::{ahash::AHashSet, roaring::RoaringBitmap, write::BatchBuilder};
use trc::AddContext;
use types::{
acl::{Acl, AclGrant, ArchivedAclGrant},
collection::Collection,
};
use utils::map::bitmap::Bitmap;
pub(crate) trait DavAclHandler: Sync + Send {
fn handle_acl_request(
&self,
access_token: &AccessToken,
headers: &RequestHeaders<'_>,
request: dav_proto::schema::request::Acl,
) -> impl Future<Output = crate::Result<HttpResponse>> + Send;
fn handle_acl_prop_set(
&self,
access_token: &AccessToken,
headers: &RequestHeaders<'_>,
request: AclPrincipalPropSet,
) -> impl Future<Output = crate::Result<HttpResponse>> + Send;
fn validate_and_map_aces(
&self,
access_token: &AccessToken,
acl: dav_proto::schema::request::Acl,
collection: Collection,
) -> impl Future<Output = crate::Result<Vec<AclGrant>>> + Send;
fn resolve_ace(
&self,
access_token: &AccessToken,
account_id: u32,
grants: &ArchivedVec<ArchivedAclGrant>,
expand: Option<&PropFind>,
) -> impl Future<Output = crate::Result<Vec<Ace>>> + Send;
}
pub(crate) trait ResourceAcl {
fn validate_and_map_parent_acl(
&self,
access_token: &AccessToken,
is_member: bool,
parent_id: Option<u32>,
check_acls: impl Into<Bitmap<Acl>> + Send,
) -> crate::Result<u32>;
}
impl DavAclHandler for Server {
async fn handle_acl_request(
&self,
access_token: &AccessToken,
headers: &RequestHeaders<'_>,
request: dav_proto::schema::request::Acl,
) -> crate::Result<HttpResponse> {
// Validate URI
let resource_ = self
.validate_uri(access_token, headers.uri)
.await?
.into_owned_uri()?;
let account_id = resource_.account_id;
let collection = resource_.collection;
if !matches!(
collection,
Collection::AddressBook | Collection::Calendar | Collection::FileNode
) {
return Err(DavError::Code(StatusCode::FORBIDDEN));
}
let resources = self
.fetch_dav_resources(access_token.account_id(), account_id, collection.into())
.await
.caused_by(trc::location!())?;
let resource = resource_
.resource
.and_then(|r| resources.by_path(r))
.ok_or(DavError::Code(StatusCode::NOT_FOUND))?;
if !resource.resource.is_container() && !matches!(collection, Collection::FileNode) {
return Err(DavError::Code(StatusCode::FORBIDDEN));
}
// Fetch node
let archive = self
.store()
.get_value::<Archive<AlignedBytes>>(ValueKey::archive(
account_id,
collection,
resource.document_id(),
))
.await
.caused_by(trc::location!())?
.ok_or(DavError::Code(StatusCode::NOT_FOUND))?;
let container =
ArchivedResource::from_archive(&archive, collection).caused_by(trc::location!())?;
// Validate ACL
let acls = container.acls().unwrap();
if !access_token.is_member(account_id)
&& !acls.effective_acl(access_token).contains(Acl::Share)
{
return Err(DavError::Code(StatusCode::FORBIDDEN));
}
// Validate ACEs
let grants = self
.validate_and_map_aces(access_token, request, collection)
.await?;
if grants.len() != acls.len() || acls.iter().zip(grants.iter()).any(|(a, b)| a != b) {
// Refresh ACLs
self.refresh_archived_acls(&grants, acls)
.await
.caused_by(trc::location!())?;
let mut batch = BatchBuilder::new();
match container {
ArchivedResource::Calendar(calendar) => {
let mut new_calendar = calendar
.deserialize::<Calendar>()
.caused_by(trc::location!())?;
new_calendar.acls = grants;
new_calendar
.update(
access_token.account_tenant_ids(),
calendar,
account_id,
resource.document_id(),
&mut batch,
)
.caused_by(trc::location!())?;
}
ArchivedResource::AddressBook(book) => {
let mut new_book = book
.deserialize::<AddressBook>()
.caused_by(trc::location!())?;
new_book.acls = grants;
new_book
.update(
access_token.account_tenant_ids(),
book,
account_id,
resource.document_id(),
&mut batch,
)
.caused_by(trc::location!())?;
}
ArchivedResource::FileNode(node) => {
let mut new_node =
node.deserialize::<FileNode>().caused_by(trc::location!())?;
new_node.acls = grants;
new_node
.update(
access_token.account_tenant_ids(),
node,
account_id,
resource.document_id(),
true,
&mut batch,
)
.caused_by(trc::location!())?;
}
_ => unreachable!(),
}
self.commit_batch(batch).await.caused_by(trc::location!())?;
}
Ok(HttpResponse::new(StatusCode::OK))
}
async fn handle_acl_prop_set(
&self,
access_token: &AccessToken,
headers: &RequestHeaders<'_>,
mut request: AclPrincipalPropSet,
) -> crate::Result<HttpResponse> {
let uri = self
.validate_uri(access_token, headers.uri)
.await
.and_then(|uri| uri.into_owned_uri())?;
let uri = self
.map_uri_resource(access_token, uri)
.await
.caused_by(trc::location!())?
.ok_or(DavError::Code(StatusCode::NOT_FOUND))?;
if !matches!(
uri.collection,
Collection::Calendar | Collection::AddressBook | Collection::FileNode
) {
return Err(DavError::Code(StatusCode::FORBIDDEN));
}
let archive = self
.store()
.get_value::<Archive<AlignedBytes>>(ValueKey::archive(
uri.account_id,
uri.collection,
uri.resource,
))
.await
.caused_by(trc::location!())?
.ok_or(DavError::Code(StatusCode::NOT_FOUND))?;
let acls = match uri.collection {
Collection::FileNode => {
&archive
.unarchive::<FileNode>()
.caused_by(trc::location!())?
.acls
}
Collection::AddressBook => {
&archive
.unarchive::<AddressBook>()
.caused_by(trc::location!())?
.acls
}
Collection::Calendar => {
&archive
.unarchive::<Calendar>()
.caused_by(trc::location!())?
.acls
}
_ => unreachable!(),
};
// Validate ACLs
if !access_token.is_member(uri.account_id)
&& !acls.effective_acl(access_token).contains(Acl::Share)
{
return Err(DavError::Code(StatusCode::FORBIDDEN));
}
// Validate
let account_ids = RoaringBitmap::from_iter(acls.iter().map(|a| u32::from(a.account_id)));
let mut response = MultiStatus::new(Vec::with_capacity(16));
if !account_ids.is_empty() {
if request.properties.is_empty() {
request
.properties
.push(DavProperty::WebDav(WebDavProperty::DisplayName));
}
let request = PropFind::Prop(request.properties);
self.prepare_principal_propfind_response(
access_token,
Collection::Principal,
account_ids.into_iter(),
&request,
&mut response,
)
.await?;
}
Ok(HttpResponse::new(StatusCode::MULTI_STATUS).with_xml_body(response.to_string()))
}
async fn validate_and_map_aces(
&self,
access_token: &AccessToken,
acl: dav_proto::schema::request::Acl,
collection: Collection,
) -> crate::Result<Vec<AclGrant>> {
let mut grants = Vec::with_capacity(acl.aces.len());
for ace in acl.aces {
if ace.invert {
return Err(DavError::Condition(DavErrorCondition::new(
StatusCode::FORBIDDEN,
BaseCondition::NoInvert,
)));
}
let privileges = match ace.grant_deny {
GrantDeny::Grant(list) => list.0,
GrantDeny::Deny(_) => {
return Err(DavError::Condition(DavErrorCondition::new(
StatusCode::FORBIDDEN,
BaseCondition::GrantOnly,
)));
}
};
let principal_uri = match ace.principal {
Principal::Href(href) => href.0,
_ => {
return Err(DavError::Condition(DavErrorCondition::new(
StatusCode::FORBIDDEN,
BaseCondition::AllowedPrincipal,
)));
}
};
let mut acls = Bitmap::<Acl>::default();
for privilege in privileges {
match privilege {
Privilege::Read => {
acls.insert(Acl::Read);
acls.insert(Acl::ReadItems);
}
Privilege::Write => {
acls.insert(Acl::Modify);
acls.insert(Acl::Delete);
acls.insert(Acl::AddItems);
acls.insert(Acl::ModifyItems);
acls.insert(Acl::RemoveItems);
}
Privilege::WriteContent => {
acls.insert(Acl::AddItems);
acls.insert(Acl::Modify);
acls.insert(Acl::ModifyItems);
}
Privilege::WriteProperties => {
acls.insert(Acl::Modify);
}
Privilege::ReadCurrentUserPrivilegeSet
| Privilege::Unlock
| Privilege::Bind
| Privilege::Unbind => {}
Privilege::All => {
return Err(DavError::Condition(DavErrorCondition::new(
StatusCode::FORBIDDEN,
BaseCondition::NoAbstract,
)));
}
Privilege::ReadAcl => {}
Privilege::WriteAcl => {
acls.insert(Acl::Share);
}
Privilege::ReadFreeBusy
| Privilege::ScheduleQueryFreeBusy
| Privilege::ScheduleSendFreeBusy => {
if collection == Collection::Calendar {
acls.insert(Acl::SchedulingReadFreeBusy);
} else {
return Err(DavError::Condition(DavErrorCondition::new(
StatusCode::FORBIDDEN,
BaseCondition::NotSupportedPrivilege,
)));
}
}
Privilege::ScheduleDeliver | Privilege::ScheduleSend => {
if collection == Collection::Calendar {
acls.insert(Acl::SchedulingReadFreeBusy);
acls.insert(Acl::SchedulingInvite);
acls.insert(Acl::SchedulingReply);
} else {
return Err(DavError::Condition(DavErrorCondition::new(
StatusCode::FORBIDDEN,
BaseCondition::NotSupportedPrivilege,
)));
}
}
Privilege::ScheduleDeliverInvite | Privilege::ScheduleSendInvite => {
if collection == Collection::Calendar {
acls.insert(Acl::SchedulingInvite);
} else {
return Err(DavError::Condition(DavErrorCondition::new(
StatusCode::FORBIDDEN,
BaseCondition::NotSupportedPrivilege,
)));
}
}
Privilege::ScheduleDeliverReply | Privilege::ScheduleSendReply => {
if collection == Collection::Calendar {
acls.insert(Acl::SchedulingReply);
} else {
return Err(DavError::Condition(DavErrorCondition::new(
StatusCode::FORBIDDEN,
BaseCondition::NotSupportedPrivilege,
)));
}
}
}
}
if acls.is_empty() {
continue;
}
let principal_id = self
.validate_uri(access_token, &principal_uri)
.await
.map_err(|_| {
DavError::Condition(DavErrorCondition::new(
StatusCode::FORBIDDEN,
BaseCondition::AllowedPrincipal,
))
})?
.account_id
.ok_or_else(|| {
DavError::Condition(DavErrorCondition::new(
StatusCode::FORBIDDEN,
BaseCondition::AllowedPrincipal,
))
})?;
// Verify that the principal is a valid principal
/*let principal = self
.directory()
.query(QueryParams::id(principal_id).with_return_member_of(false))
.await
.caused_by(trc::location!())?
.ok_or_else(|| {
DavError::Condition(DavErrorCondition::new(
StatusCode::FORBIDDEN,
BaseCondition::AllowedPrincipal,
))
})?;
if !matches!(principal.typ(), Type::Individual | Type::Group) {
return Err(DavError::Condition(DavErrorCondition::new(
StatusCode::FORBIDDEN,
BaseCondition::AllowedPrincipal,
)));
}*/
grants.push(AclGrant {
account_id: principal_id,
grants: acls,
});
}
Ok(grants)
}
async fn resolve_ace(
&self,
access_token: &AccessToken,
account_id: u32,
grants: &ArchivedVec<ArchivedAclGrant>,
expand: Option<&PropFind>,
) -> crate::Result<Vec<Ace>> {
let mut aces = Vec::with_capacity(grants.len());
if access_token.is_member(account_id)
|| grants.effective_acl(access_token).contains(Acl::Share)
{
for grant in grants.iter() {
let grant_account_id = u32::from(grant.account_id);
let principal = if let Some(expand) = expand {
self.expand_principal(access_token, grant_account_id, expand)
.await?
.map(Principal::Response)
.unwrap_or_else(|| {
Principal::Href(Href(format!(
"{}/_{grant_account_id}/",
DavResourceName::Principal.base_path(),
)))
})
} else {
let grant_account = self
.account(grant_account_id)
.await
.caused_by(trc::location!())?;
Principal::Href(Href(format!(
"{}/{}/",
DavResourceName::Principal.base_path(),
percent_encoding::utf8_percent_encode(grant_account.name(), RFC_3986),
)))
};
aces.push(Ace::new(
principal,
GrantDeny::grant(current_user_privilege_set(Bitmap::<Acl>::from(
&grant.grants,
))),
));
}
}
Ok(aces)
}
}
impl ResourceAcl for DavResources {
fn validate_and_map_parent_acl(
&self,
access_token: &AccessToken,
is_member: bool,
parent_id: Option<u32>,
check_acls: impl Into<Bitmap<Acl>> + Send,
) -> crate::Result<u32> {
match parent_id {
Some(parent_id) => {
if is_member || self.has_access_to_container(access_token, parent_id, check_acls) {
Ok(parent_id + 1)
} else {
Err(DavError::Code(StatusCode::FORBIDDEN))
}
}
None => {
if is_member {
Ok(0)
} else {
Err(DavError::Code(StatusCode::FORBIDDEN))
}
}
}
}
}
pub(crate) trait Privileges {
fn current_privilege_set(
&self,
account_id: u32,
grants: &ArchivedVec<ArchivedAclGrant>,
is_calendar: bool,
) -> Vec<Privilege>;
}
impl Privileges for AccessToken {
fn current_privilege_set(
&self,
account_id: u32,
grants: &ArchivedVec<ArchivedAclGrant>,
is_calendar: bool,
) -> Vec<Privilege> {
if self.is_member(account_id) {
Privilege::all(is_calendar)
} else {
current_user_privilege_set(grants.effective_acl(self))
}
}
}
pub(crate) fn current_user_privilege_set(acl_bitmap: Bitmap<Acl>) -> Vec<Privilege> {
let mut acls = AHashSet::with_capacity(16);
for grant in acl_bitmap {
match grant {
Acl::Read | Acl::ReadItems => {
acls.insert(Privilege::Read);
acls.insert(Privilege::ReadCurrentUserPrivilegeSet);
}
Acl::Modify => {
acls.insert(Privilege::WriteProperties);
}
Acl::ModifyItems => {
acls.insert(Privilege::WriteContent);
}
Acl::Delete | Acl::RemoveItems => {
acls.insert(Privilege::Write);
}
Acl::Share => {
acls.insert(Privilege::ReadAcl);
acls.insert(Privilege::WriteAcl);
}
Acl::SchedulingReadFreeBusy => {
acls.insert(Privilege::ReadFreeBusy);
}
_ => {}
}
}
acls.into_iter().collect()
}
+892
View File
@@ -0,0 +1,892 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use super::ETag;
use super::uri::{DavUriResource, OwnedUri, UriResource, Urn};
use crate::{DavError, DavErrorCondition, DavMethod};
use common::KV_LOCK_DAV;
use common::{Server, auth::AccessToken};
use dav_proto::schema::property::{ActiveLock, LockScope, WebDavProperty};
use dav_proto::schema::request::DavPropertyValue;
use dav_proto::schema::response::{BaseCondition, List, PropResponse};
use dav_proto::{Condition, Depth, Timeout};
use dav_proto::{RequestHeaders, schema::request::LockInfo};
use groupware::cache::GroupwareCache;
use http_proto::HttpResponse;
use hyper::StatusCode;
use std::collections::HashMap;
use store::ValueKey;
use store::dispatch::lookup::KeyValue;
use store::write::serialize::rkyv_deserialize;
use store::write::{AlignedBytes, Archive, Archiver, now};
use store::{Serialize, U32_LEN};
use trc::AddContext;
use types::collection::Collection;
use types::dead_property::DeadProperty;
#[derive(Debug, Default, Clone)]
pub struct ResourceState<'x> {
pub account_id: u32,
pub collection: Collection,
pub document_id: Option<u32>,
pub etag: Option<String>,
pub lock_tokens: Vec<String>,
pub sync_token: Option<String>,
pub path: &'x str,
}
#[derive(Debug, Default, Clone, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
pub(crate) struct LockData {
locks: HashMap<String, LockItems>,
}
#[derive(Debug, Default, Clone, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
#[repr(transparent)]
pub(crate) struct LockItems(Vec<LockItem>);
#[derive(Debug, Default, Clone, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
pub(crate) struct LockItem {
lock_id: u64,
owner: u32,
expires: u64,
depth_infinity: bool,
exclusive: bool,
owner_dav: Option<DeadProperty>,
}
struct LockCache<'x> {
account_id: u32,
collection: Collection,
lock_archive: LockArchive<'x>,
}
enum LockArchive<'x> {
Unarchived(&'x ArchivedLockData),
Archived(Archive<AlignedBytes>),
}
#[derive(Default)]
pub(crate) struct LockCaches<'x> {
caches: Vec<LockCache<'x>>,
}
pub(crate) trait LockRequestHandler: Sync + Send {
fn handle_lock_request(
&self,
access_token: &AccessToken,
headers: &RequestHeaders<'_>,
lock_info: LockRequest,
) -> impl Future<Output = crate::Result<HttpResponse>> + Send;
fn validate_headers(
&self,
access_token: &AccessToken,
headers: &RequestHeaders<'_>,
resources: Vec<ResourceState<'_>>,
locks: LockCaches<'_>,
method: DavMethod,
) -> impl Future<Output = crate::Result<()>> + Send;
}
pub(crate) enum LockRequest {
Lock(LockInfo),
Unlock,
Refresh,
}
impl LockRequestHandler for Server {
async fn handle_lock_request(
&self,
access_token: &AccessToken,
headers: &RequestHeaders<'_>,
lock_info: LockRequest,
) -> crate::Result<HttpResponse> {
let resource = self
.validate_uri(access_token, headers.uri)
.await?
.into_owned_uri()?;
let resource_hash = resource.lock_key();
let resource_path = resource
.resource
.ok_or(DavError::Code(StatusCode::CONFLICT))?;
let account_id = resource.account_id;
if !access_token.is_member(account_id) {
return Err(DavError::Code(StatusCode::FORBIDDEN));
}
let resources = vec![ResourceState {
account_id,
collection: resource.collection,
path: resource_path,
..Default::default()
}];
let mut base_path = None;
let is_lock_request = !matches!(lock_info, LockRequest::Unlock);
let if_lock_token = headers
.if_
.iter()
.flat_map(|if_| if_.list.iter())
.find_map(|cond| {
if let Condition::StateToken { token, .. } = cond {
Urn::parse(token).and_then(|u| u.try_unwrap_lock())
} else {
None
}
})
.unwrap_or_default();
let mut lock_data = if let Some(lock_data) = self
.in_memory_store()
.key_get::<Archive<AlignedBytes>>(resource_hash.as_slice())
.await
.caused_by(trc::location!())?
{
let lock_data = lock_data
.unarchive::<LockData>()
.caused_by(trc::location!())?;
self.validate_headers(
access_token,
headers,
resources,
LockCaches::new_shared(account_id, resource.collection, lock_data),
if is_lock_request {
DavMethod::LOCK
} else {
DavMethod::UNLOCK
},
)
.await?;
if let LockRequest::Lock(lock_info) = &lock_info {
let mut failed_locks = Vec::new();
let is_exclusive = matches!(lock_info.lock_scope, LockScope::Exclusive);
let is_infinity = matches!(headers.depth, Depth::Infinity);
for (lock_path, lock_item) in lock_data.find_locks(resource_path, true) {
if if_lock_token != lock_item.lock_id
&& (lock_item.exclusive || is_exclusive)
&& (lock_path.len() == resource_path.len()
|| lock_item.depth_infinity && resource_path.len() > lock_path.len()
|| is_infinity && lock_path.len() > resource_path.len())
{
let base_path =
base_path.get_or_insert_with(|| headers.base_uri().unwrap_or_default());
failed_locks.push(format!("{base_path}/{lock_path}").into());
}
}
if !failed_locks.is_empty() {
return Err(DavErrorCondition::new(
StatusCode::LOCKED,
BaseCondition::LockTokenSubmitted(List(failed_locks)),
)
.into());
}
// Validate lock_info
if lock_info.owner.as_ref().is_some_and(|o| {
o.size() > self.core.groupware.dead_property_size.unwrap_or(512)
}) {
return Err(DavError::Code(StatusCode::PAYLOAD_TOO_LARGE));
}
if self.core.groupware.max_locks_per_user > 0
&& lock_data
.locks
.values()
.flat_map(|locks| {
locks
.0
.iter()
.filter(|lock| lock.owner == access_token.account_id())
})
.count()
>= self.core.groupware.max_locks_per_user
{
return Err(DavError::Code(StatusCode::TOO_MANY_REQUESTS));
}
}
rkyv_deserialize(lock_data).caused_by(trc::location!())?
} else if is_lock_request {
self.validate_headers(
access_token,
headers,
resources,
Default::default(),
DavMethod::LOCK,
)
.await?;
LockData::default()
} else {
return Err(DavErrorCondition::new(
StatusCode::CONFLICT,
BaseCondition::LockTokenMatchesRequestUri,
)
.into());
};
let now = now();
let response = if is_lock_request {
let timeout = if let Timeout::Second(seconds) = headers.timeout {
std::cmp::min(seconds, self.core.groupware.max_lock_timeout)
} else {
self.core.groupware.max_lock_timeout
};
let expires = now + timeout;
let lock_item = if if_lock_token > 0 {
if let Some(lock_item) = lock_data
.locks
.values_mut()
.flat_map(|locks| locks.0.iter_mut())
.find(|lock| lock.lock_id == if_lock_token)
{
lock_item
} else {
return Err(DavError::Code(StatusCode::PRECONDITION_FAILED));
}
} else {
let locks = lock_data
.locks
.entry(resource_path.to_string())
.or_insert_with(Default::default);
locks.0.push(LockItem::default());
locks.0.last_mut().unwrap()
};
lock_item.expires = expires;
if let LockRequest::Lock(lock_info) = lock_info {
// Validate lock_info
if lock_info.owner.as_ref().is_some_and(|o| {
o.size() > self.core.groupware.dead_property_size.unwrap_or(512)
}) {
return Err(DavError::Code(StatusCode::PAYLOAD_TOO_LARGE));
}
lock_item.lock_id = store::rand::random::<u64>() ^ expires;
lock_item.owner = access_token.account_id();
lock_item.depth_infinity = matches!(headers.depth, Depth::Infinity);
lock_item.owner_dav = lock_info.owner;
lock_item.exclusive = matches!(lock_info.lock_scope, LockScope::Exclusive);
}
let base_path = base_path.get_or_insert_with(|| headers.base_uri().unwrap_or_default());
let active_lock = lock_item.to_active_lock(format!("{base_path}/{resource_path}"));
HttpResponse::new(if if_lock_token == 0 {
StatusCode::CREATED
} else {
StatusCode::OK
})
.with_lock_token(&active_lock.lock_token.as_ref().unwrap().0)
.with_xml_body(
PropResponse::new(vec![DavPropertyValue::new(
WebDavProperty::LockDiscovery,
vec![active_lock],
)])
.to_string(),
)
} else {
let lock_id = headers
.lock_token
.and_then(Urn::parse)
.and_then(|urn| urn.try_unwrap_lock())
.ok_or(DavError::Code(StatusCode::BAD_REQUEST))?;
if lock_data.remove_lock(lock_id) {
HttpResponse::new(StatusCode::NO_CONTENT)
} else {
return Err(DavErrorCondition::new(
StatusCode::CONFLICT,
BaseCondition::LockTokenMatchesRequestUri,
)
.into());
}
};
// Remove expired locks
let max_expire = lock_data.remove_expired();
if max_expire > 0 {
self.in_memory_store()
.key_set(
KeyValue::new(
resource_hash,
Archiver::new(lock_data)
.untrusted()
.serialize()
.caused_by(trc::location!())?,
)
.expires(max_expire),
)
.await
.caused_by(trc::location!())?;
} else {
self.in_memory_store()
.key_delete(resource_hash)
.await
.caused_by(trc::location!())?;
}
Ok(response)
}
async fn validate_headers(
&self,
access_token: &AccessToken,
headers: &RequestHeaders<'_>,
mut resources: Vec<ResourceState<'_>>,
mut locks_: LockCaches<'_>,
method: DavMethod,
) -> crate::Result<()> {
let no_if_headers = headers.if_.is_empty();
match method {
DavMethod::GET | DavMethod::HEAD if no_if_headers => {
// Return early for GET/HEAD requests without If headers
return Ok(());
}
DavMethod::COPY
| DavMethod::MOVE
| DavMethod::POST
| DavMethod::PUT
| DavMethod::PATCH
if headers.overwrite_fail
&& resources.last().is_some_and(|r| {
r.etag.is_some() || r.document_id.is_some_and(|id| id != u32::MAX)
}) =>
{
return Err(DavError::Code(StatusCode::PRECONDITION_FAILED));
}
_ => {}
}
// Add lock data to the cache
for resource in &resources {
if locks_.is_cached(resource).is_none() {
locks_.insert_lock_data(self, resource).await?;
}
}
// Unarchive lock data
let mut locks = locks_.to_unarchived().caused_by(trc::location!())?;
// Validate locks for write operations
let mut lock_response = Ok(());
if !matches!(
method,
DavMethod::GET | DavMethod::HEAD | DavMethod::LOCK | DavMethod::UNLOCK
) {
let mut base_path = None;
'outer: for (pos, resource) in resources.iter().enumerate() {
if pos == 0 && matches!(method, DavMethod::COPY) {
continue;
}
if let Some(idx) = locks.find_cache_pos(self, resource).await? {
let mut failed_locks = Vec::new();
for (lock_path, lock_item) in locks.find_locks_by_pos(idx, resource, true)? {
let lock_token = lock_item.urn().to_string();
if headers.if_.iter().any(|if_| {
if_.resource
.is_none_or(|r| {
r.trim_end_matches('/').ends_with(lock_path)})
&& if_.list.iter().any(|cond| matches!(cond, Condition::StateToken { token, .. } if token == &lock_token))
}) {
break 'outer;
} else {
let base_path = base_path.get_or_insert_with(|| {
headers.base_uri()
.unwrap_or_default()
});
failed_locks.push(format!("{base_path}/{lock_path}").into());
}
}
if !failed_locks.is_empty() {
lock_response = Err(DavErrorCondition::new(
StatusCode::LOCKED,
BaseCondition::LockTokenSubmitted(List(failed_locks)),
)
.into());
break;
}
}
}
}
// There are no If headers, so we can return early
if no_if_headers {
return lock_response;
}
let mut resource_not_found = ResourceState {
account_id: u32::MAX,
collection: Collection::None,
path: "",
..Default::default()
};
'outer: for if_ in &headers.if_ {
if if_.list.is_empty() {
continue;
}
let mut resource_state = &mut resource_not_found;
if let Some(resource) = if_.resource {
if let Some(resource) = self
.validate_uri(access_token, resource)
.await
.ok()
.and_then(|r| {
let path = r.resource?;
Some(ResourceState {
account_id: r.account_id?,
collection: if !matches!(r.collection, Collection::FileNode)
&& path.contains('/')
{
r.collection.child_collection().unwrap_or(r.collection)
} else {
r.collection
},
path,
..Default::default()
})
})
{
if let Some(known_resource) = resources.iter_mut().find(|r| {
r.account_id == resource.account_id
&& r.collection == resource.collection
&& r.path == resource.path
}) {
resource_state = known_resource;
} else if access_token.has_access(resource.account_id, resource.collection) {
resources.push(resource);
resource_state = resources.last_mut().unwrap();
}
}
} else if let Some(resource) = resources.first_mut() {
resource_state = resource;
};
// Fill missing data for resource
if resource_state.collection != Collection::None
&& (resource_state.etag.is_none()
|| resource_state.lock_tokens.is_empty()
|| resource_state.sync_token.is_none())
{
let mut needs_lock_token = false;
let mut needs_sync_token = false;
let mut needs_etag = false;
for cond in &if_.list {
match cond {
Condition::StateToken { token, .. } => {
if token.starts_with("urn:stalwart:davsync:") {
needs_sync_token = true;
} else {
needs_lock_token = true;
}
}
Condition::ETag { .. } | Condition::Exists { .. } => {
needs_etag = true;
}
}
}
// Fetch eTag
if needs_etag && resource_state.etag.is_none() {
if resource_state.document_id.is_none() {
resource_state.document_id = self
.map_uri_resource(
access_token,
UriResource {
collection: resource_state.collection,
account_id: resource_state.account_id,
resource: resource_state.path.into(),
},
)
.await
.caused_by(trc::location!())?
.map(|uri| uri.resource)
.unwrap_or(u32::MAX)
.into();
}
if let Some(document_id) =
resource_state.document_id.filter(|&id| id != u32::MAX)
&& let Some(archive) = self
.store()
.get_value::<Archive<AlignedBytes>>(ValueKey::archive(
resource_state.account_id,
resource_state.collection,
document_id,
))
.await
.caused_by(trc::location!())?
{
resource_state.etag = archive.etag().into();
}
}
// Fetch lock token
if needs_lock_token
&& resource_state.lock_tokens.is_empty()
&& let Some(idx) = locks.find_cache_pos(self, resource_state).await?
{
let found_locks = locks
.find_locks_by_pos(idx, resource_state, false)?
.iter()
.map(|(_, lock)| lock.urn().to_string())
.collect::<Vec<_>>();
resource_state.lock_tokens = found_locks;
}
// Fetch sync token
if needs_sync_token && resource_state.sync_token.is_none() {
let id = self
.fetch_dav_resources(
access_token.account_id(),
resource_state.account_id,
resource_state.collection.into(),
)
.await
.caused_by(trc::location!())?
.highest_change_id;
resource_state.sync_token = Some(Urn::Sync { id, seq: 0 }.to_string());
}
}
for cond in &if_.list {
match cond {
Condition::StateToken { is_not, token } => {
if let Some(token) = Urn::try_extract_sync_id(token) {
if !((resource_state
.sync_token
.as_deref()
.and_then(Urn::try_extract_sync_id)
.is_some_and(|sync_token| sync_token == token))
^ is_not)
{
continue 'outer;
}
} else if !((resource_state.lock_tokens.iter().any(|t| t == token))
^ is_not)
{
continue 'outer;
}
}
Condition::ETag { is_not, tag } => {
if !((resource_state.etag.as_ref().is_some_and(|etag| etag == tag))
^ is_not)
{
continue 'outer;
}
}
Condition::Exists { is_not } => {
if !((resource_state.etag.is_some()) ^ is_not) {
continue 'outer;
}
}
}
}
return lock_response;
}
Err(DavError::Code(
if matches!(method, DavMethod::GET | DavMethod::HEAD)
&& headers
.if_
.iter()
.any(|if_| if_.list.iter().any(|cond| cond.is_none_match()))
{
StatusCode::NOT_MODIFIED
} else {
StatusCode::PRECONDITION_FAILED
},
))
}
}
impl LockData {
pub fn remove_lock(&mut self, lock_id: u64) -> bool {
for (lock_path, lock_items) in self.locks.iter_mut() {
for (idx, lock_item) in lock_items.0.iter().enumerate() {
if lock_item.lock_id == lock_id {
lock_items.0.swap_remove(idx);
if lock_items.0.is_empty() {
let lock_path = lock_path.clone();
self.locks.remove(&lock_path);
}
return true;
}
}
}
false
}
pub fn remove_expired(&mut self) -> u64 {
let mut max_expire = 0;
let now = now();
self.locks.retain(|_, locks| {
locks.0.retain(|lock| {
if lock.expires > now {
max_expire = std::cmp::max(max_expire, lock.expires);
true
} else {
false
}
});
!locks.0.is_empty()
});
max_expire
}
}
impl<'x> LockArchive<'x> {
fn unarchive(&'x self) -> trc::Result<&'x ArchivedLockData> {
match self {
LockArchive::Unarchived(archived_lock_data) => Ok(archived_lock_data),
LockArchive::Archived(archive) => {
archive.unarchive::<LockData>().caused_by(trc::location!())
}
}
}
}
impl<'x> LockCaches<'x> {
pub(self) fn new_shared(
account_id: u32,
collection: Collection,
lock_data: &'x ArchivedLockData,
) -> Self {
Self {
caches: vec![LockCache {
account_id,
collection,
lock_archive: LockArchive::Unarchived(lock_data),
}],
}
}
pub fn to_unarchived(&'x self) -> trc::Result<LockCaches<'x>> {
let caches = self
.caches
.iter()
.map(|cache| {
Ok(LockCache {
account_id: cache.account_id,
collection: cache.collection,
lock_archive: LockArchive::Unarchived(
cache.lock_archive.unarchive().caused_by(trc::location!())?,
),
})
})
.collect::<trc::Result<Vec<_>>>()?;
Ok(LockCaches { caches })
}
#[inline]
pub fn is_cached(&self, resource_state: &ResourceState<'_>) -> Option<usize> {
self.caches.iter().position(|cache| {
resource_state.account_id == cache.account_id
&& resource_state.collection.main_collection() == cache.collection.main_collection()
})
}
pub async fn find_cache_pos(
&mut self,
server: &Server,
resource_state: &ResourceState<'_>,
) -> trc::Result<Option<usize>> {
if let Some(idx) = self.is_cached(resource_state) {
Ok(Some(idx))
} else if resource_state.collection != Collection::None {
if self.insert_lock_data(server, resource_state).await? {
Ok(Some(self.caches.len() - 1))
} else {
Ok(None)
}
} else {
Ok(None)
}
}
fn find_locks_by_pos(
&'x self,
pos: usize,
resource_state: &'x ResourceState<'_>,
include_children: bool,
) -> trc::Result<Vec<(&'x str, &'x ArchivedLockItem)>> {
self.caches[pos]
.lock_archive
.unarchive()
.map(|l| l.find_locks(resource_state.path, include_children))
}
async fn insert_lock_data(
&mut self,
server: &Server,
resource_state: &ResourceState<'_>,
) -> trc::Result<bool> {
if let Some(lock_archive) = server
.in_memory_store()
.key_get::<Archive<AlignedBytes>>(resource_state.lock_key().as_slice())
.await
.caused_by(trc::location!())?
{
self.caches.push(LockCache {
account_id: resource_state.account_id,
collection: resource_state.collection,
lock_archive: LockArchive::Archived(lock_archive),
});
Ok(true)
} else {
Ok(false)
}
}
}
impl LockItem {
pub fn to_active_lock(&self, href: String) -> ActiveLock {
ActiveLock::new(
href,
if self.exclusive {
LockScope::Exclusive
} else {
LockScope::Shared
},
)
.with_depth(if self.depth_infinity {
Depth::Infinity
} else {
Depth::Zero
})
.with_owner_opt(self.owner_dav.clone())
.with_timeout(self.expires.saturating_sub(now()))
.with_lock_token(self.urn().to_string())
}
pub fn urn(&self) -> Urn {
Urn::Lock(self.lock_id)
}
}
impl ArchivedLockData {
pub fn find_locks<'x: 'y, 'y>(
&'x self,
resource: &'y str,
include_children: bool,
) -> Vec<(&'y str, &'x ArchivedLockItem)> {
let now = now();
let mut resource_part = resource;
let mut found_locks = Vec::new();
loop {
if let Some(locks) = self.locks.get(resource_part) {
found_locks.extend(
locks
.0
.iter()
.filter(|lock| {
lock.expires > now && (resource == resource_part || lock.depth_infinity)
})
.map(|lock| (resource_part, lock)),
);
}
if let Some((resource_part_, _)) = resource_part.rsplit_once('/') {
resource_part = resource_part_;
} else {
break;
}
}
if include_children {
let prefix = format!("{}/", resource);
for (resource_part, locks) in self.locks.iter() {
if resource_part.starts_with(&prefix) {
found_locks.extend(
locks
.0
.iter()
.filter(|lock| lock.expires > now)
.map(|lock| (resource_part.as_str(), lock)),
);
}
}
}
found_locks
}
}
impl ArchivedLockItem {
pub fn to_active_lock(&self, href: String) -> ActiveLock {
ActiveLock::new(
href,
if self.exclusive {
LockScope::Exclusive
} else {
LockScope::Shared
},
)
.with_depth(if self.depth_infinity {
Depth::Infinity
} else {
Depth::Zero
})
.with_owner_opt(self.owner_dav.as_ref().map(Into::into))
.with_timeout(u64::from(self.expires).saturating_sub(now()))
.with_lock_token(self.urn().to_string())
}
pub fn urn(&self) -> Urn {
Urn::Lock(self.lock_id.into())
}
}
impl OwnedUri<'_> {
pub fn lock_key(&self) -> Vec<u8> {
build_lock_key(self.account_id, self.collection.main_collection())
}
}
impl ResourceState<'_> {
pub fn lock_key(&self) -> Vec<u8> {
build_lock_key(self.account_id, self.collection.main_collection())
}
}
pub(crate) fn build_lock_key(account_id: u32, collection: Collection) -> Vec<u8> {
let mut result = Vec::with_capacity(U32_LEN + 2);
result.push(KV_LOCK_DAV);
result.extend_from_slice(account_id.to_be_bytes().as_slice());
result.push(u8::from(collection));
result
}
impl PartialEq for ResourceState<'_> {
fn eq(&self, other: &Self) -> bool {
self.account_id == other.account_id
&& self.collection == other.collection
&& self.document_id == other.document_id
}
}
impl Eq for ResourceState<'_> {}
+520
View File
@@ -0,0 +1,520 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use calcard::{
icalendar::{ICalendarComponentType, ICalendarParameterName, ICalendarProperty},
vcard::{VCardParameterName, VCardVersion},
};
use dav_proto::{
Depth, RequestHeaders, Return,
schema::{
Namespace,
property::{DavProperty, ReportSet, ResourceType},
request::{
AddressbookQuery, CalendarQuery, ExpandProperty, Filter, MultiGet, PropFind,
SyncCollection, Timezone, VCardPropertyWithGroup,
},
},
};
use groupware::{
calendar::{
ArchivedCalendar, ArchivedCalendarEvent, ArchivedCalendarEventNotification, Calendar,
CalendarEvent, CalendarEventNotification,
},
contact::{AddressBook, ArchivedAddressBook, ArchivedContactCard, ContactCard},
file::{ArchivedFileNode, FileNode},
};
use propfind::PropFindItem;
use rkyv::vec::ArchivedVec;
use store::write::{AlignedBytes, Archive, BatchBuilder, Operation, ValueClass, ValueOp};
use types::{
TimeRange, acl::ArchivedAclGrant, collection::Collection, dead_property::ArchivedDeadProperty,
field::Field,
};
use uri::{OwnedUri, Urn};
pub mod acl;
pub mod lock;
pub mod propfind;
pub mod uri;
#[derive(Debug)]
pub(crate) struct DavQuery<'x> {
pub uri: &'x str,
pub resource: DavQueryResource<'x>,
pub propfind: PropFind,
pub sync_type: SyncType,
pub depth: usize,
pub limit: Option<u32>,
pub vcard_version: Option<VCardVersion>,
pub ret: Return,
pub depth_no_root: bool,
pub expand: bool,
}
#[derive(Default, Debug)]
pub(crate) enum SyncType {
#[default]
None,
Initial,
From {
id: u64,
seq: u32,
},
}
#[derive(Default, Debug)]
pub(crate) enum DavQueryResource<'x> {
Uri(OwnedUri<'x>),
Multiget {
parent_collection: Collection,
hrefs: Vec<String>,
},
Query {
filter: DavQueryFilter,
parent_collection: Collection,
items: Vec<PropFindItem>,
},
#[default]
None,
}
pub(crate) type AddressbookFilter = Vec<Filter<(), VCardPropertyWithGroup, VCardParameterName>>;
pub(crate) type CalendarFilter =
Vec<Filter<Vec<ICalendarComponentType>, ICalendarProperty, ICalendarParameterName>>;
#[derive(Debug)]
pub(crate) enum DavQueryFilter {
Addressbook(AddressbookFilter),
Calendar {
filter: CalendarFilter,
max_time_range: Option<TimeRange>,
timezone: Timezone,
},
}
pub(crate) trait ETag {
fn etag(&self) -> String;
}
pub(crate) trait ExtractETag {
fn etag(&self) -> Option<String>;
}
impl<T> ETag for Archive<T> {
fn etag(&self) -> String {
format!("\"{}\"", self.version.hash().unwrap_or_default())
}
}
impl ExtractETag for BatchBuilder {
fn etag(&self) -> Option<String> {
let p_value = u8::from(Field::ARCHIVE);
for op in self.ops().iter().rev() {
match op {
Operation::Value {
class: ValueClass::Property(p_id),
op: ValueOp::Set(value),
} if *p_id == p_value => {
return Archive::<AlignedBytes>::extract_hash(value)
.map(|hash| format!("\"{}\"", hash));
}
Operation::Value {
class: ValueClass::Property(p_id),
op: ValueOp::SetFnc(set_fnc),
} if *p_id == p_value => {
return Archive::<AlignedBytes>::extract_hash(set_fnc.params().bytes(0))
.map(|hash| format!("\"{}\"", hash));
}
_ => {}
}
}
None
}
}
pub(crate) trait DavCollection {
fn namespace(&self) -> Namespace;
}
impl DavCollection for Collection {
fn namespace(&self) -> Namespace {
match self {
Collection::Calendar
| Collection::CalendarEvent
| Collection::CalendarEventNotification => Namespace::CalDav,
Collection::AddressBook | Collection::ContactCard => Namespace::CardDav,
_ => Namespace::Dav,
}
}
}
impl<'x> DavQuery<'x> {
pub fn propfind(
resource: OwnedUri<'x>,
propfind: PropFind,
headers: &RequestHeaders<'x>,
) -> Self {
Self {
resource: DavQueryResource::Uri(resource),
propfind,
depth: match headers.depth {
Depth::Zero => 0,
_ => 1,
},
ret: headers.ret,
depth_no_root: headers.depth_no_root,
uri: headers.uri,
vcard_version: headers.vcard_version,
sync_type: Default::default(),
limit: Default::default(),
expand: Default::default(),
}
}
pub fn multiget(
multiget: MultiGet,
collection: Collection,
headers: &RequestHeaders<'x>,
) -> Self {
Self {
resource: DavQueryResource::Multiget {
hrefs: multiget.hrefs,
parent_collection: collection,
},
propfind: multiget.properties,
ret: headers.ret,
depth_no_root: headers.depth_no_root,
uri: headers.uri,
vcard_version: headers.vcard_version,
sync_type: Default::default(),
depth: Default::default(),
limit: Default::default(),
expand: Default::default(),
}
}
pub fn addressbook_query(
query: AddressbookQuery,
items: Vec<PropFindItem>,
headers: &RequestHeaders<'x>,
) -> Self {
Self {
resource: DavQueryResource::Query {
filter: DavQueryFilter::Addressbook(query.filters),
parent_collection: Collection::AddressBook,
items,
},
propfind: query.properties,
limit: query.limit,
ret: headers.ret,
depth_no_root: headers.depth_no_root,
uri: headers.uri,
vcard_version: headers.vcard_version,
sync_type: Default::default(),
depth: Default::default(),
expand: Default::default(),
}
}
pub fn calendar_query(
query: CalendarQuery,
max_time_range: Option<TimeRange>,
items: Vec<PropFindItem>,
headers: &RequestHeaders<'x>,
) -> Self {
Self {
resource: DavQueryResource::Query {
filter: DavQueryFilter::Calendar {
filter: query.filters,
timezone: query.timezone,
max_time_range,
},
parent_collection: Collection::Calendar,
items,
},
propfind: query.properties,
ret: headers.ret,
depth_no_root: headers.depth_no_root,
uri: headers.uri,
sync_type: Default::default(),
depth: Default::default(),
limit: Default::default(),
vcard_version: Default::default(),
expand: Default::default(),
}
}
pub fn changes(
resource: OwnedUri<'x>,
changes: SyncCollection,
headers: &RequestHeaders<'x>,
) -> Self {
Self {
resource: DavQueryResource::Uri(resource),
propfind: changes.properties,
sync_type: changes
.sync_token
.as_deref()
.and_then(Urn::parse)
.and_then(|urn| urn.try_unwrap_sync())
.map(|(id, seq)| SyncType::From { id, seq })
.unwrap_or(SyncType::Initial),
depth: match changes.depth {
Depth::One => 1,
Depth::Infinity => usize::MAX,
_ => 0,
},
limit: changes.limit,
ret: headers.ret,
depth_no_root: headers.depth_no_root,
expand: false,
uri: headers.uri,
vcard_version: headers.vcard_version,
}
}
pub fn expand(
resource: OwnedUri<'x>,
expand: ExpandProperty,
headers: &RequestHeaders<'x>,
) -> Self {
let mut props = Vec::with_capacity(expand.properties.len());
for item in expand.properties {
if !matches!(item.property, DavProperty::DeadProperty(_))
&& !props.contains(&item.property)
{
props.push(item.property);
}
}
Self {
resource: DavQueryResource::Uri(resource),
propfind: PropFind::Prop(props),
depth: match headers.depth {
Depth::Zero => 0,
_ => 1,
},
ret: headers.ret,
depth_no_root: headers.depth_no_root,
expand: true,
uri: headers.uri,
sync_type: Default::default(),
limit: Default::default(),
vcard_version: headers.vcard_version,
}
}
pub fn is_minimal(&self) -> bool {
self.ret == Return::Minimal
}
}
pub(crate) enum ArchivedResource<'x> {
Calendar(Archive<&'x ArchivedCalendar>),
CalendarEvent(Archive<&'x ArchivedCalendarEvent>),
CalendarEventNotification(Archive<&'x ArchivedCalendarEventNotification>),
CalendarEventNotificationCollection(bool),
AddressBook(Archive<&'x ArchivedAddressBook>),
ContactCard(Archive<&'x ArchivedContactCard>),
FileNode(Archive<&'x ArchivedFileNode>),
}
impl<'x> ArchivedResource<'x> {
pub fn from_archive(
archive: &'x Archive<AlignedBytes>,
collection: Collection,
) -> trc::Result<Self> {
match collection {
Collection::Calendar => archive
.to_unarchived::<Calendar>()
.map(ArchivedResource::Calendar),
Collection::CalendarEvent => archive
.to_unarchived::<CalendarEvent>()
.map(ArchivedResource::CalendarEvent),
Collection::CalendarEventNotification => archive
.to_unarchived::<CalendarEventNotification>()
.map(ArchivedResource::CalendarEventNotification),
Collection::AddressBook => archive
.to_unarchived::<AddressBook>()
.map(ArchivedResource::AddressBook),
Collection::FileNode => archive
.to_unarchived::<FileNode>()
.map(ArchivedResource::FileNode),
Collection::ContactCard => archive
.to_unarchived::<ContactCard>()
.map(ArchivedResource::ContactCard),
_ => unreachable!(),
}
}
pub fn acls(&self) -> Option<&ArchivedVec<ArchivedAclGrant>> {
match self {
Self::Calendar(archive) => Some(&archive.inner.acls),
Self::AddressBook(archive) => Some(&archive.inner.acls),
Self::FileNode(archive) => Some(&archive.inner.acls),
_ => None,
}
}
pub fn created(&self) -> i64 {
match self {
ArchivedResource::Calendar(archive) => archive.inner.created.to_native(),
ArchivedResource::CalendarEvent(archive) => archive.inner.created.to_native(),
ArchivedResource::AddressBook(archive) => archive.inner.created.to_native(),
ArchivedResource::ContactCard(archive) => archive.inner.created.to_native(),
ArchivedResource::FileNode(archive) => archive.inner.created.to_native(),
ArchivedResource::CalendarEventNotification(archive) => {
archive.inner.created.to_native()
}
ArchivedResource::CalendarEventNotificationCollection(_) => 1634515200,
}
}
pub fn modified(&self) -> i64 {
match self {
ArchivedResource::Calendar(archive) => archive.inner.modified.to_native(),
ArchivedResource::CalendarEvent(archive) => archive.inner.modified.to_native(),
ArchivedResource::AddressBook(archive) => archive.inner.modified.to_native(),
ArchivedResource::ContactCard(archive) => archive.inner.modified.to_native(),
ArchivedResource::FileNode(archive) => archive.inner.modified.to_native(),
ArchivedResource::CalendarEventNotification(archive) => {
archive.inner.modified.to_native()
}
ArchivedResource::CalendarEventNotificationCollection(_) => 1634515200,
}
}
pub fn dead_properties(&self) -> Option<&ArchivedDeadProperty> {
match self {
ArchivedResource::Calendar(archive) => Some(&archive.inner.dead_properties),
ArchivedResource::CalendarEvent(archive) => Some(&archive.inner.dead_properties),
ArchivedResource::AddressBook(archive) => Some(&archive.inner.dead_properties),
ArchivedResource::ContactCard(archive) => Some(&archive.inner.dead_properties),
ArchivedResource::FileNode(archive) => Some(&archive.inner.dead_properties),
ArchivedResource::CalendarEventNotification(_)
| ArchivedResource::CalendarEventNotificationCollection(_) => None,
}
}
pub fn content_length(&self) -> Option<u32> {
match self {
ArchivedResource::FileNode(archive) => {
archive.inner.file.as_ref().map(|f| f.size.to_native())
}
ArchivedResource::CalendarEvent(archive) => archive.inner.size.to_native().into(),
ArchivedResource::CalendarEventNotification(archive) => {
archive.inner.size.to_native().into()
}
ArchivedResource::ContactCard(archive) => archive.inner.size.to_native().into(),
ArchivedResource::AddressBook(_)
| ArchivedResource::Calendar(_)
| ArchivedResource::CalendarEventNotificationCollection(_) => None,
}
}
pub fn content_type(&self) -> Option<&str> {
match self {
ArchivedResource::FileNode(archive) => archive
.inner
.file
.as_ref()
.and_then(|f| f.media_type.as_deref()),
ArchivedResource::CalendarEvent(_) | ArchivedResource::CalendarEventNotification(_) => {
"text/calendar".into()
}
ArchivedResource::ContactCard(_) => "text/vcard".into(),
ArchivedResource::AddressBook(_)
| ArchivedResource::Calendar(_)
| ArchivedResource::CalendarEventNotificationCollection(_) => None,
}
}
pub fn display_name(&self, account_id: u32) -> Option<&str> {
match self {
ArchivedResource::Calendar(archive) => {
Some(archive.inner.preferences(account_id).name.as_str())
}
ArchivedResource::CalendarEvent(archive) => archive.inner.display_name.as_deref(),
ArchivedResource::AddressBook(archive) => {
Some(archive.inner.preferences(account_id).name.as_str())
}
ArchivedResource::ContactCard(archive) => archive.inner.display_name.as_deref(),
ArchivedResource::FileNode(archive) => archive.inner.display_name.as_deref(),
ArchivedResource::CalendarEventNotification(_)
| ArchivedResource::CalendarEventNotificationCollection(_) => None,
}
}
pub fn supported_report_set(&self) -> Option<Vec<ReportSet>> {
match self {
ArchivedResource::Calendar(_) => vec![
ReportSet::SyncCollection,
ReportSet::AclPrincipalPropSet,
ReportSet::PrincipalMatch,
ReportSet::ExpandProperty,
ReportSet::CalendarQuery,
ReportSet::CalendarMultiGet,
ReportSet::FreeBusyQuery,
]
.into(),
ArchivedResource::AddressBook(_) => vec![
ReportSet::SyncCollection,
ReportSet::AclPrincipalPropSet,
ReportSet::PrincipalMatch,
ReportSet::ExpandProperty,
ReportSet::AddressbookQuery,
ReportSet::AddressbookMultiGet,
]
.into(),
ArchivedResource::FileNode(archive) if archive.inner.file.is_none() => vec![
ReportSet::SyncCollection,
ReportSet::AclPrincipalPropSet,
ReportSet::PrincipalMatch,
]
.into(),
ArchivedResource::CalendarEventNotificationCollection(_) => vec![
ReportSet::SyncCollection,
ReportSet::CalendarQuery,
ReportSet::CalendarMultiGet,
]
.into(),
_ => None,
}
}
pub fn resource_type(&self) -> Option<Vec<ResourceType>> {
match self {
ArchivedResource::Calendar(_) => {
vec![ResourceType::Collection, ResourceType::Calendar].into()
}
ArchivedResource::AddressBook(_) => {
vec![ResourceType::Collection, ResourceType::AddressBook].into()
}
ArchivedResource::FileNode(archive) if archive.inner.file.is_none() => {
vec![ResourceType::Collection].into()
}
ArchivedResource::CalendarEventNotificationCollection(true) => {
vec![ResourceType::Collection, ResourceType::ScheduleInbox].into()
}
ArchivedResource::CalendarEventNotificationCollection(false) => {
vec![ResourceType::Collection, ResourceType::ScheduleOutbox].into()
}
_ => None,
}
}
}
impl SyncType {
pub fn is_none(&self) -> bool {
matches!(self, SyncType::None)
}
pub fn is_none_or_initial(&self) -> bool {
matches!(self, SyncType::None | SyncType::Initial)
}
}
File diff suppressed because it is too large Load Diff
+236
View File
@@ -0,0 +1,236 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::{DavError, DavResourceName};
use common::{Server, auth::AccessToken};
use groupware::cache::GroupwareCache;
use http_proto::request::decode_path_element;
use hyper::StatusCode;
use std::fmt::Display;
use trc::AddContext;
use types::collection::Collection;
#[derive(Debug)]
pub(crate) struct UriResource<A, R> {
pub collection: Collection,
pub account_id: A,
pub resource: R,
}
pub(crate) enum Urn {
Lock(u64),
Sync { id: u64, seq: u32 },
}
pub(crate) type UnresolvedUri<'x> = UriResource<Option<u32>, Option<&'x str>>;
pub(crate) type OwnedUri<'x> = UriResource<u32, Option<&'x str>>;
pub(crate) type DocumentUri = UriResource<u32, u32>;
pub(crate) trait DavUriResource: Sync + Send {
fn validate_uri_with_status<'x>(
&self,
access_token: &AccessToken,
uri: &'x str,
error_status: StatusCode,
) -> impl Future<Output = crate::Result<UnresolvedUri<'x>>> + Send;
fn validate_uri<'x>(
&self,
access_token: &AccessToken,
uri: &'x str,
) -> impl Future<Output = crate::Result<UnresolvedUri<'x>>> + Send;
fn map_uri_resource(
&self,
access_token: &AccessToken,
uri: OwnedUri<'_>,
) -> impl Future<Output = trc::Result<Option<DocumentUri>>> + Send;
}
impl DavUriResource for Server {
async fn validate_uri<'x>(
&self,
access_token: &AccessToken,
uri: &'x str,
) -> crate::Result<UnresolvedUri<'x>> {
self.validate_uri_with_status(access_token, uri, StatusCode::NOT_FOUND)
.await
}
async fn validate_uri_with_status<'x>(
&self,
access_token: &AccessToken,
uri: &'x str,
error_status: StatusCode,
) -> crate::Result<UnresolvedUri<'x>> {
let (_, uri_parts) = uri
.split_once("/dav/")
.ok_or(DavError::Code(error_status))?;
let mut uri_parts = uri_parts
.trim_end_matches('/')
.splitn(3, '/')
.filter(|x| !x.is_empty());
let mut resource = UriResource {
collection: uri_parts
.next()
.and_then(DavResourceName::parse)
.ok_or(DavError::Code(error_status))?
.into(),
account_id: None,
resource: None,
};
if let Some(account) = uri_parts.next() {
// Parse account id
let account_id = if let Some(account_id) = account.strip_prefix('_') {
account_id
.parse::<u32>()
.map_err(|_| DavError::Code(error_status))?
} else {
let account = decode_path_element(account);
self.account_id_from_email(&account, false)
.await
.caused_by(trc::location!())?
.ok_or(DavError::Code(error_status))?
};
// Validate access
if resource.collection != Collection::Principal
&& !access_token.has_access(account_id, resource.collection)
{
return Err(DavError::Code(StatusCode::FORBIDDEN));
}
// Obtain remaining path
resource.account_id = Some(account_id);
resource.resource = uri_parts.next();
}
Ok(resource)
}
async fn map_uri_resource(
&self,
access_token: &AccessToken,
uri: OwnedUri<'_>,
) -> trc::Result<Option<DocumentUri>> {
if let Some(resource) = uri.resource {
if let Some(resource) = self
.fetch_dav_resources(
access_token.account_id(),
uri.account_id,
uri.collection.into(),
)
.await
.caused_by(trc::location!())?
.by_path(resource)
{
Ok(Some(DocumentUri {
collection: if resource.is_container() {
uri.collection
} else {
uri.collection.child_collection().unwrap_or(uri.collection)
},
account_id: uri.account_id,
resource: resource.document_id(),
}))
} else {
Ok(None)
}
} else {
Ok(None)
}
}
}
impl<'x> UnresolvedUri<'x> {
pub fn into_owned_uri(self) -> crate::Result<OwnedUri<'x>> {
Ok(OwnedUri {
collection: self.collection,
account_id: self
.account_id
.ok_or(DavError::Code(StatusCode::FORBIDDEN))?,
resource: self.resource,
})
}
}
impl OwnedUri<'_> {
pub fn new_owned(
collection: Collection,
account_id: u32,
resource: Option<&str>,
) -> OwnedUri<'_> {
OwnedUri {
collection,
account_id,
resource,
}
}
}
/*impl<A, R> UriResource<A, R> {
pub fn collection_path(&self) -> &'static str {
DavResourceName::from(self.collection).collection_path()
}
}*/
impl Urn {
pub fn try_extract_sync_id(token: &str) -> Option<&str> {
token
.strip_prefix("urn:stalwart:davsync:")
.map(|x| x.split_once(':').map(|(x, _)| x).unwrap_or(x))
}
pub fn parse(input: &str) -> Option<Self> {
let inbox = input.strip_prefix("urn:stalwart:")?;
let (kind, id) = inbox.split_once(':')?;
match kind {
"davlock" => u64::from_str_radix(id, 16).ok().map(Urn::Lock),
"davsync" => {
if let Some((id, seq)) = id.split_once(':') {
let id = u64::from_str_radix(id, 16).ok()?;
let seq = u32::from_str_radix(seq, 16).ok()?;
Some(Urn::Sync { id, seq })
} else {
u64::from_str_radix(id, 16)
.ok()
.map(|id| Urn::Sync { id, seq: 0 })
}
}
_ => None,
}
}
pub fn try_unwrap_lock(&self) -> Option<u64> {
match self {
Urn::Lock(id) => Some(*id),
_ => None,
}
}
pub fn try_unwrap_sync(&self) -> Option<(u64, u32)> {
match self {
Urn::Sync { id, seq } => Some((*id, *seq)),
_ => None,
}
}
}
impl Display for Urn {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Urn::Lock(id) => write!(f, "urn:stalwart:davlock:{id:x}",),
Urn::Sync { id, seq } => {
if *seq == 0 {
write!(f, "urn:stalwart:davsync:{id:x}")
} else {
write!(f, "urn:stalwart:davsync:{id:x}:{seq:x}")
}
}
}
}
}
+907
View File
@@ -0,0 +1,907 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use super::FromDavResource;
use crate::{
DavError, DavMethod,
common::{
ExtractETag,
lock::{LockRequestHandler, ResourceState},
uri::{DavUriResource, UriResource},
},
file::{DavFileResource, FileItemId},
};
use common::{
DavResourcePath, DavResources, Server, auth::AccessToken, storage::index::ObjectIndexBuilder,
};
use dav_proto::{Depth, RequestHeaders};
use groupware::{DestroyArchive, cache::GroupwareCache, file::FileNode};
use http_proto::HttpResponse;
use hyper::StatusCode;
use std::sync::Arc;
use store::{
ValueKey,
write::{AlignedBytes, Archive},
};
use store::{
ahash::AHashMap,
write::{BatchBuilder, now},
};
use trc::AddContext;
use types::{
acl::Acl,
collection::{Collection, SyncCollection, VanishedCollection},
};
pub(crate) trait FileCopyMoveRequestHandler: Sync + Send {
fn handle_file_copy_move_request(
&self,
access_token: &AccessToken,
headers: &RequestHeaders<'_>,
is_move: bool,
) -> impl Future<Output = crate::Result<HttpResponse>> + Send;
}
impl FileCopyMoveRequestHandler for Server {
async fn handle_file_copy_move_request(
&self,
access_token: &AccessToken,
headers: &RequestHeaders<'_>,
is_move: bool,
) -> crate::Result<HttpResponse> {
// Validate source
let from_resource_ = self
.validate_uri(access_token, headers.uri)
.await?
.into_owned_uri()?;
let from_account_id = from_resource_.account_id;
let from_resources = self
.fetch_dav_resources(
access_token.account_id(),
from_account_id,
SyncCollection::FileNode,
)
.await
.caused_by(trc::location!())?;
let from_resource = from_resources.map_resource::<FileItemId>(&from_resource_)?;
let from_resource_name = from_resource_.resource.unwrap();
// Validate source ACLs
if !access_token.is_member(from_account_id) {
let shared = from_resources.shared_containers(
access_token,
if is_move {
[Acl::Read, Acl::Delete].as_slice().iter().copied()
} else {
[Acl::Read].as_slice().iter().copied()
},
false,
);
for resource in from_resources.subtree(from_resource_.resource.unwrap()) {
if !shared.contains(resource.document_id()) {
return Err(DavError::Code(StatusCode::FORBIDDEN));
}
}
}
// Validate destination
let destination = self
.validate_uri_with_status(
access_token,
headers
.destination
.ok_or(DavError::Code(StatusCode::BAD_GATEWAY))?,
StatusCode::BAD_GATEWAY,
)
.await?;
if destination.collection != Collection::FileNode {
return Err(DavError::Code(StatusCode::BAD_GATEWAY));
}
let to_account_id = destination
.account_id
.ok_or(DavError::Code(StatusCode::BAD_GATEWAY))?;
let to_resources = if to_account_id == from_account_id {
from_resources.clone()
} else {
self.fetch_dav_resources(
access_token.account_id(),
to_account_id,
SyncCollection::FileNode,
)
.await
.caused_by(trc::location!())?
};
// Map file item
let destination_resource_name = destination
.resource
.ok_or(DavError::Code(StatusCode::BAD_GATEWAY))?;
if from_account_id == to_account_id
&& (from_resource_name == destination_resource_name
|| from_resource_name
.strip_prefix(destination_resource_name)
.is_some_and(|v| v.is_empty() || v.starts_with('/')))
{
return Ok(HttpResponse::new(StatusCode::BAD_GATEWAY));
}
// Check if the resource exists
let mut delete_destination = None;
let mut destination = if let Some((destination, new_name)) =
to_resources.map_parent(destination_resource_name)
{
if let Some(mut existing_destination) = to_resources
.by_path(destination_resource_name)
.map(Destination::from_dav_resource)
{
if !headers.overwrite_fail {
existing_destination.account_id = to_account_id;
delete_destination = Some(existing_destination);
} else {
return Ok(HttpResponse::new(StatusCode::PRECONDITION_FAILED));
}
}
let mut destination = destination
.map(Destination::from_dav_resource)
.unwrap_or_default();
destination.new_name = Some(new_name.to_string());
destination
} else {
return Err(DavError::Code(StatusCode::CONFLICT));
};
destination.account_id = to_account_id;
// Validate destination ACLs
if let Some(document_id) = destination.document_id {
if let Some(delete_destination) = &delete_destination
&& !access_token.is_member(to_account_id)
&& !to_resources.has_access_to_container(
access_token,
delete_destination.document_id.unwrap(),
Acl::Delete,
)
{
return Err(DavError::Code(StatusCode::FORBIDDEN));
}
if !access_token.is_member(to_account_id)
&& !to_resources.has_access_to_container(access_token, document_id, Acl::Modify)
{
return Err(DavError::Code(StatusCode::FORBIDDEN));
}
} else if !access_token.is_member(to_account_id) {
return Err(DavError::Code(StatusCode::FORBIDDEN));
}
// Validate headers
self.validate_headers(
access_token,
headers,
vec![
ResourceState {
account_id: from_account_id,
collection: Collection::FileNode,
document_id: Some(from_resource.resource.document_id),
path: from_resource_name,
..Default::default()
},
ResourceState {
account_id: to_account_id,
collection: Collection::FileNode,
document_id: Some(
delete_destination
.as_ref()
.and_then(|d| d.document_id)
.unwrap_or(u32::MAX),
),
path: destination_resource_name,
..Default::default()
},
],
Default::default(),
if is_move {
DavMethod::MOVE
} else {
DavMethod::COPY
},
)
.await?;
if delete_destination.is_none()
&& from_account_id == destination.account_id
&& from_resource.resource.parent_id == destination.document_id
&& destination.new_name.is_some()
&& is_move
{
// Rename
let from_resource_path = if from_resource.resource.is_container {
from_resources.format_collection(from_resource_name)
} else {
from_resources.format_item(from_resource_name)
};
return rename_item(
self,
access_token,
from_resource,
from_resource_path,
destination,
)
.await;
}
// Validate quota
if !is_move || from_account_id != to_account_id {
let space_needed = from_resources
.subtree(from_resource_name)
.map(|a| a.size() as u64)
.sum::<u64>();
self.has_available_quota(self.account(to_account_id).await?.as_ref(), space_needed)
.await?;
}
// Delete collection
let is_overwrite = delete_destination
.as_ref()
.is_some_and(|d| d.is_container || from_resource.resource.is_container);
if is_overwrite {
delete_destination = None;
// Find ids to delete
let mut ids = to_resources
.subtree(destination_resource_name)
.collect::<Vec<_>>();
if !ids.is_empty() {
ids.sort_unstable_by_key(|b| std::cmp::Reverse(b.hierarchy_seq()));
let mut sorted_ids = Vec::with_capacity(ids.len());
sorted_ids.extend(ids.into_iter().map(|a| a.document_id()));
DestroyArchive(sorted_ids)
.delete(
self,
access_token.account_tenant_ids(),
destination.account_id,
None,
)
.await
.caused_by(trc::location!())?;
}
}
match (from_resource.resource.is_container, is_move) {
(true, true) => {
move_container(
self,
access_token,
from_resources,
from_resource,
from_resource_name,
destination,
headers.depth,
)
.await
}
(true, false) => {
copy_container(
self,
access_token,
from_resources,
from_resource,
from_resource_name,
destination,
headers.depth,
false,
)
.await
}
(false, true) => {
if let Some(delete_destination) = delete_destination {
overwrite_and_delete_item(
self,
access_token,
from_resource,
from_resources.format_item(from_resource_name),
delete_destination,
)
.await
} else {
move_item(
self,
access_token,
from_resource,
from_resources.format_item(from_resource_name),
destination,
)
.await
}
}
(false, false) => {
if let Some(delete_destination) = delete_destination {
overwrite_item(self, access_token, from_resource, delete_destination).await
} else {
copy_item(self, access_token, from_resource, destination).await
}
}
}
.map(|r| {
if is_overwrite && r.status() == StatusCode::CREATED {
r.with_status_code(StatusCode::NO_CONTENT)
} else {
r
}
})
}
}
#[derive(Debug)]
pub(crate) struct Destination {
pub account_id: u32,
pub new_name: Option<String>,
pub document_id: Option<u32>,
pub is_container: bool,
}
impl Default for Destination {
fn default() -> Self {
Self {
account_id: Default::default(),
document_id: Default::default(),
new_name: Default::default(),
is_container: true,
}
}
}
// Moves a container under an existing container
async fn move_container(
server: &Server,
access_token: &AccessToken,
from_resources: Arc<DavResources>,
from_resource: UriResource<u32, FileItemId>,
from_resource_name: &str,
destination: Destination,
depth: Depth,
) -> crate::Result<HttpResponse> {
let from_account_id = from_resource.account_id;
let to_account_id = destination.account_id;
let from_document_id = from_resource.resource.document_id;
let parent_id = destination.document_id.map(|id| id + 1).unwrap_or(0);
if from_account_id == to_account_id {
let node_ = server
.store()
.get_value::<Archive<AlignedBytes>>(ValueKey::archive(
from_account_id,
Collection::FileNode,
from_document_id,
))
.await
.caused_by(trc::location!())?
.ok_or(DavError::Code(StatusCode::NOT_FOUND))?;
let node = node_
.to_unarchived::<FileNode>()
.caused_by(trc::location!())?;
let mut new_node = node.deserialize::<FileNode>().caused_by(trc::location!())?;
new_node.parent_id = parent_id;
if let Some(new_name) = destination.new_name {
new_node.name = new_name;
}
let mut batch = BatchBuilder::new();
let etag = new_node
.update(
access_token.account_tenant_ids(),
node,
from_account_id,
from_document_id,
true,
&mut batch,
)
.caused_by(trc::location!())?
.etag();
batch.with_account_id(from_account_id).log_vanished_item(
VanishedCollection::FileNode,
from_resources.format_collection(from_resource_name),
);
server
.commit_batch(batch)
.await
.caused_by(trc::location!())?;
Ok(HttpResponse::new(StatusCode::CREATED).with_etag_opt(etag))
} else {
copy_container(
server,
access_token,
from_resources,
from_resource,
from_resource_name,
destination,
depth,
true,
)
.await
}
}
#[allow(clippy::too_many_arguments)]
async fn copy_container(
server: &Server,
access_token: &AccessToken,
from_resources: Arc<DavResources>,
from_resource: UriResource<u32, FileItemId>,
from_resource_name: &str,
mut destination: Destination,
depth: Depth,
delete_source: bool,
) -> crate::Result<HttpResponse> {
let infinity_copy = match depth {
Depth::Zero if !delete_source => {
return copy_item(server, access_token, from_resource, destination).await;
}
Depth::One if !delete_source => false,
_ => true,
};
let from_account_id = from_resource.account_id;
let to_account_id = destination.account_id;
let parent_id = destination.document_id.map(|id| id + 1).unwrap_or(0);
// Obtain files to copy
let mut copy_files = if infinity_copy {
from_resources
.subtree(from_resource_name)
.map(|r| (r.document_id(), r.hierarchy_seq()))
.collect::<Vec<_>>()
} else {
from_resources
.subtree_with_depth(from_resource_name, 1)
.map(|r| (r.document_id(), r.hierarchy_seq()))
.collect::<Vec<_>>()
};
// Top-down copy
let mut batch = BatchBuilder::new();
let mut id_map = AHashMap::with_capacity(copy_files.len());
let mut delete_files = if delete_source {
Vec::with_capacity(copy_files.len())
} else {
Vec::new()
};
copy_files.sort_unstable_by_key(|a| a.1);
let now = now() as i64;
let mut next_document_id = server
.store()
.assign_document_ids(to_account_id, Collection::FileNode, copy_files.len() as u64)
.await
.caused_by(trc::location!())?;
for (document_id, _) in copy_files.into_iter() {
let node_ = server
.store()
.get_value::<Archive<AlignedBytes>>(ValueKey::archive(
from_account_id,
Collection::FileNode,
document_id,
))
.await
.caused_by(trc::location!())?
.ok_or(DavError::Code(StatusCode::NOT_FOUND))?
.into_deserialized::<FileNode>()
.caused_by(trc::location!())?;
// Build node
let mut node = if !delete_source {
node_.inner
} else {
let node = node_.inner.clone();
delete_files.push((document_id, node_));
node
};
node.modified = now;
node.created = now;
if let Some(new_name) = destination.new_name.take() {
node.name = new_name;
}
node.parent_id = if let Some(&prev_document_id) = id_map.get(&node.parent_id) {
prev_document_id
} else {
parent_id
};
// Prepare write batch
let new_document_id = next_document_id;
next_document_id -= 1;
batch
.with_account_id(to_account_id)
.with_collection(Collection::FileNode)
.with_document(new_document_id)
.custom(
ObjectIndexBuilder::<(), _>::new()
.with_changes(node)
.with_changed_by(access_token.account_tenant_ids()),
)
.caused_by(trc::location!())?
.commit_point();
id_map.insert(document_id + 1, new_document_id + 1);
}
// Delete nodes
if !delete_files.is_empty() {
for (document_id, node) in delete_files.into_iter().rev() {
// Delete record
batch
.with_account_id(from_account_id)
.with_collection(Collection::FileNode)
.with_document(document_id)
.custom(
ObjectIndexBuilder::<_, ()>::new()
.with_changed_by(access_token.account_tenant_ids())
.with_current(node),
)
.caused_by(trc::location!())?
.commit_point();
}
batch.with_account_id(from_account_id).log_vanished_item(
VanishedCollection::FileNode,
from_resources.format_collection(from_resource_name),
);
}
// Write changes
if !batch.is_empty() {
server
.commit_batch(batch)
.await
.caused_by(trc::location!())?;
}
Ok(HttpResponse::new(StatusCode::CREATED))
}
// Overwrites the contents of one file with another, then deletes the original
async fn overwrite_and_delete_item(
server: &Server,
access_token: &AccessToken,
from_resource: UriResource<u32, FileItemId>,
from_resource_path: String,
destination: Destination,
) -> crate::Result<HttpResponse> {
let from_account_id = from_resource.account_id;
let to_account_id = destination.account_id;
let from_document_id = from_resource.resource.document_id;
let to_document_id = destination.document_id.unwrap();
// dest_node is the current file at the destination
let dest_node_ = server
.store()
.get_value::<Archive<AlignedBytes>>(ValueKey::archive(
to_account_id,
Collection::FileNode,
to_document_id,
))
.await
.caused_by(trc::location!())?
.ok_or(DavError::Code(StatusCode::NOT_FOUND))?;
let dest_node = dest_node_
.to_unarchived::<FileNode>()
.caused_by(trc::location!())?;
// source_node is the file to be copied
let source_node__ = server
.store()
.get_value::<Archive<AlignedBytes>>(ValueKey::archive(
from_account_id,
Collection::FileNode,
from_document_id,
))
.await
.caused_by(trc::location!())?
.ok_or(DavError::Code(StatusCode::NOT_FOUND))?;
let source_node_ = source_node__
.to_unarchived::<FileNode>()
.caused_by(trc::location!())?;
let mut source_node = source_node_
.deserialize::<FileNode>()
.caused_by(trc::location!())?;
source_node.name = if let Some(new_name) = destination.new_name {
new_name
} else {
dest_node.inner.name.to_string()
};
source_node.parent_id = dest_node.inner.parent_id.into();
let mut batch = BatchBuilder::new();
let etag = source_node
.update(
access_token.account_tenant_ids(),
dest_node,
to_account_id,
to_document_id,
true,
&mut batch,
)
.caused_by(trc::location!())?
.etag();
DestroyArchive(source_node_)
.delete(
access_token.account_tenant_ids(),
from_account_id,
from_document_id,
&mut batch,
from_resource_path,
)
.caused_by(trc::location!())?;
server
.commit_batch(batch)
.await
.caused_by(trc::location!())?;
Ok(HttpResponse::new(StatusCode::NO_CONTENT).with_etag_opt(etag))
}
// Overwrites the contents of one file with another
async fn overwrite_item(
server: &Server,
access_token: &AccessToken,
from_resource: UriResource<u32, FileItemId>,
destination: Destination,
) -> crate::Result<HttpResponse> {
let from_account_id = from_resource.account_id;
let to_account_id = destination.account_id;
let from_document_id = from_resource.resource.document_id;
let to_document_id = destination.document_id.unwrap();
// dest_node is the current file at the destination
let dest_node_ = server
.store()
.get_value::<Archive<AlignedBytes>>(ValueKey::archive(
to_account_id,
Collection::FileNode,
to_document_id,
))
.await
.caused_by(trc::location!())?
.ok_or(DavError::Code(StatusCode::NOT_FOUND))?;
let dest_node = dest_node_
.to_unarchived::<FileNode>()
.caused_by(trc::location!())?;
// source_node is the file to be copied
let mut source_node = server
.store()
.get_value::<Archive<AlignedBytes>>(ValueKey::archive(
from_account_id,
Collection::FileNode,
from_document_id,
))
.await
.caused_by(trc::location!())?
.ok_or(DavError::Code(StatusCode::NOT_FOUND))?
.deserialize::<FileNode>()
.caused_by(trc::location!())?;
source_node.name = if let Some(new_name) = destination.new_name {
new_name
} else {
dest_node.inner.name.to_string()
};
source_node.parent_id = dest_node.inner.parent_id.into();
let mut batch = BatchBuilder::new();
let etag = source_node
.update(
access_token.account_tenant_ids(),
dest_node,
to_account_id,
to_document_id,
true,
&mut batch,
)
.caused_by(trc::location!())?
.etag();
server
.commit_batch(batch)
.await
.caused_by(trc::location!())?;
Ok(HttpResponse::new(StatusCode::NO_CONTENT).with_etag_opt(etag))
}
// Moves an item under an existing container
async fn move_item(
server: &Server,
access_token: &AccessToken,
from_resource: UriResource<u32, FileItemId>,
from_resource_path: String,
destination: Destination,
) -> crate::Result<HttpResponse> {
let from_account_id = from_resource.account_id;
let to_account_id = destination.account_id;
let from_document_id = from_resource.resource.document_id;
let parent_id = destination.document_id.map(|id| id + 1).unwrap_or(0);
let node_ = server
.store()
.get_value::<Archive<AlignedBytes>>(ValueKey::archive(
from_account_id,
Collection::FileNode,
from_document_id,
))
.await
.caused_by(trc::location!())?
.ok_or(DavError::Code(StatusCode::NOT_FOUND))?;
let node = node_
.to_unarchived::<FileNode>()
.caused_by(trc::location!())?;
let mut new_node = node.deserialize::<FileNode>().caused_by(trc::location!())?;
new_node.parent_id = parent_id;
if let Some(new_name) = destination.new_name {
new_node.name = new_name;
}
let mut batch = BatchBuilder::new();
let etag = if from_account_id == to_account_id {
// Destination is in the same account: just update the parent id
batch.log_vanished_item(VanishedCollection::FileNode, from_resource_path);
new_node
.update(
access_token.account_tenant_ids(),
node,
from_account_id,
from_document_id,
true,
&mut batch,
)
.caused_by(trc::location!())?
.etag()
} else {
// Destination is in a different account: insert a new node, then delete the old one
let to_document_id = server
.store()
.assign_document_ids(to_account_id, Collection::FileNode, 1)
.await
.caused_by(trc::location!())?;
let etag = new_node
.insert(
access_token.account_tenant_ids(),
to_account_id,
to_document_id,
true,
true,
&mut batch,
)
.caused_by(trc::location!())?
.etag();
DestroyArchive(node)
.delete(
access_token.account_tenant_ids(),
from_account_id,
from_document_id,
&mut batch,
from_resource_path,
)
.caused_by(trc::location!())?;
etag
};
server
.commit_batch(batch)
.await
.caused_by(trc::location!())?;
Ok(HttpResponse::new(StatusCode::CREATED).with_etag_opt(etag))
}
// Copies an item under an existing container
async fn copy_item(
server: &Server,
access_token: &AccessToken,
from_resource: UriResource<u32, FileItemId>,
destination: Destination,
) -> crate::Result<HttpResponse> {
let from_account_id = from_resource.account_id;
let to_account_id = destination.account_id;
let from_document_id = from_resource.resource.document_id;
let parent_id = destination.document_id.map(|id| id + 1).unwrap_or(0);
let mut node = server
.store()
.get_value::<Archive<AlignedBytes>>(ValueKey::archive(
from_account_id,
Collection::FileNode,
from_document_id,
))
.await
.caused_by(trc::location!())?
.ok_or(DavError::Code(StatusCode::NOT_FOUND))?
.deserialize::<FileNode>()
.caused_by(trc::location!())?;
node.parent_id = parent_id;
if let Some(new_name) = destination.new_name {
node.name = new_name;
}
let mut batch = BatchBuilder::new();
let to_document_id = server
.store()
.assign_document_ids(to_account_id, Collection::FileNode, 1)
.await
.caused_by(trc::location!())?;
let etag = node
.insert(
access_token.account_tenant_ids(),
to_account_id,
to_document_id,
true,
true,
&mut batch,
)
.caused_by(trc::location!())?
.etag();
server
.commit_batch(batch)
.await
.caused_by(trc::location!())?;
Ok(HttpResponse::new(StatusCode::CREATED).with_etag_opt(etag))
}
// Renames an item
async fn rename_item(
server: &Server,
access_token: &AccessToken,
from_resource: UriResource<u32, FileItemId>,
from_resource_path: String,
destination: Destination,
) -> crate::Result<HttpResponse> {
let from_account_id = from_resource.account_id;
let from_document_id = from_resource.resource.document_id;
let node_ = server
.store()
.get_value::<Archive<AlignedBytes>>(ValueKey::archive(
from_account_id,
Collection::FileNode,
from_document_id,
))
.await
.caused_by(trc::location!())?
.ok_or(DavError::Code(StatusCode::NOT_FOUND))?;
let node = node_
.to_unarchived::<FileNode>()
.caused_by(trc::location!())?;
let mut new_node = node.deserialize::<FileNode>().caused_by(trc::location!())?;
if let Some(new_name) = destination.new_name {
new_node.name = new_name;
}
let mut batch = BatchBuilder::new();
let etag = new_node
.update(
access_token.account_tenant_ids(),
node,
from_account_id,
from_document_id,
true,
&mut batch,
)
.caused_by(trc::location!())?
.etag();
batch.log_vanished_item(VanishedCollection::FileNode, from_resource_path);
server
.commit_batch(batch)
.await
.caused_by(trc::location!())?;
Ok(HttpResponse::new(StatusCode::CREATED).with_etag_opt(etag))
}
impl FromDavResource for Destination {
fn from_dav_resource(item: DavResourcePath<'_>) -> Self {
Destination {
account_id: u32::MAX,
document_id: Some(item.document_id()),
is_container: item.is_container(),
new_name: None,
}
}
}
+107
View File
@@ -0,0 +1,107 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::{
DavError, DavMethod,
common::{
lock::{LockRequestHandler, ResourceState},
uri::DavUriResource,
},
};
use common::{Server, auth::AccessToken};
use dav_proto::RequestHeaders;
use groupware::{DestroyArchive, cache::GroupwareCache};
use http_proto::HttpResponse;
use hyper::StatusCode;
use trc::AddContext;
use types::{acl::Acl, collection::SyncCollection};
pub(crate) trait FileDeleteRequestHandler: Sync + Send {
fn handle_file_delete_request(
&self,
access_token: &AccessToken,
headers: &RequestHeaders<'_>,
) -> impl Future<Output = crate::Result<HttpResponse>> + Send;
}
impl FileDeleteRequestHandler for Server {
async fn handle_file_delete_request(
&self,
access_token: &AccessToken,
headers: &RequestHeaders<'_>,
) -> crate::Result<HttpResponse> {
// Validate URI
let resource = self
.validate_uri(access_token, headers.uri)
.await?
.into_owned_uri()?;
let account_id = resource.account_id;
let delete_path = resource
.resource
.filter(|r| !r.is_empty())
.ok_or(DavError::Code(StatusCode::FORBIDDEN))?;
let resources = self
.fetch_dav_resources(
access_token.account_id(),
account_id,
SyncCollection::FileNode,
)
.await
.caused_by(trc::location!())?;
// Find ids to delete
let mut ids = resources.subtree(delete_path).collect::<Vec<_>>();
if ids.is_empty() {
return Err(DavError::Code(StatusCode::NOT_FOUND));
}
// Sort ids descending from the deepest to the root
ids.sort_unstable_by_key(|b| std::cmp::Reverse(b.hierarchy_seq()));
let (document_id, full_delete_path) = ids
.last()
.map(|a| (a.document_id(), resources.format_resource(*a)))
.unwrap();
let mut sorted_ids = Vec::with_capacity(ids.len());
sorted_ids.extend(ids.into_iter().map(|a| a.document_id()));
// Validate ACLs
if !access_token.is_member(account_id) {
let permissions = resources.shared_containers(access_token, [Acl::Delete], false);
if permissions.len() < sorted_ids.len() as u64
|| !sorted_ids.iter().all(|id| permissions.contains(*id))
{
return Err(DavError::Code(StatusCode::FORBIDDEN));
}
}
// Validate headers
self.validate_headers(
access_token,
headers,
vec![ResourceState {
account_id,
collection: resource.collection,
document_id: document_id.into(),
path: delete_path,
..Default::default()
}],
Default::default(),
DavMethod::DELETE,
)
.await?;
DestroyArchive(sorted_ids)
.delete(
self,
access_token.account_tenant_ids(),
account_id,
full_delete_path.into(),
)
.await?;
Ok(HttpResponse::new(StatusCode::NO_CONTENT))
}
}
+168
View File
@@ -0,0 +1,168 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::{
DavError, DavMethod,
common::{
ETag,
lock::{LockRequestHandler, ResourceState},
uri::DavUriResource,
},
file::DavFileResource,
};
use common::{Server, auth::AccessToken, sharing::EffectiveAcl};
use dav_proto::{RequestHeaders, schema::property::Rfc1123DateTime};
use groupware::{cache::GroupwareCache, file::FileNode};
use http_proto::HttpResponse;
use hyper::StatusCode;
use store::{
ValueKey,
write::{AlignedBytes, Archive, now},
};
use trc::AddContext;
use types::{
acl::Acl,
collection::{Collection, SyncCollection},
};
pub(crate) trait FileGetRequestHandler: Sync + Send {
fn handle_file_get_request(
&self,
access_token: &AccessToken,
headers: &RequestHeaders<'_>,
is_head: bool,
) -> impl Future<Output = crate::Result<HttpResponse>> + Send;
}
impl FileGetRequestHandler for Server {
async fn handle_file_get_request(
&self,
access_token: &AccessToken,
headers: &RequestHeaders<'_>,
is_head: bool,
) -> crate::Result<HttpResponse> {
// Validate URI
let resource_ = self
.validate_uri(access_token, headers.uri)
.await?
.into_owned_uri()?;
let account_id = resource_.account_id;
let files = self
.fetch_dav_resources(
access_token.account_id(),
account_id,
SyncCollection::FileNode,
)
.await
.caused_by(trc::location!())?;
let resource = files.map_resource(&resource_)?;
// Fetch node
let node_ = self
.store()
.get_value::<Archive<AlignedBytes>>(ValueKey::archive(
account_id,
Collection::FileNode,
resource.resource,
))
.await
.caused_by(trc::location!())?
.ok_or(DavError::Code(StatusCode::NOT_FOUND))?;
let node = node_.unarchive::<FileNode>().caused_by(trc::location!())?;
// Validate ACL
if !access_token.is_member(account_id)
&& !node.acls.effective_acl(access_token).contains(Acl::Read)
{
return Err(DavError::Code(StatusCode::FORBIDDEN));
}
let (hash, size, content_type) = if let Some(file) = node.file.as_ref() {
(
file.blob_hash.0.as_ref(),
u32::from(file.size) as usize,
file.media_type.as_ref().map(|s| s.as_str()),
)
} else {
return Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED));
};
// Validate headers
let etag = node_.etag();
self.validate_headers(
access_token,
headers,
vec![ResourceState {
account_id,
collection: resource.collection,
document_id: resource.resource.into(),
etag: etag.clone().into(),
path: resource_.resource.unwrap(),
..Default::default()
}],
Default::default(),
DavMethod::GET,
)
.await?;
let modified = i64::from(node.modified);
let last_modified = Rfc1123DateTime::new(modified).to_string();
let byte_range = if !is_head && size > 0 {
headers
.range
.filter(|_| {
headers.eval_if_range(
&etag,
((modified as u64) < now()).then_some(last_modified.as_str()),
)
})
.map(|range| range.resolve(size as u64))
} else {
None
};
let byte_range = match byte_range {
Some(Some(range)) => Some(range.start as usize..range.end as usize),
Some(None) => {
return Ok(HttpResponse::new(StatusCode::RANGE_NOT_SATISFIABLE)
.with_accept_ranges()
.with_etag(etag)
.with_content_range(format!("bytes */{size}")));
}
None => None,
};
let response = HttpResponse::new(StatusCode::OK)
.with_content_type(content_type.unwrap_or("application/octet-stream"))
.with_etag(etag)
.with_last_modified(last_modified)
.with_accept_ranges();
if is_head {
return Ok(response.with_content_length(size));
}
let contents = self
.blob_store()
.get_blob(hash, byte_range.clone().unwrap_or(0..usize::MAX))
.await
.caused_by(trc::location!())?
.ok_or(DavError::Code(StatusCode::NOT_FOUND))?;
Ok(match byte_range {
Some(byte_range) if !contents.is_empty() => response
.with_status_code(StatusCode::PARTIAL_CONTENT)
.with_content_range(format!(
"bytes {}-{}/{}",
byte_range.start,
byte_range.start + contents.len() - 1,
size
)),
Some(_) => return Err(DavError::Code(StatusCode::NOT_FOUND)),
None => response,
}
.with_binary_body(contents))
}
}
+146
View File
@@ -0,0 +1,146 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use super::proppatch::FilePropPatchRequestHandler;
use crate::{
DavMethod, PropStatBuilder,
common::{
ExtractETag,
acl::ResourceAcl,
lock::{LockRequestHandler, ResourceState},
uri::DavUriResource,
},
file::DavFileResource,
};
use common::{Server, auth::AccessToken, storage::index::ObjectIndexBuilder};
use dav_proto::{
RequestHeaders, Return,
schema::{Namespace, request::MkCol, response::MkColResponse},
};
use groupware::{cache::GroupwareCache, file::FileNode};
use http_proto::HttpResponse;
use hyper::StatusCode;
use store::write::{BatchBuilder, now};
use trc::AddContext;
use types::{
acl::Acl,
collection::{Collection, SyncCollection},
};
pub(crate) trait FileMkColRequestHandler: Sync + Send {
fn handle_file_mkcol_request(
&self,
access_token: &AccessToken,
headers: &RequestHeaders<'_>,
request: Option<MkCol>,
) -> impl Future<Output = crate::Result<HttpResponse>> + Send;
}
impl FileMkColRequestHandler for Server {
async fn handle_file_mkcol_request(
&self,
access_token: &AccessToken,
headers: &RequestHeaders<'_>,
request: Option<MkCol>,
) -> crate::Result<HttpResponse> {
// Validate URI
let resource_ = self
.validate_uri(access_token, headers.uri)
.await?
.into_owned_uri()?;
let account_id = resource_.account_id;
let resources = self
.fetch_dav_resources(
access_token.account_id(),
account_id,
SyncCollection::FileNode,
)
.await
.caused_by(trc::location!())?;
let resource = resources.map_parent_resource(&resource_)?;
// Validate and map parent ACL
let parent_id = resources.validate_and_map_parent_acl(
access_token,
access_token.is_member(account_id),
resource.resource.0,
Acl::AddItems,
)?;
// Validate headers
self.validate_headers(
access_token,
headers,
vec![ResourceState {
account_id,
collection: resource.collection,
document_id: Some(u32::MAX),
path: resource_.resource.unwrap(),
..Default::default()
}],
Default::default(),
DavMethod::MKCOL,
)
.await?;
// Build file container
let now = now();
let mut node = FileNode {
parent_id,
name: resource.resource.1.to_string(),
display_name: None,
file: None,
created: now as i64,
modified: now as i64,
dead_properties: Default::default(),
acls: Default::default(),
};
// Apply MKCOL properties
let mut return_prop_stat = None;
if let Some(mkcol) = request {
let mut prop_stat = PropStatBuilder::default();
if !self.apply_file_properties(&mut node, false, mkcol.props, &mut prop_stat) {
return Ok(HttpResponse::new(StatusCode::FORBIDDEN).with_xml_body(
MkColResponse::new(prop_stat.build())
.with_namespace(Namespace::Dav)
.to_string(),
));
}
if headers.ret != Return::Minimal {
return_prop_stat = Some(prop_stat);
}
}
// Prepare write batch
let document_id = self
.store()
.assign_document_ids(account_id, Collection::FileNode, 1)
.await
.caused_by(trc::location!())?;
let mut batch = BatchBuilder::new();
batch
.with_account_id(account_id)
.with_collection(Collection::FileNode)
.with_document(document_id)
.custom(ObjectIndexBuilder::<(), _>::new().with_changes(node))
.caused_by(trc::location!())?;
let etag = batch.etag();
self.commit_batch(batch).await.caused_by(trc::location!())?;
if let Some(prop_stat) = return_prop_stat {
Ok(HttpResponse::new(StatusCode::CREATED)
.with_xml_body(
MkColResponse::new(prop_stat.build())
.with_namespace(Namespace::Dav)
.to_string(),
)
.with_etag_opt(etag))
} else {
Ok(HttpResponse::new(StatusCode::CREATED).with_etag_opt(etag))
}
}
}
+153
View File
@@ -0,0 +1,153 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::{
DavError,
common::uri::{OwnedUri, UriResource},
};
use common::{DavResourcePath, DavResources};
use dav_proto::schema::property::{DavProperty, WebDavProperty};
use hyper::StatusCode;
pub mod copy_move;
pub mod delete;
pub mod get;
pub mod mkcol;
pub mod proppatch;
pub mod update;
pub(crate) static FILE_CONTAINER_PROPS: [DavProperty; 19] = [
DavProperty::WebDav(WebDavProperty::CreationDate),
DavProperty::WebDav(WebDavProperty::DisplayName),
DavProperty::WebDav(WebDavProperty::GetETag),
DavProperty::WebDav(WebDavProperty::GetLastModified),
DavProperty::WebDav(WebDavProperty::ResourceType),
DavProperty::WebDav(WebDavProperty::LockDiscovery),
DavProperty::WebDav(WebDavProperty::SupportedLock),
DavProperty::WebDav(WebDavProperty::CurrentUserPrincipal),
DavProperty::WebDav(WebDavProperty::SyncToken),
DavProperty::WebDav(WebDavProperty::Owner),
DavProperty::WebDav(WebDavProperty::SupportedPrivilegeSet),
DavProperty::WebDav(WebDavProperty::CurrentUserPrivilegeSet),
DavProperty::WebDav(WebDavProperty::Acl),
DavProperty::WebDav(WebDavProperty::AclRestrictions),
DavProperty::WebDav(WebDavProperty::InheritedAclSet),
DavProperty::WebDav(WebDavProperty::PrincipalCollectionSet),
DavProperty::WebDav(WebDavProperty::SupportedReportSet),
DavProperty::WebDav(WebDavProperty::QuotaAvailableBytes),
DavProperty::WebDav(WebDavProperty::QuotaUsedBytes),
];
pub(crate) static FILE_ITEM_PROPS: [DavProperty; 19] = [
DavProperty::WebDav(WebDavProperty::CreationDate),
DavProperty::WebDav(WebDavProperty::DisplayName),
DavProperty::WebDav(WebDavProperty::GetETag),
DavProperty::WebDav(WebDavProperty::GetLastModified),
DavProperty::WebDav(WebDavProperty::ResourceType),
DavProperty::WebDav(WebDavProperty::LockDiscovery),
DavProperty::WebDav(WebDavProperty::SupportedLock),
DavProperty::WebDav(WebDavProperty::CurrentUserPrincipal),
DavProperty::WebDav(WebDavProperty::SyncToken),
DavProperty::WebDav(WebDavProperty::Owner),
DavProperty::WebDav(WebDavProperty::SupportedPrivilegeSet),
DavProperty::WebDav(WebDavProperty::CurrentUserPrivilegeSet),
DavProperty::WebDav(WebDavProperty::Acl),
DavProperty::WebDav(WebDavProperty::AclRestrictions),
DavProperty::WebDav(WebDavProperty::InheritedAclSet),
DavProperty::WebDav(WebDavProperty::PrincipalCollectionSet),
DavProperty::WebDav(WebDavProperty::GetContentLanguage),
DavProperty::WebDav(WebDavProperty::GetContentLength),
DavProperty::WebDav(WebDavProperty::GetContentType),
];
pub(crate) trait FromDavResource {
fn from_dav_resource(item: DavResourcePath<'_>) -> Self;
}
pub(crate) struct FileItemId {
pub document_id: u32,
pub parent_id: Option<u32>,
pub is_container: bool,
}
pub(crate) trait DavFileResource {
fn map_resource<T: FromDavResource>(
&self,
resource: &OwnedUri<'_>,
) -> crate::Result<UriResource<u32, T>>;
fn map_parent<'x>(&self, resource: &'x str) -> Option<(Option<DavResourcePath<'_>>, &'x str)>;
#[allow(clippy::type_complexity)]
fn map_parent_resource<'x, T: FromDavResource>(
&self,
resource: &OwnedUri<'x>,
) -> crate::Result<UriResource<u32, (Option<T>, &'x str)>>;
}
impl DavFileResource for DavResources {
fn map_resource<T: FromDavResource>(
&self,
resource: &OwnedUri<'_>,
) -> crate::Result<UriResource<u32, T>> {
resource
.resource
.and_then(|r| self.by_path(r))
.map(|r| UriResource {
collection: resource.collection,
account_id: resource.account_id,
resource: T::from_dav_resource(r),
})
.ok_or(DavError::Code(StatusCode::NOT_FOUND))
}
fn map_parent<'x>(&self, resource: &'x str) -> Option<(Option<DavResourcePath<'_>>, &'x str)> {
let (parent, child) = if let Some((parent, child)) = resource.rsplit_once('/') {
(Some(self.by_path(parent)?), child)
} else {
(None, resource)
};
Some((parent, child))
}
fn map_parent_resource<'x, T: FromDavResource>(
&self,
resource: &OwnedUri<'x>,
) -> crate::Result<UriResource<u32, (Option<T>, &'x str)>> {
if let Some(r) = resource.resource {
if self.by_path(r).is_none() {
self.map_parent(r)
.map(|(parent, child)| UriResource {
collection: resource.collection,
account_id: resource.account_id,
resource: (parent.map(T::from_dav_resource), child),
})
.ok_or(DavError::Code(StatusCode::CONFLICT))
} else {
Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED))
}
} else {
Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED))
}
}
}
impl FromDavResource for u32 {
fn from_dav_resource(item: DavResourcePath) -> Self {
item.document_id()
}
}
impl FromDavResource for FileItemId {
fn from_dav_resource(item: DavResourcePath) -> Self {
FileItemId {
document_id: item.document_id(),
parent_id: item.parent_id(),
is_container: item.is_container(),
}
}
}
+304
View File
@@ -0,0 +1,304 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::{
DavError, DavMethod, PropStatBuilder,
common::{
ETag, ExtractETag,
lock::{LockRequestHandler, ResourceState},
uri::DavUriResource,
},
file::DavFileResource,
};
use common::{Server, auth::AccessToken, sharing::EffectiveAcl};
use dav_proto::{
RequestHeaders, Return,
schema::{
property::{DavProperty, DavValue, ResourceType, WebDavProperty},
request::{DavPropertyValue, PropertyUpdate},
response::{BaseCondition, MultiStatus, Response},
},
};
use groupware::{cache::GroupwareCache, file::FileNode};
use http_proto::HttpResponse;
use hyper::StatusCode;
use store::write::BatchBuilder;
use store::{
ValueKey,
write::{AlignedBytes, Archive},
};
use trc::AddContext;
use types::{
acl::Acl,
collection::{Collection, SyncCollection},
};
pub(crate) trait FilePropPatchRequestHandler: Sync + Send {
fn handle_file_proppatch_request(
&self,
access_token: &AccessToken,
headers: &RequestHeaders<'_>,
request: PropertyUpdate,
) -> impl Future<Output = crate::Result<HttpResponse>> + Send;
fn apply_file_properties(
&self,
file: &mut FileNode,
is_update: bool,
properties: Vec<DavPropertyValue>,
items: &mut PropStatBuilder,
) -> bool;
}
impl FilePropPatchRequestHandler for Server {
async fn handle_file_proppatch_request(
&self,
access_token: &AccessToken,
headers: &RequestHeaders<'_>,
mut request: PropertyUpdate,
) -> crate::Result<HttpResponse> {
// Validate URI
let resource_ = self
.validate_uri(access_token, headers.uri)
.await?
.into_owned_uri()?;
let uri = headers.uri;
let account_id = resource_.account_id;
let files = self
.fetch_dav_resources(
access_token.account_id(),
account_id,
SyncCollection::FileNode,
)
.await
.caused_by(trc::location!())?;
let resource = files.map_resource(&resource_)?;
if !request.has_changes() {
return Ok(HttpResponse::new(StatusCode::NO_CONTENT));
}
// Fetch node
let node_ = self
.store()
.get_value::<Archive<AlignedBytes>>(ValueKey::archive(
account_id,
Collection::FileNode,
resource.resource,
))
.await
.caused_by(trc::location!())?
.ok_or(DavError::Code(StatusCode::NOT_FOUND))?;
let node = node_
.to_unarchived::<FileNode>()
.caused_by(trc::location!())?;
// Validate ACL
if !access_token.is_member(account_id)
&& !node
.inner
.acls
.effective_acl(access_token)
.contains(Acl::Modify)
{
return Err(DavError::Code(StatusCode::FORBIDDEN));
}
// Validate headers
self.validate_headers(
access_token,
headers,
vec![ResourceState {
account_id,
collection: resource.collection,
document_id: resource.resource.into(),
etag: node_.etag().into(),
path: resource_.resource.unwrap(),
..Default::default()
}],
Default::default(),
DavMethod::PROPPATCH,
)
.await?;
// Deserialize
let mut new_node = node.deserialize::<FileNode>().caused_by(trc::location!())?;
// Remove properties
let mut items = PropStatBuilder::default();
if !request.set_first && !request.remove.is_empty() {
remove_file_properties(
&mut new_node,
std::mem::take(&mut request.remove),
&mut items,
);
}
// Set properties
let is_success = self.apply_file_properties(&mut new_node, true, request.set, &mut items);
// Remove properties
if is_success && !request.remove.is_empty() {
remove_file_properties(&mut new_node, request.remove, &mut items);
}
let etag = if is_success {
let mut batch = BatchBuilder::new();
let etag = new_node
.update(
access_token.account_tenant_ids(),
node,
account_id,
resource.resource,
true,
&mut batch,
)
.caused_by(trc::location!())?
.etag();
self.commit_batch(batch).await.caused_by(trc::location!())?;
etag
} else {
node_.etag().into()
};
if headers.ret != Return::Minimal || !is_success {
Ok(HttpResponse::new(StatusCode::MULTI_STATUS)
.with_xml_body(
MultiStatus::new(vec![Response::new_propstat(uri, items.build())]).to_string(),
)
.with_etag_opt(etag))
} else {
Ok(HttpResponse::new(StatusCode::NO_CONTENT).with_etag_opt(etag))
}
}
fn apply_file_properties(
&self,
file: &mut FileNode,
is_update: bool,
properties: Vec<DavPropertyValue>,
items: &mut PropStatBuilder,
) -> bool {
let mut has_errors = false;
for property in properties {
match (&property.property, property.value) {
(DavProperty::WebDav(WebDavProperty::DisplayName), DavValue::String(name)) => {
if name.len() <= self.core.groupware.live_property_size {
file.display_name = Some(name);
items.insert_ok(property.property);
} else {
items.insert_error_with_description(
property.property,
StatusCode::INSUFFICIENT_STORAGE,
"Property value is too long",
);
has_errors = true;
}
}
(DavProperty::WebDav(WebDavProperty::CreationDate), DavValue::Timestamp(dt)) => {
file.created = dt;
items.insert_ok(property.property);
}
(DavProperty::WebDav(WebDavProperty::GetContentType), DavValue::String(name))
if file.file.is_some() =>
{
if name.len() <= self.core.groupware.live_property_size {
file.file.as_mut().unwrap().media_type = Some(name);
items.insert_ok(property.property);
} else {
items.insert_error_with_description(
property.property,
StatusCode::INSUFFICIENT_STORAGE,
"Property value is too long",
);
has_errors = true;
}
}
(
DavProperty::WebDav(WebDavProperty::ResourceType),
DavValue::ResourceTypes(types),
) if file.file.is_none() => {
if types.0.len() != 1 || types.0.first() != Some(&ResourceType::Collection) {
items.insert_precondition_failed(
property.property,
StatusCode::FORBIDDEN,
BaseCondition::ValidResourceType,
);
has_errors = true;
} else {
items.insert_ok(property.property);
}
}
(DavProperty::DeadProperty(dead), DavValue::DeadProperty(values))
if self.core.groupware.dead_property_size.is_some() =>
{
if is_update {
file.dead_properties.remove_element(dead);
}
if file.dead_properties.size() + values.size() + dead.size()
< self.core.groupware.dead_property_size.unwrap()
{
file.dead_properties.add_element(dead.clone(), values.0);
items.insert_ok(property.property);
} else {
items.insert_error_with_description(
property.property,
StatusCode::INSUFFICIENT_STORAGE,
"Property value is too long",
);
has_errors = true;
}
}
(_, DavValue::Null) => {
items.insert_ok(property.property);
}
_ => {
items.insert_error_with_description(
property.property,
StatusCode::CONFLICT,
"Property cannot be modified",
);
has_errors = true;
}
}
}
!has_errors
}
}
fn remove_file_properties(
node: &mut FileNode,
properties: Vec<DavProperty>,
items: &mut PropStatBuilder,
) {
for property in properties {
match &property {
DavProperty::WebDav(WebDavProperty::DisplayName) => {
node.display_name = None;
items.insert_with_status(property, StatusCode::NO_CONTENT);
}
DavProperty::WebDav(WebDavProperty::GetContentType) if node.file.is_some() => {
node.file.as_mut().unwrap().media_type = None;
items.insert_with_status(property, StatusCode::NO_CONTENT);
}
DavProperty::DeadProperty(dead) => {
node.dead_properties.remove_element(dead);
items.insert_with_status(property, StatusCode::NO_CONTENT);
}
_ => {
items.insert_error_with_description(
property,
StatusCode::CONFLICT,
"Property cannot be deleted",
);
}
}
}
}
+305
View File
@@ -0,0 +1,305 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::{
DavError, DavMethod,
common::{
ETag, ExtractETag,
acl::ResourceAcl,
lock::{LockRequestHandler, ResourceState},
uri::DavUriResource,
},
file::DavFileResource,
};
use common::{
Server, auth::AccessToken, sharing::EffectiveAcl, storage::index::ObjectIndexBuilder,
};
use dav_proto::{RequestHeaders, Return, schema::property::Rfc1123DateTime};
use groupware::{
cache::GroupwareCache,
file::{FileNode, FileProperties},
};
use http_proto::HttpResponse;
use hyper::StatusCode;
use store::write::{BatchBuilder, now};
use store::{
ValueKey,
write::{AlignedBytes, Archive},
};
use trc::AddContext;
use types::{
acl::Acl,
blob_hash::BlobHash,
collection::{Collection, SyncCollection},
};
pub(crate) trait FileUpdateRequestHandler: Sync + Send {
fn handle_file_update_request(
&self,
access_token: &AccessToken,
headers: &RequestHeaders<'_>,
bytes: Vec<u8>,
is_patch: bool,
) -> impl Future<Output = crate::Result<HttpResponse>> + Send;
}
impl FileUpdateRequestHandler for Server {
async fn handle_file_update_request(
&self,
access_token: &AccessToken,
headers: &RequestHeaders<'_>,
bytes: Vec<u8>,
_is_patch: bool,
) -> crate::Result<HttpResponse> {
// Validate URI
let resource = self
.validate_uri(access_token, headers.uri)
.await?
.into_owned_uri()?;
let account_id = resource.account_id;
let resources = self
.fetch_dav_resources(
access_token.account_id(),
account_id,
SyncCollection::FileNode,
)
.await
.caused_by(trc::location!())?;
let resource_name = resource
.resource
.ok_or(DavError::Code(StatusCode::CONFLICT))?;
if bytes.len() > self.core.groupware.max_file_size {
return Err(DavError::Code(StatusCode::PAYLOAD_TOO_LARGE));
}
if let Some(document_id) = resources
.by_path(resource_name.as_ref())
.map(|r| r.document_id())
{
// Update
let node_ = self
.store()
.get_value::<Archive<AlignedBytes>>(ValueKey::archive(
account_id,
Collection::FileNode,
document_id,
))
.await
.caused_by(trc::location!())?
.ok_or(DavError::Code(StatusCode::NOT_FOUND))?;
let node = node_
.to_unarchived::<FileNode>()
.caused_by(trc::location!())?;
// Validate ACL
if !access_token.is_member(account_id)
&& !node
.inner
.acls
.effective_acl(access_token)
.contains(Acl::Modify)
{
return Err(DavError::Code(StatusCode::FORBIDDEN));
}
// Validate headers
match self
.validate_headers(
access_token,
headers,
vec![ResourceState {
account_id,
collection: resource.collection,
document_id: Some(document_id),
etag: node.etag().into(),
path: resource_name,
..Default::default()
}],
Default::default(),
DavMethod::PUT,
)
.await
{
Ok(_) => {}
Err(DavError::Code(StatusCode::PRECONDITION_FAILED))
if headers.ret == Return::Representation =>
{
let file = node.inner.file.as_ref().unwrap();
let contents = self
.blob_store()
.get_blob(file.blob_hash.0.as_slice(), 0..usize::MAX)
.await
.caused_by(trc::location!())?
.ok_or(DavError::Code(StatusCode::PRECONDITION_FAILED))?;
return Ok(HttpResponse::new(StatusCode::PRECONDITION_FAILED)
.with_content_type(
file.media_type
.as_ref()
.map(|v| v.as_str())
.unwrap_or("application/octet-stream"),
)
.with_etag(node.etag())
.with_last_modified(
Rfc1123DateTime::new(i64::from(node.inner.modified)).to_string(),
)
.with_header("Preference-Applied", "return=representation")
.with_binary_body(contents));
}
Err(e) => return Err(e),
}
// Verify that the node is a file
if let Some(file) = node.inner.file.as_ref() {
if BlobHash::generate(&bytes).as_slice() == file.blob_hash.0.as_slice() {
return Ok(HttpResponse::new(StatusCode::NO_CONTENT));
}
} else {
return Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED));
}
// Validate quota
let extra_bytes = (bytes.len() as u64)
.saturating_sub(u32::from(node.inner.file.as_ref().unwrap().size) as u64);
if extra_bytes > 0 {
self.has_available_quota(self.account(account_id).await?.as_ref(), extra_bytes)
.await?;
}
// Write blob
let (blob_hash, blob_hold) = self
.put_temporary_blob(account_id, &bytes, 60)
.await
.caused_by(trc::location!())?;
// Build node
let mut new_node = node.deserialize::<FileNode>().caused_by(trc::location!())?;
let new_file = new_node.file.as_mut().unwrap();
new_file.blob_hash = blob_hash;
new_file.media_type = headers
.content_type
.filter(|ct| !ct.is_empty() && *ct != "application/octet-stream")
.map(|v| v.to_string());
new_file.size = bytes.len() as u32;
new_node.modified = now() as i64;
// Prepare write batch
let mut batch = BatchBuilder::new();
batch
.with_account_id(account_id)
.with_collection(Collection::FileNode)
.with_document(document_id)
.clear(blob_hold)
.custom(
ObjectIndexBuilder::new()
.with_current(node)
.with_changes(new_node)
.with_changed_by(access_token.account_tenant_ids()),
)
.caused_by(trc::location!())?;
let etag = batch.etag();
self.commit_batch(batch).await.caused_by(trc::location!())?;
Ok(HttpResponse::new(StatusCode::NO_CONTENT).with_etag_opt(etag))
} else {
// Insert
let orig_resource_name = resource_name;
let (parent, resource_name) = resources
.map_parent(orig_resource_name.as_ref())
.ok_or(DavError::Code(StatusCode::CONFLICT))?;
// Validate ACL
let parent_id = resources.validate_and_map_parent_acl(
access_token,
access_token.is_member(account_id),
parent.map(|r| r.document_id()),
Acl::AddItems,
)?;
// Verify that parent is a collection
if parent.as_ref().is_some_and(|r| !r.is_container()) {
return Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED));
}
// Validate headers
self.validate_headers(
access_token,
headers,
vec![ResourceState {
account_id,
collection: resource.collection,
document_id: Some(u32::MAX),
path: orig_resource_name,
..Default::default()
}],
Default::default(),
DavMethod::PUT,
)
.await?;
// Validate quota
if !bytes.is_empty() {
self.has_available_quota(
self.account(account_id).await?.as_ref(),
bytes.len() as u64,
)
.await?;
}
// Write blob
let (blob_hash, blob_hold) = self
.put_temporary_blob(account_id, &bytes, 60)
.await
.caused_by(trc::location!())?;
// Build node
let now = now();
let node = FileNode {
parent_id,
name: resource_name.to_string(),
display_name: None,
file: Some(FileProperties {
blob_hash,
size: bytes.len() as u32,
media_type: headers.content_type.map(|v| v.to_string()),
executable: false,
}),
created: now as i64,
modified: now as i64,
dead_properties: Default::default(),
acls: parent
.as_ref()
.and_then(|p| p.resource.acls())
.map(|acls| acls.to_vec())
.unwrap_or_default(),
};
// Prepare write batch
let mut batch = BatchBuilder::new();
let document_id = self
.store()
.assign_document_ids(account_id, Collection::FileNode, 1)
.await
.caused_by(trc::location!())?;
batch
.with_account_id(account_id)
.with_collection(Collection::FileNode)
.with_document(document_id)
.clear(blob_hold)
.custom(
ObjectIndexBuilder::<(), _>::new()
.with_changes(node)
.with_changed_by(access_token.account_tenant_ids()),
)
.caused_by(trc::location!())?;
let etag = batch.etag();
self.commit_batch(batch).await.caused_by(trc::location!())?;
Ok(HttpResponse::new(StatusCode::CREATED).with_etag_opt(etag))
}
}
}
+256
View File
@@ -0,0 +1,256 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* 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, is_uri_segment};
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)
};
if is_uri_segment(name) {
path.into()
} else {
let name = percent_encoding::utf8_percent_encode(name, RFC_3986);
if let Some(parent) = parent {
Cow::Owned(format!("{parent}/{name}"))
} else {
Cow::Owned(name.to_string())
}
}
}
+100
View File
@@ -0,0 +1,100 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use super::propfind::PrincipalPropFind;
use crate::{
DavError,
common::{
DavQuery, DavQueryResource,
propfind::PropFindRequestHandler,
uri::{DavUriResource, UriResource},
},
};
use common::{Server, auth::AccessToken};
use dav_proto::{
RequestHeaders,
schema::{
property::{DavProperty, WebDavProperty},
request::{PrincipalMatch, PropFind},
response::MultiStatus,
},
};
use http_proto::HttpResponse;
use hyper::StatusCode;
use store::roaring::RoaringBitmap;
use types::collection::Collection;
pub(crate) trait PrincipalMatching: Sync + Send {
fn handle_principal_match(
&self,
access_token: &AccessToken,
headers: &RequestHeaders<'_>,
request: PrincipalMatch,
) -> impl Future<Output = crate::Result<HttpResponse>> + Send;
}
impl PrincipalMatching for Server {
async fn handle_principal_match(
&self,
access_token: &AccessToken,
headers: &RequestHeaders<'_>,
mut request: PrincipalMatch,
) -> crate::Result<HttpResponse> {
let resource = self.validate_uri(access_token, headers.uri).await?;
match resource.collection {
Collection::AddressBook | Collection::Calendar | Collection::FileNode => {
if request.properties.is_empty() {
request
.properties
.push(DavProperty::WebDav(WebDavProperty::Owner));
}
if let Some(account_id) = resource.account_id {
return self
.handle_dav_query(
access_token,
DavQuery {
resource: DavQueryResource::Uri(UriResource {
collection: resource.collection,
account_id,
resource: resource.resource,
}),
propfind: PropFind::Prop(request.properties),
depth: usize::MAX,
ret: headers.ret,
depth_no_root: headers.depth_no_root,
uri: headers.uri,
sync_type: Default::default(),
limit: Default::default(),
vcard_version: Default::default(),
expand: Default::default(),
},
)
.await;
}
}
Collection::Principal => {}
_ => return Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED)),
}
let mut response = MultiStatus::new(Vec::with_capacity(16));
if request.properties.is_empty() {
request
.properties
.push(DavProperty::WebDav(WebDavProperty::DisplayName));
}
let request = PropFind::Prop(request.properties);
self.prepare_principal_propfind_response(
access_token,
resource.collection,
RoaringBitmap::from_iter(access_token.all_ids()).into_iter(),
&request,
&mut response,
)
.await?;
Ok(HttpResponse::new(StatusCode::MULTI_STATUS).with_xml_body(response.to_string()))
}
}
+29
View File
@@ -0,0 +1,29 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use common::auth::AccountCache;
use dav_proto::schema::response::Href;
use groupware::RFC_3986;
use crate::DavResourceName;
pub mod matching;
pub mod propfind;
pub mod propsearch;
pub trait CurrentUserPrincipal {
fn current_user_principal(&self) -> Href;
}
impl CurrentUserPrincipal for AccountCache {
fn current_user_principal(&self) -> Href {
Href(format!(
"{}/{}/",
DavResourceName::Principal.base_path(),
percent_encoding::utf8_percent_encode(self.name(), RFC_3986)
))
}
}
+464
View File
@@ -0,0 +1,464 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use super::CurrentUserPrincipal;
use crate::{
DavResourceName,
common::propfind::{PropFindRequestHandler, SyncTokenUrn},
};
use common::{
Server,
auth::{AccessToken, AccountCache},
};
use dav_proto::schema::{
Namespace,
property::{
DavProperty, DavValue, PrincipalProperty, Privilege, ReportSet, ResourceType,
WebDavProperty,
},
request::{DavPropertyValue, PropFind},
response::{Href, MultiStatus, PropStat, Response},
};
use groupware::RFC_3986;
use groupware::cache::GroupwareCache;
use hyper::StatusCode;
use std::borrow::Cow;
use trc::AddContext;
use types::collection::Collection;
pub(crate) trait PrincipalPropFind: Sync + Send {
fn prepare_principal_propfind_response(
&self,
access_token: &AccessToken,
collection: Collection,
documents: impl Iterator<Item = u32> + Sync + Send,
request: &PropFind,
response: &mut MultiStatus,
) -> impl Future<Output = crate::Result<()>> + Send;
fn expand_principal(
&self,
access_token: &AccessToken,
account_id: u32,
propfind: &PropFind,
) -> impl Future<Output = crate::Result<Option<Response>>> + Send;
fn owner_href(
&self,
account_info: &AccountCache,
account_id: u32,
) -> impl Future<Output = trc::Result<Href>> + Send;
}
impl PrincipalPropFind for Server {
async fn prepare_principal_propfind_response(
&self,
access_token: &AccessToken,
collection: Collection,
account_ids: impl Iterator<Item = u32> + Sync + Send,
request: &PropFind,
response: &mut MultiStatus,
) -> crate::Result<()> {
let access_account_info = self
.account(access_token.account_id())
.await
.caused_by(trc::location!())?;
let properties = match request {
PropFind::PropName => {
let props = all_props(collection, None);
for property in &props {
response.set_namespace(property.namespace());
}
for account_id in account_ids {
response.add_response(Response::new_propstat(
self.owner_href(&access_account_info, account_id)
.await
.caused_by(trc::location!())?,
vec![PropStat::new_list(
props.iter().cloned().map(DavPropertyValue::empty).collect(),
)],
));
}
return Ok(());
}
PropFind::AllProp(items) => Cow::Owned(all_props(collection, items.as_slice().into())),
PropFind::Prop(items) => Cow::Borrowed(items),
};
for property in properties.as_slice() {
response.set_namespace(property.namespace());
}
let is_principal = match collection {
Collection::AddressBook | Collection::ContactCard => {
response.set_namespace(Namespace::CardDav);
false
}
Collection::Calendar
| Collection::CalendarEvent
| Collection::CalendarEventNotification => {
response.set_namespace(Namespace::CalDav);
false
}
Collection::Principal => true,
_ => false,
};
let base_path = DavResourceName::from(collection).base_path();
let needs_quota = properties.iter().any(|property| {
matches!(
property,
DavProperty::WebDav(
WebDavProperty::QuotaAvailableBytes | WebDavProperty::QuotaUsedBytes
)
)
});
for account_id in account_ids {
let mut fields = Vec::with_capacity(properties.len());
let mut fields_not_found = Vec::new();
let account = self.account(account_id).await.caused_by(trc::location!())?;
// Fetch quota
let quota = if needs_quota {
self.dav_quota(account_id)
.await
.caused_by(trc::location!())?
} else {
Default::default()
};
for property in properties.as_slice() {
match property {
DavProperty::WebDav(dav_property) => match dav_property {
WebDavProperty::DisplayName => {
fields.push(DavPropertyValue::new(
property.clone(),
account.description().unwrap_or(account.name()).to_string(),
));
}
WebDavProperty::ResourceType => {
let resource_type = if !is_principal {
vec![ResourceType::Collection]
} else {
vec![ResourceType::Principal, ResourceType::Collection]
};
fields.push(DavPropertyValue::new(property.clone(), resource_type));
}
WebDavProperty::SupportedReportSet => {
let reports = match collection {
Collection::Principal => ReportSet::principal(),
Collection::Calendar | Collection::CalendarEvent => {
ReportSet::calendar()
}
Collection::AddressBook | Collection::ContactCard => {
ReportSet::addressbook()
}
_ => ReportSet::file(),
};
fields.push(DavPropertyValue::new(property.clone(), reports));
}
WebDavProperty::CurrentUserPrincipal => {
fields.push(DavPropertyValue::new(
property.clone(),
vec![access_account_info.current_user_principal()],
));
}
WebDavProperty::QuotaAvailableBytes if !is_principal => {
if let Some(available) = quota.available {
fields.push(DavPropertyValue::new(property.clone(), available));
} else {
fields_not_found.push(DavPropertyValue::empty(property.clone()));
}
}
WebDavProperty::QuotaUsedBytes if !is_principal => {
fields.push(DavPropertyValue::new(property.clone(), quota.used));
}
WebDavProperty::SyncToken if !is_principal => {
let sync_token = self
.fetch_dav_resources(
access_token.account_id(),
account_id,
collection.into(),
)
.await
.caused_by(trc::location!())?
.sync_token();
fields.push(DavPropertyValue::new(property.clone(), sync_token));
}
WebDavProperty::GetCTag if !is_principal => {
let ctag = self
.fetch_dav_resources(
access_token.account_id(),
account_id,
collection.into(),
)
.await
.caused_by(trc::location!())?
.highest_change_id;
fields.push(DavPropertyValue::new(
property.clone(),
DavValue::String(format!("\"{ctag}\"")),
));
}
WebDavProperty::Owner => {
fields.push(DavPropertyValue::new(
property.clone(),
vec![Href(format!(
"{}/{}/",
DavResourceName::Principal.base_path(),
percent_encoding::utf8_percent_encode(account.name(), RFC_3986),
))],
));
}
WebDavProperty::Group if !is_principal => {
fields.push(DavPropertyValue::empty(property.clone()));
}
WebDavProperty::CurrentUserPrivilegeSet if !is_principal => {
fields.push(DavPropertyValue::new(
property.clone(),
if access_token.is_member(account_id) {
Privilege::all(matches!(
collection,
Collection::Calendar | Collection::CalendarEvent
))
} else {
vec![Privilege::Read]
},
));
}
WebDavProperty::PrincipalCollectionSet => {
fields.push(DavPropertyValue::new(
property.clone(),
vec![Href(
DavResourceName::Principal.collection_path().to_string(),
)],
));
}
_ => {
fields_not_found.push(DavPropertyValue::empty(property.clone()));
}
},
DavProperty::Principal(principal_property) => match principal_property {
PrincipalProperty::AlternateURISet
| PrincipalProperty::GroupMemberSet
| PrincipalProperty::GroupMembership => {
fields.push(DavPropertyValue::empty(property.clone()));
}
PrincipalProperty::PrincipalURL => {
fields.push(DavPropertyValue::new(
property.clone(),
vec![Href(format!(
"{}/{}/",
DavResourceName::Principal.base_path(),
percent_encoding::utf8_percent_encode(account.name(), RFC_3986),
))],
));
}
PrincipalProperty::CalendarHomeSet => {
let hrefs = build_home_set(
self,
access_token,
account.name(),
account_id,
true,
)
.await
.caused_by(trc::location!())?;
fields.push(DavPropertyValue::new(property.clone(), hrefs));
}
PrincipalProperty::AddressbookHomeSet => {
let hrefs = build_home_set(
self,
access_token,
account.name(),
account_id,
false,
)
.await
.caused_by(trc::location!())?;
fields.push(DavPropertyValue::new(property.clone(), hrefs));
}
PrincipalProperty::PrincipalAddress => {
fields_not_found.push(DavPropertyValue::empty(property.clone()));
}
PrincipalProperty::CalendarUserAddressSet => {
fields.push(DavPropertyValue::new(
property.clone(),
vec![Href(format!("mailto:{}", account.name()))],
));
}
PrincipalProperty::CalendarUserType => {
fields.push(DavPropertyValue::new(
property.clone(),
if account.is_user_account() {
DavValue::String("INDIVIDUAL".to_string())
} else {
DavValue::String("GROUP".to_string())
},
));
}
PrincipalProperty::ScheduleInboxURL => {
fields.push(DavPropertyValue::new(
property.clone(),
vec![Href(format!(
"{}/{}/inbox/",
DavResourceName::Scheduling.base_path(),
percent_encoding::utf8_percent_encode(account.name(), RFC_3986),
))],
));
}
PrincipalProperty::ScheduleOutboxURL => {
fields.push(DavPropertyValue::new(
property.clone(),
vec![Href(format!(
"{}/{}/outbox/",
DavResourceName::Scheduling.base_path(),
percent_encoding::utf8_percent_encode(account.name(), RFC_3986),
))],
));
}
},
_ => {
fields_not_found.push(DavPropertyValue::empty(property.clone()));
}
}
}
let mut prop_stats = Vec::with_capacity(2);
if !fields_not_found.is_empty() {
prop_stats
.push(PropStat::new_list(fields_not_found).with_status(StatusCode::NOT_FOUND));
}
if !fields.is_empty() || prop_stats.is_empty() {
prop_stats.push(PropStat::new_list(fields));
}
response.add_response(Response::new_propstat(
Href(format!(
"{}/{}/",
base_path,
percent_encoding::utf8_percent_encode(account.name(), RFC_3986),
)),
prop_stats,
));
}
Ok(())
}
async fn expand_principal(
&self,
access_token: &AccessToken,
account_id: u32,
propfind: &PropFind,
) -> crate::Result<Option<Response>> {
let mut status = MultiStatus::new(vec![]);
self.prepare_principal_propfind_response(
access_token,
Collection::Principal,
[account_id].into_iter(),
propfind,
&mut status,
)
.await?;
Ok(status.response.0.into_iter().next())
}
async fn owner_href(&self, account_info: &AccountCache, account_id: u32) -> trc::Result<Href> {
if account_info.account_id() == account_id {
Ok(account_info.current_user_principal())
} else {
let account_info = self.account(account_id).await.caused_by(trc::location!())?;
Ok(Href(format!(
"{}/{}/",
DavResourceName::Principal.base_path(),
percent_encoding::utf8_percent_encode(account_info.name(), RFC_3986),
)))
}
}
}
pub(crate) async fn build_home_set(
server: &Server,
access_token: &AccessToken,
name: &str,
account_id: u32,
is_calendar: bool,
) -> trc::Result<Vec<Href>> {
let (collection, resource_name) = if is_calendar {
(Collection::Calendar, DavResourceName::Cal)
} else {
(Collection::AddressBook, DavResourceName::Card)
};
let mut hrefs = Vec::new();
hrefs.push(Href(format!(
"{}/{}/",
resource_name.base_path(),
percent_encoding::utf8_percent_encode(name, RFC_3986),
)));
if !server.core.groupware.assisted_discovery && account_id == access_token.account_id() {
for account_id in access_token.all_ids_by_collection(collection) {
if account_id != access_token.account_id() {
let other = server
.account(account_id)
.await
.caused_by(trc::location!())?;
hrefs.push(Href(format!(
"{}/{}/",
resource_name.base_path(),
percent_encoding::utf8_percent_encode(other.name(), RFC_3986),
)));
}
}
}
Ok(hrefs)
}
fn all_props(collection: Collection, all_props: Option<&[DavProperty]>) -> Vec<DavProperty> {
if collection == Collection::Principal {
vec![
DavProperty::WebDav(WebDavProperty::DisplayName),
DavProperty::WebDav(WebDavProperty::ResourceType),
DavProperty::WebDav(WebDavProperty::SupportedReportSet),
DavProperty::WebDav(WebDavProperty::CurrentUserPrincipal),
DavProperty::WebDav(WebDavProperty::PrincipalCollectionSet),
DavProperty::Principal(PrincipalProperty::AlternateURISet),
DavProperty::Principal(PrincipalProperty::PrincipalURL),
DavProperty::Principal(PrincipalProperty::GroupMemberSet),
DavProperty::Principal(PrincipalProperty::GroupMembership),
]
} else {
let mut props = vec![
DavProperty::WebDav(WebDavProperty::DisplayName),
DavProperty::WebDav(WebDavProperty::ResourceType),
DavProperty::WebDav(WebDavProperty::SupportedReportSet),
DavProperty::WebDav(WebDavProperty::CurrentUserPrincipal),
DavProperty::WebDav(WebDavProperty::SyncToken),
DavProperty::WebDav(WebDavProperty::Owner),
DavProperty::WebDav(WebDavProperty::PrincipalCollectionSet),
];
if let Some(all_props) = all_props {
props.extend(all_props.iter().filter(|p| !p.is_all_prop()).cloned());
props
} else {
props
}
}
}
+79
View File
@@ -0,0 +1,79 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use super::propfind::PrincipalPropFind;
use common::{Server, auth::AccessToken};
use dav_proto::schema::{
property::{DavProperty, WebDavProperty},
request::{PrincipalPropertySearch, PropFind},
response::MultiStatus,
};
use http_proto::HttpResponse;
use hyper::StatusCode;
use registry::schema::prelude::{ObjectType, Property};
use store::{registry::RegistryQuery, roaring::RoaringBitmap};
use trc::AddContext;
use types::collection::Collection;
pub(crate) trait PrincipalPropSearch: Sync + Send {
fn handle_principal_property_search(
&self,
access_token: &AccessToken,
request: PrincipalPropertySearch,
) -> impl Future<Output = crate::Result<HttpResponse>> + Send;
}
impl PrincipalPropSearch for Server {
async fn handle_principal_property_search(
&self,
access_token: &AccessToken,
mut request: PrincipalPropertySearch,
) -> crate::Result<HttpResponse> {
let mut search_for = None;
for prop_search in request.property_search {
if matches!(
prop_search.property,
DavProperty::WebDav(WebDavProperty::DisplayName)
) && !prop_search.match_.is_empty()
{
search_for = Some(prop_search.match_);
}
}
let mut response = MultiStatus::new(Vec::with_capacity(16));
if let Some(search_for) = search_for {
let ids = self
.registry()
.query::<RoaringBitmap>(
RegistryQuery::new(ObjectType::Account)
.with_tenant(access_token.tenant_id())
.text(Property::Text, search_for),
)
.await
.caused_by(trc::location!())?;
if !ids.is_empty() {
if request.properties.is_empty() {
request
.properties
.push(DavProperty::WebDav(WebDavProperty::DisplayName));
}
let request = PropFind::Prop(request.properties);
self.prepare_principal_propfind_response(
access_token,
Collection::Principal,
ids.into_iter(),
&request,
&mut response,
)
.await?;
}
}
Ok(HttpResponse::new(StatusCode::MULTI_STATUS).with_xml_body(response.to_string()))
}
}
+772
View File
@@ -0,0 +1,772 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::{
DavError, DavErrorCondition, DavMethod, DavResourceName,
calendar::{
copy_move::CalendarCopyMoveRequestHandler, delete::CalendarDeleteRequestHandler,
freebusy::CalendarFreebusyRequestHandler, get::CalendarGetRequestHandler,
mkcol::CalendarMkColRequestHandler, proppatch::CalendarPropPatchRequestHandler,
query::CalendarQueryRequestHandler, scheduling::CalendarEventNotificationHandler,
update::CalendarUpdateRequestHandler,
},
card::{
copy_move::CardCopyMoveRequestHandler, delete::CardDeleteRequestHandler,
get::CardGetRequestHandler, mkcol::CardMkColRequestHandler,
proppatch::CardPropPatchRequestHandler, query::CardQueryRequestHandler,
update::CardUpdateRequestHandler,
},
common::{
DavQuery,
acl::DavAclHandler,
lock::{LockRequest, LockRequestHandler},
propfind::PropFindRequestHandler,
uri::DavUriResource,
},
file::{
copy_move::FileCopyMoveRequestHandler, delete::FileDeleteRequestHandler,
get::FileGetRequestHandler, mkcol::FileMkColRequestHandler,
proppatch::FilePropPatchRequestHandler, update::FileUpdateRequestHandler,
},
principal::{matching::PrincipalMatching, propsearch::PrincipalPropSearch},
};
use common::{Server, auth::AccessToken};
use compact_str::{CompactString, ToCompactString};
use dav_proto::{
RequestHeaders,
parser::{DavParser, tokenizer::Tokenizer},
schema::{
Namespace,
property::WebDavProperty,
request::{Acl, LockInfo, MkCol, PropFind, PropertyUpdate, Report},
response::{
BaseCondition, ErrorResponse, List, PrincipalSearchProperty, PrincipalSearchPropertySet,
},
},
};
use http_proto::{HttpRequest, HttpResponse, HttpSessionData, request::fetch_body};
use hyper::{StatusCode, header};
use registry::schema::enums::Permission;
use std::time::Instant;
use trc::{EventType, LimitEvent, StoreEvent, WebDavEvent};
use types::collection::Collection;
pub trait DavRequestHandler: Sync + Send {
fn handle_dav_request(
&self,
request: HttpRequest,
access_token: AccessToken,
session: &HttpSessionData,
resource: DavResourceName,
method: DavMethod,
) -> impl Future<Output = HttpResponse> + Send;
}
pub(crate) trait DavRequestDispatcher: Sync + Send {
fn dispatch_dav_request(
&self,
headers: &RequestHeaders<'_>,
access_token: AccessToken,
resource: DavResourceName,
method: DavMethod,
body: Vec<u8>,
) -> impl Future<Output = crate::Result<HttpResponse>> + Send;
}
impl DavRequestDispatcher for Server {
async fn dispatch_dav_request(
&self,
headers: &RequestHeaders<'_>,
access_token: AccessToken,
resource: DavResourceName,
method: DavMethod,
body: Vec<u8>,
) -> crate::Result<HttpResponse> {
// Dispatch
match method {
DavMethod::PROPFIND => {
let request = PropFind::parse(&mut Tokenizer::new(&body))?;
self.handle_propfind_request(&access_token, headers, request)
.await
}
DavMethod::GET | DavMethod::HEAD => match resource {
DavResourceName::Card => {
// Validate permissions
let access_token =
access_token.assert_has_permission(Permission::DavCardGet)?;
self.handle_card_get_request(
&access_token,
headers,
matches!(method, DavMethod::HEAD),
)
.await
}
DavResourceName::Cal => {
// Validate permissions
let access_token = access_token.assert_has_permission(Permission::DavCalGet)?;
self.handle_calendar_get_request(
&access_token,
headers,
matches!(method, DavMethod::HEAD),
)
.await
}
DavResourceName::File => {
// Validate permissions
let access_token =
access_token.assert_has_permission(Permission::DavFileGet)?;
// Deal with Litmus bug
/*self.handle_file_get_request(
&access_token,
headers,
matches!(method, DavMethod::HEAD)
&& !request.headers().contains_key("x-litmus"),
)
.await*/
self.handle_file_get_request(
&access_token,
headers,
matches!(method, DavMethod::HEAD),
)
.await
}
DavResourceName::Scheduling => {
// Validate permissions
let access_token = access_token.assert_has_permission(Permission::DavCalGet)?;
self.handle_scheduling_get_request(
&access_token,
headers,
matches!(method, DavMethod::HEAD),
)
.await
}
DavResourceName::Principal => Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED)),
},
DavMethod::REPORT => match Report::parse(&mut Tokenizer::new(&body))? {
Report::SyncCollection(sync_collection) => {
// Validate permissions
let access_token =
access_token.assert_has_permission(Permission::DavSyncCollection)?;
let uri = self
.validate_uri(&access_token, headers.uri)
.await
.and_then(|d| d.into_owned_uri())?;
match resource {
DavResourceName::Card
| DavResourceName::Cal
| DavResourceName::File
| DavResourceName::Scheduling => {
self.handle_dav_query(
&access_token,
DavQuery::changes(uri, sync_collection, headers),
)
.await
}
DavResourceName::Principal => {
Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED))
}
}
}
Report::AclPrincipalPropSet(report) => {
// Validate permissions
if !self.core.groupware.allow_directory_query
&& !access_token.has_permission(Permission::DavPrincipalAcl)
{
return Err(DavError::Condition(
DavErrorCondition::new(
StatusCode::FORBIDDEN,
BaseCondition::NeedPrivileges(List(Default::default())),
)
.with_details("The administrator has disabled directory queries."),
));
}
let access_token =
access_token.assert_has_permission(Permission::DavPrincipalAcl)?;
self.handle_acl_prop_set(&access_token, headers, report)
.await
}
Report::PrincipalMatch(report) => {
// Validate permissions
if !self.core.groupware.allow_directory_query
&& !access_token.has_permission(Permission::DavPrincipalMatch)
{
return Err(DavError::Condition(
DavErrorCondition::new(
StatusCode::FORBIDDEN,
BaseCondition::NeedPrivileges(List(Default::default())),
)
.with_details("The administrator has disabled directory queries."),
));
}
let access_token =
access_token.assert_has_permission(Permission::DavPrincipalMatch)?;
self.handle_principal_match(&access_token, headers, report)
.await
}
Report::PrincipalPropertySearch(report) => {
if resource == DavResourceName::Principal {
// Validate permissions
if !self.core.groupware.allow_directory_query
&& !access_token.has_permission(Permission::DavPrincipalSearch)
{
return Err(DavError::Condition(
DavErrorCondition::new(
StatusCode::FORBIDDEN,
BaseCondition::NeedPrivileges(List(Default::default())),
)
.with_details("The administrator has disabled directory queries."),
));
}
let access_token =
access_token.assert_has_permission(Permission::DavPrincipalSearch)?;
self.handle_principal_property_search(&access_token, report)
.await
} else {
Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED))
}
}
Report::PrincipalSearchPropertySet => {
if resource == DavResourceName::Principal {
// Validate permissions
access_token
.assert_has_permission(Permission::DavPrincipalSearchPropSet)?;
Ok(HttpResponse::new(StatusCode::OK).with_xml_body(
PrincipalSearchPropertySet::new(vec![PrincipalSearchProperty::new(
WebDavProperty::DisplayName,
"Account or Group name",
)])
.to_string(),
))
} else {
Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED))
}
}
Report::AddressbookQuery(report) => {
// Validate permissions
let access_token =
access_token.assert_has_permission(Permission::DavCardQuery)?;
self.handle_card_query_request(&access_token, headers, report)
.await
}
Report::AddressbookMultiGet(report) => {
// Validate permissions
let access_token =
access_token.assert_has_permission(Permission::DavCardMultiGet)?;
self.handle_dav_query(
&access_token,
DavQuery::multiget(report, Collection::AddressBook, headers),
)
.await
}
Report::CalendarQuery(report) => {
// Validate permissions
let access_token =
access_token.assert_has_permission(Permission::DavCalQuery)?;
self.handle_calendar_query_request(&access_token, headers, report)
.await
}
Report::CalendarMultiGet(report) => {
// Validate permissions
let access_token =
access_token.assert_has_permission(Permission::DavCalMultiGet)?;
self.handle_dav_query(
&access_token,
DavQuery::multiget(report, Collection::Calendar, headers),
)
.await
}
Report::FreeBusyQuery(report) => {
// Validate permissions
let access_token =
access_token.assert_has_permission(Permission::DavCalFreeBusyQuery)?;
self.handle_calendar_freebusy_request(&access_token, headers, report)
.await
}
Report::ExpandProperty(report) => {
let uri = self
.validate_uri(&access_token, headers.uri)
.await
.and_then(|d| d.into_owned_uri())?;
// Validate permissions
let access_token =
access_token.assert_has_permission(Permission::DavExpandProperty)?;
match resource {
DavResourceName::Card | DavResourceName::Cal | DavResourceName::File => {
self.handle_dav_query(
&access_token,
DavQuery::expand(uri, report, headers),
)
.await
}
DavResourceName::Principal | DavResourceName::Scheduling => {
Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED))
}
}
}
},
DavMethod::PROPPATCH => {
let request = PropertyUpdate::parse(&mut Tokenizer::new(&body))?;
match resource {
DavResourceName::Card => {
// Validate permissions
let access_token =
access_token.assert_has_permission(Permission::DavCardPropPatch)?;
self.handle_card_proppatch_request(&access_token, headers, request)
.await
}
DavResourceName::Cal => {
// Validate permissions
let access_token =
access_token.assert_has_permission(Permission::DavCalPropPatch)?;
self.handle_calendar_proppatch_request(&access_token, headers, request)
.await
}
DavResourceName::File => {
// Validate permissions
let access_token =
access_token.assert_has_permission(Permission::DavFilePropPatch)?;
self.handle_file_proppatch_request(&access_token, headers, request)
.await
}
DavResourceName::Principal | DavResourceName::Scheduling => {
Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED))
}
}
}
DavMethod::MKCOL => {
let request = if !body.is_empty() {
Some(MkCol::parse(&mut Tokenizer::new(&body))?)
} else {
None
};
match resource {
DavResourceName::Card => {
// Validate permissions
let access_token =
access_token.assert_has_permission(Permission::DavCardMkCol)?;
self.handle_card_mkcol_request(&access_token, headers, request)
.await
}
DavResourceName::Cal => {
// Validate permissions
let access_token =
access_token.assert_has_permission(Permission::DavCalMkCol)?;
self.handle_calendar_mkcol_request(&access_token, headers, request)
.await
}
DavResourceName::File => {
// Validate permissions
let access_token =
access_token.assert_has_permission(Permission::DavFileMkCol)?;
self.handle_file_mkcol_request(&access_token, headers, request)
.await
}
DavResourceName::Principal | DavResourceName::Scheduling => {
Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED))
}
}
}
DavMethod::DELETE => match resource {
DavResourceName::Card => {
// Validate permissions
let access_token =
access_token.assert_has_permission(Permission::DavCardDelete)?;
self.handle_card_delete_request(&access_token, headers)
.await
}
DavResourceName::Cal => {
// Validate permissions
let access_token =
access_token.assert_has_permission(Permission::DavCalDelete)?;
self.handle_calendar_delete_request(&access_token, headers)
.await
}
DavResourceName::File => {
// Validate permissions
let access_token =
access_token.assert_has_permission(Permission::DavFileDelete)?;
self.handle_file_delete_request(&access_token, headers)
.await
}
DavResourceName::Scheduling => {
// Validate permissions
let access_token =
access_token.assert_has_permission(Permission::DavCalDelete)?;
self.handle_scheduling_delete_request(&access_token, headers)
.await
}
DavResourceName::Principal => Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED)),
},
DavMethod::PUT | DavMethod::POST | DavMethod::PATCH => match resource {
DavResourceName::Card => {
// Validate permissions
let access_token =
access_token.assert_has_permission(Permission::DavCardPut)?;
self.handle_card_update_request(
&access_token,
headers,
body,
matches!(method, DavMethod::PATCH),
)
.await
}
DavResourceName::Cal => {
// Validate permissions
let access_token = access_token.assert_has_permission(Permission::DavCalPut)?;
self.handle_calendar_update_request(
&access_token,
headers,
body,
matches!(method, DavMethod::PATCH),
)
.await
}
DavResourceName::File => {
// Validate permissions
let access_token =
access_token.assert_has_permission(Permission::DavFilePut)?;
self.handle_file_update_request(
&access_token,
headers,
body,
matches!(method, DavMethod::PATCH),
)
.await
}
DavResourceName::Scheduling => {
// Validate permissions
let access_token =
access_token.assert_has_permission(Permission::DavCalFreeBusyQuery)?;
self.handle_scheduling_post_request(&access_token, headers, body)
.await
}
DavResourceName::Principal => Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED)),
},
DavMethod::COPY | DavMethod::MOVE => {
let is_move = matches!(method, DavMethod::MOVE);
match resource {
DavResourceName::Card => {
// Validate permissions
let access_token = access_token.assert_has_permission(if is_move {
Permission::DavCardMove
} else {
Permission::DavCardCopy
})?;
self.handle_card_copy_move_request(&access_token, headers, is_move)
.await
}
DavResourceName::Cal => {
// Validate permissions
let access_token = access_token.assert_has_permission(if is_move {
Permission::DavCalMove
} else {
Permission::DavCalCopy
})?;
self.handle_calendar_copy_move_request(&access_token, headers, is_move)
.await
}
DavResourceName::File => {
// Validate permissions
let access_token = access_token.assert_has_permission(if is_move {
Permission::DavFileMove
} else {
Permission::DavFileCopy
})?;
self.handle_file_copy_move_request(&access_token, headers, is_move)
.await
}
DavResourceName::Principal | DavResourceName::Scheduling => {
Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED))
}
}
}
DavMethod::MKCALENDAR => match resource {
DavResourceName::Cal => {
// Validate permissions
let access_token =
access_token.assert_has_permission(Permission::DavCalMkCol)?;
self.handle_calendar_mkcol_request(
&access_token,
headers,
Some(MkCol::parse(&mut Tokenizer::new(&body))?),
)
.await
}
_ => Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED)),
},
DavMethod::LOCK => {
// Validate permissions
let access_token = access_token.assert_has_permission(match resource {
DavResourceName::File => Permission::DavFileLock,
DavResourceName::Cal => Permission::DavCalLock,
DavResourceName::Card => Permission::DavCardLock,
_ => return Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED)),
})?;
self.handle_lock_request(
&access_token,
headers,
if !body.is_empty() {
LockRequest::Lock(LockInfo::parse(&mut Tokenizer::new(&body))?)
} else {
LockRequest::Refresh
},
)
.await
}
DavMethod::UNLOCK => {
// Validate permissions
let access_token = access_token.assert_has_permission(match resource {
DavResourceName::File => Permission::DavFileLock,
DavResourceName::Cal => Permission::DavCalLock,
DavResourceName::Card => Permission::DavCardLock,
_ => return Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED)),
})?;
self.handle_lock_request(&access_token, headers, LockRequest::Unlock)
.await
}
DavMethod::ACL => {
// Validate permissions
let access_token = access_token.assert_has_permission(match resource {
DavResourceName::File => Permission::DavFileAcl,
DavResourceName::Cal => Permission::DavCalAcl,
DavResourceName::Card => Permission::DavCardAcl,
_ => return Err(DavError::Code(StatusCode::METHOD_NOT_ALLOWED)),
})?;
self.handle_acl_request(
&access_token,
headers,
Acl::parse(&mut Tokenizer::new(&body))?,
)
.await
}
DavMethod::OPTIONS => unreachable!(),
}
}
}
impl DavRequestHandler for Server {
async fn handle_dav_request(
&self,
mut request: HttpRequest,
access_token: AccessToken,
session: &HttpSessionData,
resource: DavResourceName,
method: DavMethod,
) -> HttpResponse {
let body = if method.has_body()
|| request
.headers()
.get(header::CONTENT_LENGTH)
.and_then(|v| v.to_str().ok())
.and_then(|v| v.parse::<u64>().ok())
.is_some_and(|len| len > 0)
{
if let Some(body) = fetch_body(
&mut request,
if !access_token.has_permission(Permission::UnlimitedUploads) {
self.core.groupware.max_request_size
} else {
0
},
session.session_id,
)
.await
{
body
} else {
trc::event!(
Limit(trc::LimitEvent::SizeRequest),
SpanId = session.session_id,
Contents = "Request body too large",
);
return HttpResponse::new(StatusCode::PAYLOAD_TOO_LARGE);
}
} else {
Vec::new()
};
// Parse headers
let mut headers = RequestHeaders::new(request.uri().path());
for (key, value) in request.headers() {
headers.parse(key.as_str(), value.to_str().unwrap_or_default());
}
let start_time = Instant::now();
match self
.dispatch_dav_request(&headers, access_token, resource, method, body)
.await
{
Ok(response) => {
let event = WebDavEvent::from(method);
trc::event!(
WebDav(event),
SpanId = session.session_id,
Url = headers.uri.to_compact_string(),
Type = resource.name(),
Details = &headers,
Result = response.status().as_u16(),
Elapsed = start_time.elapsed(),
);
response
}
Err(DavError::Internal(err)) => {
let err_type = err.event_type();
trc::error!(
err.span_id(session.session_id)
.ctx(trc::Key::Url, headers.uri.to_compact_string())
.ctx(trc::Key::Type, resource.name())
.ctx(trc::Key::Elapsed, start_time.elapsed())
);
match err_type {
EventType::Limit(LimitEvent::Quota | LimitEvent::TenantQuota) => {
HttpResponse::new(StatusCode::PRECONDITION_FAILED)
.with_xml_body(
ErrorResponse::new(BaseCondition::QuotaNotExceeded)
.with_namespace(match resource {
DavResourceName::Card => Namespace::CardDav,
DavResourceName::Cal | DavResourceName::Scheduling => {
Namespace::CalDav
}
DavResourceName::File | DavResourceName::Principal => {
Namespace::Dav
}
})
.to_string(),
)
.with_no_cache()
}
EventType::Store(StoreEvent::AssertValueFailed) => {
HttpResponse::new(StatusCode::CONFLICT)
}
EventType::Security(_) => HttpResponse::new(StatusCode::FORBIDDEN),
_ => HttpResponse::new(StatusCode::INTERNAL_SERVER_ERROR),
}
}
Err(DavError::Parse(err)) => {
let result = if headers.content_type.is_some_and(|h| h.contains("/xml")) {
StatusCode::BAD_REQUEST
} else {
StatusCode::UNSUPPORTED_MEDIA_TYPE
};
trc::event!(
WebDav(WebDavEvent::Error),
SpanId = session.session_id,
Url = headers.uri.to_compact_string(),
Type = resource.name(),
Details = &headers,
Result = result.as_u16(),
Reason = err.to_compact_string(),
Elapsed = start_time.elapsed(),
);
HttpResponse::new(result)
}
Err(DavError::Condition(condition)) => {
let event = WebDavEvent::from(method);
trc::event!(
WebDav(event),
SpanId = session.session_id,
Url = headers.uri.to_compact_string(),
Type = resource.name(),
Details = &headers,
Code = condition.code.as_u16(),
Result = CompactString::const_new(condition.condition.display_name()),
Reason = condition.details,
Elapsed = start_time.elapsed(),
);
HttpResponse::new(condition.code)
.with_xml_body(
ErrorResponse::new(condition.condition)
.with_namespace(match resource {
DavResourceName::Card => Namespace::CardDav,
DavResourceName::Cal | DavResourceName::Scheduling => {
Namespace::CalDav
}
DavResourceName::File | DavResourceName::Principal => {
Namespace::Dav
}
})
.to_string(),
)
.with_no_cache()
}
Err(DavError::Code(code)) => {
let event = WebDavEvent::from(method);
trc::event!(
WebDav(event),
SpanId = session.session_id,
Url = headers.uri.to_compact_string(),
Type = resource.name(),
Details = &headers,
Result = code.as_u16(),
Elapsed = start_time.elapsed(),
);
HttpResponse::new(code)
}
}
}
}
impl From<dav_proto::parser::Error> for DavError {
fn from(err: dav_proto::parser::Error) -> Self {
DavError::Parse(err)
}
}
impl From<trc::Error> for DavError {
fn from(err: trc::Error) -> Self {
DavError::Internal(err)
}
}