261 lines
8.4 KiB
Rust
261 lines
8.4 KiB
Rust
use std::{
|
|
collections::HashMap,
|
|
io, iter,
|
|
net::{IpAddr, Ipv4Addr},
|
|
sync::Arc,
|
|
};
|
|
|
|
use color_eyre::eyre::WrapErr as _;
|
|
|
|
use crate::{
|
|
config::Config,
|
|
ipdb,
|
|
proxy::{Proxy, ProxyType},
|
|
utils::is_docker,
|
|
};
|
|
|
|
const fn sort_by_timeout(proxy: &Proxy) -> tokio::time::Duration {
|
|
if let Some(timeout) = proxy.timeout {
|
|
timeout
|
|
} else {
|
|
tokio::time::Duration::MAX
|
|
}
|
|
}
|
|
|
|
fn sort_naturally(proxy: &Proxy) -> (ProxyType, Vec<u8>, u16) {
|
|
let host_key = proxy.host.parse::<Ipv4Addr>().map_or_else(
|
|
move |_| iter::repeat_n(u8::MAX, 4).chain(proxy.host.bytes()).collect(),
|
|
|ip| ip.octets().to_vec(),
|
|
);
|
|
(proxy.protocol.clone(), host_key, proxy.port)
|
|
}
|
|
|
|
#[derive(serde::Serialize)]
|
|
struct ProxyJson<'a> {
|
|
protocol: ProxyType,
|
|
username: Option<String>,
|
|
password: Option<String>,
|
|
host: String,
|
|
port: u16,
|
|
timeout: Option<f64>,
|
|
exit_ip: Option<String>,
|
|
asn: Option<maxminddb::geoip2::Asn<'a>>,
|
|
geolocation: Option<maxminddb::geoip2::City<'a>>,
|
|
}
|
|
|
|
fn group_proxies<'a>(
|
|
config: &Config,
|
|
proxies: &'a [Proxy],
|
|
) -> HashMap<ProxyType, Vec<&'a Proxy>> {
|
|
let mut groups: HashMap<_, _> =
|
|
config.enabled_protocols().map(|p| (p.clone(), Vec::new())).collect();
|
|
for proxy in proxies {
|
|
if let Some(proxies) = groups.get_mut(&proxy.protocol) {
|
|
proxies.push(proxy);
|
|
}
|
|
}
|
|
groups
|
|
}
|
|
|
|
#[expect(clippy::too_many_lines)]
|
|
pub async fn save_proxies(
|
|
config: Arc<Config>,
|
|
mut proxies: Vec<Proxy>,
|
|
) -> color_eyre::Result<()> {
|
|
if config.output.sort_by_speed {
|
|
proxies.sort_by_key(sort_by_timeout);
|
|
} else {
|
|
proxies.sort_by_key(sort_naturally);
|
|
}
|
|
|
|
if config.output.json.enabled {
|
|
let (maybe_asn_db, maybe_geo_db) = tokio::try_join!(
|
|
async {
|
|
if config.output.json.include_asn {
|
|
ipdb::DbType::Asn.open_mmap().await.map(Some)
|
|
} else {
|
|
Ok(None)
|
|
}
|
|
},
|
|
async {
|
|
if config.output.json.include_geolocation {
|
|
ipdb::DbType::Geo.open_mmap().await.map(Some)
|
|
} else {
|
|
Ok(None)
|
|
}
|
|
}
|
|
)?;
|
|
|
|
let mut proxy_dicts = Vec::with_capacity(proxies.len());
|
|
for proxy in &proxies {
|
|
proxy_dicts.push(ProxyJson {
|
|
protocol: proxy.protocol.clone(),
|
|
username: proxy.username.clone(),
|
|
password: proxy.password.clone(),
|
|
host: proxy.host.clone(),
|
|
port: proxy.port,
|
|
timeout: proxy
|
|
.timeout
|
|
.map(|d| (d.as_secs_f64() * 100.0).round() / 100.0_f64),
|
|
exit_ip: proxy.exit_ip.clone(),
|
|
asn: if let Some(asn_db) = &maybe_asn_db {
|
|
if let Some(exit_ip) = proxy.exit_ip.clone() {
|
|
let exit_ip_addr: IpAddr = exit_ip.parse().wrap_err(
|
|
"failed to parse proxy's exit ip as IpAddr",
|
|
)?;
|
|
asn_db
|
|
.lookup::<maxminddb::geoip2::Asn>(exit_ip_addr)
|
|
.wrap_err_with(move || {
|
|
format!(
|
|
"failed to lookup {exit_ip_addr} in ASN \
|
|
database"
|
|
)
|
|
})?
|
|
} else {
|
|
None
|
|
}
|
|
} else {
|
|
None
|
|
},
|
|
geolocation: if let Some(geo_db) = &maybe_geo_db {
|
|
if let Some(exit_ip) = proxy.exit_ip.clone() {
|
|
let exit_ip_addr: IpAddr = exit_ip.parse().wrap_err(
|
|
"failed to parse proxy's exit ip as IpAddr",
|
|
)?;
|
|
geo_db
|
|
.lookup::<maxminddb::geoip2::City>(exit_ip_addr)
|
|
.wrap_err_with(move || {
|
|
format!(
|
|
"failed to lookup {exit_ip_addr} in \
|
|
geolocation database"
|
|
)
|
|
})?
|
|
} else {
|
|
None
|
|
}
|
|
} else {
|
|
None
|
|
},
|
|
});
|
|
}
|
|
|
|
for (path, pretty) in [
|
|
(config.output.path.join("proxies.json"), false),
|
|
(config.output.path.join("proxies_pretty.json"), true),
|
|
] {
|
|
match tokio::fs::remove_file(&path).await {
|
|
Ok(()) => Ok(()),
|
|
Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(()),
|
|
Err(e) => Err(e).wrap_err_with(|| {
|
|
format!("failed to remove file {}", path.display())
|
|
}),
|
|
}?;
|
|
let json_data = if pretty {
|
|
serde_json::to_vec_pretty(&proxy_dicts)
|
|
.wrap_err("failed to serialize proxies to pretty json")?
|
|
} else {
|
|
serde_json::to_vec(&proxy_dicts)
|
|
.wrap_err("failed to serialize proxies to json")?
|
|
};
|
|
tokio::fs::write(&path, json_data).await.wrap_err_with(
|
|
move || {
|
|
format!("failed to write proxies to {}", path.display())
|
|
},
|
|
)?;
|
|
}
|
|
}
|
|
|
|
if config.output.txt.enabled {
|
|
let grouped_proxies = group_proxies(&config, &proxies);
|
|
|
|
for (anonymous_only, directory) in
|
|
[(false, "proxies"), (true, "proxies_anonymous")]
|
|
{
|
|
let directory_path = config.output.path.join(directory);
|
|
match tokio::fs::remove_dir_all(&directory_path).await {
|
|
Ok(()) => Ok(()),
|
|
Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(()),
|
|
Err(e) => Err(e).wrap_err_with(|| {
|
|
format!(
|
|
"failed to remove directory {}",
|
|
directory_path.display()
|
|
)
|
|
}),
|
|
}?;
|
|
tokio::fs::create_dir_all(&directory_path).await.wrap_err_with(
|
|
|| {
|
|
format!(
|
|
"failed to create directory: {}",
|
|
directory_path.display()
|
|
)
|
|
},
|
|
)?;
|
|
|
|
let text = create_proxy_list_str(
|
|
&proxies.iter().collect::<Vec<_>>(),
|
|
anonymous_only,
|
|
true,
|
|
);
|
|
tokio::fs::write(directory_path.join("all.txt"), text)
|
|
.await
|
|
.wrap_err_with(|| {
|
|
format!(
|
|
"failed to write proxies to {}",
|
|
directory_path.join("all.txt").display()
|
|
)
|
|
})?;
|
|
|
|
for (proto, proxies) in &grouped_proxies {
|
|
let text =
|
|
create_proxy_list_str(proxies, anonymous_only, false);
|
|
tokio::fs::write(
|
|
directory_path.join(format!("{proto}.txt")),
|
|
text,
|
|
)
|
|
.await
|
|
.wrap_err_with(|| {
|
|
format!(
|
|
"failed to write proxies to {}",
|
|
directory_path.join(format!("{proto}.txt")).display()
|
|
)
|
|
})?;
|
|
}
|
|
}
|
|
}
|
|
|
|
let path = config
|
|
.output
|
|
.path
|
|
.canonicalize()
|
|
.unwrap_or_else(move |_| config.output.path.clone());
|
|
if is_docker().await {
|
|
tracing::info!(
|
|
"Proxies have been saved to ./out ({} in container)",
|
|
path.display()
|
|
);
|
|
} else {
|
|
tracing::info!("Proxies have been saved to {}", path.display());
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn create_proxy_list_str(
|
|
proxies: &[&Proxy],
|
|
anonymous_only: bool,
|
|
include_protocol: bool,
|
|
) -> String {
|
|
proxies
|
|
.iter()
|
|
.filter(move |proxy| {
|
|
!anonymous_only
|
|
|| proxy
|
|
.exit_ip
|
|
.as_ref()
|
|
.is_some_and(move |ip| *ip != proxy.host)
|
|
})
|
|
.map(move |proxy| proxy.as_str(include_protocol))
|
|
.collect::<Vec<_>>()
|
|
.join("\n")
|
|
}
|