httpx-rs 0.1.1

Fast web server scanner with technology detection and screenshot capabilities
httpx-rs-0.1.1 is not a library.

use anyhow::Result; use clap::Parser; use colored::*; use indicatif::{ProgressBar, ProgressStyle}; use ipnet::IpNet; use once_cell::sync::Lazy; use regex::Regex; use reqwest::Client; use scraper::{Html, Selector}; use serde::{Deserialize, Serialize}; use std::collections::HashSet; use std::fs::{self, File}; use std::io::Write; use std::net::IpAddr; use std::sync::Arc; use std::time::Duration; use tokio::sync::Semaphore;

static TECH_PATTERNS: Lazy<Vec<(String, Regex)>> = Lazy::new(|| { vec![ ("WordPress".to_string(), Regex::new(r"wp-content|wp-includes").unwrap()), ("Nginx".to_string(), Regex::new(r"nginx").unwrap()), ("Apache".to_string(), Regex::new(r"Apache").unwrap()), ("PHP".to_string(), Regex::new(r"X-Powered-By: PHP").unwrap()), ("ASP.NET".to_string(), Regex::new(r"ASP.NET|X-AspNet").unwrap()), ("Django".to_string(), Regex::new(r"csrfmiddlewaretoken|django").unwrap()), ("React".to_string(), Regex::new(r"_react|react-root").unwrap()), ("Vue.js".to_string(), Regex::new(r"Vue.js|v-cloak").unwrap()), ("Angular".to_string(), Regex::new(r"ng-version|angular").unwrap()), ("jQuery".to_string(), Regex::new(r"jquery|jQuery").unwrap()), ("Bootstrap".to_string(), Regex::new(r"bootstrap").unwrap()), ("CloudFlare".to_string(), Regex::new(r"cloudflare|cf-ray").unwrap()), ] });

#[derive(Parser, Debug)] #[command(name = "httpx-rs")] #[command(about = "⚔ Fast web server scanner with beautiful output", long_about = None)] #[command(version)] struct Args { /// IP address or CIDR range to scan (e.g., 192.168.1.0/24 or 192.168.1.1) #[arg(required_unless_present = "list")] target: Option,

/// File containing list of IPs/CIDRs to scan (one per line)
#[arg(short = 'l', long, conflicts_with = "target")]
list: Option<String>,

/// Ports to scan (comma-separated)
#[arg(short, long, default_value = "80,443")]
ports: String,

/// Scan common ports (80,443,8000,8001,8443,8080,8081,9000,9001,2083,2087,8060,8090,8880,9043,10000,902,4343,5985,9389)
#[arg(short = 'c', long)]
common_ports: bool,

/// Follow redirects
#[arg(short = 'r', long)]
follow_redirects: bool,

/// Identify technologies
#[arg(short = 't', long)]
tech_detect: bool,

/// Save output to JSON file
#[arg(short = 'j', long)]
json: Option<String>,

/// Save output to text file
#[arg(short = 'o', long)]
output: Option<String>,

/// Take screenshots and save to HTML
#[arg(short = 's', long)]
screenshot: bool,

/// Number of concurrent requests
#[arg(short = 'n', long, default_value = "50")]
threads: usize,

/// Timeout for each request (in seconds)
#[arg(long, default_value = "10")]
timeout: u64,

/// Verbose output
#[arg(short, long)]
verbose: bool,

}

#[derive(Debug, Serialize, Deserialize)] struct ScanResult { url: String, status_code: u16, title: Option, technologies: Vec, redirect_url: Option, screenshot_path: Option, response_time_ms: u128, }

impl ScanResult { fn display(&self) { let status*color = match self.status_code { 200..=299 => "green", 300..=399 => "yellow", 400..=499 => "red", 500..=599 => "magenta",

  • => "white", };

      let status = format!("[{}]", self.status_code).color(status_color);
      let url = self.url.bright_cyan();
      let title = self.title.as_ref()
          .map(|t| format!(" [{}]", t).bright_white())
          .unwrap_or_default();
    
      print!("{} {} {}", status, url, title);
    
      if !self.technologies.is_empty() {
          print!(" {}", format!("[{}]", self.technologies.join(", ")).bright_magenta());
      }
    
