2025-08-05 09:56:11 +03:00
|
|
|
use std::{
|
2025-08-30 23:02:18 +03:00
|
|
|
io,
|
2025-08-12 11:32:17 +03:00
|
|
|
net::SocketAddr,
|
|
|
|
|
sync::Arc,
|
2025-08-05 09:56:11 +03:00
|
|
|
time::{Duration, SystemTime},
|
|
|
|
|
};
|
2025-07-24 10:19:22 +03:00
|
|
|
|
2025-08-30 23:02:18 +03:00
|
|
|
use crate::config::Config;
|
2025-08-04 13:21:53 +03:00
|
|
|
|
2025-07-24 10:19:22 +03:00
|
|
|
const DEFAULT_MAX_RETRIES: u32 = 2;
|
|
|
|
|
const INITIAL_RETRY_DELAY: Duration = Duration::from_millis(500);
|
|
|
|
|
const MAX_RETRY_DELAY: Duration = Duration::from_secs(8);
|
|
|
|
|
|
|
|
|
|
static RETRY_STATUSES: &[reqwest::StatusCode] = &[
|
|
|
|
|
reqwest::StatusCode::REQUEST_TIMEOUT,
|
|
|
|
|
reqwest::StatusCode::TOO_MANY_REQUESTS,
|
|
|
|
|
reqwest::StatusCode::INTERNAL_SERVER_ERROR,
|
|
|
|
|
reqwest::StatusCode::BAD_GATEWAY,
|
|
|
|
|
reqwest::StatusCode::SERVICE_UNAVAILABLE,
|
|
|
|
|
reqwest::StatusCode::GATEWAY_TIMEOUT,
|
|
|
|
|
];
|
|
|
|
|
|
2025-08-05 09:56:11 +03:00
|
|
|
#[derive(Clone, serde::Deserialize)]
|
|
|
|
|
pub struct BasicAuth {
|
2025-12-24 11:24:11 +03:00
|
|
|
pub username: compact_str::CompactString,
|
|
|
|
|
pub password: Option<compact_str::CompactString>,
|
2025-08-05 09:56:11 +03:00
|
|
|
}
|
|
|
|
|
|
2026-01-08 16:19:57 +03:00
|
|
|
#[derive(Clone)]
|
2025-08-12 11:32:17 +03:00
|
|
|
pub struct HickoryDnsResolver(Arc<hickory_resolver::TokioResolver>);
|
|
|
|
|
|
|
|
|
|
impl HickoryDnsResolver {
|
2026-04-16 12:32:42 +00:00
|
|
|
pub async fn new() -> crate::Result<Self> {
|
2026-04-16 21:07:59 +03:00
|
|
|
let mut builder = tokio::task::spawn_blocking(
|
2025-10-01 09:24:55 +03:00
|
|
|
hickory_resolver::TokioResolver::builder_tokio,
|
|
|
|
|
)
|
|
|
|
|
.await?
|
|
|
|
|
.unwrap_or_else(|_| {
|
|
|
|
|
hickory_resolver::TokioResolver::builder_with_config(
|
2026-04-16 12:32:42 +00:00
|
|
|
hickory_resolver::config::ResolverConfig::udp_and_tcp(
|
|
|
|
|
&hickory_resolver::config::GOOGLE,
|
2025-08-12 11:32:17 +03:00
|
|
|
),
|
2026-04-16 12:32:42 +00:00
|
|
|
hickory_resolver::net::runtime::TokioRuntimeProvider::default(),
|
2025-08-12 11:32:17 +03:00
|
|
|
)
|
2026-04-16 21:07:59 +03:00
|
|
|
});
|
|
|
|
|
builder.options_mut().ip_strategy =
|
|
|
|
|
hickory_resolver::config::LookupIpStrategy::Ipv4AndIpv6;
|
|
|
|
|
Ok(Self(Arc::new(builder.build()?)))
|
2025-08-12 11:32:17 +03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl reqwest::dns::Resolve for HickoryDnsResolver {
|
|
|
|
|
fn resolve(&self, name: reqwest::dns::Name) -> reqwest::dns::Resolving {
|
|
|
|
|
let resolver = Arc::clone(&self.0);
|
|
|
|
|
Box::pin(async move {
|
2025-10-03 09:57:00 +03:00
|
|
|
let lookup = resolver.lookup_ip(name.as_str()).await;
|
|
|
|
|
drop(name);
|
2025-08-15 17:19:50 +03:00
|
|
|
drop(resolver);
|
2025-08-12 11:32:17 +03:00
|
|
|
let addrs: reqwest::dns::Addrs = Box::new(
|
2026-04-16 12:32:42 +00:00
|
|
|
lookup?
|
|
|
|
|
.iter()
|
|
|
|
|
.collect::<Vec<_>>()
|
2026-04-16 19:23:21 +03:00
|
|
|
.into_iter()
|
|
|
|
|
.map(|ip_addr| SocketAddr::new(ip_addr, 0)),
|
2025-08-12 11:32:17 +03:00
|
|
|
);
|
|
|
|
|
Ok(addrs)
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-15 20:03:05 +03:00
|
|
|
pub async fn build_rustls_config() -> crate::Result<rustls::ClientConfig> {
|
2026-01-14 19:45:48 +03:00
|
|
|
let provider = Arc::new(rustls::crypto::aws_lc_rs::default_provider());
|
|
|
|
|
|
|
|
|
|
Ok(rustls::ClientConfig::builder_with_provider(Arc::clone(&provider))
|
|
|
|
|
.with_protocol_versions(rustls::ALL_VERSIONS)?
|
|
|
|
|
.dangerous()
|
|
|
|
|
.with_custom_certificate_verifier(Arc::new(
|
2026-01-15 20:03:05 +03:00
|
|
|
tokio::task::spawn_blocking(move || {
|
|
|
|
|
rustls_platform_verifier::Verifier::new(provider)
|
|
|
|
|
})
|
|
|
|
|
.await??,
|
2026-01-14 19:45:48 +03:00
|
|
|
))
|
|
|
|
|
.with_no_client_auth())
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-15 23:35:35 +03:00
|
|
|
pub struct RetryMiddleware;
|
|
|
|
|
|
2025-07-24 10:19:22 +03:00
|
|
|
fn parse_retry_after(headers: &reqwest::header::HeaderMap) -> Option<Duration> {
|
|
|
|
|
if let Some(val) = headers.get("retry-after-ms")
|
|
|
|
|
&& let Ok(s) = val.to_str()
|
|
|
|
|
&& let Ok(ms) = s.parse()
|
|
|
|
|
{
|
|
|
|
|
return Some(Duration::from_millis(ms));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if let Some(val) = headers.get(reqwest::header::RETRY_AFTER)
|
|
|
|
|
&& let Ok(s) = val.to_str()
|
|
|
|
|
{
|
|
|
|
|
if let Ok(sec) = s.parse() {
|
|
|
|
|
return Some(Duration::from_secs(sec));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if let Ok(parsed) = httpdate::parse_http_date(s)
|
|
|
|
|
&& let Ok(dur) = parsed.duration_since(SystemTime::now())
|
|
|
|
|
{
|
|
|
|
|
return Some(dur);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
None
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn calculate_retry_timeout(
|
|
|
|
|
headers: Option<&reqwest::header::HeaderMap>,
|
|
|
|
|
attempt: u32,
|
|
|
|
|
) -> Option<Duration> {
|
|
|
|
|
if let Some(h) = headers
|
|
|
|
|
&& let Some(after) = parse_retry_after(h)
|
|
|
|
|
{
|
2026-03-06 12:00:29 +03:00
|
|
|
if after > Duration::from_mins(1) {
|
2025-07-24 10:19:22 +03:00
|
|
|
return None;
|
|
|
|
|
}
|
|
|
|
|
return Some(after);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let base = INITIAL_RETRY_DELAY
|
|
|
|
|
.saturating_mul(2_u32.pow(attempt))
|
|
|
|
|
.min(MAX_RETRY_DELAY);
|
|
|
|
|
let jitter = 0.25_f64.mul_add(-rand::random::<f64>(), 1.0);
|
|
|
|
|
Some(base.mul_f64(jitter))
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-30 23:02:18 +03:00
|
|
|
#[async_trait::async_trait]
|
|
|
|
|
impl reqwest_middleware::Middleware for RetryMiddleware {
|
|
|
|
|
async fn handle(
|
|
|
|
|
&self,
|
|
|
|
|
req: reqwest::Request,
|
|
|
|
|
extensions: &mut http::Extensions,
|
|
|
|
|
next: reqwest_middleware::Next<'_>,
|
|
|
|
|
) -> reqwest_middleware::Result<reqwest::Response> {
|
|
|
|
|
let mut attempt: u32 = 0;
|
|
|
|
|
loop {
|
2025-08-30 23:36:39 +03:00
|
|
|
let req = req.try_clone().ok_or_else(|| {
|
2025-08-30 23:02:18 +03:00
|
|
|
reqwest_middleware::Error::middleware(io::Error::other(
|
|
|
|
|
"Request object is not cloneable",
|
|
|
|
|
))
|
|
|
|
|
})?;
|
|
|
|
|
|
2025-08-30 23:36:39 +03:00
|
|
|
match next.clone().run(req, extensions).await {
|
2025-08-30 23:02:18 +03:00
|
|
|
Ok(resp) => {
|
|
|
|
|
let status = resp.status();
|
|
|
|
|
if status.is_client_error() || status.is_server_error() {
|
|
|
|
|
if attempt < DEFAULT_MAX_RETRIES
|
|
|
|
|
&& RETRY_STATUSES.contains(&status)
|
|
|
|
|
&& let Some(delay) = calculate_retry_timeout(
|
|
|
|
|
Some(resp.headers()),
|
|
|
|
|
attempt,
|
|
|
|
|
)
|
|
|
|
|
{
|
|
|
|
|
tokio::time::sleep(delay).await;
|
|
|
|
|
attempt = attempt.saturating_add(1);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
resp.error_for_status_ref()?;
|
|
|
|
|
}
|
|
|
|
|
return Ok(resp);
|
|
|
|
|
}
|
|
|
|
|
Err(err) => {
|
2025-07-24 10:19:22 +03:00
|
|
|
if attempt < DEFAULT_MAX_RETRIES
|
2025-08-30 23:02:18 +03:00
|
|
|
&& err.is_connect()
|
|
|
|
|
&& let Some(delay) =
|
|
|
|
|
calculate_retry_timeout(None, attempt)
|
2025-07-24 10:19:22 +03:00
|
|
|
{
|
|
|
|
|
tokio::time::sleep(delay).await;
|
|
|
|
|
attempt = attempt.saturating_add(1);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
2025-08-30 23:02:18 +03:00
|
|
|
return Err(err);
|
2025-07-24 10:19:22 +03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-07-24 10:42:23 +03:00
|
|
|
|
2025-08-12 11:32:17 +03:00
|
|
|
pub fn create_reqwest_client<R: reqwest::dns::Resolve + 'static>(
|
2025-08-04 13:21:53 +03:00
|
|
|
config: &Config,
|
2026-01-08 16:19:57 +03:00
|
|
|
dns_resolver: R,
|
2026-01-14 19:45:48 +03:00
|
|
|
mut tls_backend: rustls::ClientConfig,
|
|
|
|
|
) -> crate::Result<reqwest_middleware::ClientWithMiddleware> {
|
|
|
|
|
tls_backend.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()];
|
2025-08-05 09:56:11 +03:00
|
|
|
let mut builder = reqwest::ClientBuilder::new()
|
2025-12-25 10:46:25 +03:00
|
|
|
.user_agent(config.scraping.user_agent.as_bytes())
|
2025-08-04 13:21:53 +03:00
|
|
|
.timeout(config.scraping.timeout)
|
|
|
|
|
.connect_timeout(config.scraping.connect_timeout)
|
2026-01-14 19:45:48 +03:00
|
|
|
.tls_backend_preconfigured(tls_backend)
|
2025-08-12 11:32:17 +03:00
|
|
|
.dns_resolver(dns_resolver);
|
2025-08-12 16:11:50 +03:00
|
|
|
|
2025-10-03 09:57:00 +03:00
|
|
|
if let Some(proxy) = config.scraping.proxy.clone() {
|
|
|
|
|
builder = builder.proxy(reqwest::Proxy::all(proxy)?);
|
2025-08-05 09:56:11 +03:00
|
|
|
}
|
2025-08-12 16:11:50 +03:00
|
|
|
|
2025-08-30 23:02:18 +03:00
|
|
|
let client = builder.build()?;
|
|
|
|
|
let client_with_middleware = reqwest_middleware::ClientBuilder::new(client)
|
|
|
|
|
.with(RetryMiddleware)
|
|
|
|
|
.build();
|
|
|
|
|
|
|
|
|
|
Ok(client_with_middleware)
|
2025-07-24 10:42:23 +03:00
|
|
|
}
|