1use std::io::Read;
2
3use crate::control::ControlFile;
4
5#[derive(Debug, thiserror::Error)]
6pub enum HttpError {
7 #[error("HTTP error: {0}")]
8 Http(String),
9 #[error("IO error: {0}")]
10 Io(#[from] std::io::Error),
11 #[error("Invalid URL: {0}")]
12 InvalidUrl(String),
13 #[error("No URLs available")]
14 NoUrls,
15}
16
17pub struct HttpRangeReader {
18 reader: Box<dyn std::io::Read + Send + Sync>,
19}
20
21impl std::io::Read for HttpRangeReader {
22 fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
23 self.reader.read(buf)
24 }
25}
26
27pub struct HttpClient {
29 agent: ureq::Agent,
30}
31
32impl Default for HttpClient {
33 fn default() -> Self {
34 Self::new()
35 }
36}
37
38impl HttpClient {
39 pub fn new() -> Self {
47 Self {
48 agent: ureq::Agent::config_builder()
49 .https_only(false)
50 .build()
51 .new_agent(),
52 }
53 }
54
55 pub fn with_agent(agent: ureq::Agent) -> Self {
62 Self { agent }
63 }
64
65 pub fn fetch_control_file(&self, url: &str) -> Result<ControlFile, HttpError> {
66 let response = self
67 .agent
68 .get(url)
69 .call()
70 .map_err(|e| HttpError::Http(e.to_string()))?;
71
72 let mut reader = response.into_body().into_reader();
73 ControlFile::parse(&mut reader).map_err(|e| HttpError::Http(e.to_string()))
74 }
75
76 pub fn fetch_range_reader(
84 &self,
85 url: &str,
86 start: u64,
87 end: u64,
88 ) -> Result<HttpRangeReader, HttpError> {
89 let range_header = format!("bytes={}-{}", start, end);
90 let requested = end.saturating_sub(start).saturating_add(1);
91
92 let response = self
93 .agent
94 .get(url)
95 .header("Range", &range_header)
96 .call()
97 .map_err(|e| HttpError::Http(e.to_string()))?;
98
99 let status = response.status();
100 if status != 206 && status != 200 {
101 return Err(HttpError::Http(format!(
102 "Expected 206 Partial Content, got {}",
103 status
104 )));
105 }
106
107 Ok(HttpRangeReader {
108 reader: Box::new(response.into_body().into_reader().take(requested)),
109 })
110 }
111
112 pub fn fetch_range(&self, url: &str, start: u64, end: u64) -> Result<Vec<u8>, HttpError> {
113 let mut reader = self.fetch_range_reader(url, start, end)?;
114 let mut buf = Vec::new();
115 reader.read_to_end(&mut buf)?;
116 Ok(buf)
117 }
118
119 pub fn fetch_ranges(
120 &self,
121 url: &str,
122 ranges: &[(u64, u64)],
123 blocksize: usize,
124 ) -> Result<Vec<(u64, Vec<u8>)>, HttpError> {
125 let mut results = Vec::new();
126
127 for &(start, end) in ranges {
128 let data = self.fetch_range(url, start, end)?;
129 let aligned_start = (start / blocksize as u64) * blocksize as u64;
130 results.push((aligned_start, data));
131 }
132
133 Ok(results)
134 }
135}
136
137pub const DEFAULT_RANGE_GAP_THRESHOLD: u64 = 256 * 1024;
139
140pub fn merge_byte_ranges(ranges: &[(u64, u64)], gap_threshold: u64) -> Vec<(u64, u64)> {
143 if ranges.len() <= 1 {
144 return ranges.to_vec();
145 }
146
147 let mut merged = vec![ranges[0]];
148 for &(start, end) in &ranges[1..] {
149 let last = merged.last_mut().unwrap();
150 let gap = start.saturating_sub(last.1 + 1);
151 if gap <= gap_threshold {
152 last.1 = end;
153 } else {
154 merged.push((start, end));
155 }
156 }
157 merged
158}
159
160pub fn byte_ranges_from_block_ranges(
161 block_ranges: &[(usize, usize)],
162 blocksize: usize,
163 file_length: u64,
164) -> Vec<(u64, u64)> {
165 block_ranges
166 .iter()
167 .map(|&(start_block, end_block)| {
168 let start = start_block as u64 * blocksize as u64;
169 let end =
170 ((end_block as u64 * blocksize as u64).saturating_sub(1)).min(file_length - 1);
171 (start, end)
172 })
173 .collect()
174}
175
176#[cfg(test)]
177mod tests {
178 use super::*;
179
180 fn overlong_server(body_len: usize) -> String {
183 use std::io::Write;
184 let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
185 let addr = listener.local_addr().unwrap();
186 std::thread::spawn(move || {
187 if let Ok((mut sock, _)) = listener.accept() {
188 let mut req = [0u8; 1024];
189 let _ = std::io::Read::read(&mut sock, &mut req);
190 let hdr = format!(
191 "HTTP/1.1 206 Partial Content\r\nContent-Range: bytes 0-1023/{}\r\nContent-Length: {}\r\n\r\n",
192 body_len, body_len
193 );
194 let _ = sock.write_all(hdr.as_bytes());
195 let chunk = vec![0u8; 64 * 1024];
196 let mut sent = 0;
197 while sent < body_len {
198 let n = chunk.len().min(body_len - sent);
199 if sock.write_all(&chunk[..n]).is_err() {
200 break;
201 }
202 sent += n;
203 }
204 }
205 });
206 format!("http://{}/f", addr)
207 }
208
209 #[test]
210 fn a_range_response_is_capped_at_what_was_requested() {
211 let url = overlong_server(8 * 1024 * 1024);
214 let client = HttpClient::new();
215 let data = client.fetch_range(&url, 0, 1023).expect("fetch");
216 assert_eq!(
217 data.len(),
218 1024,
219 "a range response must be capped at the requested length"
220 );
221 }
222
223 #[test]
224 fn a_supplied_agent_carries_its_own_policy() {
225 let strict = HttpClient::with_agent(
229 ureq::Agent::config_builder()
230 .https_only(true)
231 .build()
232 .new_agent(),
233 );
234 let err = strict
235 .fetch_control_file("http://127.0.0.1:1/nothing.zsync")
236 .expect_err("an HTTPS-only agent must refuse a plain-HTTP URL");
237 let msg = err.to_string();
238 assert!(
239 msg.to_lowercase().contains("http"),
240 "expected a scheme rejection, got: {msg}"
241 );
242 }
243
244 #[test]
245 fn test_byte_ranges_from_block_ranges() {
246 let block_ranges = vec![(0, 2), (4, 6)];
247 let byte_ranges = byte_ranges_from_block_ranges(&block_ranges, 1024, 10000);
248 assert_eq!(byte_ranges, vec![(0, 2047), (4096, 6143)]);
249 }
250
251 #[test]
252 fn test_byte_ranges_clamped_to_file_length() {
253 let block_ranges = vec![(9, 10)];
254 let byte_ranges = byte_ranges_from_block_ranges(&block_ranges, 1024, 9500);
255 assert_eq!(byte_ranges, vec![(9216, 9499)]);
256 }
257}