//! WebDAV XML: request bodies into typed values, and response bodies out. use std::fmt::Write as _; use xmltree::{Element, XMLNode}; pub const DAV: &str = "DAV:"; pub const CALDAV: &str = "urn:ietf:params:xml:ns:caldav"; pub const CARDDAV: &str = "urn:ietf:params:xml:ns:carddav"; pub const CALSERVER: &str = "http://calendarserver.org/ns/"; pub const APPLE: &str = "http://apple.com/ns/ical/"; /// Prefixes declared once on every response root. const PREFIXES: [(&str, &str); 5] = [ ("d", DAV), ("c", CALDAV), ("card", CARDDAV), ("cs", CALSERVER), ("ical", APPLE), ]; /// A property or element name. #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct Name { pub ns: String, pub local: String, } impl Name { pub fn new(ns: &str, local: &str) -> Self { Name { ns: ns.to_string(), local: local.to_string(), } } pub fn of(e: &Element) -> Self { Name { ns: e.namespace.clone().unwrap_or_default(), local: e.name.clone(), } } pub fn is(&self, ns: &str, local: &str) -> bool { self.ns == ns && self.local == local } /// An element with this name, to be filled with a value. pub fn element(&self) -> Element { el(&self.ns, &self.local) } } #[derive(Debug, Clone, PartialEq, Eq)] pub struct Invalid; #[derive(Debug, Clone, PartialEq)] pub enum Propfind { /// With the names an `include` element adds. AllProp(Vec), PropName, Prop(Vec), } /// A PROPFIND body. An empty body means `allprop`. pub fn propfind(body: &[u8]) -> Result { if body.iter().all(u8::is_ascii_whitespace) { return Ok(Propfind::AllProp(Vec::new())); } let root = parse(body, DAV, "propfind")?; let names = |e: &Element| elements(e).map(Name::of).collect(); for e in elements(&root) { match (e.namespace.as_deref(), e.name.as_str()) { (Some(DAV), "prop") => return Ok(Propfind::Prop(names(e))), (Some(DAV), "propname") => return Ok(Propfind::PropName), (Some(DAV), "allprop") => { let include = child(&root, DAV, "include").map_or_else(Vec::new, names); return Ok(Propfind::AllProp(include)); } _ => {} } } Err(Invalid) } /// Properties to set and remove, from a PROPPATCH, MKCALENDAR or extended /// MKCOL body. An empty body sets nothing. #[derive(Debug, Default)] pub struct Update { /// Property elements with their values. pub set: Vec, pub remove: Vec, } pub fn update(body: &[u8]) -> Result { let mut out = Update::default(); if body.iter().all(u8::is_ascii_whitespace) { return Ok(out); } let root = tree(body)?; let expected = [ (DAV, "propertyupdate"), (CALDAV, "mkcalendar"), (DAV, "mkcol"), ]; if !expected.iter().any(|(ns, n)| Name::of(&root).is(ns, n)) { return Err(Invalid); } for op in elements(&root) { let props = child(op, DAV, "prop").into_iter().flat_map(elements); match (op.namespace.as_deref(), op.name.as_str()) { (Some(DAV), "set") => out.set.extend(props.cloned()), (Some(DAV), "remove") => out.remove.extend(props.map(Name::of)), _ => {} } } Ok(out) } /// Nesting allowed in a request body. Building and dropping the tree /// recurse once per level, so a deep body would overflow the stack. const MAX_DEPTH: usize = 64; /// Whether `body` nests elements deeper than [`MAX_DEPTH`]. Malformed XML is /// left to the parser. pub fn too_deep(body: &[u8]) -> bool { let mut depth = 0; for e in xml::reader::EventReader::new(body) { match e { Ok(xml::reader::XmlEvent::StartElement { .. }) if depth == MAX_DEPTH => return true, Ok(xml::reader::XmlEvent::StartElement { .. }) => depth += 1, Ok(xml::reader::XmlEvent::EndElement { .. }) => depth -= 1, Ok(_) => {} Err(_) => return false, } } false } /// A request body as a tree, refused when nested deeper than [`MAX_DEPTH`]. pub(crate) fn tree(body: &[u8]) -> Result { if too_deep(body) { return Err(Invalid); } Element::parse(body).map_err(|_| Invalid) } fn parse(body: &[u8], ns: &str, local: &str) -> Result { let root = tree(body)?; if Name::of(&root).is(ns, local) { Ok(root) } else { Err(Invalid) } } pub fn elements(e: &Element) -> impl Iterator { e.children.iter().filter_map(XMLNode::as_element) } pub fn child<'a>(e: &'a Element, ns: &str, local: &str) -> Option<&'a Element> { elements(e).find(|c| Name::of(c).is(ns, local)) } /// The concatenated text content, trimmed. pub fn text(e: &Element) -> String { e.get_text() .map_or_else(String::new, |t| t.trim().to_string()) } pub fn el(ns: &str, local: &str) -> Element { let mut e = Element::new(local); e.namespace = Some(ns.to_string()); e } pub fn with_text(mut e: Element, text: impl Into) -> Element { e.children.push(XMLNode::Text(text.into())); e } pub fn with_children(mut e: Element, children: impl IntoIterator) -> Element { e.children .extend(children.into_iter().map(XMLNode::Element)); e } pub fn with_attr(mut e: Element, name: &str, value: &str) -> Element { e.attributes.insert(name.to_string(), value.to_string()); e } /// `` elements. pub fn hrefs<'a>(hrefs: impl IntoIterator) -> Vec { hrefs .into_iter() .map(|h| with_text(el(DAV, "href"), h)) .collect() } /// One `` of a multistatus. #[derive(Debug, Default)] pub struct Response { pub href: String, /// Status code and the properties that share it. pub propstats: Vec<(u16, Vec)>, /// The status of the whole resource, for a response without properties. pub status: Option, pub error: Option, } impl Response { pub fn new(href: impl Into) -> Self { Response { href: href.into(), ..Default::default() } } pub fn status(href: impl Into, status: u16) -> Self { Response { href: href.into(), status: Some(status), ..Default::default() } } /// Adds `prop` under `status`, next to the others with that status. pub fn push(&mut self, status: u16, prop: Element) { match self.propstats.iter_mut().find(|(s, _)| *s == status) { Some((_, props)) => props.push(prop), None => self.propstats.push((status, vec![prop])), } } } /// A `` body, or another root such as /// `` holding the same propstats. pub fn multistatus(root: &Name, responses: &[Response]) -> String { multistatus_with(root, responses, None) } /// A multistatus with `tail`, such as a ``, after the /// responses. pub fn multistatus_with(root: &Name, responses: &[Response], tail: Option) -> String { let body = responses.iter().map(|r| { let mut children = vec![with_text(el(DAV, "href"), r.href.as_str())]; children.extend( r.status .map(|s| with_text(el(DAV, "status"), status_line(s))), ); children.extend( r.propstats .iter() .map(|(status, props)| propstat(*status, props)), ); children.extend( r.error .iter() .map(|e| with_children(el(DAV, "error"), [e.clone()])), ); with_children(el(DAV, "response"), children) }); let root = with_children(root.element(), body.chain(tail)); document(&root) } /// The propstats alone, for roots that hold them without ``. pub fn propstat_document(root: &Name, propstats: &[(u16, Vec)]) -> String { let root = with_children( root.element(), propstats.iter().map(|(s, p)| propstat(*s, p)), ); document(&root) } fn propstat(status: u16, props: &[Element]) -> Element { with_children( el(DAV, "propstat"), [ with_children(el(DAV, "prop"), props.iter().cloned()), with_text(el(DAV, "status"), status_line(status)), ], ) } /// A `` body naming the failed precondition. pub fn error(condition: Element) -> String { document(&with_children(el(DAV, "error"), [condition])) } pub fn status_line(code: u16) -> String { let reason = match code { 200 => "OK", 201 => "Created", 403 => "Forbidden", 404 => "Not Found", 409 => "Conflict", 424 => "Failed Dependency", 507 => "Insufficient Storage", _ => "", }; format!("HTTP/1.1 {code} {reason}") } pub fn document(root: &Element) -> String { let mut out = String::from("\n"); write(&mut out, root, true); out } /// Known namespaces use the fixed prefixes. Any other element declares its /// namespace as the default on itself. fn write(out: &mut String, e: &Element, root: bool) { let ns = e.namespace.as_deref().unwrap_or(""); let tag = match PREFIXES.iter().find(|(_, uri)| *uri == ns) { Some((p, _)) => format!("{p}:{}", e.name), None => e.name.clone(), }; let _ = write!(out, "<{tag}"); if !tag.contains(':') { let _ = write!(out, " xmlns=\"{}\"", escape(ns)); } if root { for (p, uri) in PREFIXES { let _ = write!(out, " xmlns:{p}=\"{uri}\""); } } let mut attrs: Vec<_> = e.attributes.iter().collect(); attrs.sort(); for (k, v) in attrs { let _ = write!(out, " {k}=\"{}\"", escape(v)); } if e.children.is_empty() { out.push_str("/>"); return; } out.push('>'); for c in &e.children { match c { XMLNode::Element(c) => write(out, c, false), XMLNode::Text(t) | XMLNode::CData(t) => out.push_str(&escape(t)), _ => {} } } let _ = write!(out, ""); } fn escape(s: &str) -> String { let mut out = String::with_capacity(s.len()); for c in s.chars() { match c { '&' => out.push_str("&"), '<' => out.push_str("<"), '>' => out.push_str(">"), '"' => out.push_str("""), // A raw CR reaches the client as LF: XML parsers normalize line // ends. iCalendar and vCard data need their CRLF. '\r' => out.push_str(" "), // XML 1.0 forbids these even as character references. '\u{0}'..='\u{8}' | '\u{b}' | '\u{c}' | '\u{e}'..='\u{1f}' | '\u{fffe}' | '\u{ffff}' => out.push('\u{fffd}'), _ => out.push(c), } } out }