      if let Some(redirect) = &self.redirect_url {
          print!(" {} {}", "→".yellow(), redirect.yellow());
      }
    
      print!(" {}ms", self.response_time_ms.to_string().bright_black());
    
      println!();
    

    }

}

fn parse_targets(target: &str) -> Result<Vec> { let mut ips = Vec::new();

// Check if it's a CIDR range
if target.contains('/') {
    let network: IpNet = target.parse()?;
    for ip in network.hosts() {
        ips.push(ip);
    }
} else {
    // Single IP
    let ip: IpAddr = target.parse()?;
    ips.push(ip);
}

Ok(ips)

}

fn read_targets_from_file(file_path: &str) -> Result<Vec> { let mut all_ips = Vec::new(); let content = fs::read_to_string(file_path)?;

for line in content.lines() {
    let line = line.trim();

    // Skip empty lines and comments
    if line.is_empty() || line.starts_with('#') {
        continue;
    }

    // Parse each line as target
    match parse_targets(line) {
        Ok(mut ips) => all_ips.append(&mut ips),
        Err(e) => {
            eprintln!("{} {} {}: {}",
                "āš ļø".yellow(),
                "Skipping invalid target".yellow(),
                line.bright_black(),
                e
            );
        }
    }
}

// Remove duplicates
all_ips.sort();
all_ips.dedup();

Ok(all_ips)

}

fn parse_ports(ports_str: &str, use_common: bool) -> Vec { if use_common { vec![80, 443, 8000, 8001, 8443, 8080, 8081, 9000, 9001, 2083, 2087, 8060, 8090, 8880, 9043, 10000, 902, 4343, 5985, 9389] } else { ports_str .split(',') .filter_map(|p| p.trim().parse().ok()) .collect() } }

async fn check_http( client: &Client, ip: &IpAddr, port: u16, follow_redirects: bool, tech_detect: bool, ) -> Option { let protocols = if port == 443 || port == 8443 || port == 2083 || port == 2087 { vec!["https"] } else { vec!["http", "https"] };

for protocol in protocols {
    let url = format!("{}://{}:{}", protocol, ip, port);
    let start = std::time::Instant::now();

    // Create a client with appropriate redirect policy
    let temp_client = if !follow_redirects {
        Client::builder()
            .redirect(reqwest::redirect::Policy::none())
            .danger_accept_invalid_certs(true)
            .timeout(Duration::from_secs(10))
            .build()
            .ok()?
    } else {
        client.clone()
    };

    match temp_client.get(&url).send().await {
        Ok(response) => {
            let status = response.status();
            let headers = response.headers().clone();
            let final_url = response.url().to_string();
            let redirect_url = if final_url != url {
                Some(final_url.clone())
            } else {
                None
            };

            let body = response.text().await.unwrap_or_default();
            let response_time_ms = start.elapsed().as_millis();

            // Parse title
            let title = extract_title(&body);

            // Detect technologies
            let mut technologies = Vec::new();
            if tech_detect {
                technologies = detect_technologies(&body, &headers);
            }

            return Some(ScanResult {
                url,
                status_code: status.as_u16(),
                title,
                technologies,
                redirect_url,
                screenshot_path: None,
                response_time_ms,
            });
        }
        Err(_) => continue,
    }
}

None

}

fn extract_title(html: &str) -> Option { let document = Html::parse_document(html); let selector = Selector::parse("title").ok()?;

document
    .select(&selector)
    .next()
    .map(|element| {
        let title = element.text().collect::<String>().trim().to_string();
        if title.len() > 100 {
            format!("{}...", &title[..100])
        } else {
            title
        }
    })

}

fn detect_technologies(body: &str, headers: &reqwest::header::HeaderMap) -> Vec { let mut techs = HashSet::new();

// Check headers
let headers_str = format!("{:?}", headers);

// Check body and headers against patterns
for (tech, pattern) in TECH_PATTERNS.iter() {
    if pattern.is_match(body) || pattern.is_match(&headers_str) {
        techs.insert(tech.clone());
    }
}

// Check for specific headers
if headers.get("x-powered-by").is_some() {
    if let Some(value) = headers.get("x-powered-by") {
        if let Ok(v) = value.to_str() {
            techs.insert(v.to_string());
        }
    }
}

if headers.get("server").is_some() {
    if let Some(value) = headers.get("server") {
        if let Ok(v) = value.to_str() {
            let server = v.split('/').next().unwrap_or(v);
            techs.insert(server.to_string());
        }
    }
}

techs.into_iter().collect()

}

