use reqwest::{ Client, Url, header };
use scraper::{ Html, Selector };
use std::collections::{ HashSet, VecDeque };
use std::fs::{ create_dir_all, File };
use std::io::Write;
use std::path::Path;
use tokio::io::AsyncWriteExt;
use regex::Regex;
use std::time::Duration;
use tokio::time::sleep;
pub fn random_user_agent() -> String {
let user_agents = vec![
"Mozilla/5.0 (Windows NT 10.0; Win64; x64)...",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)...",
"Mozilla/5.0 (iPhone; CPU iPhone OS 14_6 like Mac OS X)...",
];
let index = rand::random::<usize>() % user_agents.len();
user_agents[index].to_string()
}
use futures::Future;
use std::pin::Pin;
pub fn recursive_scrape<'a>(
url: &'a str,
client: &'a Client,
visited: &'a mut HashSet<String>,
) -> Pin<Box<dyn Future<Output = ()> + 'a>> {
Box::pin(async move {
if visited.contains(url) {
return;
}
visited.insert(url.to_string());
let user_agent = random_user_agent();
match client.get(url).header("User-Agent", user_agent).send().await {
Ok(response) => {
match response.text().await {
Ok(html) => {
println!("Scraping: {}", url);
scrape_content(&html, url, client).await;
scrape_js(&html);
scrape_for_errors(&html);
let links = extract_links(&html, url);
for link in links {
if !visited.contains(&link) {
recursive_scrape(&link, client, visited).await;
}
}
}
Err(e) => {
let error_message = format!("Failed to get HTML content from '{}': {}", url, e);
eprintln!("{}", error_message);
log_error_to_file(&error_message);
}
}
}
Err(e) => {
let error_message = format!("Failed to request '{}': {}", url, e);
eprintln!("{}", error_message);
log_error_to_file(&error_message);
}
}
})
}
pub fn extract_links(html: &str, base_url: &str) -> HashSet<String> {
let document = Html::parse_document(html);
let selector = Selector::parse("a[href]").unwrap();
let mut urls = HashSet::new();
for element in document.select(&selector) {
if let Some(link) = element.value().attr("href") {
let absolute_link = normalize_link(link, base_url);
urls.insert(absolute_link);
}
}
urls
}
pub fn normalize_link(link: &str, base_url: &str) -> String {
if link.starts_with("http") {
link.to_string() } else {
match Url::parse(base_url) {
Ok(base) => base.join(link).map(|url| url.to_string()).unwrap_or_default(),
Err(_) => link.to_string(), }
}
}
pub async fn download_media(client: &Client, media_url: &str, file_path: &Path) {
if let Ok(response) = client.get(media_url).send().await {
if response.status().is_success() {
if let Ok(bytes) = response.bytes().await {
if let Some(parent) = file_path.parent() {
if let Err(e) = tokio::fs::create_dir_all(parent).await {
let error_message = format!("Failed to create directory '{}': {}", parent.display(), e);
eprintln!("{}", error_message);
log_error_to_file(&error_message);
return;
}
}
let mut file = match tokio::fs::File::create(file_path).await {
Ok(f) => f,
Err(e) => {
let error_message = format!("Failed to create file '{}': {}", file_path.display(), e);
eprintln!("{}", error_message);
log_error_to_file(&error_message);
return;
}
};
if let Err(e) = file.write_all(&bytes).await {
let error_message = format!("Failed to write file '{}': {}", file_path.display(), e);
eprintln!("{}", error_message);
log_error_to_file(&error_message);
} else {
println!("Successfully downloaded and saved the media file: {}", file_path.display());
}
} else {
let error_message = format!("Failed to read bytes from the response for '{}'", media_url);
eprintln!("{}", error_message);
log_error_to_file(&error_message);
}
} else {
let error_message = format!("Failed to download media from '{}': Status code {}", media_url, response.status());
eprintln!("{}", error_message);
log_error_to_file(&error_message);
}
} else {
let error_message = format!("Failed to make request to '{}'", media_url);
eprintln!("{}", error_message);
log_error_to_file(&error_message);
}
}
pub async fn scrape_content(html: &str, url: &str, client: &Client) {
let domain = extract_domain(url);
let dir = format!("./scraped_data/{}", domain);
if let Err(e) = create_dir_all(&dir) {
eprintln!("Failed to create directory '{}': {}", dir, e);
return;
}
let mut text_file = match File::create(format!("{}/content.txt", dir)) {
Ok(file) => file,
Err(e) => {
eprintln!("Failed to create text file: {}", e);
return;
}
};
let document = Html::parse_document(html);
let header_selector = Selector::parse("h1, h2, h3, h4, h5, h6").unwrap();
for header in document.select(&header_selector) {
writeln!(text_file, "Header: {}", header.inner_html()).unwrap();
}
let paragraph_selector = Selector::parse("p").unwrap();
for paragraph in document.select(¶graph_selector) {
writeln!(text_file, "Paragraph: {}", paragraph.inner_html()).unwrap();
}
let img_selector = Selector::parse("img[src]").unwrap();
for img in document.select(&img_selector) {
if let Some(src) = img.value().attr("src") {
let img_url = normalize_link(src, url);
let file_name = img_url
.split('/')
.last()
.unwrap_or("image.jpg")
.to_string();
let file_path = Path::new(&dir).join(file_name);
println!("Downloading image: {}", img_url);
download_media(client, &img_url, &file_path).await;
}
}
let video_selector = Selector::parse("video[src], source[src]").unwrap();
for video in document.select(&video_selector) {
if let Some(src) = video.value().attr("src") {
let video_url = normalize_link(src, url);
let file_name = video_url
.split('/')
.last()
.unwrap_or("video.mp4")
.to_string();
let file_path = Path::new(&dir).join(file_name);
println!("Downloading video: {}", video_url);
download_media(client, &video_url, &file_path).await;
}
}
let meta_selector = Selector::parse("meta[name][content]").unwrap();
for meta in document.select(&meta_selector) {
let name = meta.value().attr("name").unwrap_or("Unnamed");
let content = meta.value().attr("content").unwrap_or("");
writeln!(text_file, "Meta Tag - Name: {}, Content: {}", name, content).unwrap();
}
let form_selector = Selector::parse("form").unwrap();
for form in document.select(&form_selector) {
writeln!(text_file, "Form found!").unwrap();
let input_selector = Selector::parse("input").unwrap();
for input in form.select(&input_selector) {
let input_name = input.value().attr("name").unwrap_or("Unnamed Input");
let input_type = input.value().attr("type").unwrap_or("text");
writeln!(
text_file,
"Input - Name: {}, Type: {}",
input_name, input_type
)
.unwrap();
}
}
scrape_for_emails(html, &dir);
}
pub fn extract_domain(url: &str) -> String {
let parsed_url = Url::parse(url).expect("Invalid URL");
parsed_url.host_str().unwrap_or("unknown_domain").to_string()
}
pub fn scrape_js(html: &str) {
let document = Html::parse_document(html);
let script_selector = Selector::parse("script").unwrap();
for script in document.select(&script_selector) {
let script_content = script.inner_html();
if script_content.contains("apiKey") || script_content.contains("token") {
println!("Potential API key or token found in JS: {}", script_content);
}
}
}
pub fn scrape_for_errors(html: &str) {
if html.contains("Exception") || html.contains("Stack trace") {
println!("Potential error or stack trace found in the page:\n{}", html);
}
}
pub fn scrape_for_emails(html: &str, dir: &str) {
let email_regex = match Regex::new(r"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}") {
Ok(regex) => regex,
Err(e) => {
eprintln!("Failed to compile email regex: {}", e);
return;
}
};
let email_file_path = format!("{}/emails.txt", dir);
let mut email_file = match File::create(&email_file_path) {
Ok(file) => file,
Err(e) => {
eprintln!("Failed to create email file '{}': {}", email_file_path, e);
return;
}
};
for email in email_regex.find_iter(html) {
if writeln!(email_file, "{}", email.as_str()).is_err() {
eprintln!("Failed to write email '{}' to file '{}'", email.as_str(), email_file_path);
}
}
}
pub async fn fetch_with_cookies(url: &str, client: &Client) {
if let Ok(response) = client.get(url).send().await {
println!("Response status: {}", response.status());
}
}
pub async fn check_open_directories(url: &str, client: &Client) {
let directories = vec!["/backup", "/config", "/logs", "/uploads"];
for dir in directories {
let full_url = format!("{}{}", url, dir);
if let Ok(response) = client.get(&full_url).send().await {
if response.status().is_success() {
println!("Open directory found: {}", full_url);
}
}
}
}
pub async fn fetch_robots_txt(url: &str, client: &Client) {
let robots_url = format!("{}/robots.txt", url.trim_end_matches('/'));
if let Ok(response) = client.get(&robots_url).send().await {
if let Ok(body) = response.text().await {
let disallowed_paths: Vec<&str> = body
.lines()
.filter(|line| line.starts_with("Disallow"))
.map(|line| line.split(": ").nth(1).unwrap_or("/"))
.collect();
for path in disallowed_paths {
println!("Disallowed path found: {}", path);
}
}
}
}
pub async fn run(url: &str, client: &Client) {
let mut visited = HashSet::new();
println!("Starting scraping workflow for {}", url);
fetch_robots_txt(url, client).await;
check_open_directories(url, client).await;
fetch_with_cookies(url, client).await;
recursive_scrape(url, client, &mut visited).await;
random_delay(2, 5).await;
println!("Scraping workflow completed for {}", url);
}
use std::fs::OpenOptions;
fn log_error_to_file(message: &str) {
let log_file_path = "error.log";
let mut file = match OpenOptions::new()
.create(true)
.append(true)
.open(log_file_path)
{
Ok(f) => f,
Err(e) => {
eprintln!("Failed to open or create error log file '{}': {}", log_file_path, e);
return;
}
};
if let Err(e) = writeln!(file, "{}", message) {
eprintln!("Failed to write to error log file '{}': {}", log_file_path, e);
}
}
pub async fn random_delay(min_secs: u64, max_secs: u64) {
let delay = rand::random::<u64>() % (max_secs - min_secs + 1) + min_secs;
sleep(Duration::from_secs(delay)).await;
}
async fn rec_scrape(url: &str, client: &Client, config: Option<&ScraperConfig>, visited: &mut HashSet<String>, target_phrase: &str) {
let mut queue = VecDeque::new();
queue.push_back(url.to_string());
let mut current_depth = 0;
let follow_links = config.map_or(true, |c| c.follow_links()); let max_depth = config.map_or(3, |c| c.max_depth()); let user_agent = config.and_then(|c| c.user_agent().cloned());
while let Some(current_url) = queue.pop_front() {
if visited.contains(¤t_url) {
continue;
}
println!("Visiting: {}", current_url);
visited.insert(current_url.clone());
let mut request = client.get(¤t_url);
if let Some(ref agent) = user_agent {
request = request.header(header::USER_AGENT, agent);
}
let response = match request.send().await {
Ok(response) => response,
Err(_) => continue, };
if response.status().is_success() {
let html = match response.text().await {
Ok(html) => html,
Err(_) => continue, };
if should_scrape_content(&html, target_phrase) {
println!("Target phrase found in: {}", current_url);
if follow_links && current_depth < max_depth {
let links = extract_links(&html, ¤t_url);
for link in links {
if !visited.contains(&link) {
queue.push_back(link); }
}
current_depth += 1; }
} else {
println!("Target phrase not found in: {}", current_url);
continue;
}
}
}
}
fn should_scrape_content(content: &str, target_phrase: &str) -> bool {
content.contains(target_phrase)
}
struct ScraperConfig {
follow_links: bool,
max_depth: i32,
user_agent: Option<String>,
}
impl ScraperConfig {
pub fn new(follow_links: bool, max_depth: i32, user_agent: Option<String>) -> Self {
ScraperConfig {
follow_links,
max_depth,
user_agent,
}
}
pub fn set_follow_links(&mut self, follow: bool) {
self.follow_links = follow;
}
pub fn set_max_depth(&mut self, depth: i32) {
self.max_depth = depth;
}
pub fn set_user_agent(&mut self, agent: Option<String>) {
self.user_agent = agent;
}
pub fn follow_links(&self) -> bool {
self.follow_links
}
pub fn max_depth(&self) -> i32 {
self.max_depth
}
pub fn user_agent(&self) -> Option<&String> {
self.user_agent.as_ref()
}
}
pub async fn scrape_js_content(html: &str, url: &str, client: &Client, keywords: &[&str]) {
let document = Html::parse_document(html);
let script_selector = Selector::parse("script").unwrap();
for script in document.select(&script_selector) {
let script_content = script.inner_html();
if !script_content.is_empty() {
for &keyword in keywords {
if script_content.contains(keyword) {
println!("Found '{}' in inline JS: {}", keyword, script_content);
}
}
}
if let Some(src) = script.value().attr("src") {
let js_url = normalize_link(src, url);
match client.get(&js_url).send().await {
Ok(response) => {
if response.status().is_success() {
if let Ok(js_content) = response.text().await {
for &keyword in keywords {
if js_content.contains(keyword) {
println!("Found '{}' in external JS: {}", keyword, js_content);
}
}
let file_name = js_url.split('/').last().unwrap_or("script.js").to_string();
let file_path = format!("./scraped_js/{}", file_name);
if let Err(e) = save_js_file(&file_path, &js_content) {
eprintln!("Failed to save JS file '{}': {}", file_path, e);
}
}
} else {
eprintln!("Failed to download JS file from '{}': Status code {}", js_url, response.status());
}
}
Err(e) => eprintln!("Error fetching JS file '{}': {}", js_url, e),
}
}
}
}
fn save_js_file(file_path: &str, js_content: &str) -> Result<(), std::io::Error> {
let mut file = File::create(file_path)?;
file.write_all(js_content.as_bytes())?;
println!("Saved JS file to '{}'", file_path);
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use reqwest::Client;
use std::collections::HashSet;
use std::path::Path;
#[test]
fn test_random_user_agent() {
let user_agent = random_user_agent();
assert!(user_agent.contains("Mozilla"), "User-Agent should contain 'Mozilla'");
}
#[test]
fn test_extract_links() {
let html = r#"<a href="/about">About</a> <a href="https://example.com">Home</a>"#;
let base_url = "https://test.com";
let links = extract_links(html, base_url);
assert!(links.contains("https://test.com/about"));
assert!(links.contains("https://example.com"));
}
#[test]
fn test_normalize_link() {
let link = "/about";
let base_url = "https://example.com";
let normalized = normalize_link(link, base_url);
assert_eq!(normalized, "https://example.com/about");
}
#[test]
fn test_scrape_for_emails() {
let html = r#"<p>Contact us at info@example.com</p>"#;
let dir = "./test_output";
create_dir_all(dir).unwrap();
scrape_for_emails(html, dir);
let emails_path = format!("{}/emails.txt", dir);
let emails_file = std::fs::read_to_string(emails_path).unwrap();
assert!(emails_file.contains("info@example.com"), "Should find the email");
}
#[tokio::test]
async fn test_download_media() {
let client = Client::new();
let media_url = "https://via.placeholder.com/150";
let file_path = Path::new("./test_output/image.jpg");
download_media(&client, media_url, &file_path).await;
assert!(file_path.exists(), "Image should be downloaded and saved");
}
#[tokio::test]
async fn test_recursive_scrape() {
let client = Client::new();
let mut visited = HashSet::new();
let url = "https://example.com";
recursive_scrape(url, &client, &mut visited).await;
assert!(visited.contains(url), "URL should be marked as visited");
}
fn clean_test_output() {
std::fs::remove_dir_all("./test_output").unwrap_or_else(|_| {
eprintln!("Could not delete test_output directory");
});
}
#[test]
fn test_cleanup() {
clean_test_output();
}
}