1use super::xml::{self, Attribute, Document, Node};
8use anyhow::{bail, ensure, Context, Result};
9use serde_json::{json, Map, Value};
10
11pub const NC: &str = "urn:ietf:params:xml:ns:netconf:base:1.0";
12pub const BASE_10: &str = "urn:ietf:params:netconf:base:1.0";
13pub const BASE_11: &str = "urn:ietf:params:netconf:base:1.1";
14pub const WRITABLE_RUNNING: &str = "urn:ietf:params:netconf:capability:writable-running:1.0";
15pub const CANDIDATE: &str = "urn:ietf:params:netconf:capability:candidate:1.0";
16pub const STARTUP: &str = "urn:ietf:params:netconf:capability:startup:1.0";
17pub const VALIDATE_10: &str = "urn:ietf:params:netconf:capability:validate:1.0";
18pub const VALIDATE_11: &str = "urn:ietf:params:netconf:capability:validate:1.1";
19pub const XPATH: &str = "urn:ietf:params:netconf:capability:xpath:1.0";
20pub const ROLLBACK: &str = "urn:ietf:params:netconf:capability:rollback-on-error:1.0";
21
22pub const MAX_CAPABILITIES: usize = 256;
23pub const MAX_CAPABILITY_BYTES: usize = 1024;
24pub const MAX_MESSAGE_ID_BYTES: usize = 256;
25
26pub const ERROR_TAGS: &[&str] = &[
28 "in-use",
29 "invalid-value",
30 "too-big",
31 "missing-attribute",
32 "bad-attribute",
33 "unknown-attribute",
34 "missing-element",
35 "bad-element",
36 "unknown-element",
37 "unknown-namespace",
38 "access-denied",
39 "lock-denied",
40 "resource-denied",
41 "rollback-failed",
42 "data-exists",
43 "data-missing",
44 "operation-not-supported",
45 "operation-failed",
46 "malformed-message",
47];
48pub const ERROR_TYPES: &[&str] = &["transport", "rpc", "protocol", "application"];
49
50pub const BASE_OPERATIONS: &[&str] = &[
53 "get",
54 "get-config",
55 "edit-config",
56 "lock",
57 "unlock",
58 "close-session",
59 "kill-session",
60 "commit",
61 "discard-changes",
62 "validate",
63];
64
65pub fn capability_ok(uri: &str) -> bool {
66 !uri.is_empty()
67 && uri.len() <= MAX_CAPABILITY_BYTES
68 && uri.is_ascii()
69 && !uri
70 .bytes()
71 .any(|b| b.is_ascii_whitespace() || b < 0x20 || b == 0x7f)
72 && uri.contains(':')
73}
74
75fn escape_text(text: &str) -> String {
76 text.replace('&', "&")
77 .replace('<', "<")
78 .replace('>', ">")
79}
80fn escape_attribute(text: &str) -> String {
81 escape_text(text)
82 .replace('"', """)
83 .replace('\r', " ")
84 .replace('\n', " ")
85 .replace('\t', "	")
86}
87
88pub fn hello(capabilities: &[String], session_id: Option<u32>) -> Result<Vec<u8>> {
90 ensure!(
91 !capabilities.is_empty() && capabilities.len() <= MAX_CAPABILITIES,
92 "NETCONF capability count"
93 );
94 let mut out =
95 format!("<?xml version=\"1.0\" encoding=\"UTF-8\"?><hello xmlns=\"{NC}\"><capabilities>");
96 for capability in capabilities {
97 ensure!(capability_ok(capability), "NETCONF capability URI");
98 out.push_str("<capability>");
99 out.push_str(&escape_text(capability));
100 out.push_str("</capability>");
101 }
102 out.push_str("</capabilities>");
103 if let Some(id) = session_id {
104 ensure!(id > 0, "NETCONF session-id must be positive");
105 out.push_str(&format!("<session-id>{id}</session-id>"));
106 }
107 out.push_str("</hello>");
108 xml::parse(out.as_bytes())?;
109 Ok(out.into_bytes())
110}
111
112pub struct Hello {
113 pub capabilities: Vec<String>,
114 pub session_id: Option<u32>,
115}
116
117pub fn parse_hello(message: &[u8]) -> Result<Hello> {
118 let doc = xml::parse(message)?;
119 ensure!(
120 xml::is_element(&doc, 0, "hello", NC),
121 "NETCONF expected <hello> in the base namespace"
122 );
123 let mut capabilities = Vec::new();
124 let mut session_id = None;
125 let mut seen_capabilities = false;
126 for child in xml::children(&doc, 0)? {
127 if xml::is_element(&doc, child, "capabilities", NC) {
128 ensure!(!seen_capabilities, "NETCONF duplicate <capabilities>");
129 seen_capabilities = true;
130 for capability in xml::children(&doc, child)? {
131 ensure!(
132 xml::is_element(&doc, capability, "capability", NC),
133 "NETCONF unexpected element in <capabilities>"
134 );
135 let uri = xml::text(&doc, capability)?.trim().to_owned();
136 ensure!(capability_ok(&uri), "NETCONF capability URI");
137 ensure!(
138 capabilities.len() < MAX_CAPABILITIES,
139 "NETCONF capability count"
140 );
141 if !capabilities.contains(&uri) {
142 capabilities.push(uri);
143 }
144 }
145 } else if xml::is_element(&doc, child, "session-id", NC) {
146 ensure!(session_id.is_none(), "NETCONF duplicate <session-id>");
147 let text = xml::text(&doc, child)?;
148 let id: u32 = text
149 .trim()
150 .parse()
151 .context("NETCONF session-id must be a 32-bit number")?;
152 ensure!(id > 0, "NETCONF session-id must be positive");
153 session_id = Some(id);
154 }
155 }
157 ensure!(
158 seen_capabilities && !capabilities.is_empty(),
159 "NETCONF <hello> without capabilities"
160 );
161 Ok(Hello {
162 capabilities,
163 session_id,
164 })
165}
166
167pub fn negotiate(local: &[String], remote: &[String]) -> Option<super::wire::Framing> {
169 let both = |v: &str| local.iter().any(|c| c == v) && remote.iter().any(|c| c == v);
170 if both(BASE_11) {
171 Some(super::wire::Framing::Chunked)
172 } else if both(BASE_10) {
173 Some(super::wire::Framing::Delimiter)
174 } else {
175 None
176 }
177}
178
179#[derive(Clone, Debug, PartialEq)]
181pub struct RpcError {
182 pub error_type: String,
183 pub tag: String,
184 pub severity: String,
185 pub message: Option<String>,
186 pub path: Option<String>,
187 pub info: Option<Document>,
188}
189impl RpcError {
190 pub fn new(error_type: &str, tag: &str, message: &str) -> Self {
191 Self {
192 error_type: error_type.into(),
193 tag: tag.into(),
194 severity: "error".into(),
195 message: Some(message.into()),
196 path: None,
197 info: None,
198 }
199 }
200 pub fn from_value(v: &Value) -> Result<Self> {
201 let obj = v.as_object().context("NETCONF error must be an object")?;
202 for key in obj.keys() {
203 ensure!(
204 matches!(
205 key.as_str(),
206 "error_type"
207 | "error_tag"
208 | "error_severity"
209 | "error_message"
210 | "error_path"
211 | "error_info_xml"
212 ),
213 "NETCONF error has unknown field '{key}'"
214 );
215 }
216 let s = |k: &str| {
217 obj.get(k)
218 .map(|v| {
219 v.as_str()
220 .with_context(|| format!("NETCONF {k} must be a string"))
221 })
222 .transpose()
223 };
224 let error_type = s("error_type")?.unwrap_or("application");
225 let tag = s("error_tag")?.context("NETCONF error requires error_tag")?;
226 let severity = s("error_severity")?.unwrap_or("error");
227 ensure!(
228 ERROR_TYPES.contains(&error_type),
229 "NETCONF error_type must be transport, rpc, protocol or application"
230 );
231 ensure!(
232 ERROR_TAGS.contains(&tag),
233 "NETCONF error_tag '{tag}' is not an RFC 6241 error tag"
234 );
235 ensure!(
236 matches!(severity, "error" | "warning"),
237 "NETCONF error_severity must be error or warning"
238 );
239 let bounded = |v: Option<&str>| -> Result<Option<String>> {
240 v.map(|t| {
241 ensure!(t.len() <= 4096, "NETCONF error text bound");
242 Ok(t.to_owned())
243 })
244 .transpose()
245 };
246 let info = s("error_info_xml")?
247 .map(|t| xml::parse_fragment_in(t, Some(NC)))
248 .transpose()?;
249 Ok(Self {
250 error_type: error_type.into(),
251 tag: tag.into(),
252 severity: severity.into(),
253 message: bounded(s("error_message")?)?,
254 path: bounded(s("error_path")?)?,
255 info,
256 })
257 }
258 fn render(&self, out: &mut String) -> Result<()> {
259 out.push_str("<rpc-error>");
260 out.push_str(&format!("<error-type>{}</error-type>", self.error_type));
261 out.push_str(&format!("<error-tag>{}</error-tag>", self.tag));
262 out.push_str(&format!(
263 "<error-severity>{}</error-severity>",
264 self.severity
265 ));
266 if let Some(path) = &self.path {
267 out.push_str(&format!("<error-path>{}</error-path>", escape_text(path)));
268 }
269 if let Some(message) = &self.message {
270 out.push_str(&format!(
271 "<error-message xml:lang=\"en\">{}</error-message>",
272 escape_text(message)
273 ));
274 }
275 if let Some(info) = &self.info {
276 out.push_str("<error-info>");
277 out.push_str(&xml::render_fragment(info)?);
278 out.push_str("</error-info>");
279 }
280 out.push_str("</rpc-error>");
281 Ok(())
282 }
283 pub fn to_value(&self) -> Result<Value> {
284 let mut v = json!({"error_type": self.error_type, "error_tag": self.tag, "error_severity": self.severity});
285 if let Some(m) = &self.message {
286 v["error_message"] = json!(m);
287 }
288 if let Some(p) = &self.path {
289 v["error_path"] = json!(p);
290 }
291 if let Some(i) = &self.info {
292 v["error_info_xml"] = json!(xml::render_fragment(i)?);
293 }
294 Ok(v)
295 }
296}
297
298#[derive(Clone, Debug)]
300pub enum ReplyBody {
301 Ok,
302 Data(Document),
304 Output(Document),
306 Errors(Vec<RpcError>),
307}
308
309pub fn reply(
312 attributes: &[Attribute],
313 bindings: &[xml::Binding],
314 body: &ReplyBody,
315) -> Result<Vec<u8>> {
316 let mut out = format!("<?xml version=\"1.0\" encoding=\"UTF-8\"?><rpc-reply xmlns=\"{NC}\"");
317 for b in bindings {
318 if b.prefix.is_empty() || b.prefix == "xml" {
320 continue;
321 }
322 out.push_str(&format!(
323 " xmlns:{}=\"{}\"",
324 b.prefix,
325 escape_attribute(&b.uri)
326 ));
327 }
328 for a in attributes {
329 out.push_str(&format!(" {}=\"{}\"", a.name, escape_attribute(&a.value)));
330 }
331 out.push('>');
332 match body {
333 ReplyBody::Ok => out.push_str("<ok/>"),
334 ReplyBody::Data(doc) => {
335 out.push_str("<data>");
336 out.push_str(&xml::render_fragment(doc)?);
337 out.push_str("</data>");
338 }
339 ReplyBody::Output(doc) => out.push_str(&xml::render_fragment(doc)?),
340 ReplyBody::Errors(errors) => {
341 ensure!(
342 !errors.is_empty() && errors.len() <= 64,
343 "NETCONF rpc-error count"
344 );
345 for e in errors {
346 e.render(&mut out)?;
347 }
348 }
349 }
350 out.push_str("</rpc-reply>");
351 ensure!(
352 out.len() <= super::wire::MAX_MESSAGE_BYTES,
353 "NETCONF reply byte bound"
354 );
355 xml::parse(out.as_bytes()).context("NETCONF rendered reply is not well-formed")?;
356 Ok(out.into_bytes())
357}
358
359pub fn datastore_allowed(datastore: &str, capabilities: &[String], writing: bool) -> Result<()> {
361 let has = |c: &str| capabilities.iter().any(|v| v == c);
362 match datastore {
363 "running" => ensure!(
364 !writing || has(WRITABLE_RUNNING),
365 "running is not writable without :writable-running"
366 ),
367 "candidate" => ensure!(
368 has(CANDIDATE),
369 "candidate datastore requires the :candidate capability"
370 ),
371 "startup" => ensure!(
372 has(STARTUP),
373 "startup datastore requires the :startup capability"
374 ),
375 other => bail!("datastore '{other}' is not supported; url and other sources are excluded"),
376 }
377 Ok(())
378}
379
380pub struct IncomingRpc {
382 pub message_id: String,
383 pub attributes: Vec<Attribute>,
384 pub bindings: Vec<xml::Binding>,
385 pub operation: String,
386 pub namespace: String,
387 pub fields: Map<String, Value>,
389}
390
391pub enum RpcRefusal {
394 Reply {
395 attributes: Vec<Attribute>,
396 bindings: Vec<xml::Binding>,
397 error: RpcError,
398 },
399 Fatal(anyhow::Error),
400}
401
402fn datastore(doc: &Document, element: usize) -> Result<String, RpcError> {
403 let kids = xml::children(doc, element)
404 .map_err(|e| RpcError::new("protocol", "malformed-message", &e.to_string()))?;
405 if kids.len() != 1 {
406 return Err(RpcError::new(
407 "protocol",
408 "missing-element",
409 "exactly one datastore is required",
410 ));
411 }
412 let (name, ns, _) = xml::element(doc, kids[0])
413 .map_err(|e| RpcError::new("protocol", "malformed-message", &e.to_string()))?;
414 if ns != NC {
415 return Err(RpcError::new(
416 "protocol",
417 "unknown-namespace",
418 "datastore must be in the base namespace",
419 ));
420 }
421 Ok(name.to_owned())
422}
423
424fn subtree(doc: &Document, element: usize) -> Result<String, RpcError> {
425 let inner = xml::inner(doc, element)
426 .map_err(|e| RpcError::new("protocol", "malformed-message", &e.to_string()))?;
427 xml::render_fragment(&inner)
428 .map_err(|e| RpcError::new("application", "too-big", &e.to_string()))
429}
430
431pub fn parse_rpc(message: &[u8], capabilities: &[String]) -> Result<IncomingRpc, RpcRefusal> {
433 let doc = xml::parse(message).map_err(RpcRefusal::Fatal)?;
434 let (name, ns, attrs) = xml::element(&doc, 0).map_err(RpcRefusal::Fatal)?;
435 if name != "rpc" || ns != NC {
436 return Err(RpcRefusal::Fatal(anyhow::anyhow!(
437 "NETCONF expected <rpc> in the base namespace"
438 )));
439 }
440 let attributes = attrs.to_vec();
441 let bindings = match &doc.nodes[0] {
442 Node::Start { bindings, .. } => bindings.clone(),
443 _ => Vec::new(),
444 };
445 let refuse = |error: RpcError| RpcRefusal::Reply {
446 attributes: attributes.clone(),
447 bindings: bindings.clone(),
448 error,
449 };
450 let message_id = attributes
451 .iter()
452 .find(|a| a.name == "message-id" && a.namespace.is_empty())
453 .map(|a| a.value.clone());
454 let Some(message_id) = message_id else {
455 let mut error = RpcError::new("rpc", "missing-attribute", "message-id is required");
456 error.info = xml::parse_fragment_in(
457 "<bad-attribute>message-id</bad-attribute><bad-element>rpc</bad-element>",
458 Some(NC),
459 )
460 .ok();
461 return Err(refuse(error));
462 };
463 if message_id.len() > MAX_MESSAGE_ID_BYTES {
464 return Err(refuse(RpcError::new(
465 "rpc",
466 "bad-attribute",
467 "message-id is too long",
468 )));
469 }
470 let kids = xml::children(&doc, 0)
471 .map_err(|e| refuse(RpcError::new("rpc", "malformed-message", &e.to_string())))?;
472 if kids.len() != 1 {
473 return Err(refuse(RpcError::new(
474 "rpc",
475 "malformed-message",
476 "an rpc carries exactly one operation",
477 )));
478 }
479 let op = kids[0];
480 let (operation, op_ns, _) = xml::element(&doc, op).map_err(RpcRefusal::Fatal)?;
481 let (operation, op_ns) = (operation.to_owned(), op_ns.to_owned());
482 let mut fields = Map::new();
483 fields.insert("message_id".into(), json!(message_id));
484 fields.insert("operation".into(), json!(operation));
485 fields.insert("namespace".into(), json!(op_ns));
486 if op_ns != NC {
487 fields.insert("custom".into(), json!(true));
488 let rendered = xml::inner(&doc, op)
489 .and_then(|d| xml::render_fragment(&d))
490 .map_err(|e| refuse(RpcError::new("application", "too-big", &e.to_string())))?;
491 fields.insert("input_xml".into(), json!(rendered));
492 return Ok(IncomingRpc {
493 message_id,
494 attributes,
495 bindings,
496 operation,
497 namespace: op_ns,
498 fields,
499 });
500 }
501 if !BASE_OPERATIONS.contains(&operation.as_str()) {
502 return Err(refuse(RpcError::new(
503 "protocol",
504 "operation-not-supported",
505 "this base operation is not implemented",
506 )));
507 }
508 let children = xml::children(&doc, op).map_err(|e| {
509 refuse(RpcError::new(
510 "protocol",
511 "malformed-message",
512 &e.to_string(),
513 ))
514 })?;
515 let mut seen = std::collections::BTreeSet::new();
516 for &child in &children {
517 let (child_name, child_ns, child_attrs) =
518 xml::element(&doc, child).map_err(RpcRefusal::Fatal)?;
519 if child_ns != NC {
520 return Err(refuse(RpcError::new(
521 "protocol",
522 "unknown-namespace",
523 "operation parameters must be in the base namespace",
524 )));
525 }
526 if !seen.insert(child_name.to_owned()) {
527 return Err(refuse(RpcError::new(
528 "protocol",
529 "bad-element",
530 "duplicate operation parameter",
531 )));
532 }
533 let writing = matches!(
534 operation.as_str(),
535 "edit-config" | "commit" | "discard-changes"
536 );
537 match (operation.as_str(), child_name) {
538 ("get-config" | "validate", "source")
539 | ("edit-config" | "lock" | "unlock", "target") => {
540 let store = datastore(&doc, child).map_err(refuse)?;
541 if let Err(e) = datastore_allowed(
542 &store,
543 capabilities,
544 operation == "edit-config" || (writing && child_name == "target"),
545 ) {
546 return Err(refuse(RpcError::new(
547 "protocol",
548 "invalid-value",
549 &e.to_string(),
550 )));
551 }
552 fields.insert(child_name.into(), json!(store));
553 }
554 ("get" | "get-config", "filter") => {
555 let kind = child_attrs
556 .iter()
557 .find(|a| a.name == "type")
558 .map(|a| a.value.as_str())
559 .unwrap_or("subtree");
560 match kind {
561 "subtree" => {
562 fields.insert("filter_type".into(), json!("subtree"));
563 fields.insert(
564 "filter_xml".into(),
565 json!(subtree(&doc, child).map_err(refuse)?),
566 );
567 }
568 "xpath" if capabilities.iter().any(|c| c == XPATH) => {
569 let select = child_attrs
570 .iter()
571 .find(|a| a.name == "select")
572 .map(|a| a.value.clone());
573 let Some(select) = select else {
574 return Err(refuse(RpcError::new(
575 "protocol",
576 "missing-attribute",
577 "xpath filter requires select",
578 )));
579 };
580 fields.insert("filter_type".into(), json!("xpath"));
581 fields.insert("filter_select".into(), json!(select));
582 }
583 _ => {
584 return Err(refuse(RpcError::new(
585 "protocol",
586 "bad-attribute",
587 "unsupported filter type",
588 )))
589 }
590 }
591 }
592 ("edit-config", "config") => {
593 fields.insert(
594 "config_xml".into(),
595 json!(subtree(&doc, child).map_err(refuse)?),
596 );
597 }
598 ("edit-config", "default-operation") => {
599 let v = xml::text(&doc, child).map_err(RpcRefusal::Fatal)?;
600 if !matches!(v.trim(), "merge" | "replace" | "none") {
601 return Err(refuse(RpcError::new(
602 "protocol",
603 "invalid-value",
604 "default-operation must be merge, replace or none",
605 )));
606 }
607 fields.insert("default_operation".into(), json!(v.trim()));
608 }
609 ("edit-config", "test-option") => {
610 let v = xml::text(&doc, child).map_err(RpcRefusal::Fatal)?;
611 let validate = capabilities
612 .iter()
613 .any(|c| c == VALIDATE_10 || c == VALIDATE_11);
614 if !validate || !matches!(v.trim(), "test-then-set" | "set" | "test-only") {
615 return Err(refuse(RpcError::new(
616 "protocol",
617 "invalid-value",
618 "test-option requires :validate and a known value",
619 )));
620 }
621 fields.insert("test_option".into(), json!(v.trim()));
622 }
623 ("edit-config", "error-option") => {
624 let v = xml::text(&doc, child).map_err(RpcRefusal::Fatal)?;
625 let ok = match v.trim() {
626 "stop-on-error" | "continue-on-error" => true,
627 "rollback-on-error" => capabilities.iter().any(|c| c == ROLLBACK),
628 _ => false,
629 };
630 if !ok {
631 return Err(refuse(RpcError::new(
632 "protocol",
633 "invalid-value",
634 "unsupported error-option",
635 )));
636 }
637 fields.insert("error_option".into(), json!(v.trim()));
638 }
639 ("kill-session", "session-id") => {
640 let v = xml::text(&doc, child).map_err(RpcRefusal::Fatal)?;
641 let id: u32 = v.trim().parse().map_err(|_| {
642 refuse(RpcError::new(
643 "protocol",
644 "invalid-value",
645 "session-id must be a number",
646 ))
647 })?;
648 fields.insert("session_id".into(), json!(id));
649 }
650 ("commit", "confirmed" | "confirm-timeout" | "persist" | "persist-id") => {
651 return Err(refuse(RpcError::new(
652 "protocol",
653 "operation-not-supported",
654 "confirmed commit is not supported",
655 )));
656 }
657 ("validate", "config") => {
658 return Err(refuse(RpcError::new(
659 "protocol",
660 "operation-not-supported",
661 "validate supports a datastore source only",
662 )));
663 }
664 ("edit-config", "url") | ("get-config" | "validate", "url") => {
665 return Err(refuse(RpcError::new(
666 "protocol",
667 "operation-not-supported",
668 "url is excluded",
669 )));
670 }
671 _ => {
672 return Err(refuse(RpcError::new(
673 "protocol",
674 "unknown-element",
675 "unexpected operation parameter",
676 )))
677 }
678 }
679 }
680 let need = |key: &str| -> Result<(), RpcRefusal> {
681 if fields.contains_key(key) {
682 Ok(())
683 } else {
684 Err(refuse(RpcError::new(
685 "protocol",
686 "missing-element",
687 &format!("{key} is required"),
688 )))
689 }
690 };
691 match operation.as_str() {
692 "get-config" => need("source")?,
693 "edit-config" => {
694 need("target")?;
695 need("config_xml")?;
696 }
697 "lock" | "unlock" => need("target")?,
698 "kill-session" => need("session_id")?,
699 "validate" => {
700 if !capabilities
701 .iter()
702 .any(|c| c == VALIDATE_10 || c == VALIDATE_11)
703 {
704 return Err(refuse(RpcError::new(
705 "protocol",
706 "operation-not-supported",
707 "validate requires the :validate capability",
708 )));
709 }
710 need("source")?
711 }
712 "commit" | "discard-changes" if !capabilities.iter().any(|c| c == CANDIDATE) => {
713 return Err(refuse(RpcError::new(
714 "protocol",
715 "operation-not-supported",
716 "requires the :candidate capability",
717 )));
718 }
719 _ => {}
720 }
721 Ok(IncomingRpc {
722 message_id,
723 attributes,
724 bindings,
725 operation,
726 namespace: op_ns,
727 fields,
728 })
729}
730
731pub fn build_rpc(
733 message_id: u64,
734 action: &Value,
735 server_capabilities: &[String],
736) -> Result<(String, Vec<u8>)> {
737 let operation = action["operation"]
738 .as_str()
739 .context("NETCONF operation required")?;
740 let s = |k: &str| action.get(k).and_then(Value::as_str);
741 let mut body = String::new();
742 let store = |k: &str, writing: bool, body: &mut String| -> Result<()> {
743 let v = s(k).with_context(|| format!("NETCONF {operation} requires {k}"))?;
744 datastore_allowed(v, server_capabilities, writing)?;
745 body.push_str(&format!("<{k}><{v}/></{k}>"));
746 Ok(())
747 };
748 let filter = |body: &mut String| -> Result<()> {
749 if let Some(f) = s("filter_xml") {
750 let doc = xml::parse_fragment(f)?;
751 body.push_str("<filter type=\"subtree\">");
752 body.push_str(&xml::render_fragment(&doc)?);
753 body.push_str("</filter>");
754 }
755 Ok(())
756 };
757 let op_name = match operation {
758 "get" => {
759 filter(&mut body)?;
760 "get"
761 }
762 "get-config" => {
763 store("source", false, &mut body)?;
764 filter(&mut body)?;
765 "get-config"
766 }
767 "edit-config" => {
768 store("target", true, &mut body)?;
769 if let Some(d) = s("default_operation") {
770 ensure!(
771 matches!(d, "merge" | "replace" | "none"),
772 "default_operation must be merge, replace or none"
773 );
774 body.push_str(&format!("<default-operation>{d}</default-operation>"));
775 }
776 if let Some(e) = s("error_option") {
777 ensure!(
778 matches!(
779 e,
780 "stop-on-error" | "continue-on-error" | "rollback-on-error"
781 ),
782 "unknown error_option"
783 );
784 if e == "rollback-on-error" {
785 ensure!(
786 server_capabilities.iter().any(|c| c == ROLLBACK),
787 "server lacks :rollback-on-error"
788 );
789 }
790 body.push_str(&format!("<error-option>{e}</error-option>"));
791 }
792 let config =
793 xml::parse_fragment(s("config_xml").context("edit-config requires config_xml")?)?;
794 ensure!(
795 !config.nodes.is_empty(),
796 "edit-config requires non-empty config_xml"
797 );
798 body.push_str("<config>");
799 body.push_str(&xml::render_fragment(&config)?);
800 body.push_str("</config>");
801 "edit-config"
802 }
803 "lock" | "unlock" => {
804 store("target", false, &mut body)?;
805 operation
806 }
807 "commit" | "discard-changes" => {
808 ensure!(
809 server_capabilities.iter().any(|c| c == CANDIDATE),
810 "server lacks :candidate"
811 );
812 operation
813 }
814 "validate" => {
815 ensure!(
816 server_capabilities
817 .iter()
818 .any(|c| c == VALIDATE_10 || c == VALIDATE_11),
819 "server lacks :validate"
820 );
821 store("source", false, &mut body)?;
822 "validate"
823 }
824 "close-session" => "close-session",
825 "kill-session" => {
826 let id = action["session_id"]
827 .as_u64()
828 .context("kill-session requires session_id")?;
829 ensure!(
830 id > 0 && id <= u64::from(u32::MAX),
831 "session_id out of range"
832 );
833 body.push_str(&format!("<session-id>{id}</session-id>"));
834 "kill-session"
835 }
836 "custom" => {
837 let input =
838 xml::parse_fragment(s("input_xml").context(
839 "custom requires input_xml: one operation element with its namespace",
840 )?)?;
841 let top = input
842 .nodes
843 .iter()
844 .filter(|n| matches!(n, Node::Start { .. }))
845 .count();
846 ensure!(top >= 1, "custom input_xml needs an operation element");
847 let rendered = xml::render_fragment(&input)?;
848 let doc =
849 xml::parse(format!("<x xmlns=\"urn:netget:probe\">{rendered}</x>").as_bytes())?;
850 ensure!(
851 xml::children(&doc, 0)?.len() == 1,
852 "custom input_xml must be exactly one operation element"
853 );
854 let (_, ns, _) = xml::element(&doc, 1)?;
855 ensure!(
856 !ns.is_empty() && ns != NC,
857 "custom operation must be in its own non-base namespace"
858 );
859 body = rendered;
860 ""
861 }
862 other => bail!("unsupported NETCONF operation '{other}'"),
863 };
864 let message = if op_name.is_empty() {
865 format!("<?xml version=\"1.0\" encoding=\"UTF-8\"?><rpc xmlns=\"{NC}\" message-id=\"{message_id}\">{body}</rpc>")
866 } else {
867 format!("<?xml version=\"1.0\" encoding=\"UTF-8\"?><rpc xmlns=\"{NC}\" message-id=\"{message_id}\"><{op_name}>{body}</{op_name}></rpc>")
868 };
869 ensure!(
870 message.len() <= super::wire::MAX_MESSAGE_BYTES,
871 "NETCONF request byte bound"
872 );
873 xml::parse(message.as_bytes())?;
874 Ok((operation.to_owned(), message.into_bytes()))
875}
876
877pub struct IncomingReply {
879 pub message_id: Option<String>,
880 pub body: Value,
881}
882
883pub fn parse_reply(message: &[u8]) -> Result<IncomingReply> {
884 let doc = xml::parse(message)?;
885 let (name, ns, attrs) = xml::element(&doc, 0)?;
886 ensure!(
887 name == "rpc-reply" && ns == NC,
888 "NETCONF expected <rpc-reply> in the base namespace"
889 );
890 let message_id = attrs
891 .iter()
892 .find(|a| a.name == "message-id" && a.namespace.is_empty())
893 .map(|a| a.value.clone());
894 let kids = xml::children(&doc, 0)?;
895 let mut ok = false;
896 let mut data: Option<String> = None;
897 let mut errors = Vec::new();
898 let mut output = Vec::new();
899 for child in kids {
900 let (child_name, child_ns, _) = xml::element(&doc, child)?;
901 if child_ns == NC && child_name == "ok" {
902 ok = true;
903 } else if child_ns == NC && child_name == "data" {
904 ensure!(data.is_none(), "NETCONF duplicate <data>");
905 data = Some(xml::render_fragment(&xml::inner(&doc, child)?)?);
906 } else if child_ns == NC && child_name == "rpc-error" {
907 ensure!(errors.len() < 64, "NETCONF rpc-error count");
908 let mut e = Map::new();
909 for field in xml::children(&doc, child)? {
910 let (f, fns, _) = xml::element(&doc, field)?;
911 ensure!(fns == NC, "NETCONF rpc-error field namespace");
912 let key = match f {
913 "error-type" => "error_type",
914 "error-tag" => "error_tag",
915 "error-severity" => "error_severity",
916 "error-app-tag" => "error_app_tag",
917 "error-path" => "error_path",
918 "error-message" => "error_message",
919 "error-info" => {
920 e.insert(
921 "error_info_xml".into(),
922 json!(xml::render_fragment(&xml::inner(&doc, field)?)?),
923 );
924 continue;
925 }
926 _ => continue,
927 };
928 e.insert(key.into(), json!(xml::text(&doc, field)?.trim()));
929 }
930 ensure!(
931 e.contains_key("error_tag"),
932 "NETCONF rpc-error without error-tag"
933 );
934 errors.push(Value::Object(e));
935 } else {
936 let mut piece = xml::inner(&doc, 0)?;
937 let end = xml::end_index(&doc, child)?;
938 piece.nodes = doc.nodes[child..=end].to_vec();
939 output.push(xml::render_fragment(&piece)?);
940 }
941 }
942 let mut body = Map::new();
943 if ok {
944 body.insert("ok".into(), json!(true));
945 }
946 if let Some(d) = data {
947 body.insert("data_xml".into(), json!(d));
948 }
949 if !errors.is_empty() {
950 body.insert("errors".into(), Value::Array(errors));
951 }
952 if !output.is_empty() {
953 body.insert("output_xml".into(), json!(output.concat()));
954 }
955 ensure!(
956 !body.is_empty(),
957 "NETCONF <rpc-reply> carries no recognised content"
958 );
959 Ok(IncomingReply {
960 message_id,
961 body: Value::Object(body),
962 })
963}