use crate::Proxy;
use crate::{
FileId, ProgressEntry, PullResult, PullStream,
http::{HttpError, HttpPuller, ReferrerPolicy},
reqwest::{BuildClientError, SmartRedirectClient},
};
use fast_pull::Puller;
use parking_lot::Mutex;
use reqwest::{ClientBuilder, Response, header::HeaderMap, redirect::Policy};
use std::sync::Arc;
use url::Url;
pub fn build_client(
mut headers: HeaderMap,
proxy: Proxy<&str>,
#[allow(unused)] accept_invalid_certs: bool,
#[allow(unused)] accept_invalid_hostnames: bool,
#[allow(unused)] cookie_store: bool,
local_addr: Option<std::net::IpAddr>,
max_redirects: usize,
) -> Result<SmartRedirectClient, BuildClientError> {
let referer = headers.remove("referer");
let referrer_policy = headers
.remove("referrer-policy")
.and_then(|v| v.to_str().ok().and_then(ReferrerPolicy::parse));
let origin = headers.remove("origin");
let authorization = headers.remove("authorization");
let cookie = headers.remove("cookie");
let mut client = ClientBuilder::new()
.default_headers(headers)
.local_address(local_addr)
.redirect(Policy::none());
client = match proxy {
Proxy::No => client.no_proxy(),
Proxy::System => client,
Proxy::Custom(p) => client.proxy(reqwest::Proxy::all(p)?),
};
#[cfg(feature = "reqwest-tls")]
{
client = client
.danger_accept_invalid_certs(accept_invalid_certs)
.danger_accept_invalid_hostnames(accept_invalid_hostnames);
}
#[cfg(feature = "cookie-store")]
{
client = client.cookie_store(cookie_store);
}
Ok(SmartRedirectClient::new(
client.build()?,
referer,
referrer_policy,
origin,
authorization,
cookie,
max_redirects,
))
}
#[derive(Debug)]
pub struct FastDownPuller {
inner: HttpPuller<SmartRedirectClient>,
headers: Arc<HeaderMap>,
proxy: Proxy<Arc<str>>,
url: Arc<Url>,
accept_invalid_certs: bool,
accept_invalid_hostnames: bool,
cookie_store: bool,
file_id: FileId,
resp: Option<Arc<Mutex<Option<Response>>>>,
available_ips: Arc<[std::net::IpAddr]>,
turn: Arc<std::sync::atomic::AtomicUsize>,
max_redirects: usize,
}
#[derive(Debug)]
pub struct FastDownPullerOptions<'a> {
pub url: Url,
pub headers: Arc<HeaderMap>,
pub proxy: Proxy<&'a str>,
pub accept_invalid_certs: bool,
pub accept_invalid_hostnames: bool,
pub cookie_store: bool,
pub file_id: FileId,
pub resp: Option<Arc<Mutex<Option<Response>>>>,
pub available_ips: Arc<[std::net::IpAddr]>,
pub max_redirects: usize,
}
impl FastDownPuller {
pub fn new(option: FastDownPullerOptions<'_>) -> Result<Self, BuildClientError> {
let turn = Arc::new(std::sync::atomic::AtomicUsize::new(1));
let available_ips = option.available_ips;
let client = build_client(
option.headers.as_ref().clone(),
option.proxy,
option.accept_invalid_certs,
option.accept_invalid_hostnames,
option.cookie_store,
if available_ips.is_empty() {
None
} else {
available_ips
.get(
turn.fetch_add(1, std::sync::atomic::Ordering::AcqRel)
% available_ips.len(),
)
.copied()
},
option.max_redirects,
)?;
let url = Arc::new(option.url);
Ok(Self {
inner: HttpPuller::new(
url.clone(),
client,
option.resp.clone(),
option.file_id.clone(),
),
resp: option.resp,
headers: option.headers,
proxy: option.proxy.map(Arc::from),
url,
accept_invalid_certs: option.accept_invalid_certs,
accept_invalid_hostnames: option.accept_invalid_hostnames,
cookie_store: option.cookie_store,
file_id: option.file_id,
available_ips,
turn,
max_redirects: option.max_redirects,
})
}
}
impl Clone for FastDownPuller {
fn clone(&self) -> Self {
let available_ips = self.available_ips.clone();
let turn = self.turn.clone();
Self {
inner: build_client(
self.headers.as_ref().clone(),
self.proxy.as_deref(),
self.accept_invalid_certs,
self.accept_invalid_hostnames,
self.cookie_store,
if available_ips.is_empty() {
None
} else {
available_ips
.get(
turn.fetch_add(1, std::sync::atomic::Ordering::AcqRel)
% available_ips.len(),
)
.copied()
},
self.max_redirects,
)
.map_or_else(
|_| self.inner.clone(),
|client| {
HttpPuller::new(
self.url.clone(),
client,
self.resp.clone(),
self.file_id.clone(),
)
},
),
resp: self.resp.clone(),
headers: self.headers.clone(),
proxy: self.proxy.clone(),
url: self.url.clone(),
accept_invalid_certs: self.accept_invalid_certs,
accept_invalid_hostnames: self.accept_invalid_hostnames,
cookie_store: self.cookie_store,
file_id: self.file_id.clone(),
available_ips,
turn,
max_redirects: self.max_redirects,
}
}
}
impl Puller for FastDownPuller {
type Error = HttpError<SmartRedirectClient>;
async fn pull(
&mut self,
range: Option<&ProgressEntry>,
) -> PullResult<impl PullStream<Self::Error>, Self::Error> {
Puller::pull(&mut self.inner, range).await
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used, clippy::expect_used)]
use super::*;
fn make_options(url: Url) -> FastDownPullerOptions<'static> {
FastDownPullerOptions {
url,
headers: Arc::new(HeaderMap::new()),
proxy: Proxy::No,
accept_invalid_certs: false,
accept_invalid_hostnames: false,
cookie_store: false,
file_id: FileId::default(),
resp: None,
available_ips: Arc::from(Vec::<std::net::IpAddr>::new()),
max_redirects: 10,
}
}
#[test]
fn test_fast_down_puller_clone_shares_url_arc() {
let opts = make_options(Url::parse("http://example.com/a.bin").unwrap());
let puller = FastDownPuller::new(opts).expect("FastDownPuller::new must succeed");
let cloned = puller.clone();
assert!(
Arc::ptr_eq(&puller.url, &cloned.url),
"clone() must share the same Arc<Url> (URL stored once)"
);
}
#[test]
fn build_client_custom_proxy_succeeds() {
let headers = HeaderMap::new();
let client = build_client(
headers,
Proxy::Custom("http://127.0.0.1:1080"),
false,
false,
false,
None,
10,
)
.expect("build_client with a custom proxy must succeed");
let _ = client;
}
#[test]
fn new_with_available_ips_rotates_local_address() {
let ips: Vec<std::net::IpAddr> = vec!["127.0.0.1".parse().unwrap()];
let mut opts = make_options(Url::parse("http://example.com/a.bin").unwrap());
opts.available_ips = Arc::from(ips);
let puller = FastDownPuller::new(opts).expect("FastDownPuller::new with ips must succeed");
let cloned = puller.clone();
assert!(Arc::ptr_eq(&puller.url, &cloned.url));
}
#[test]
fn clone_with_available_ips_rotates_local_address() {
let ips: Vec<std::net::IpAddr> = vec!["127.0.0.1".parse().unwrap()];
let mut opts = make_options(Url::parse("http://example.com/a.bin").unwrap());
opts.available_ips = Arc::from(ips);
let puller = FastDownPuller::new(opts).expect("FastDownPuller::new with ips must succeed");
let cloned = puller.clone();
assert!(Arc::ptr_eq(&puller.url, &cloned.url));
}
#[test]
fn build_client_system_proxy_succeeds() {
let headers = HeaderMap::new();
let client = build_client(headers, Proxy::System, false, false, false, None, 10)
.expect("build_client with the system proxy must succeed");
let _ = client;
}
#[tokio::test]
async fn fast_down_puller_pull_forwards_to_inner() {
let opts = make_options(Url::parse("http://example.com/a.bin").unwrap());
let mut puller = FastDownPuller::new(opts).expect("FastDownPuller::new must succeed");
let stream = FastDownPuller::pull(&mut puller, None).await;
assert!(stream.is_ok());
}
#[test]
fn clone_swallows_build_client_error() {
let mut puller = FastDownPuller::new(make_options(
Url::parse("http://example.com/a.bin").unwrap(),
))
.expect("new must succeed");
puller.proxy = Proxy::Custom(Arc::from("not a valid proxy url"));
let cloned = puller.clone();
assert_eq!(
cloned.proxy, puller.proxy,
"clone must reproduce the proxy and not panic on a bad one"
);
}
#[test]
fn turn_counter_starts_at_one_offsetting_first_ip_index() {
let ips: Vec<std::net::IpAddr> = vec![
"127.0.0.1".parse().unwrap(),
"127.0.0.2".parse().unwrap(),
"127.0.0.3".parse().unwrap(),
];
let mut opts = make_options(Url::parse("http://example.com/a.bin").unwrap());
opts.available_ips = Arc::from(ips);
let puller = FastDownPuller::new(opts).expect("new with ips must succeed");
assert_eq!(puller.turn.load(std::sync::atomic::Ordering::Acquire), 2);
let cloned = puller.clone();
assert_eq!(cloned.turn.load(std::sync::atomic::Ordering::Acquire), 3);
assert_eq!(puller.turn.load(std::sync::atomic::Ordering::Acquire), 3);
}
}