1use crate::Proxy;
12use crate::{
13 FileId, ProgressEntry, PullResult, PullStream,
14 http::{HttpError, HttpPuller, ReferrerPolicy},
15 reqwest::{BuildClientError, SmartRedirectClient},
16};
17use fast_pull::Puller;
18use parking_lot::Mutex;
19use reqwest::{ClientBuilder, Response, header::HeaderMap, redirect::Policy};
20use std::sync::Arc;
21use url::Url;
22
23pub fn build_client(
27 mut headers: HeaderMap,
28 proxy: Proxy<&str>,
29 #[allow(unused)] accept_invalid_certs: bool,
30 #[allow(unused)] accept_invalid_hostnames: bool,
31 #[allow(unused)] cookie_store: bool,
32 local_addr: Option<std::net::IpAddr>,
33 max_redirects: usize,
34) -> Result<SmartRedirectClient, BuildClientError> {
35 let referer = headers.remove("referer");
36 let referrer_policy = headers
37 .remove("referrer-policy")
38 .and_then(|v| v.to_str().ok().and_then(ReferrerPolicy::parse));
39 let origin = headers.remove("origin");
42 let authorization = headers.remove("authorization");
43 let cookie = headers.remove("cookie");
44 let mut client = ClientBuilder::new()
45 .default_headers(headers)
46 .local_address(local_addr)
47 .redirect(Policy::none());
48 client = match proxy {
49 Proxy::No => client.no_proxy(),
50 Proxy::System => client,
51 Proxy::Custom(p) => client.proxy(reqwest::Proxy::all(p)?),
52 };
53 #[cfg(feature = "reqwest-tls")]
54 {
55 client = client
56 .danger_accept_invalid_certs(accept_invalid_certs)
57 .danger_accept_invalid_hostnames(accept_invalid_hostnames);
58 }
59 #[cfg(feature = "cookie-store")]
60 {
61 client = client.cookie_store(cookie_store);
62 }
63 Ok(SmartRedirectClient::new(
64 client.build()?,
65 referer,
66 referrer_policy,
67 origin,
68 authorization,
69 cookie,
70 max_redirects,
71 ))
72}
73
74#[derive(Debug)]
80pub struct FastDownPuller {
81 inner: HttpPuller<SmartRedirectClient>,
82 headers: Arc<HeaderMap>,
83 proxy: Proxy<Arc<str>>,
84 url: Arc<Url>,
85 accept_invalid_certs: bool,
86 accept_invalid_hostnames: bool,
87 cookie_store: bool,
88 file_id: FileId,
89 resp: Option<Arc<Mutex<Option<Response>>>>,
90 available_ips: Arc<[std::net::IpAddr]>,
91 turn: Arc<std::sync::atomic::AtomicUsize>,
92 max_redirects: usize,
93}
94#[derive(Debug)]
99pub struct FastDownPullerOptions<'a> {
100 pub url: Url,
102 pub headers: Arc<HeaderMap>,
104 pub proxy: Proxy<&'a str>,
106 pub accept_invalid_certs: bool,
108 pub accept_invalid_hostnames: bool,
110 pub cookie_store: bool,
112 pub file_id: FileId,
114 pub resp: Option<Arc<Mutex<Option<Response>>>>,
116 pub available_ips: Arc<[std::net::IpAddr]>,
118 pub max_redirects: usize,
120}
121
122impl FastDownPuller {
123 pub fn new(option: FastDownPullerOptions<'_>) -> Result<Self, BuildClientError> {
127 let turn = Arc::new(std::sync::atomic::AtomicUsize::new(1));
128 let available_ips = option.available_ips;
129 let client = build_client(
130 option.headers.as_ref().clone(),
131 option.proxy,
132 option.accept_invalid_certs,
133 option.accept_invalid_hostnames,
134 option.cookie_store,
135 if available_ips.is_empty() {
136 None
137 } else {
138 available_ips
139 .get(
140 turn.fetch_add(1, std::sync::atomic::Ordering::AcqRel)
141 % available_ips.len(),
142 )
143 .copied()
144 },
145 option.max_redirects,
146 )?;
147 let url = Arc::new(option.url);
148 Ok(Self {
149 inner: HttpPuller::new(
150 url.clone(),
151 client,
152 option.resp.clone(),
153 option.file_id.clone(),
154 ),
155 resp: option.resp,
156 headers: option.headers,
157 proxy: option.proxy.map(Arc::from),
158 url,
159 accept_invalid_certs: option.accept_invalid_certs,
160 accept_invalid_hostnames: option.accept_invalid_hostnames,
161 cookie_store: option.cookie_store,
162 file_id: option.file_id,
163 available_ips,
164 turn,
165 max_redirects: option.max_redirects,
166 })
167 }
168}
169
170impl Clone for FastDownPuller {
171 fn clone(&self) -> Self {
172 let available_ips = self.available_ips.clone();
173 let turn = self.turn.clone();
174 Self {
175 inner: build_client(
176 self.headers.as_ref().clone(),
177 self.proxy.as_deref(),
178 self.accept_invalid_certs,
179 self.accept_invalid_hostnames,
180 self.cookie_store,
181 if available_ips.is_empty() {
182 None
183 } else {
184 available_ips
185 .get(
186 turn.fetch_add(1, std::sync::atomic::Ordering::AcqRel)
187 % available_ips.len(),
188 )
189 .copied()
190 },
191 self.max_redirects,
192 )
193 .map_or_else(
194 |_| self.inner.clone(),
195 |client| {
196 HttpPuller::new(
197 self.url.clone(),
198 client,
199 self.resp.clone(),
200 self.file_id.clone(),
201 )
202 },
203 ),
204 resp: self.resp.clone(),
205 headers: self.headers.clone(),
206 proxy: self.proxy.clone(),
207 url: self.url.clone(),
208 accept_invalid_certs: self.accept_invalid_certs,
209 accept_invalid_hostnames: self.accept_invalid_hostnames,
210 cookie_store: self.cookie_store,
211 file_id: self.file_id.clone(),
212 available_ips,
213 turn,
214 max_redirects: self.max_redirects,
215 }
216 }
217}
218
219impl Puller for FastDownPuller {
220 type Error = HttpError<SmartRedirectClient>;
221 async fn pull(
222 &mut self,
223 range: Option<&ProgressEntry>,
224 ) -> PullResult<impl PullStream<Self::Error>, Self::Error> {
225 Puller::pull(&mut self.inner, range).await
226 }
227}
228
229#[cfg(test)]
230mod tests {
231 #![allow(clippy::unwrap_used, clippy::expect_used)]
232 use super::*;
233
234 fn make_options(url: Url) -> FastDownPullerOptions<'static> {
235 FastDownPullerOptions {
236 url,
237 headers: Arc::new(HeaderMap::new()),
238 proxy: Proxy::No,
239 accept_invalid_certs: false,
240 accept_invalid_hostnames: false,
241 cookie_store: false,
242 file_id: FileId::default(),
243 resp: None,
244 available_ips: Arc::from(Vec::<std::net::IpAddr>::new()),
245 max_redirects: 10,
246 }
247 }
248
249 #[test]
254 fn test_fast_down_puller_clone_shares_url_arc() {
255 let opts = make_options(Url::parse("http://example.com/a.bin").unwrap());
256 let puller = FastDownPuller::new(opts).expect("FastDownPuller::new must succeed");
257 let cloned = puller.clone();
258 assert!(
259 Arc::ptr_eq(&puller.url, &cloned.url),
260 "clone() must share the same Arc<Url> (URL stored once)"
261 );
262 }
263
264 #[test]
265 fn build_client_custom_proxy_succeeds() {
266 let headers = HeaderMap::new();
267 let client = build_client(
268 headers,
269 Proxy::Custom("http://127.0.0.1:1080"),
270 false,
271 false,
272 false,
273 None,
274 10,
275 )
276 .expect("build_client with a custom proxy must succeed");
277 let _ = client;
278 }
279
280 #[test]
281 fn new_with_available_ips_rotates_local_address() {
282 let ips: Vec<std::net::IpAddr> = vec!["127.0.0.1".parse().unwrap()];
283 let mut opts = make_options(Url::parse("http://example.com/a.bin").unwrap());
284 opts.available_ips = Arc::from(ips);
285 let puller = FastDownPuller::new(opts).expect("FastDownPuller::new with ips must succeed");
286 let cloned = puller.clone();
287 assert!(Arc::ptr_eq(&puller.url, &cloned.url));
288 }
289
290 #[test]
291 fn clone_with_available_ips_rotates_local_address() {
292 let ips: Vec<std::net::IpAddr> = vec!["127.0.0.1".parse().unwrap()];
293 let mut opts = make_options(Url::parse("http://example.com/a.bin").unwrap());
294 opts.available_ips = Arc::from(ips);
295 let puller = FastDownPuller::new(opts).expect("FastDownPuller::new with ips must succeed");
296 let cloned = puller.clone();
299 assert!(Arc::ptr_eq(&puller.url, &cloned.url));
300 }
301
302 #[test]
303 fn build_client_system_proxy_succeeds() {
304 let headers = HeaderMap::new();
305 let client = build_client(headers, Proxy::System, false, false, false, None, 10)
306 .expect("build_client with the system proxy must succeed");
307 let _ = client;
308 }
309
310 #[tokio::test]
311 async fn fast_down_puller_pull_forwards_to_inner() {
312 let opts = make_options(Url::parse("http://example.com/a.bin").unwrap());
313 let mut puller = FastDownPuller::new(opts).expect("FastDownPuller::new must succeed");
314 let stream = FastDownPuller::pull(&mut puller, None).await;
317 assert!(stream.is_ok());
318 }
319
320 #[test]
321 fn clone_swallows_build_client_error() {
322 let mut puller = FastDownPuller::new(make_options(
328 Url::parse("http://example.com/a.bin").unwrap(),
329 ))
330 .expect("new must succeed");
331 puller.proxy = Proxy::Custom(Arc::from("not a valid proxy url"));
333 let cloned = puller.clone();
336 assert_eq!(
337 cloned.proxy, puller.proxy,
338 "clone must reproduce the proxy and not panic on a bad one"
339 );
340 }
341
342 #[test]
343 fn turn_counter_starts_at_one_offsetting_first_ip_index() {
344 let ips: Vec<std::net::IpAddr> = vec![
349 "127.0.0.1".parse().unwrap(),
350 "127.0.0.2".parse().unwrap(),
351 "127.0.0.3".parse().unwrap(),
352 ];
353 let mut opts = make_options(Url::parse("http://example.com/a.bin").unwrap());
354 opts.available_ips = Arc::from(ips);
355 let puller = FastDownPuller::new(opts).expect("new with ips must succeed");
356 assert_eq!(puller.turn.load(std::sync::atomic::Ordering::Acquire), 2);
358 let cloned = puller.clone();
359 assert_eq!(cloned.turn.load(std::sync::atomic::Ordering::Acquire), 3);
362 assert_eq!(puller.turn.load(std::sync::atomic::Ordering::Acquire), 3);
363 }
364}