Skip to main content

netget/server/netconf/
wire.rs

1//! RFC6242 framing. Lengths and aggregate bounds are checked before body copies.
2use anyhow::{bail, ensure, Result};
3use std::collections::VecDeque;
4
5pub const MAX_MESSAGE_BYTES: usize = 1024 * 1024;
6pub const MAX_CHUNKS: usize = 1024;
7pub const MAX_BUFFER_BYTES: usize = MAX_MESSAGE_BYTES + 256 * 1024 + 16 * MAX_CHUNKS;
8pub const DELIMITER: &[u8] = b"]]>]]>";
9
10#[derive(Clone, Copy, Debug, PartialEq, Eq)]
11pub enum Framing {
12    Delimiter,
13    Chunked,
14}
15
16pub struct Decoder {
17    framing: Framing,
18    input: VecDeque<u8>,
19    body: Vec<u8>,
20    header: Vec<u8>,
21    remaining: usize,
22    chunks: usize,
23}
24impl Decoder {
25    pub fn new(framing: Framing) -> Self {
26        Self {
27            framing,
28            input: VecDeque::new(),
29            body: Vec::new(),
30            header: Vec::new(),
31            remaining: 0,
32            chunks: 0,
33        }
34    }
35    pub fn set_framing(&mut self, framing: Framing) -> Result<()> {
36        ensure!(
37            self.body.is_empty()
38                && self.header.is_empty()
39                && self.remaining == 0
40                && self.chunks == 0,
41            "NETCONF framing transition during a message"
42        );
43        self.framing = framing;
44        Ok(())
45    }
46    pub fn feed(&mut self, bytes: &[u8]) -> Result<()> {
47        ensure!(
48            bytes.len() <= MAX_BUFFER_BYTES.saturating_sub(self.input.len()),
49            "NETCONF input queue bound"
50        );
51        self.input.extend(bytes);
52        Ok(())
53    }
54    pub fn is_partial(&self) -> bool {
55        !self.input.is_empty()
56            || !self.body.is_empty()
57            || !self.header.is_empty()
58            || self.remaining > 0
59            || self.chunks > 0
60    }
61    pub fn next_message(&mut self) -> Result<Option<Vec<u8>>> {
62        if self.framing == Framing::Delimiter {
63            while let Some(byte) = self.input.pop_front() {
64                self.body.push(byte);
65                if self.body.ends_with(DELIMITER) {
66                    self.body.truncate(self.body.len() - DELIMITER.len());
67                    ensure!(
68                        self.body.len() <= MAX_MESSAGE_BYTES,
69                        "NETCONF message byte bound"
70                    );
71                    return Ok(Some(std::mem::take(&mut self.body)));
72                }
73                ensure!(
74                    self.body.len() < MAX_MESSAGE_BYTES + DELIMITER.len(),
75                    "NETCONF message byte bound"
76                );
77            }
78            return Ok(None);
79        }
80        loop {
81            if self.remaining > 0 {
82                let available = self.remaining.min(self.input.len());
83                for _ in 0..available {
84                    self.body
85                        .push(self.input.pop_front().expect("available byte"));
86                }
87                self.remaining -= available;
88                if self.remaining > 0 {
89                    return Ok(None);
90                }
91            }
92            let Some(byte) = self.input.pop_front() else {
93                return Ok(None);
94            };
95            ensure!(self.header.len() < 13, "NETCONF chunk header bound");
96            self.header.push(byte);
97            match self.header.len() {
98                1 => ensure!(byte == b'\n', "NETCONF chunk must start with LF"),
99                2 => ensure!(byte == b'#', "NETCONF chunk must start with LF HASH"),
100                3 => ensure!(
101                    byte == b'#' || (b'1'..=b'9').contains(&byte),
102                    "NETCONF chunk size must be positive without leading zero"
103                ),
104                _ => {
105                    if self.header[2] == b'#' {
106                        ensure!(
107                            self.header.len() == 4 && byte == b'\n',
108                            "NETCONF invalid end-of-chunks"
109                        );
110                        ensure!(self.chunks > 0, "NETCONF empty chunked message");
111                        self.header.clear();
112                        self.chunks = 0;
113                        return Ok(Some(std::mem::take(&mut self.body)));
114                    }
115                    if byte != b'\n' {
116                        ensure!(byte.is_ascii_digit(), "NETCONF invalid chunk size digit");
117                        continue;
118                    }
119                    let mut length = 0u64;
120                    for digit in &self.header[2..self.header.len() - 1] {
121                        length = length * 10 + u64::from(digit - b'0');
122                    }
123                    ensure!(
124                        length <= u64::from(u32::MAX),
125                        "NETCONF native chunk size maximum"
126                    );
127                    ensure!(
128                        length <= (MAX_MESSAGE_BYTES - self.body.len()) as u64,
129                        "NETCONF aggregate message byte bound"
130                    );
131                    ensure!(self.chunks < MAX_CHUNKS, "NETCONF chunk count bound");
132                    self.chunks += 1;
133                    self.remaining = length as usize;
134                    self.header.clear();
135                }
136            }
137        }
138    }
139}
140
141pub fn frame(message: &[u8], framing: Framing) -> Result<Vec<u8>> {
142    ensure!(
143        !message.is_empty() && message.len() <= MAX_MESSAGE_BYTES,
144        "NETCONF outgoing message byte bound"
145    );
146    match framing {
147        Framing::Delimiter => {
148            if message.windows(DELIMITER.len()).any(|w| w == DELIMITER) {
149                bail!("NETCONF delimiter inside outgoing XML");
150            }
151            let mut framed = Vec::with_capacity(message.len() + DELIMITER.len());
152            framed.extend_from_slice(message);
153            framed.extend_from_slice(DELIMITER);
154            Ok(framed)
155        }
156        Framing::Chunked => {
157            let mut framed = format!("\n#{}\n", message.len()).into_bytes();
158            framed.extend_from_slice(message);
159            framed.extend_from_slice(b"\n##\n");
160            Ok(framed)
161        }
162    }
163}