1use 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
15const CONNECT_TIMEOUT: Duration = Duration::from_secs(30);
17const RESPONSE_TIMEOUT: Duration = Duration::from_secs(60);
18
19#[derive(Debug)]
21pub(crate) struct Answer {
22 pub uri: Uri,
24 pub total: Option<u64>,
26 pub starts_at: u64,
28 pub partial: bool,
31}
32
33#[derive(Clone, Copy, Debug, PartialEq, Eq)]
36pub(crate) struct ByteRange {
37 pub from: u64,
38 pub to: Option<u64>,
39}
40
41impl ByteRange {
42 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
51pub(crate) type Headers = Arc<[(HeaderName, HeaderValue)]>;
53
54#[derive(Debug, PartialEq, Eq)]
56pub(crate) enum BadHeader {
57 Malformed(String),
59 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
78pub(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
111pub(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
156fn 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}