Skip to main content

nifty_download/
http.rs

1//! Talking to web servers: the client every download uses, and what a server says about
2//! a file before sending it.
3
4use std::fmt;
5use std::sync::Arc;
6use std::time::Duration;
7
8use ureq::http::header::{CONTENT_RANGE, RANGE};
9use ureq::http::{HeaderName, HeaderValue, StatusCode, Uri};
10use ureq::tls::{RootCerts, TlsConfig, TlsProvider};
11use ureq::{Agent, Body, ResponseExt};
12
13use crate::connection::ConnectionError;
14
15/// How long connecting, and then waiting for the server to start answering, may take.
16const CONNECT_TIMEOUT: Duration = Duration::from_secs(30);
17const RESPONSE_TIMEOUT: Duration = Duration::from_secs(60);
18
19/// What a server said about the file before its body.
20#[derive(Debug)]
21pub(crate) struct Answer {
22    /// The address the body came from, after any redirects.
23    pub uri: Uri,
24    /// The whole file's length, when the server said.
25    pub total: Option<u64>,
26    /// Where in the file the body starts: `0`, unless the server sent only part of it.
27    pub starts_at: u64,
28    /// Whether the server sent part of the file, as `206 Partial Content`, which says it
29    /// can send any part asked for.
30    pub partial: bool,
31}
32
33/// The bytes of a file from `from` up to, but not including, `to`, or to the end of the
34/// file when `to` is `None`.
35#[derive(Clone, Copy, Debug, PartialEq, Eq)]
36pub(crate) struct ByteRange {
37    pub from: u64,
38    pub to: Option<u64>,
39}
40
41impl ByteRange {
42    /// As a `Range` header's value, which counts its last byte rather than the one after.
43    fn header(self) -> String {
44        match self.to {
45            Some(to) => format!("bytes={}-{}", self.from, to - 1),
46            None => format!("bytes={}-", self.from),
47        }
48    }
49}
50
51/// The headers an app adds to each of a download's requests, shared by its connections.
52pub(crate) type Headers = Arc<[(HeaderName, HeaderValue)]>;
53
54/// A header an app asked for that can't be sent.
55#[derive(Debug, PartialEq, Eq)]
56pub(crate) enum BadHeader {
57    /// Not written `Name: value`, or with characters a header can't hold.
58    Malformed(String),
59    /// A header the download sets itself.
60    Reserved(String),
61}
62
63impl fmt::Display for BadHeader {
64    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
65        match self {
66            Self::Malformed(line) => write!(
67                f,
68                "the header `{line}` isn't written as `Name: value`, such as `Authorization: Bearer …`"
69            ),
70            Self::Reserved(name) => write!(
71                f,
72                "the header `{name}` is set by the download itself, to fetch parts of the file"
73            ),
74        }
75    }
76}
77
78/// Reads each of `lines`, written `Name: value`, as a header to send.
79///
80/// # Errors
81/// The first line that isn't a header, or names one the download sets itself.
82pub(crate) fn headers(lines: &[String]) -> Result<Headers, BadHeader> {
83    lines
84        .iter()
85        .map(|line| {
86            let malformed = || BadHeader::Malformed(line.clone());
87            let (name, value) = line.split_once(':').ok_or_else(malformed)?;
88            let name = HeaderName::try_from(name.trim()).map_err(|_| malformed())?;
89            if name == RANGE {
90                return Err(BadHeader::Reserved(name.to_string()));
91            }
92            let value = HeaderValue::try_from(value.trim()).map_err(|_| malformed())?;
93            Ok((name, value))
94        })
95        .collect()
96}
97
98pub(crate) fn agent() -> Agent {
99    let tls = TlsConfig::builder()
100        .provider(TlsProvider::NativeTls)
101        .root_certs(RootCerts::PlatformVerifier)
102        .build();
103    Agent::config_builder()
104        .tls_config(tls)
105        .timeout_connect(Some(CONNECT_TIMEOUT))
106        .timeout_recv_response(Some(RESPONSE_TIMEOUT))
107        .build()
108        .into()
109}
110
111/// Asks for `url`, or just `range` of it, sending `headers` too, giving what the server
112/// said and the body still to read. A server that can't send part of a file sends all of
113/// it, which [`Answer::starts_at`] shows.
114///
115/// # Errors
116/// When the request fails, or the server sends part of the file without saying which.
117pub(crate) fn get(
118    agent: &Agent,
119    url: &str,
120    range: Option<ByteRange>,
121    headers: &[(HeaderName, HeaderValue)],
122) -> Result<(Answer, Body), ConnectionError> {
123    let mut request = agent.get(url);
124    for (name, value) in headers {
125        request = request.header(name, value);
126    }
127    if let Some(range) = range {
128        request = request.header(RANGE, range.header());
129    }
130    let response = request.call()?;
131    let uri = response.get_uri().clone();
132    let answer = if response.status() == StatusCode::PARTIAL_CONTENT {
133        let (starts_at, total) = response
134            .headers()
135            .get(CONTENT_RANGE)
136            .and_then(|range| range.to_str().ok())
137            .and_then(content_range)
138            .ok_or(ConnectionError::UnclearPart)?;
139        Answer {
140            uri,
141            total,
142            starts_at,
143            partial: true,
144        }
145    } else {
146        Answer {
147            uri,
148            total: response.body().content_length(),
149            starts_at: 0,
150            partial: false,
151        }
152    };
153    Ok((answer, response.into_body()))
154}
155
156/// Where a `Content-Range` such as `bytes 100-199/200` starts, and the whole file's
157/// length when it says, which `*` doesn't.
158fn content_range(range: &str) -> Option<(u64, Option<u64>)> {
159    let (span, total) = range.strip_prefix("bytes ")?.split_once('/')?;
160    let (start, _) = span.split_once('-')?;
161    Some((start.trim().parse().ok()?, total.trim().parse().ok()))
162}
163
164#[cfg(test)]
165mod tests {
166    use super::*;
167
168    #[test]
169    fn reads_where_a_part_starts_and_the_whole_length() {
170        assert_eq!(content_range("bytes 100-199/200"), Some((100, Some(200))));
171        assert_eq!(content_range("bytes 0-9/*"), Some((0, None)));
172        assert_eq!(content_range("bytes */200"), None);
173        assert_eq!(content_range("items 1-2/3"), None);
174    }
175
176    fn lines(lines: &[&str]) -> Vec<String> {
177        lines.iter().map(|&line| line.to_owned()).collect()
178    }
179
180    #[test]
181    fn reads_headers_written_name_colon_value() {
182        let read = headers(&lines(&["Authorization: Bearer abc", "X-Token:42"]))
183            .expect("both are headers");
184        let read: Vec<_> = read
185            .iter()
186            .map(|(name, value)| (name.as_str(), value.to_str().expect("plain text")))
187            .collect();
188        assert_eq!(read, [("authorization", "Bearer abc"), ("x-token", "42")]);
189    }
190
191    #[test]
192    fn refuses_what_is_not_a_header_or_is_the_downloads_own() {
193        assert_eq!(
194            headers(&lines(&["no colon"])).err(),
195            Some(BadHeader::Malformed("no colon".to_owned()))
196        );
197        assert_eq!(
198            headers(&lines(&["bad name: x"])).err(),
199            Some(BadHeader::Malformed("bad name: x".to_owned()))
200        );
201        assert_eq!(
202            headers(&lines(&["X: line\nbreak"])).err(),
203            Some(BadHeader::Malformed("X: line\nbreak".to_owned()))
204        );
205        assert_eq!(
206            headers(&lines(&["Range: bytes=0-"])).err(),
207            Some(BadHeader::Reserved("range".to_owned()))
208        );
209    }
210
211    #[test]
212    fn asks_for_a_range_by_its_last_byte() {
213        let rest = ByteRange { from: 4, to: None };
214        assert_eq!(rest.header(), "bytes=4-");
215        let part = ByteRange {
216            from: 0,
217            to: Some(100),
218        };
219        assert_eq!(part.header(), "bytes=0-99");
220    }
221}