async fn take_screenshot(url: &str) -> Result { use headless_chrome::{Browser, LaunchOptions};

let browser = Browser::new(LaunchOptions {
    headless: true,
    sandbox: false,
    window_size: Some((1920, 1080)),
    ..Default::default()
})?;

let tab = browser.new_tab()?;
tab.navigate_to(url)?;
tab.wait_until_navigated()?;
std::thread::sleep(Duration::from_secs(2));

// Use the simplified screenshot API
let screenshot_data = tab.capture_screenshot()?;

let filename = format!("screenshot_{}.png",
    url.replace("://", "_").replace("/", "_").replace(":", "_"));

let path = format!("screenshots/{}", filename);
fs::create_dir_all("screenshots")?;
fs::write(&path, &screenshot_data)?;

Ok(path)

}

fn generate_html_report(results: &[ScanResult]) -> Result<()> { let mut html = String::from(r#"

html.push_str(&results.len().to_string());
html.push_str(r#"</div>
            <div class="stat-label">Total Servers Found</div>
        </div>
        <div class="stat-card">
            <div class="stat-number">"#);

let success_count = results.iter().filter(|r| r.status_code >= 200 && r.status_code < 300).count();
html.push_str(&success_count.to_string());

html.push_str(r#"</div>
            <div class="stat-label">Success (2xx)</div>
        </div>
        <div class="stat-card">
            <div class="stat-number">"#);

let redirect_count = results.iter().filter(|r| r.status_code >= 300 && r.status_code < 400).count();
html.push_str(&redirect_count.to_string());

html.push_str(r#"</div>
            <div class="stat-label">Redirects (3xx)</div>
        </div>
        <div class="stat-card">
            <div class="stat-number">"#);

let error_count = results.iter().filter(|r| r.status_code >= 400).count();
html.push_str(&error_count.to_string());

html.push_str(r#"</div>
            <div class="stat-label">Errors (4xx/5xx)</div>
        </div>
    </div>
    <div class="results-grid">"#);

for result in results {
    let status_class = match result.status_code {
        200..=299 => "status-200",
        300..=399 => "status-300",
        400..=499 => "status-400",
        500..=599 => "status-500",
        _ => "",
    };

    html.push_str(&format!(r#"
        <div class="result-card">
            "#));

    if let Some(screenshot_path) = &result.screenshot_path {
        html.push_str(&format!(r#"<img src="{}" alt="Screenshot" class="screenshot">"#, screenshot_path));
    } else {
        html.push_str(r#"<div class="no-screenshot">No Screenshot</div>"#);
    }

    html.push_str(&format!(r#"
            <div class="result-info">
                <div class="url">{}</div>
                <span class="status {}">{}</span>
                "#, result.url, status_class, result.status_code));

    if let Some(title) = &result.title {
        html.push_str(&format!(r#"<div class="title">šŸ“„ {}</div>"#, title));
    }

    if !result.technologies.is_empty() {
        html.push_str(r#"<div class="techs">"#);
        for tech in &result.technologies {
            html.push_str(&format!(r#"<span class="tech-badge">{}</span>"#, tech));
        }
        html.push_str(r#"</div>"#);
    }

    html.push_str(r#"
            </div>
        </div>"#);
}

html.push_str(r#"
    </div>
</div>
fs::write("scan_report.html", html)?;
Ok(())

}

#[tokio::main] async fn main() -> Result<()> { let args = Args::parse();

// Print banner
println!("{}", r#"
╦ ╦╔╦╗╔╦╗╔═╗═╗ ╦   ╦═╗╔═╗
╠═╣ ā•‘  ā•‘ ā• ā•ā•ā•”ā•©ā•¦ā•ā”€ā”€ā”€ā• ā•¦ā•ā•šā•ā•—
ā•© ā•© ā•©  ā•© ā•©  ā•© ā•šā•   ā•©ā•šā•ā•šā•ā•
"#.bright_magenta());
println!("{}\n", "⚔ Fast Web Server Scanner".bright_cyan());

// Parse targets from either single target or file
let ips = if let Some(list_file) = &args.list {
    println!("šŸ“‹ {} {}", "Reading targets from:".bright_yellow(), list_file.bright_white());
    read_targets_from_file(list_file)?
} else if let Some(target) = &args.target {
    println!("šŸŽÆ {} {}", "Target:".bright_yellow(), target.bright_white());
    parse_targets(target)?
} else {
    eprintln!("{} No target specified. Use -h for help.", "āŒ".red());
    std::process::exit(1);
};

let ports = parse_ports(&args.ports, args.common_ports);

println!("šŸ”Œ {} {:?}", "Ports:".bright_yellow(), ports);
println!("šŸ” {} {} IPs\n", "Scanning".bright_yellow(), ips.len().to_string().bright_white());

// Create HTTP client
let client = Client::builder()
    .timeout(Duration::from_secs(args.timeout))
    .danger_accept_invalid_certs(true)
    .build()?;

let client = Arc::new(client);
let semaphore = Arc::new(Semaphore::new(args.threads));

// Create progress bar
let total_scans = ips.len() * ports.len();
let pb = ProgressBar::new(total_scans as u64);
pb.set_style(
    ProgressStyle::default_bar()
        .template("{spinner:.green} [{elapsed_precise}] [{bar:40.cyan/blue}] {pos}/{len} ({eta})")?
        .progress_chars("#>-"),
);

// Scan all targets
let mut tasks = Vec::new();

for ip in ips {
    for port in &ports {
        let client = client.clone();
        let semaphore = semaphore.clone();
        let pb = pb.clone();
        let port = *port;
        let follow = args.follow_redirects;
        let tech = args.tech_detect;

        let task = tokio::spawn(async move {
            let _permit = semaphore.acquire().await.unwrap();
            let result = check_http(&client, &ip, port, follow, tech).await;
            pb.inc(1);
            result
        });

        tasks.push(task);
    }
}

// Collect results
let mut results = Vec::new();
for task in tasks {
    if let Ok(Some(result)) = task.await {
        result.display();
        results.push(result);
    }
}

pb.finish_and_clear();

// Take screenshots if requested
if args.screenshot && !results.is_empty() {
    println!("\nšŸ“ø {} screenshots...", "Taking".bright_yellow());
    let screenshot_pb = ProgressBar::new(results.len() as u64);

    for result in &mut results {
        if let Ok(path) = take_screenshot(&result.url).await {
            result.screenshot_path = Some(path);
        }
        screenshot_pb.inc(1);
    }
    screenshot_pb.finish_and_clear();
}

// Save results
if let Some(json_file) = args.json {
    let json = serde_json::to_string_pretty(&results)?;
    fs::write(&json_file, json)?;
    println!("šŸ’¾ {} {}", "JSON saved to:".bright_green(), json_file.bright_white());
}

if let Some(text_file) = args.output {
    let mut file = File::create(&text_file)?;
    for result in &results {
        writeln!(file, "{} {} {} {}",
            result.status_code,
            result.url,
            result.title.as_ref().unwrap_or(&String::new()),
            result.technologies.join(", ")
        )?;
    }
    println!("šŸ’¾ {} {}", "Text saved to:".bright_green(), text_file.bright_white());
}

// Generate HTML report if screenshots were taken
if args.screenshot && !results.is_empty() {
    generate_html_report(&results)?;
    println!("🌐 {} scan_report.html", "HTML report saved to:".bright_green());

    // Try to open the HTML file
    #[cfg(target_os = "macos")]
    std::process::Command::new("open").arg("scan_report.html").spawn().ok();

    #[cfg(target_os = "linux")]
    std::process::Command::new("xdg-open").arg("scan_report.html").spawn().ok();

    #[cfg(target_os = "windows")]
    std::process::Command::new("cmd").args(&["/C", "start", "scan_report.html"]).spawn().ok();
}

// Print summary
println!("\n{}", "═".repeat(50).bright_black());
println!("✨ {} {}", "Scan Complete!".bright_green().bold(),
    format!("Found {} servers", results.len()).bright_white());

Ok(())

}