Skip to main content

netget/client/
response_reader.rs

1//! Bounded framing for text responses from remote servers.
2
3use tokio::io::{AsyncBufReadExt, AsyncRead, BufReader};
4
5use crate::utils::line_reader::{read_bounded_line, BoundedLine};
6
7/// Generous control/response-line limit, including CRLF.
8pub const MAX_RESPONSE_LINE_BYTES: usize = 64 * 1024;
9/// Total retained text of a dot-terminated POP3/NNTP response.
10pub const MAX_MULTILINE_BYTES: usize = 8 * 1024 * 1024;
11/// Absolute deadline after the first byte of a line or multiline body.
12pub const RESPONSE_DEADLINE: std::time::Duration = std::time::Duration::from_secs(30);
13
14/// Replace the line buffer with a complete bounded line. Partial EOF is an error;
15/// callers must close on an error because the stream is no longer synchronized.
16/// Like `read_line`, this must not be cancelled midway through a response.
17pub async fn read_response_line<R: AsyncRead + Unpin>(
18    reader: &mut BufReader<R>,
19    line: &mut String,
20) -> std::io::Result<usize> {
21    read_response_line_with_timeout(reader, line, RESPONSE_DEADLINE).await
22}
23
24/// Variant with an explicit partial-line deadline (also useful to test slow peers).
25pub async fn read_response_line_with_timeout<R: AsyncRead + Unpin>(
26    reader: &mut BufReader<R>,
27    line: &mut String,
28    deadline: std::time::Duration,
29) -> std::io::Result<usize> {
30    line.clear();
31    // Idle connections may wait indefinitely; a peer that starts a frame must finish it.
32    if reader.fill_buf().await?.is_empty() {
33        return Ok(0);
34    }
35    let result = tokio::time::timeout(deadline, read_bounded_line(reader, MAX_RESPONSE_LINE_BYTES))
36        .await
37        .map_err(|_| {
38            std::io::Error::new(
39                std::io::ErrorKind::TimedOut,
40                "response line deadline exceeded",
41            )
42        })??;
43    match result {
44        (BoundedLine::Line(value), consumed) => {
45            *line = value;
46            Ok(consumed)
47        }
48        (BoundedLine::Eof, 0) => Ok(0),
49        (BoundedLine::Eof, _) => Err(std::io::Error::new(
50            std::io::ErrorKind::UnexpectedEof,
51            "server closed midway through a response line",
52        )),
53        (BoundedLine::TooLong, _) => Err(std::io::Error::new(
54            std::io::ErrorKind::InvalidData,
55            format!("response line exceeds the {MAX_RESPONSE_LINE_BYTES}-byte cap"),
56        )),
57    }
58}
59
60/// A solicited greeting/response must begin and finish within the handshake deadline.
61pub async fn read_expected_response_line<R: AsyncRead + Unpin>(
62    reader: &mut BufReader<R>,
63    line: &mut String,
64) -> std::io::Result<usize> {
65    tokio::time::timeout(RESPONSE_DEADLINE, read_response_line(reader, line))
66        .await
67        .map_err(|_| {
68            std::io::Error::new(
69                std::io::ErrorKind::TimedOut,
70                "expected response deadline exceeded",
71            )
72        })?
73}
74
75/// Read through the exact `.` terminator. Preserve content whitespace, undo dot
76/// stuffing, and reject EOF or a size violation instead of reporting a partial reply.
77pub async fn read_dot_response<R: AsyncRead + Unpin>(
78    reader: &mut BufReader<R>,
79    response: String,
80) -> std::io::Result<String> {
81    tokio::time::timeout(RESPONSE_DEADLINE, read_dot_response_inner(reader, response))
82        .await
83        .map_err(|_| {
84            std::io::Error::new(
85                std::io::ErrorKind::TimedOut,
86                "multiline response deadline exceeded",
87            )
88        })?
89}
90
91async fn read_dot_response_inner<R: AsyncRead + Unpin>(
92    reader: &mut BufReader<R>,
93    mut response: String,
94) -> std::io::Result<String> {
95    if response.len() > MAX_MULTILINE_BYTES {
96        return Err(std::io::Error::new(
97            std::io::ErrorKind::InvalidData,
98            "multiline response exceeds the byte cap",
99        ));
100    }
101    let mut line = String::new();
102    loop {
103        if read_response_line(reader, &mut line).await? == 0 {
104            return Err(std::io::Error::new(
105                std::io::ErrorKind::UnexpectedEof,
106                "server closed before the multiline response terminator",
107            ));
108        }
109        let line = line.strip_suffix('\n').unwrap_or(&line);
110        let line = line.strip_suffix('\r').unwrap_or(line);
111        if line == "." {
112            return Ok(response);
113        }
114        let line = if line.starts_with("..") {
115            &line[1..]
116        } else {
117            line
118        };
119        if line.len() + 1 > MAX_MULTILINE_BYTES.saturating_sub(response.len()) {
120            return Err(std::io::Error::new(
121                std::io::ErrorKind::InvalidData,
122                format!("multiline response exceeds the {MAX_MULTILINE_BYTES}-byte cap"),
123            ));
124        }
125        response.push('\n');
126        response.push_str(line);
127    }
128}
129
130/// An absolute deadline for an explicitly started binary frame. No clock runs
131/// between frames or while the caller handles the completed event.
132pub struct FrameReader<R> {
133    inner: R,
134    deadline: Option<std::pin::Pin<Box<tokio::time::Sleep>>>,
135}
136impl<R> FrameReader<R> {
137    pub fn new(inner: R) -> Self {
138        Self {
139            inner,
140            deadline: None,
141        }
142    }
143    pub fn start_frame(&mut self, duration: std::time::Duration) {
144        self.deadline = Some(Box::pin(tokio::time::sleep(duration)));
145    }
146    pub fn end_frame(&mut self) {
147        self.deadline = None;
148    }
149}
150impl<R: AsyncRead + Unpin> AsyncRead for FrameReader<R> {
151    fn poll_read(
152        self: std::pin::Pin<&mut Self>,
153        cx: &mut std::task::Context<'_>,
154        buf: &mut tokio::io::ReadBuf<'_>,
155    ) -> std::task::Poll<std::io::Result<()>> {
156        use std::future::Future;
157        let this = self.get_mut();
158        if this
159            .deadline
160            .as_mut()
161            .is_some_and(|deadline| deadline.as_mut().poll(cx).is_ready())
162        {
163            return std::task::Poll::Ready(Err(std::io::Error::new(
164                std::io::ErrorKind::TimedOut,
165                "partial response frame deadline exceeded",
166            )));
167        }
168        std::pin::Pin::new(&mut this.inner).poll_read(cx, buf)
169    }
170}