use crate::fs::{self, FsError};
use async_trait::async_trait;
use reqwest::{
Client, Response,
header::{HeaderMap, HeaderName, HeaderValue},
};
use std::collections::HashMap;
use std::fmt::Debug;
use std::fs::File;
use std::io::Write;
use std::net::{IpAddr, Shutdown, SocketAddr, TcpStream, ToSocketAddrs};
use std::path::Path;
use std::sync::{Arc, mpsc};
use std::thread;
use std::time::Duration;
use tracing::{instrument, trace};
use url::Url;
pub use crate::net_error::NetError;
#[async_trait]
pub trait Downloader: Send {
async fn download(&self, url: Url) -> Result<Response, NetError>;
}
pub type BoxedDownloader = Box<dyn Downloader>;
#[derive(Default)]
pub struct DefaultDownloader {
client: reqwest::Client,
headers: Option<HeaderMap>,
}
fn headers_from_map(map: &HashMap<String, String>) -> Result<HeaderMap, NetError> {
let mut headers = HeaderMap::new();
for (k, v) in map {
let name = HeaderName::try_from(k.as_str()).map_err(|e| NetError::HttpUnknown {
url: String::new(),
error: format!("invalid header name {:?}: {}", k, e),
})?;
let value = HeaderValue::try_from(v.as_str()).map_err(|e| NetError::HttpUnknown {
url: String::new(),
error: format!("invalid header value {:?}: {}", v, e),
})?;
headers.insert(name, value);
}
Ok(headers)
}
impl DefaultDownloader {
pub fn new_with_headers(headers: HashMap<String, String>) -> Result<Self, NetError> {
Ok(Self {
client: Client::new(),
headers: Some(headers_from_map(&headers)?),
})
}
pub fn new_with_client_and_headers(
client: Client,
headers: HashMap<String, String>,
) -> Result<Self, NetError> {
Ok(Self {
client,
headers: Some(headers_from_map(&headers)?),
})
}
}
#[async_trait]
impl Downloader for DefaultDownloader {
async fn download(&self, url: Url) -> Result<Response, NetError> {
let mut request = self.client.get(url.clone());
if let Some(headers) = &self.headers {
request = request.headers(headers.clone());
}
request.send().await.map_err(|error| NetError::Http {
error: Box::new(error),
url: url.to_string(),
})
}
}
pub type OnChunkFn = Arc<dyn Fn(u64, u64) + Send + Sync>;
pub struct DownloadOptions {
pub downloader: Option<BoxedDownloader>,
pub file: Option<File>,
pub lock: bool,
pub on_chunk: Option<OnChunkFn>,
}
impl DownloadOptions {
pub fn new(downloader: impl Downloader + 'static) -> Self {
Self {
downloader: Some(Box::new(downloader)),
file: None,
lock: true,
on_chunk: None,
}
}
}
impl Default for DownloadOptions {
fn default() -> Self {
Self {
downloader: None,
file: None,
lock: true,
on_chunk: None,
}
}
}
#[instrument(skip(options))]
pub async fn download_from_url_with_options<S: AsRef<str> + Debug, D: AsRef<Path> + Debug>(
source_url: S,
dest_file: D,
options: DownloadOptions,
) -> Result<(), NetError> {
let source_url = source_url.as_ref();
let dest_file = dest_file.as_ref();
let DownloadOptions {
downloader,
file: file_handle,
lock: lock_file,
on_chunk,
} = options;
let downloader = downloader.unwrap_or_else(|| Box::new(DefaultDownloader::default()));
let handle_fs_error = |error: std::io::Error| FsError::Write {
path: dest_file.to_path_buf(),
error: Box::new(error),
};
let handle_net_error = |error: reqwest::Error| NetError::Http {
error: Box::new(error),
url: source_url.to_owned(),
};
trace!(
source_url,
dest_file = ?dest_file,
"Downloading file from remote URL to local file",
);
let mut response = downloader
.download(
Url::parse(source_url).map_err(|error| NetError::UrlParseFailed {
url: source_url.to_owned(),
error: Box::new(error),
})?,
)
.await?;
let status = response.status();
if status.as_u16() == 404 {
return Err(NetError::UrlNotFound {
url: source_url.to_owned(),
});
}
if !status.is_success() {
return Err(NetError::DownloadFailed {
url: source_url.to_owned(),
status: status.to_string(),
});
}
let mut file = match file_handle {
Some(file) => file,
None => fs::create_file_if_missing(dest_file)?,
};
#[cfg(feature = "fs-lock")]
if lock_file {
fs::acquire_exclusive_lock(dest_file, &file)?;
}
let write_result = async {
fs::truncate_file_handle(dest_file, &mut file)?;
let total_size = response.content_length().unwrap_or(0);
let mut current_size: u64 = 0;
if let Some(on_chunk) = &on_chunk {
on_chunk(0, total_size);
}
while let Some(chunk) = response.chunk().await.map_err(handle_net_error)? {
file.write_all(&chunk).map_err(handle_fs_error)?;
if let Some(on_chunk) = &on_chunk {
current_size = current_size.saturating_add(chunk.len() as u64);
on_chunk(
if total_size > 0 {
current_size.min(total_size)
} else {
current_size
},
total_size,
);
}
}
Ok::<(), NetError>(())
}
.await;
match write_result {
Ok(_) => {
#[cfg(feature = "fs-lock")]
if lock_file {
let _ = fs::release_lock(dest_file, &file);
}
Ok(())
}
Err(error) => {
#[cfg(feature = "fs-lock")]
if lock_file {
let _ = fs::release_lock(dest_file, &file);
}
let _ = fs::remove_file(dest_file);
Err(error)
}
}
}
pub async fn download_from_url_with_client<S: AsRef<str> + Debug, D: AsRef<Path> + Debug>(
source_url: S,
dest_file: D,
client: &Client,
) -> Result<(), NetError> {
download_from_url_with_options(
source_url,
dest_file,
DownloadOptions::new(DefaultDownloader {
client: client.to_owned(),
headers: None,
}),
)
.await
}
pub async fn download_from_url<S: AsRef<str> + Debug, D: AsRef<Path> + Debug>(
source_url: S,
dest_file: D,
) -> Result<(), NetError> {
download_from_url_with_options(source_url, dest_file, DownloadOptions::default()).await
}
mod offline {
use super::*;
pub fn check_connection(address: SocketAddr, timeout: u64) -> bool {
trace!("Resolving {address}");
if let Ok(stream) = TcpStream::connect_timeout(&address, Duration::from_millis(timeout)) {
let _ = stream.shutdown(Shutdown::Both);
return true;
}
false
}
pub fn check_connection_from_host(host: String, timeout: u64) -> bool {
let (sender, receiver) = mpsc::channel();
thread::spawn(move || {
let _ = sender.send(host.to_socket_addrs().ok());
});
if let Ok(Some(addresses)) = receiver.recv_timeout(Duration::from_millis(timeout)) {
for address in addresses {
if check_connection(address, timeout) {
return true;
}
}
}
false
}
}
#[derive(Debug)]
pub struct OfflineOptions {
pub check_default_hosts: bool,
pub check_default_ips: bool,
pub custom_hosts: Vec<String>,
pub custom_ips: Vec<IpAddr>,
pub ip_v4: bool,
pub ip_v6: bool,
pub timeout: u64,
}
impl Default for OfflineOptions {
fn default() -> Self {
Self {
check_default_hosts: true,
check_default_ips: true,
custom_hosts: vec![],
custom_ips: vec![],
ip_v4: true,
ip_v6: true,
timeout: 1000,
}
}
}
#[instrument]
pub fn is_offline(timeout: u64) -> bool {
is_offline_with_options(OfflineOptions {
timeout,
..Default::default()
})
}
#[instrument]
pub fn is_offline_with_hosts(timeout: u64, custom_hosts: Vec<String>) -> bool {
is_offline_with_options(OfflineOptions {
timeout,
custom_hosts,
..Default::default()
})
}
#[instrument]
pub fn is_offline_with_options(options: OfflineOptions) -> bool {
trace!(
timeout = options.timeout,
"Checking for an internet connection"
);
let mut ips = vec![];
if options.check_default_ips {
if options.ip_v4 {
ips.extend([
SocketAddr::from(([1, 1, 1, 1], 53)),
SocketAddr::from(([1, 0, 0, 1], 53)),
SocketAddr::from(([8, 8, 8, 8], 53)),
SocketAddr::from(([8, 8, 4, 4], 53)),
]);
}
if options.ip_v6 {
ips.extend([
SocketAddr::from(([0x2606, 0x4700, 0x4700, 0, 0, 0, 0, 0x1111], 53)),
SocketAddr::from(([0x2606, 0x4700, 0x4700, 0, 0, 0, 0, 0x1001], 53)),
SocketAddr::from(([0x2001, 0x4860, 0x4860, 0, 0, 0, 0, 0x8888], 53)),
SocketAddr::from(([0x2001, 0x4860, 0x4860, 0, 0, 0, 0, 0x8844], 53)),
]);
}
}
for custom_ip in options.custom_ips {
ips.push(SocketAddr::new(custom_ip, 53));
}
let (sender, receiver) = mpsc::channel();
let checks = ips.len();
for address in ips {
let sender = sender.clone();
let timeout = options.timeout;
thread::spawn(move || {
let _ = sender.send(offline::check_connection(address, timeout));
});
}
drop(sender);
let online = (0..checks).any(|_| receiver.recv().is_ok_and(|online| online));
if online {
trace!("Online!");
return false;
}
let mut hosts = vec![];
if options.check_default_hosts {
hosts.extend([
"clients3.google.com:80".to_owned(),
"detectportal.firefox.com:80".to_owned(),
"google.com:80".to_owned(),
]);
}
if !options.custom_hosts.is_empty() {
hosts.extend(options.custom_hosts);
}
let (sender, receiver) = mpsc::channel();
let checks = hosts.len();
for host in hosts {
let sender = sender.clone();
let timeout = options.timeout;
thread::spawn(move || {
let _ = sender.send(offline::check_connection_from_host(host, timeout));
});
}
drop(sender);
let online = (0..checks).any(|_| receiver.recv().is_ok_and(|online| online));
if online {
trace!("Online!");
return false;
}
trace!("Offline!!!");
true
}