Skip to main content

netget/utils/
line_reader.rs

1//! One bounded, `\n`-terminated read, shared by the line-oriented text protocols.
2//!
3//! `tokio::io::AsyncBufReadExt::read_line` grows its `String` until it finds a newline and
4//! caps nothing. A peer that connects and streams printable bytes without ever sending `\n`
5//! therefore makes the server allocate without bound — a one-connection out-of-memory, from an
6//! unauthenticated socket, before any model call. That defect was found in `ftp`, `irc` and
7//! `smtp` and fixed three separate times, then found again in `imap`, `nntp` and `pop3`.
8//!
9//! Three copies of a `fill_buf`/`consume` loop is how the control-character filters ended up
10//! with six different semantics before `utils::sanitize` existed, so this is one
11//! implementation rather than a sixth. Each protocol still owns its own `MAX_*` constant and
12//! its own refusal wording — the bound is a protocol decision, the reading is not.
13//!
14//! **The cap is on what is buffered, not on what is consumed.** A line longer than the cap is
15//! reported as [`BoundedLine::TooLong`] with the offending bytes left unconsumed, because there
16//! is no resynchronisation point: the peer is mid-line and the next byte is not the start of a
17//! command. Every caller answers in its own vocabulary and closes.
18//!
19//! Decoding is lossy. `read_line` returns `ErrorKind::InvalidData` for any non-UTF-8 byte and
20//! kills the session on it, which is wrong for protocols that advertise 8-bit transports
21//! (`smtp`'s `8BITMIME`, `imap`'s literals). These bytes become an event payload and a log
22//! line; nothing here is re-emitted on the wire, so a lossy decode loses nothing a peer can
23//! observe.
24
25use tokio::io::{AsyncBufReadExt, AsyncRead, BufReader};
26
27/// Read one complete byte-delimited frame, preserving binary payloads and enforcing
28/// the cap before allocation. EOF with a fragment is an error. Keep this future alive
29/// across unrelated commands: like `read_until`, cancellation discards its partial frame.
30pub async fn read_bounded_delimited<R: AsyncRead + Unpin>(
31    reader: &mut BufReader<R>,
32    delimiter: u8,
33    max_len: usize,
34) -> std::io::Result<Option<Vec<u8>>> {
35    let mut frame = Vec::new();
36    loop {
37        let available = reader.fill_buf().await?;
38        if available.is_empty() {
39            return if frame.is_empty() {
40                Ok(None)
41            } else {
42                Err(std::io::Error::new(
43                    std::io::ErrorKind::UnexpectedEof,
44                    "partial delimited frame",
45                ))
46            };
47        }
48        let end = available.iter().position(|&b| b == delimiter);
49        let take = end.map_or(available.len(), |n| n + 1);
50        if take > max_len.saturating_sub(frame.len()) {
51            return Err(std::io::Error::new(
52                std::io::ErrorKind::InvalidData,
53                "delimited frame exceeds cap",
54            ));
55        }
56        frame.extend_from_slice(&available[..take]);
57        reader.consume(take);
58        if end.is_some() {
59            return Ok(Some(frame));
60        }
61    }
62}
63
64/// The outcome of one bounded line read.
65#[derive(Debug)]
66pub enum BoundedLine {
67    /// A complete line. The trailing `\n` (and `\r`, if any) is still attached; callers trim.
68    Line(String),
69    /// The peer closed, or sent a partial line and then closed. Half a command is not a
70    /// command, so a trailing fragment at EOF is reported here rather than as a `Line`.
71    Eof,
72    /// `max_len` bytes went by with no `\n` in them.
73    TooLong,
74}
75
76/// Read one `\n`-terminated line, buffering at most `max_len` bytes.
77///
78/// Returns the line and the number of bytes consumed from the socket, so a caller can feed
79/// `update_connection_stats` exactly as `read_line`'s return value allowed.
80///
81/// `max_len` bounds the buffer the peer can make this function allocate. It is compared
82/// *before* each `extend_from_slice`, so the high-water mark is `max_len`, not `max_len` plus
83/// however much happened to be in the `BufReader` when the limit was crossed.
84pub async fn read_bounded_line<R>(
85    reader: &mut BufReader<R>,
86    max_len: usize,
87) -> std::io::Result<(BoundedLine, usize)>
88where
89    R: AsyncRead + Unpin,
90{
91    let mut buf: Vec<u8> = Vec::new();
92    let mut consumed = 0usize;
93
94    loop {
95        // `fill_buf` borrows the reader, so decide what to take and drop the borrow before
96        // calling `consume`.
97        let (newline_at, available_len) = {
98            let available = reader.fill_buf().await?;
99            if available.is_empty() {
100                return Ok((BoundedLine::Eof, consumed));
101            }
102            (available.iter().position(|&b| b == b'\n'), available.len())
103        };
104
105        match newline_at {
106            Some(idx) => {
107                if buf.len() + idx + 1 > max_len {
108                    return Ok((BoundedLine::TooLong, consumed));
109                }
110                let take = idx + 1;
111                buf.extend_from_slice(&reader.buffer()[..take]);
112                reader.consume(take);
113                consumed += take;
114                return Ok((
115                    BoundedLine::Line(String::from_utf8_lossy(&buf).into_owned()),
116                    consumed,
117                ));
118            }
119            None => {
120                // No newline in what is buffered. Refuse before growing past the cap: the
121                // check has to come first, or the peer decides the allocation.
122                if buf.len() + available_len > max_len {
123                    return Ok((BoundedLine::TooLong, consumed));
124                }
125                buf.extend_from_slice(&reader.buffer()[..available_len]);
126                reader.consume(available_len);
127                consumed += available_len;
128            }
129        }
130    }
131}