Skip to main content

netget/server/dav_common/
xml.rs

1//! WebDAV XML (RFC 4918) as CalDAV and CardDAV use it: request bodies parsed into a small
2//! namespace-resolved tree, PROPFIND and REPORT bodies interpreted, multistatus responses
3//! written.
4use anyhow::{bail, ensure, Context, Result};
5use quick_xml::events::Event;
6use quick_xml::name::ResolveResult;
7use quick_xml::NsReader;
8
9pub const DAV: &str = "DAV:";
10pub const CALDAV: &str = "urn:ietf:params:xml:ns:caldav";
11pub const CARDDAV: &str = "urn:ietf:params:xml:ns:carddav";
12pub const CS: &str = "http://calendarserver.org/ns/";
13pub const APPLE: &str = "http://apple.com/ns/ical/";
14pub const MAX_BODY: usize = 1024 * 1024;
15const MAX_DEPTH: usize = 32;
16const MAX_ELEMENTS: usize = 20_000;
17
18#[derive(Debug, Clone, Default)]
19pub struct El {
20    pub ns: String,
21    pub name: String,
22    pub attrs: Vec<(String, String)>,
23    pub children: Vec<El>,
24    pub text: String,
25}
26
27impl El {
28    pub fn is(&self, ns: &str, name: &str) -> bool {
29        self.ns == ns && self.name == name
30    }
31    pub fn child(&self, ns: &str, name: &str) -> Option<&El> {
32        self.children.iter().find(|c| c.is(ns, name))
33    }
34    pub fn children_named<'a>(
35        &'a self,
36        ns: &'a str,
37        name: &'a str,
38    ) -> impl Iterator<Item = &'a El> + 'a {
39        self.children.iter().filter(move |c| c.is(ns, name))
40    }
41    pub fn attr(&self, name: &str) -> Option<&str> {
42        self.attrs
43            .iter()
44            .find(|(k, _)| k == name)
45            .map(|(_, v)| v.as_str())
46    }
47}
48
49/// Parse a request body into a tree (namespaces resolved, depth and size bounded).
50pub fn parse(body: &[u8]) -> Result<El> {
51    ensure!(body.len() <= MAX_BODY, "XML body over 1 MiB");
52    let text = std::str::from_utf8(body).context("XML body is not UTF-8")?;
53    let mut reader = NsReader::from_str(text);
54    let mut stack: Vec<El> = Vec::new();
55    let mut count = 0usize;
56    let mut root: Option<El> = None;
57    let resolve = |r: ResolveResult| match r {
58        ResolveResult::Bound(ns) => String::from_utf8_lossy(ns.as_ref()).into_owned(),
59        _ => String::new(),
60    };
61    loop {
62        let (ns, ev) = reader.read_resolved_event()?;
63        let empty = matches!(ev, Event::Empty(_));
64        match ev {
65            Event::Start(e) | Event::Empty(e) => {
66                ensure!(root.is_none(), "content after the root element");
67                count += 1;
68                ensure!(count <= MAX_ELEMENTS, "XML has too many elements");
69                ensure!(stack.len() < MAX_DEPTH, "XML nests too deeply");
70                let mut el = El {
71                    ns: resolve(ns),
72                    name: String::from_utf8_lossy(e.local_name().as_ref()).into_owned(),
73                    ..Default::default()
74                };
75                for a in e.attributes().flatten() {
76                    el.attrs.push((
77                        String::from_utf8_lossy(a.key.local_name().as_ref()).into_owned(),
78                        a.decode_and_unescape_value(reader.decoder())
79                            .map(|v| v.into_owned())
80                            .unwrap_or_default(),
81                    ));
82                }
83                if empty {
84                    match stack.last_mut() {
85                        Some(p) => p.children.push(el),
86                        None => root = Some(el),
87                    }
88                } else {
89                    stack.push(el);
90                }
91            }
92            Event::End(_) => {
93                let done = stack.pop().context("unbalanced XML")?;
94                match stack.last_mut() {
95                    Some(p) => p.children.push(done),
96                    None => root = Some(done),
97                }
98            }
99            Event::Text(t) => {
100                if let Some(top) = stack.last_mut() {
101                    let s = t.unescape().map(|c| c.into_owned()).unwrap_or_default();
102                    ensure!(top.text.len() + s.len() <= MAX_BODY, "XML text too large");
103                    top.text.push_str(&s);
104                }
105            }
106            Event::CData(t) => {
107                if let Some(top) = stack.last_mut() {
108                    top.text.push_str(&String::from_utf8_lossy(&t));
109                }
110            }
111            Event::DocType(_) => bail!("DOCTYPE is not accepted"),
112            Event::Eof => break,
113            _ => {}
114        }
115    }
116    ensure!(stack.is_empty(), "unterminated XML");
117    root.context("empty XML body")
118}
119
120pub type PropName = (String, String);
121
122#[derive(Debug, Clone, PartialEq)]
123pub enum PropRequest {
124    AllProp,
125    PropName,
126    Props(Vec<PropName>),
127}
128
129fn prop_names(prop: &El) -> Vec<PropName> {
130    prop.children
131        .iter()
132        .map(|c| (c.ns.clone(), c.name.clone()))
133        .collect()
134}
135
136/// A PROPFIND body; an empty body means allprop (RFC 4918 §9.1).
137pub fn propfind(body: &[u8]) -> Result<PropRequest> {
138    if body.iter().all(u8::is_ascii_whitespace) {
139        return Ok(PropRequest::AllProp);
140    }
141    let root = parse(body)?;
142    ensure!(
143        root.is(DAV, "propfind"),
144        "PROPFIND body must be DAV:propfind"
145    );
146    if root.child(DAV, "allprop").is_some() {
147        return Ok(PropRequest::AllProp);
148    }
149    if root.child(DAV, "propname").is_some() {
150        return Ok(PropRequest::PropName);
151    }
152    let prop = root
153        .child(DAV, "prop")
154        .context("propfind needs prop, allprop or propname")?;
155    Ok(PropRequest::Props(prop_names(prop)))
156}
157
158/// A CalDAV time-range or CardDAV/CalDAV text/prop filter tree, kept as XML for the engine.
159#[derive(Debug, Clone)]
160pub enum Report {
161    CalendarQuery {
162        props: PropRequest,
163        filter: El,
164    },
165    CalendarMultiget {
166        props: PropRequest,
167        hrefs: Vec<String>,
168    },
169    AddressbookQuery {
170        props: PropRequest,
171        filter: El,
172        limit: Option<usize>,
173    },
174    AddressbookMultiget {
175        props: PropRequest,
176        hrefs: Vec<String>,
177    },
178}
179
180fn report_props(root: &El) -> PropRequest {
181    if root.child(DAV, "allprop").is_some() {
182        PropRequest::AllProp
183    } else if let Some(p) = root.child(DAV, "prop") {
184        PropRequest::Props(prop_names(p))
185    } else {
186        PropRequest::AllProp
187    }
188}
189
190pub fn report(body: &[u8]) -> Result<Report> {
191    let root = parse(body)?;
192    let hrefs = || -> Vec<String> {
193        root.children_named(DAV, "href")
194            .map(|h| h.text.trim().to_owned())
195            .collect()
196    };
197    Ok(match (root.ns.as_str(), root.name.as_str()) {
198        (CALDAV, "calendar-query") => Report::CalendarQuery {
199            props: report_props(&root),
200            filter: root
201                .child(CALDAV, "filter")
202                .cloned()
203                .context("calendar-query needs a filter")?,
204        },
205        (CALDAV, "calendar-multiget") => Report::CalendarMultiget {
206            props: report_props(&root),
207            hrefs: hrefs(),
208        },
209        (CARDDAV, "addressbook-query") => Report::AddressbookQuery {
210            props: report_props(&root),
211            filter: root.child(CARDDAV, "filter").cloned().unwrap_or_default(),
212            limit: root
213                .child(CARDDAV, "limit")
214                .and_then(|l| l.child(CARDDAV, "nresults"))
215                .and_then(|n| n.text.trim().parse().ok()),
216        },
217        (CARDDAV, "addressbook-multiget") => Report::AddressbookMultiget {
218            props: report_props(&root),
219            hrefs: hrefs(),
220        },
221        (ns, name) => bail!("unsupported REPORT {{{ns}}}{name}"),
222    })
223}
224
225pub fn escape(s: &str) -> String {
226    let mut out = String::with_capacity(s.len());
227    for c in s.chars() {
228        match c {
229            '&' => out.push_str("&amp;"),
230            '<' => out.push_str("&lt;"),
231            '>' => out.push_str("&gt;"),
232            '"' => out.push_str("&quot;"),
233            _ => out.push(c),
234        }
235    }
236    out
237}
238
239pub fn prefix(ns: &str) -> &'static str {
240    match ns {
241        DAV => "D",
242        CALDAV => "C",
243        CARDDAV => "CR",
244        CS => "CS",
245        APPLE => "A",
246        _ => "X",
247    }
248}
249
250/// An element in the multistatus output, `<P:name>inner</P:name>` (namespace `X` is declared
251/// per element for unknown namespaces).
252pub fn element(ns: &str, name: &str, inner: &str) -> String {
253    let p = prefix(ns);
254    let decl = if p == "X" {
255        format!(" xmlns:X=\"{}\"", escape(ns))
256    } else {
257        String::new()
258    };
259    if inner.is_empty() {
260        format!("<{p}:{name}{decl}/>")
261    } else {
262        format!("<{p}:{name}{decl}>{inner}</{p}:{name}>")
263    }
264}
265
266/// One `<D:response>`: found properties with 200, missing ones with 404.
267pub fn response(href: &str, found: &[(PropName, String)], missing: &[PropName]) -> String {
268    let mut out = format!("<D:response><D:href>{}</D:href>", escape(href));
269    if !found.is_empty() {
270        out.push_str("<D:propstat><D:prop>");
271        for ((ns, name), inner) in found {
272            out.push_str(&element(ns, name, inner));
273        }
274        out.push_str("</D:prop><D:status>HTTP/1.1 200 OK</D:status></D:propstat>");
275    }
276    if !missing.is_empty() {
277        out.push_str("<D:propstat><D:prop>");
278        for (ns, name) in missing {
279            out.push_str(&element(ns, name, ""));
280        }
281        out.push_str("</D:prop><D:status>HTTP/1.1 404 Not Found</D:status></D:propstat>");
282    }
283    out.push_str("</D:response>");
284    out
285}
286
287pub fn status_response(href: &str, status: &str) -> String {
288    format!(
289        "<D:response><D:href>{}</D:href><D:status>HTTP/1.1 {status}</D:status></D:response>",
290        escape(href)
291    )
292}
293
294pub fn multistatus(responses: &[String]) -> String {
295    format!(
296        "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<D:multistatus xmlns:D=\"DAV:\" xmlns:C=\"{CALDAV}\" xmlns:CR=\"{CARDDAV}\" xmlns:CS=\"{CS}\" xmlns:A=\"{APPLE}\">{}</D:multistatus>",
297        responses.concat()
298    )
299}
300
301/// A DAV error body carrying one precondition element (RFC 4918 §16).
302pub fn error_body(ns: &str, condition: &str) -> String {
303    format!("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<D:error xmlns:D=\"DAV:\" xmlns:C=\"{CALDAV}\" xmlns:CR=\"{CARDDAV}\">{}</D:error>", element(ns, condition, ""))
304}