netget/server/dav_common/
xml.rs1use 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
49pub 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
136pub 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#[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("&"),
230 '<' => out.push_str("<"),
231 '>' => out.push_str(">"),
232 '"' => out.push_str("""),
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
250pub 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
266pub 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
301pub 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}