2025-05-08 18:20:19 +03:00
|
|
|
#[cfg(feature = "tui")]
|
|
|
|
|
use std::collections::HashSet;
|
|
|
|
|
use std::sync::Arc;
|
2025-04-10 06:55:12 +00:00
|
|
|
|
2025-04-29 13:06:18 +03:00
|
|
|
use color_eyre::eyre::{OptionExt as _, WrapErr as _};
|
2025-04-10 06:55:12 +00:00
|
|
|
|
2025-04-28 08:24:01 +03:00
|
|
|
#[cfg(feature = "tui")]
|
|
|
|
|
use crate::event::{AppEvent, Event};
|
2025-04-10 06:55:12 +00:00
|
|
|
use crate::{
|
|
|
|
|
config::Config,
|
|
|
|
|
parsers::PROXY_REGEX,
|
|
|
|
|
proxy::{Proxy, ProxyType},
|
|
|
|
|
storage::ProxyStorage,
|
2025-04-29 13:06:18 +03:00
|
|
|
utils::{is_http_url, pretty_error},
|
2025-04-10 06:55:12 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
async fn fetch_text(
|
|
|
|
|
config: Arc<Config>,
|
|
|
|
|
http_client: reqwest::Client,
|
|
|
|
|
source: &str,
|
|
|
|
|
) -> color_eyre::Result<String> {
|
2025-04-11 17:05:21 +03:00
|
|
|
if is_http_url(source) {
|
2025-04-10 06:55:12 +00:00
|
|
|
http_client
|
|
|
|
|
.get(source)
|
|
|
|
|
.timeout(config.source_timeout)
|
|
|
|
|
.send()
|
|
|
|
|
.await
|
|
|
|
|
.wrap_err_with(move || {
|
|
|
|
|
format!("failed to send request to {source}")
|
|
|
|
|
})?
|
|
|
|
|
.error_for_status()
|
|
|
|
|
.wrap_err_with(move || {
|
|
|
|
|
format!("got error status code from {source}")
|
|
|
|
|
})?
|
|
|
|
|
.text()
|
|
|
|
|
.await
|
|
|
|
|
.wrap_err_with(move || {
|
|
|
|
|
format!("failed to decode {source} response as text")
|
2025-04-11 17:05:21 +03:00
|
|
|
})
|
2025-04-10 06:55:12 +00:00
|
|
|
} else {
|
|
|
|
|
tokio::fs::read_to_string(
|
|
|
|
|
source.strip_prefix("file://").unwrap_or(source),
|
|
|
|
|
)
|
|
|
|
|
.await
|
2025-04-11 17:05:21 +03:00
|
|
|
.wrap_err("failed to read file to string")
|
|
|
|
|
}
|
2025-04-10 06:55:12 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn scrape_one(
|
|
|
|
|
config: Arc<Config>,
|
|
|
|
|
http_client: reqwest::Client,
|
|
|
|
|
proto: ProxyType,
|
|
|
|
|
source: &str,
|
2025-05-08 18:20:19 +03:00
|
|
|
storage: Arc<tokio::sync::Mutex<ProxyStorage>>,
|
2025-04-27 21:59:32 +03:00
|
|
|
#[cfg(feature = "tui")] tx: tokio::sync::mpsc::UnboundedSender<Event>,
|
2025-05-08 18:20:19 +03:00
|
|
|
) -> color_eyre::Result<()> {
|
2025-04-10 06:55:12 +00:00
|
|
|
let text_result =
|
2025-04-24 08:57:50 +03:00
|
|
|
fetch_text(Arc::clone(&config), http_client.clone(), source).await;
|
2025-04-10 06:55:12 +00:00
|
|
|
|
2025-04-27 21:59:32 +03:00
|
|
|
#[cfg(feature = "tui")]
|
2025-04-10 06:55:12 +00:00
|
|
|
tx.send(Event::App(AppEvent::SourceScraped(proto.clone())))?;
|
|
|
|
|
|
|
|
|
|
let text = match text_result {
|
|
|
|
|
Ok(text) => text,
|
|
|
|
|
Err(e) => {
|
2025-04-29 13:06:18 +03:00
|
|
|
log::warn!("{} | {}", source, pretty_error(&e));
|
2025-05-08 18:20:19 +03:00
|
|
|
return Ok(());
|
2025-04-10 06:55:12 +00:00
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let matches: Vec<_> = PROXY_REGEX.captures_iter(&text).collect();
|
|
|
|
|
|
|
|
|
|
if matches.is_empty() {
|
|
|
|
|
log::warn!("{source} | No proxies found");
|
2025-05-08 18:20:19 +03:00
|
|
|
return Ok(());
|
2025-04-10 06:55:12 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if config.proxies_per_source_limit != 0
|
|
|
|
|
&& matches.len() > config.proxies_per_source_limit
|
|
|
|
|
{
|
|
|
|
|
log::warn!(
|
|
|
|
|
"{} | Too many proxies ({}) - skipped",
|
|
|
|
|
source,
|
|
|
|
|
matches.len(),
|
|
|
|
|
);
|
2025-05-08 18:20:19 +03:00
|
|
|
return Ok(());
|
2025-04-10 06:55:12 +00:00
|
|
|
}
|
|
|
|
|
|
2025-05-08 18:20:19 +03:00
|
|
|
#[cfg(feature = "tui")]
|
|
|
|
|
let mut seen_protocols = HashSet::with_capacity(1);
|
|
|
|
|
let mut storage = storage.lock().await;
|
2025-04-10 06:55:12 +00:00
|
|
|
for capture in matches {
|
2025-04-29 13:06:18 +03:00
|
|
|
let capture =
|
|
|
|
|
capture.wrap_err("failed to match regex captures groups")?;
|
2025-04-10 06:55:12 +00:00
|
|
|
let proxy = Proxy {
|
2025-04-29 13:06:18 +03:00
|
|
|
protocol: match capture.name("protocol") {
|
|
|
|
|
Some(m) => m.as_str().try_into()?,
|
|
|
|
|
None => proto.clone(),
|
|
|
|
|
},
|
|
|
|
|
host: capture
|
|
|
|
|
.name("host")
|
|
|
|
|
.ok_or_eyre("failed to match \"host\" regex capture group")?
|
|
|
|
|
.as_str()
|
|
|
|
|
.to_owned(),
|
|
|
|
|
port: capture
|
|
|
|
|
.name("port")
|
|
|
|
|
.ok_or_eyre("failed to match \"port\" regex capture group")?
|
|
|
|
|
.as_str()
|
|
|
|
|
.parse()?,
|
2025-05-05 14:47:59 +03:00
|
|
|
username: capture
|
|
|
|
|
.name("username")
|
|
|
|
|
.map(move |m| m.as_str().to_owned()),
|
|
|
|
|
password: capture
|
|
|
|
|
.name("password")
|
|
|
|
|
.map(move |m| m.as_str().to_owned()),
|
2025-04-10 06:55:12 +00:00
|
|
|
timeout: None,
|
|
|
|
|
exit_ip: None,
|
|
|
|
|
};
|
2025-05-08 18:20:19 +03:00
|
|
|
#[cfg(feature = "tui")]
|
|
|
|
|
seen_protocols.insert(proxy.protocol.clone());
|
|
|
|
|
storage.insert(proxy);
|
2025-04-10 06:55:12 +00:00
|
|
|
}
|
2025-05-08 18:20:19 +03:00
|
|
|
#[cfg(feature = "tui")]
|
|
|
|
|
for proto in seen_protocols {
|
|
|
|
|
let count = storage.iter().filter(|p| p.protocol == proto).count();
|
|
|
|
|
tx.send(Event::App(AppEvent::TotalProxies(proto, count)))?;
|
|
|
|
|
}
|
|
|
|
|
drop(storage);
|
|
|
|
|
Ok(())
|
2025-04-10 06:55:12 +00:00
|
|
|
}
|
|
|
|
|
|
2025-04-24 08:57:50 +03:00
|
|
|
pub async fn scrape_all(
|
2025-04-24 13:44:02 +03:00
|
|
|
config: Arc<Config>,
|
2025-04-10 06:55:12 +00:00
|
|
|
http_client: reqwest::Client,
|
2025-04-27 21:59:32 +03:00
|
|
|
#[cfg(feature = "tui")] tx: tokio::sync::mpsc::UnboundedSender<Event>,
|
2025-04-10 06:55:12 +00:00
|
|
|
) -> color_eyre::Result<ProxyStorage> {
|
2025-05-08 18:20:19 +03:00
|
|
|
let storage = Arc::new(tokio::sync::Mutex::new(ProxyStorage::new(
|
|
|
|
|
config.sources.keys().cloned().collect(),
|
|
|
|
|
)));
|
|
|
|
|
|
2025-04-10 06:55:12 +00:00
|
|
|
let mut join_set = tokio::task::JoinSet::new();
|
|
|
|
|
for (proto, sources) in config.sources.clone() {
|
2025-04-27 21:59:32 +03:00
|
|
|
#[cfg(feature = "tui")]
|
2025-04-10 06:55:12 +00:00
|
|
|
tx.send(Event::App(AppEvent::SourcesTotal(
|
|
|
|
|
proto.clone(),
|
|
|
|
|
sources.len(),
|
|
|
|
|
)))?;
|
|
|
|
|
for source in sources {
|
2025-04-24 08:57:50 +03:00
|
|
|
let config = Arc::clone(&config);
|
2025-04-10 06:55:12 +00:00
|
|
|
let http_client = http_client.clone();
|
|
|
|
|
let proto = proto.clone();
|
2025-05-08 18:20:19 +03:00
|
|
|
let storage = Arc::clone(&storage);
|
2025-04-27 21:59:32 +03:00
|
|
|
#[cfg(feature = "tui")]
|
2025-04-10 06:55:12 +00:00
|
|
|
let tx = tx.clone();
|
|
|
|
|
join_set.spawn(async move {
|
2025-04-27 21:59:32 +03:00
|
|
|
scrape_one(
|
|
|
|
|
config,
|
|
|
|
|
http_client,
|
|
|
|
|
proto,
|
|
|
|
|
&source,
|
2025-05-08 18:20:19 +03:00
|
|
|
storage,
|
2025-04-27 21:59:32 +03:00
|
|
|
#[cfg(feature = "tui")]
|
|
|
|
|
tx,
|
|
|
|
|
)
|
|
|
|
|
.await
|
2025-04-10 06:55:12 +00:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
while let Some(res) = join_set.join_next().await {
|
2025-05-08 18:20:19 +03:00
|
|
|
res.wrap_err("failed to join proxy scrape task")?
|
|
|
|
|
.wrap_err("proxy scrape task failed")?;
|
2025-04-10 06:55:12 +00:00
|
|
|
}
|
2025-05-08 18:20:19 +03:00
|
|
|
|
|
|
|
|
Ok(Arc::into_inner(storage)
|
|
|
|
|
.ok_or_eyre("failed to unwrap Arc")?
|
|
|
|
|
.into_inner())
|
2025-04-10 06:55:12 +00:00
|
|
|
}
|