Skip to main content

netget/server/epp/
xml.rs

1//! A small, bounded, namespace-aware XML tree for EPP frames, built without recursion; and
2//! escaping for the documents NetGet writes. No DTD and no entities beyond the five predefined.
3use anyhow::{bail, ensure, Context, Result};
4use quick_xml::events::Event;
5use quick_xml::name::ResolveResult;
6use quick_xml::NsReader;
7
8pub const MAX_DEPTH: usize = 32;
9pub const MAX_NODES: usize = 4096;
10
11pub const EPP: &str = "urn:ietf:params:xml:ns:epp-1.0";
12pub const DOMAIN: &str = "urn:ietf:params:xml:ns:domain-1.0";
13pub const HOST: &str = "urn:ietf:params:xml:ns:host-1.0";
14pub const CONTACT: &str = "urn:ietf:params:xml:ns:contact-1.0";
15
16#[derive(Debug, Default, Clone)]
17pub struct Node {
18    pub ns: String,
19    pub name: String,
20    pub attrs: Vec<(String, String)>,
21    pub text: String,
22    pub children: Vec<Node>,
23}
24
25impl Node {
26    pub fn child(&self, name: &str) -> Option<&Node> {
27        self.children.iter().find(|c| c.name == name)
28    }
29    pub fn all<'a>(&'a self, name: &'a str) -> impl Iterator<Item = &'a Node> + 'a {
30        self.children.iter().filter(move |c| c.name == name)
31    }
32    pub fn attr(&self, name: &str) -> Option<&str> {
33        self.attrs
34            .iter()
35            .find(|(k, _)| k == name)
36            .map(|(_, v)| v.as_str())
37    }
38    pub fn text(&self) -> String {
39        self.text.trim().to_owned()
40    }
41    pub fn text_of(&self, name: &str) -> Option<String> {
42        self.child(name).map(Node::text)
43    }
44    pub fn texts_of(&self, name: &str) -> Vec<String> {
45        self.all(name).map(Node::text).collect()
46    }
47    /// The first element child, for the command or object element under a wrapper.
48    pub fn first(&self) -> Option<&Node> {
49        self.children.first()
50    }
51}
52
53/// Parse one EPP document into its root element.
54pub fn parse(bytes: &[u8]) -> Result<Node> {
55    let mut reader = NsReader::from_reader(bytes);
56    reader.config_mut().trim_text(false);
57    let mut buf = Vec::new();
58    let mut stack: Vec<Node> = Vec::new();
59    let mut root: Option<Node> = None;
60    let mut nodes = 0usize;
61    loop {
62        let (ns, event) = reader
63            .read_resolved_event_into(&mut buf)
64            .context("the frame is not well-formed XML")?;
65        let ns = match ns {
66            ResolveResult::Bound(n) => String::from_utf8_lossy(n.as_ref()).into_owned(),
67            ResolveResult::Unbound => String::new(),
68            ResolveResult::Unknown(p) => {
69                bail!("the prefix {} is not declared", String::from_utf8_lossy(&p))
70            }
71        };
72        match event {
73            Event::Start(e) | Event::Empty(e) if root.is_some() => {
74                let _ = e;
75                bail!("content after the root element")
76            }
77            Event::Start(ref e) | Event::Empty(ref e) => {
78                nodes += 1;
79                ensure!(nodes <= MAX_NODES, "more than {MAX_NODES} elements");
80                ensure!(stack.len() < MAX_DEPTH, "nested deeper than {MAX_DEPTH}");
81                let mut node = Node {
82                    ns,
83                    name: String::from_utf8_lossy(e.local_name().as_ref()).into_owned(),
84                    ..Default::default()
85                };
86                for a in e.attributes() {
87                    let a = a.context("a malformed attribute")?;
88                    let key = a.key.as_ref();
89                    if key == b"xmlns" || key.starts_with(b"xmlns:") {
90                        continue;
91                    }
92                    ensure!(node.attrs.len() < 32, "too many attributes");
93                    let local = String::from_utf8_lossy(a.key.local_name().as_ref()).into_owned();
94                    let value = a
95                        .decode_and_unescape_value(reader.decoder())
96                        .context("an attribute value is not valid")?
97                        .into_owned();
98                    node.attrs.push((local, value));
99                }
100                if matches!(event, Event::Empty(_)) {
101                    match stack.last_mut() {
102                        Some(parent) => parent.children.push(node),
103                        None => root = Some(node),
104                    }
105                } else {
106                    stack.push(node);
107                }
108            }
109            Event::End(_) => {
110                let node = stack.pop().context("an unbalanced end tag")?;
111                match stack.last_mut() {
112                    Some(parent) => parent.children.push(node),
113                    None => root = Some(node),
114                }
115            }
116            Event::Text(t) => {
117                let text = t.unescape().context("text is not valid")?;
118                match stack.last_mut() {
119                    Some(n) => {
120                        ensure!(n.text.len() + text.len() <= 65536, "text too long");
121                        n.text.push_str(&text);
122                    }
123                    None => ensure!(text.trim().is_empty(), "text outside the root element"),
124                }
125            }
126            Event::CData(t) => {
127                if let Some(n) = stack.last_mut() {
128                    ensure!(n.text.len() + t.len() <= 65536, "text too long");
129                    n.text.push_str(&String::from_utf8_lossy(&t));
130                }
131            }
132            Event::DocType(_) => bail!("a DOCTYPE is not allowed"),
133            Event::Eof => break,
134            Event::Decl(_) | Event::Comment(_) | Event::PI(_) => {}
135        }
136        buf.clear();
137    }
138    ensure!(stack.is_empty(), "the document ends inside an element");
139    root.context("the frame holds no element")
140}
141
142pub fn escape(s: &str) -> String {
143    let mut out = String::with_capacity(s.len());
144    for c in s.chars() {
145        match c {
146            '&' => out.push_str("&amp;"),
147            '<' => out.push_str("&lt;"),
148            '>' => out.push_str("&gt;"),
149            '"' => out.push_str("&quot;"),
150            '\'' => out.push_str("&apos;"),
151            c => out.push(c),
152        }
153    }
154    out
155}
156
157/// `<p:tag>text</p:tag>`, escaped.
158pub fn el(tag: &str, text: &str) -> String {
159    format!("<{tag}>{}</{tag}>", escape(text))
160}