Skip to main content

netget/server/tr069/
wire.rs

1//! TR-069 (CWMP) on the wire, for both roles: SOAP envelopes read into a bounded element tree
2//! and from there into JSON shapes the model can read, and the envelopes NetGet writes. Every
3//! value NetGet writes is XML-escaped; nothing the model says is spliced in raw.
4use anyhow::{bail, ensure, Context, Result};
5use quick_xml::escape::escape;
6use quick_xml::events::Event as XmlEvent;
7use serde_json::{json, Map, Value};
8
9/// A SOAP envelope at most.
10pub const MAX_ENVELOPE: usize = 1024 * 1024;
11/// Element nesting at most.
12pub const MAX_DEPTH: usize = 32;
13/// Elements in one envelope at most.
14pub const MAX_ELEMENTS: usize = 50_000;
15/// Parameters one message may carry.
16pub const MAX_PARAMETERS: usize = 10_000;
17pub const CWMP_NS: &str = "urn:dslforum-org:cwmp-1-0";
18
19#[derive(Debug, Default, Clone)]
20pub struct Element {
21    pub name: String,
22    pub attrs: Vec<(String, String)>,
23    pub children: Vec<Element>,
24    pub text: String,
25}
26
27fn local(name: &[u8]) -> String {
28    let s = String::from_utf8_lossy(name);
29    s.rsplit(':').next().unwrap_or_default().to_string()
30}
31
32impl Element {
33    pub fn child(&self, name: &str) -> Option<&Element> {
34        self.children.iter().find(|c| c.name == name)
35    }
36    pub fn text_of(&self, name: &str) -> Option<String> {
37        self.child(name).map(|c| c.text.trim().to_string())
38    }
39    pub fn attr(&self, name: &str) -> Option<&str> {
40        self.attrs
41            .iter()
42            .find(|(k, _)| k == name)
43            .map(|(_, v)| v.as_str())
44    }
45}
46
47/// The element tree of an XML document, bounded in size, depth and element count.
48pub fn tree(xml: &[u8]) -> Result<Element> {
49    ensure!(
50        xml.len() <= MAX_ENVELOPE,
51        "envelope larger than {MAX_ENVELOPE} bytes"
52    );
53    let mut reader = quick_xml::Reader::from_reader(xml);
54    reader.config_mut().trim_text(false);
55    let mut stack: Vec<Element> = vec![Element::default()];
56    let mut count = 0usize;
57    let mut buf = Vec::new();
58    loop {
59        let ev = reader
60            .read_event_into(&mut buf)
61            .context("the envelope is not well-formed XML")?;
62        let empty = matches!(ev, XmlEvent::Empty(_));
63        match ev {
64            XmlEvent::Start(e) | XmlEvent::Empty(e) => {
65                count += 1;
66                ensure!(count <= MAX_ELEMENTS, "more than {MAX_ELEMENTS} elements");
67                ensure!(stack.len() <= MAX_DEPTH, "nested deeper than {MAX_DEPTH}");
68                let mut el = Element {
69                    name: local(e.name().as_ref()),
70                    ..Default::default()
71                };
72                for a in e.attributes().flatten() {
73                    let v = a
74                        .decode_and_unescape_value(reader.decoder())
75                        .map(|v| v.into_owned())
76                        .unwrap_or_default();
77                    el.attrs.push((local(a.key.as_ref()), v));
78                }
79                if empty {
80                    if let Some(parent) = stack.last_mut() {
81                        parent.children.push(el);
82                    }
83                } else {
84                    stack.push(el);
85                }
86            }
87            XmlEvent::End(_) => {
88                ensure!(stack.len() > 1, "an end tag with nothing open");
89                let el = stack.pop().unwrap_or_default();
90                if let Some(parent) = stack.last_mut() {
91                    parent.children.push(el);
92                }
93            }
94            XmlEvent::Text(t) => {
95                if let Some(top) = stack.last_mut() {
96                    top.text
97                        .push_str(&t.unescape().context("bad character reference")?);
98                }
99            }
100            XmlEvent::CData(t) => {
101                if let Some(top) = stack.last_mut() {
102                    top.text.push_str(&String::from_utf8_lossy(&t));
103                }
104            }
105            XmlEvent::DocType(_) => bail!("a DOCTYPE is not allowed in a SOAP envelope"),
106            XmlEvent::Eof => break,
107            _ => {}
108        }
109        buf.clear();
110    }
111    ensure!(stack.len() == 1, "the document ends inside an element");
112    stack
113        .pop()
114        .unwrap_or_default()
115        .children
116        .into_iter()
117        .next()
118        .context("an empty document")
119}
120
121/// One SOAP message: its cwmp:ID, the method element's name and its content as JSON.
122#[derive(Debug)]
123pub struct Message {
124    pub id: Option<String>,
125    pub method: String,
126    pub content: Value,
127}
128
129pub fn parse(xml: &[u8]) -> Result<Message> {
130    let env = tree(xml)?;
131    ensure!(env.name == "Envelope", "not a SOAP envelope");
132    let id = env.child("Header").and_then(|h| h.text_of("ID"));
133    let body = env.child("Body").context("the envelope has no Body")?;
134    let el = body.children.first().context("the Body is empty")?;
135    let content = match el.name.as_str() {
136        "Fault" => fault_json(el),
137        m => method_json(m, el)?,
138    };
139    let method = if el.name == "Fault" {
140        "Fault".to_string()
141    } else {
142        el.name.clone()
143    };
144    Ok(Message {
145        id,
146        method,
147        content,
148    })
149}
150
151fn param_values(list: Option<&Element>) -> Result<Vec<Value>> {
152    let items: Vec<&Element> = list
153        .map(|l| l.children.iter().collect())
154        .unwrap_or_default();
155    ensure!(
156        items.len() <= MAX_PARAMETERS,
157        "more than {MAX_PARAMETERS} parameters"
158    );
159    Ok(items
160        .into_iter()
161        .map(|p| {
162            let value = p.child("Value");
163            json!({"name": p.text_of("Name").unwrap_or_default(),
164                   "value": value.map(|v| v.text.clone()).unwrap_or_default(),
165                   "type": value.and_then(|v| v.attr("type")).unwrap_or("xsd:string")})
166        })
167        .collect())
168}
169
170fn method_json(method: &str, el: &Element) -> Result<Value> {
171    Ok(match method {
172        "Inform" => {
173            let d = el.child("DeviceId");
174            let field = |n: &str| d.and_then(|d| d.text_of(n)).unwrap_or_default();
175            json!({
176                "device_id": {"manufacturer": field("Manufacturer"), "oui": field("OUI"),
177                              "product_class": field("ProductClass"), "serial_number": field("SerialNumber")},
178                "events": el.child("Event").map(|e| e.children.iter().map(|s| json!({
179                    "code": s.text_of("EventCode").unwrap_or_default(),
180                    "command_key": s.text_of("CommandKey").unwrap_or_default()})).collect::<Vec<_>>()).unwrap_or_default(),
181                "max_envelopes": el.text_of("MaxEnvelopes"),
182                "current_time": el.text_of("CurrentTime"),
183                "retry_count": el.text_of("RetryCount").and_then(|r| r.parse::<u64>().ok()).unwrap_or(0),
184                "parameters": param_values(el.child("ParameterList"))?,
185            })
186        }
187        "InformResponse" => json!({"max_envelopes": el.text_of("MaxEnvelopes")}),
188        "GetParameterValuesResponse" => {
189            json!({"parameters": param_values(el.child("ParameterList"))?})
190        }
191        "SetParameterValuesResponse" | "AddObjectResponse" | "DeleteObjectResponse" => {
192            let mut o = json!({"status": el.text_of("Status").and_then(|s| s.parse::<u64>().ok()).unwrap_or(0)});
193            if let Some(n) = el.text_of("InstanceNumber") {
194                o["instance_number"] = json!(n.parse::<u64>().unwrap_or(0));
195            }
196            o
197        }
198        "GetParameterNamesResponse" => {
199            let items: Vec<&Element> = el
200                .child("ParameterList")
201                .map(|l| l.children.iter().collect())
202                .unwrap_or_default();
203            ensure!(
204                items.len() <= MAX_PARAMETERS,
205                "more than {MAX_PARAMETERS} parameters"
206            );
207            json!({"parameters": items.into_iter().map(|p| json!({
208                "name": p.text_of("Name").unwrap_or_default(),
209                "writable": matches!(p.text_of("Writable").as_deref(), Some("1" | "true"))})).collect::<Vec<_>>()})
210        }
211        "GetRPCMethodsResponse" => {
212            json!({"methods": el.child("MethodList").map(|l| l.children.iter().map(|m| m.text.trim().to_string()).collect::<Vec<_>>()).unwrap_or_default()})
213        }
214        "GetParameterValues" => {
215            json!({"names": el.child("ParameterNames").map(|l| l.children.iter().map(|n| n.text.trim().to_string()).collect::<Vec<_>>()).unwrap_or_default()})
216        }
217        "SetParameterValues" => {
218            json!({"parameters": param_values(el.child("ParameterList"))?, "parameter_key": el.text_of("ParameterKey").unwrap_or_default()})
219        }
220        "GetParameterNames" => json!({"path": el.text_of("ParameterPath").unwrap_or_default(),
221                                      "next_level": matches!(el.text_of("NextLevel").as_deref(), Some("1" | "true"))}),
222        "AddObject" | "DeleteObject" => {
223            json!({"object": el.text_of("ObjectName").unwrap_or_default(), "parameter_key": el.text_of("ParameterKey").unwrap_or_default()})
224        }
225        "Reboot" => json!({"command_key": el.text_of("CommandKey").unwrap_or_default()}),
226        // Anything else: its child elements as text, for the model to read.
227        _ => {
228            let mut m = Map::new();
229            for c in &el.children {
230                m.insert(c.name.clone(), json!(c.text.trim()));
231            }
232            Value::Object(m)
233        }
234    })
235}
236
237fn fault_json(el: &Element) -> Value {
238    let f = el.child("detail").and_then(|d| d.child("Fault"));
239    json!({
240        "code": f.and_then(|f| f.text_of("FaultCode")).and_then(|c| c.parse::<u64>().ok()).unwrap_or(0),
241        "message": f.and_then(|f| f.text_of("FaultString")).or_else(|| el.text_of("faultstring")).unwrap_or_default(),
242    })
243}
244
245fn esc(s: &str) -> String {
246    escape(s).into_owned()
247}
248
249/// A complete envelope around one method element.
250pub fn envelope(id: &str, method: &str) -> String {
251    format!(
252        "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<soap-env:Envelope xmlns:soap-env=\"http://schemas.xmlsoap.org/soap/envelope/\" xmlns:soap-enc=\"http://schemas.xmlsoap.org/soap/encoding/\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:cwmp=\"{CWMP_NS}\"><soap-env:Header><cwmp:ID soap-env:mustUnderstand=\"1\">{}</cwmp:ID></soap-env:Header><soap-env:Body>{method}</soap-env:Body></soap-env:Envelope>",
253        esc(id)
254    )
255}
256
257/// `(name, value, xsd type)` as a ParameterValueStruct list.
258pub fn value_list(params: &[(String, String, String)]) -> String {
259    let mut s = format!(
260        "<ParameterList soap-enc:arrayType=\"cwmp:ParameterValueStruct[{}]\">",
261        params.len()
262    );
263    for (n, v, t) in params {
264        s.push_str(&format!(
265            "<ParameterValueStruct><Name>{}</Name><Value xsi:type=\"{}\">{}</Value></ParameterValueStruct>",
266            esc(n),
267            esc(t),
268            esc(v)
269        ));
270    }
271    s.push_str("</ParameterList>");
272    s
273}
274
275pub struct DeviceId {
276    pub manufacturer: String,
277    pub oui: String,
278    pub product_class: String,
279    pub serial_number: String,
280}
281
282pub fn inform(
283    d: &DeviceId,
284    events: &[(String, String)],
285    params: &[(String, String, String)],
286    retry: u32,
287    now: &str,
288) -> String {
289    let mut ev = format!(
290        "<Event soap-enc:arrayType=\"cwmp:EventStruct[{}]\">",
291        events.len()
292    );
293    for (code, key) in events {
294        ev.push_str(&format!(
295            "<EventStruct><EventCode>{}</EventCode><CommandKey>{}</CommandKey></EventStruct>",
296            esc(code),
297            esc(key)
298        ));
299    }
300    ev.push_str("</Event>");
301    format!(
302        "<cwmp:Inform><DeviceId><Manufacturer>{}</Manufacturer><OUI>{}</OUI><ProductClass>{}</ProductClass><SerialNumber>{}</SerialNumber></DeviceId>{ev}<MaxEnvelopes>1</MaxEnvelopes><CurrentTime>{}</CurrentTime><RetryCount>{retry}</RetryCount>{}</cwmp:Inform>",
303        esc(&d.manufacturer), esc(&d.oui), esc(&d.product_class), esc(&d.serial_number), esc(now), value_list(params)
304    )
305}
306
307pub fn inform_response() -> String {
308    "<cwmp:InformResponse><MaxEnvelopes>1</MaxEnvelopes></cwmp:InformResponse>".into()
309}
310
311pub fn fault(code: u32, message: &str) -> String {
312    format!(
313        "<soap-env:Fault><faultcode>{}</faultcode><faultstring>CWMP fault</faultstring><detail><cwmp:Fault><FaultCode>{code}</FaultCode><FaultString>{}</FaultString></cwmp:Fault></detail></soap-env:Fault>",
314        if (8000..9000).contains(&code) { "Server" } else { "Client" },
315        esc(message)
316    )
317}
318
319fn string_list(tag: &str, kind: &str, items: &[String]) -> String {
320    let mut s = format!("<{tag} soap-enc:arrayType=\"{kind}[{}]\">", items.len());
321    for i in items {
322        s.push_str(&format!("<string>{}</string>", esc(i)));
323    }
324    s.push_str(&format!("</{tag}>"));
325    s
326}
327
328pub fn get_parameter_values(names: &[String]) -> String {
329    format!(
330        "<cwmp:GetParameterValues>{}</cwmp:GetParameterValues>",
331        string_list("ParameterNames", "xsd:string", names)
332    )
333}
334
335pub fn set_parameter_values(params: &[(String, String, String)], key: &str) -> String {
336    format!(
337        "<cwmp:SetParameterValues>{}<ParameterKey>{}</ParameterKey></cwmp:SetParameterValues>",
338        value_list(params),
339        esc(key)
340    )
341}
342
343pub fn get_parameter_names(path: &str, next_level: bool) -> String {
344    format!("<cwmp:GetParameterNames><ParameterPath>{}</ParameterPath><NextLevel>{}</NextLevel></cwmp:GetParameterNames>", esc(path), u8::from(next_level))
345}
346
347pub fn add_object(object: &str, key: &str) -> String {
348    format!("<cwmp:AddObject><ObjectName>{}</ObjectName><ParameterKey>{}</ParameterKey></cwmp:AddObject>", esc(object), esc(key))
349}
350
351pub fn delete_object(object: &str, key: &str) -> String {
352    format!("<cwmp:DeleteObject><ObjectName>{}</ObjectName><ParameterKey>{}</ParameterKey></cwmp:DeleteObject>", esc(object), esc(key))
353}
354
355pub fn reboot(key: &str) -> String {
356    format!(
357        "<cwmp:Reboot><CommandKey>{}</CommandKey></cwmp:Reboot>",
358        esc(key)
359    )
360}
361
362pub fn factory_reset() -> String {
363    "<cwmp:FactoryReset></cwmp:FactoryReset>".into()
364}
365
366pub fn get_parameter_values_response(params: &[(String, String, String)]) -> String {
367    format!(
368        "<cwmp:GetParameterValuesResponse>{}</cwmp:GetParameterValuesResponse>",
369        value_list(params)
370    )
371}
372
373pub fn get_parameter_names_response(params: &[(String, bool)]) -> String {
374    let mut s = format!("<cwmp:GetParameterNamesResponse><ParameterList soap-enc:arrayType=\"cwmp:ParameterInfoStruct[{}]\">", params.len());
375    for (n, w) in params {
376        s.push_str(&format!(
377            "<ParameterInfoStruct><Name>{}</Name><Writable>{}</Writable></ParameterInfoStruct>",
378            esc(n),
379            u8::from(*w)
380        ));
381    }
382    s.push_str("</ParameterList></cwmp:GetParameterNamesResponse>");
383    s
384}
385
386/// The empty-bodied response a CPE sends for methods that return only a status (or nothing).
387pub fn status_response(method: &str, status: u32, instance: Option<u64>) -> String {
388    match method {
389        "SetParameterValues" => format!("<cwmp:SetParameterValuesResponse><Status>{status}</Status></cwmp:SetParameterValuesResponse>"),
390        "AddObject" => format!(
391            "<cwmp:AddObjectResponse><InstanceNumber>{}</InstanceNumber><Status>{status}</Status></cwmp:AddObjectResponse>",
392            instance.unwrap_or(1)
393        ),
394        "DeleteObject" => format!("<cwmp:DeleteObjectResponse><Status>{status}</Status></cwmp:DeleteObjectResponse>"),
395        other => format!("<cwmp:{other}Response></cwmp:{other}Response>"),
396    }
397}
398
399pub fn get_rpc_methods_response(methods: &[&str]) -> String {
400    let items: Vec<String> = methods.iter().map(|m| m.to_string()).collect();
401    format!(
402        "<cwmp:GetRPCMethodsResponse>{}</cwmp:GetRPCMethodsResponse>",
403        string_list("MethodList", "xsd:string", &items)
404    )
405}
406
407/// `{name: value}` (and optional `{name: type}`) from the model as value triples.
408pub fn triples(values: &Value, types: Option<&Value>) -> Result<Vec<(String, String, String)>> {
409    let o = values
410        .as_object()
411        .context("parameters must be an object of name: value")?;
412    ensure!(
413        o.len() <= MAX_PARAMETERS,
414        "more than {MAX_PARAMETERS} parameters"
415    );
416    o.iter()
417        .map(|(k, v)| {
418            ensure!(
419                !k.is_empty() && k.len() <= 256,
420                "parameter names must be 1-256 bytes"
421            );
422            let value = match v {
423                Value::String(s) => s.clone(),
424                Value::Bool(b) => b.to_string(),
425                Value::Number(n) => n.to_string(),
426                _ => bail!("the value of {k} must be a string, number or boolean"),
427            };
428            let kind = types
429                .and_then(|t| t[k].as_str())
430                .map(str::to_string)
431                .unwrap_or_else(|| match v {
432                    Value::Bool(_) => "xsd:boolean".into(),
433                    Value::Number(n) if n.is_u64() => "xsd:unsignedInt".into(),
434                    Value::Number(_) => "xsd:int".into(),
435                    _ => "xsd:string".into(),
436                });
437            ensure!(
438                kind.starts_with("xsd:") && kind.len() <= 32,
439                "a type is xsd:string, xsd:boolean, xsd:unsignedInt and the like"
440            );
441            Ok((k.clone(), value, kind))
442        })
443        .collect()
444}