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:
commit
7dae9b29fd
1650 files changed
+485521
No files matched your search
@@ -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()
|
||||
}
|
||||
@@ -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<'_> {}
|
||||
@@ -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
@@ -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}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user