Skip to main content

zsync_rs/
http.rs

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
27/// HTTP transport for fetching control files and byte ranges.
28pub 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    /// A client with ureq's default policy, permitting plain HTTP.
40    ///
41    /// zsync is routinely served over HTTP and across mirror redirects, so
42    /// the defaults are permissive on purpose. An embedder with a stricter
43    /// policy, such as one that must refuse a downgrade to HTTP or pin its
44    /// own trust roots, should build an agent and pass it to
45    /// [`HttpClient::with_agent`] rather than relying on these.
46    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    /// A client using a caller-supplied agent.
56    ///
57    /// The agent carries the whole transport policy: TLS roots, whether a
58    /// redirect may downgrade to HTTP, timeouts and proxies. Supplying one
59    /// keeps that decision with the embedder, which is the only place that
60    /// knows what the transferred bytes are trusted for.
61    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    /// A reader over `start..=end` of `url`.
77    ///
78    /// The reader is capped at the requested length. Without that cap the
79    /// origin decides how much the client reads: a response longer than
80    /// the range is consumed in full, and callers that accumulate it, such
81    /// as block assembly, grow a buffer to match. Bytes past the range
82    /// were never asked for and are of no use, so they are not read.
83    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
137/// Default gap threshold for range merging (256 KiB, same as zsync2).
138pub const DEFAULT_RANGE_GAP_THRESHOLD: u64 = 256 * 1024;
139
140/// Merge byte ranges to minimize HTTP requests.
141/// Gaps smaller than the threshold are merged to save HTTP round-trips.
142pub 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    /// A server that answers any request with far more data than asked
181    /// for, which is what a hostile or broken origin does.
182    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        // 1 KiB asked for, 8 MiB offered. Accepting the surplus lets any
212        // origin decide how much memory the client spends.
213        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        // The point of `with_agent` is that the embedder's policy reaches
226        // the transfer. A client built with an HTTPS-only agent must refuse
227        // a plain-HTTP URL, where the default client would allow it.
228        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}