mirror of
https://github.com/stalwartlabs/stalwart.git
synced 2026-11-11 19:37:52 +00:00
Fix WebDAV: PROPFIND responses do not declare the namespace of every requested property on the DAV:multistatus element
This commit is contained in:
1 parent
515d6a90a5
commit
4f7a73a423
6 files changed
+64
-18
No files matched your search
@@ -11,6 +11,7 @@ If you are upgrading from v0.16.x, replace the binary (or run `docker pull`). If
|
||||
## Changed
|
||||
|
||||
## Fixed
|
||||
- WebDAV: `PROPFIND` responses do not declare the namespace of every requested property on the `DAV:multistatus` element.
|
||||
- MTA: SMTP session state obtained before `STARTTLS` is discarded once the TLS handshake completes, as required by RFC 3207.
|
||||
|
||||
## [0.16.21] - 2026-09-06
|
||||
|
||||
@@ -228,7 +228,9 @@ impl DavProperty {
|
||||
match self {
|
||||
DavProperty::WebDav(WebDavProperty::GetCTag) => Namespace::CalendarServer,
|
||||
DavProperty::CardDav(_)
|
||||
| DavProperty::Principal(PrincipalProperty::AddressbookHomeSet) => Namespace::CardDav,
|
||||
| DavProperty::Principal(
|
||||
PrincipalProperty::AddressbookHomeSet | PrincipalProperty::PrincipalAddress,
|
||||
) => Namespace::CardDav,
|
||||
DavProperty::CalDav(_)
|
||||
| DavProperty::Principal(
|
||||
PrincipalProperty::CalendarHomeSet
|
||||
|
||||
@@ -377,6 +377,10 @@ impl PropFindRequestHandler for Server {
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
for property in container_props.iter().chain(children_props) {
|
||||
response.set_namespace(property.namespace());
|
||||
}
|
||||
|
||||
for item in paths {
|
||||
let props = if item.is_container {
|
||||
container_props
|
||||
@@ -412,6 +416,10 @@ impl PropFindRequestHandler for Server {
|
||||
PropFind::Prop(items) => items.clone(),
|
||||
};
|
||||
|
||||
for property in &properties {
|
||||
response.set_namespace(property.namespace());
|
||||
}
|
||||
|
||||
let is_scheduling = collection_container == Collection::CalendarEventNotification;
|
||||
let account_info = self
|
||||
.account(access_token.account_id())
|
||||
@@ -559,7 +567,6 @@ impl PropFindRequestHandler for Server {
|
||||
} else {
|
||||
fields_not_found.push(DavPropertyValue::empty(property.clone()));
|
||||
}
|
||||
response.set_namespace(Namespace::CalendarServer);
|
||||
}
|
||||
WebDavProperty::GetLastModified => {
|
||||
fields.push(DavPropertyValue::new(
|
||||
@@ -1062,7 +1069,6 @@ impl PropFindRequestHandler for Server {
|
||||
|
||||
_ => {
|
||||
if !skip_not_found {
|
||||
response.set_namespace(property.namespace());
|
||||
fields_not_found.push(DavPropertyValue::empty(property.clone()));
|
||||
}
|
||||
}
|
||||
@@ -1070,7 +1076,6 @@ impl PropFindRequestHandler for Server {
|
||||
|
||||
property => {
|
||||
if !skip_not_found {
|
||||
response.set_namespace(property.namespace());
|
||||
fields_not_found.push(DavPropertyValue::empty(property.clone()));
|
||||
}
|
||||
}
|
||||
@@ -1737,6 +1742,7 @@ async fn add_base_collection_response(
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
for prop in properties {
|
||||
response.set_namespace(prop.namespace());
|
||||
match &prop {
|
||||
DavProperty::WebDav(WebDavProperty::ResourceType) => {
|
||||
fields.push(DavPropertyValue::new(
|
||||
@@ -1762,7 +1768,6 @@ async fn add_base_collection_response(
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
fields.push(DavPropertyValue::new(prop.clone(), hrefs));
|
||||
response.set_namespace(Namespace::CalDav);
|
||||
}
|
||||
DavProperty::Principal(PrincipalProperty::AddressbookHomeSet) => {
|
||||
let hrefs = build_home_set(
|
||||
@@ -1776,7 +1781,6 @@ async fn add_base_collection_response(
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
fields.push(DavPropertyValue::new(prop.clone(), hrefs));
|
||||
response.set_namespace(Namespace::CardDav);
|
||||
}
|
||||
DavProperty::WebDav(WebDavProperty::SupportedReportSet) => {
|
||||
let reports = match collection {
|
||||
@@ -1787,9 +1791,9 @@ async fn add_base_collection_response(
|
||||
};
|
||||
|
||||
fields.push(DavPropertyValue::new(prop.clone(), reports));
|
||||
response.set_namespace(collection.namespace());
|
||||
}
|
||||
_ => {
|
||||
response.set_namespace(prop.namespace());
|
||||
fields_not_found.push(DavPropertyValue::empty(prop.clone()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,6 +69,9 @@ impl PrincipalPropFind for Server {
|
||||
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)
|
||||
@@ -84,6 +87,9 @@ impl PrincipalPropFind for Server {
|
||||
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);
|
||||
@@ -235,7 +241,6 @@ impl PrincipalPropFind for Server {
|
||||
));
|
||||
}
|
||||
_ => {
|
||||
response.set_namespace(property.namespace());
|
||||
fields_not_found.push(DavPropertyValue::empty(property.clone()));
|
||||
}
|
||||
},
|
||||
@@ -267,7 +272,6 @@ impl PrincipalPropFind for Server {
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
fields.push(DavPropertyValue::new(property.clone(), hrefs));
|
||||
response.set_namespace(Namespace::CalDav);
|
||||
}
|
||||
PrincipalProperty::AddressbookHomeSet => {
|
||||
let hrefs = build_home_set(
|
||||
@@ -281,19 +285,16 @@ impl PrincipalPropFind for Server {
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
fields.push(DavPropertyValue::new(property.clone(), hrefs));
|
||||
response.set_namespace(Namespace::CardDav);
|
||||
}
|
||||
|
||||
PrincipalProperty::PrincipalAddress => {
|
||||
fields_not_found.push(DavPropertyValue::empty(property.clone()));
|
||||
response.set_namespace(Namespace::CardDav);
|
||||
}
|
||||
PrincipalProperty::CalendarUserAddressSet => {
|
||||
fields.push(DavPropertyValue::new(
|
||||
property.clone(),
|
||||
vec![Href(format!("mailto:{}", account.name()))],
|
||||
));
|
||||
response.set_namespace(Namespace::CalDav);
|
||||
}
|
||||
PrincipalProperty::CalendarUserType => {
|
||||
fields.push(DavPropertyValue::new(
|
||||
@@ -304,7 +305,6 @@ impl PrincipalPropFind for Server {
|
||||
DavValue::String("GROUP".to_string())
|
||||
},
|
||||
));
|
||||
response.set_namespace(Namespace::CalDav);
|
||||
}
|
||||
PrincipalProperty::ScheduleInboxURL => {
|
||||
fields.push(DavPropertyValue::new(
|
||||
@@ -315,7 +315,6 @@ impl PrincipalPropFind for Server {
|
||||
percent_encoding::utf8_percent_encode(account.name(), RFC_3986),
|
||||
))],
|
||||
));
|
||||
response.set_namespace(Namespace::CalDav);
|
||||
}
|
||||
PrincipalProperty::ScheduleOutboxURL => {
|
||||
fields.push(DavPropertyValue::new(
|
||||
@@ -326,11 +325,9 @@ impl PrincipalPropFind for Server {
|
||||
percent_encoding::utf8_percent_encode(account.name(), RFC_3986),
|
||||
))],
|
||||
));
|
||||
response.set_namespace(Namespace::CalDav);
|
||||
}
|
||||
},
|
||||
_ => {
|
||||
response.set_namespace(property.namespace());
|
||||
fields_not_found.push(DavPropertyValue::empty(property.clone()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ use dav_proto::{
|
||||
};
|
||||
use groupware::DavResourceName;
|
||||
use hyper::{HeaderMap, Method, StatusCode, header::AUTHORIZATION};
|
||||
use quick_xml::{Reader, XmlVersion, events::Event};
|
||||
use quick_xml::{NsReader, Reader, XmlVersion, events::Event, name::ResolveResult};
|
||||
use std::{borrow::Cow, time::Duration};
|
||||
use store::rand::{RngExt, distr::Alphanumeric, rng};
|
||||
|
||||
@@ -135,7 +135,15 @@ impl DummyWebDavClient {
|
||||
.map(|bytes| String::from_utf8(bytes.to_vec()).unwrap())
|
||||
.map_err(|err| err.to_string());
|
||||
let xml = match &body {
|
||||
Ok(body) if body.starts_with("<?xml") => flatten_xml(body),
|
||||
Ok(body) if body.starts_with("<?xml") => {
|
||||
if let Some(prefix) = undeclared_xml_prefix(body) {
|
||||
panic!(
|
||||
"Response uses undeclared namespace prefix {prefix:?}: {}",
|
||||
xml_pretty_print(body)
|
||||
);
|
||||
}
|
||||
flatten_xml(body)
|
||||
}
|
||||
_ => vec![],
|
||||
};
|
||||
|
||||
@@ -1119,6 +1127,29 @@ impl Default for DavItem {
|
||||
}
|
||||
}
|
||||
|
||||
fn undeclared_xml_prefix(xml: &str) -> Option<String> {
|
||||
let mut reader = NsReader::from_str(xml);
|
||||
let mut buf = Vec::new();
|
||||
|
||||
loop {
|
||||
let (resolved, event) = reader.read_resolved_event_into(&mut buf).unwrap();
|
||||
let name = match &event {
|
||||
Event::Start(e) | Event::Empty(e) => e.name(),
|
||||
Event::Eof => return None,
|
||||
_ => {
|
||||
buf.clear();
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
if matches!(resolved, ResolveResult::Unknown(_)) {
|
||||
return Some(String::from_utf8_lossy(name.as_ref()).into_owned());
|
||||
}
|
||||
|
||||
buf.clear();
|
||||
}
|
||||
}
|
||||
|
||||
fn flatten_xml(xml: &str) -> Vec<(String, String)> {
|
||||
let mut reader = Reader::from_str(xml);
|
||||
|
||||
|
||||
@@ -65,6 +65,17 @@ pub async fn test(test: &TestServer, assisted_discovery: bool) {
|
||||
.with_status(StatusCode::MULTI_STATUS)
|
||||
.with_hrefs([format!("{user_base_path}/").as_str()]);
|
||||
|
||||
client
|
||||
.propfind_with_headers(
|
||||
&user_base_path,
|
||||
[DavProperty::WebDav(WebDavProperty::GetCTag)],
|
||||
[("depth", "0")],
|
||||
)
|
||||
.await
|
||||
.properties(&format!("{user_base_path}/"))
|
||||
.get(DavProperty::WebDav(WebDavProperty::GetCTag))
|
||||
.is_not_empty();
|
||||
|
||||
// Test 3: PROPFIND Depth 1 on root
|
||||
client
|
||||
.request_with_headers("PROPFIND", resource_type.base_path(), [("depth", "1")], "")
|
||||
|
||||
Reference in new issue
Block a user