This commit is contained in:
delorean 2024-05-02 16:30:16 -05:00
commit 4578bfe896
10 changed files with 1928 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/target

1701
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

14
Cargo.toml Normal file
View File

@ -0,0 +1,14 @@
[package]
name = "speedboat"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
clap = { version = "4.5.4", features = ["derive"] }
colored = "2.1.0"
futures = "0.3.30"
reqwest = "0.12.4"
select = "0.6.0"
tokio = { version = "1", features = ["full"] }

3
README.md Normal file
View File

@ -0,0 +1,3 @@
# speedboat
lightweight web service aggregator, offering performance/reliability that httpX lacks for protracted scans

69
src/common/conf.rs Normal file
View File

@ -0,0 +1,69 @@
use super::console::fatal;
use clap::Parser;
pub const VERSION: &str = "1.0.0";
pub struct Params {
pub statcodes: Vec<u16>,
pub exclude: bool,
}
#[derive(Parser, Default)]
#[clap(
author = "tommy touchdown",
about = "speedboat - lightweight web content aggregator",
version = VERSION
)]
pub struct Config {
#[clap(short, long)]
/// list of target domains and/or ip addresses
pub list: String,
#[clap(default_value_t = 100, short, long)]
/// concurrent workers
pub threads: usize,
#[clap(long = "mc")]
/// status codes to match, comma separated
pub matchcodes: Option<String>,
#[clap(long = "ec")]
/// status codes to exclude, comma separated
pub excludecodes: Option<String>,
#[clap(long = "title")]
/// retrieve http titles
pub pulltitles: bool,
#[clap(long = "redirects")]
/// follow redirects
pub follow: bool,
}
pub fn load() -> Config {
Config::parse()
}
fn parsecodes(raw: String) -> Vec<u16> {
let mut codes: Vec<u16> = vec![];
for code in raw.split(",") {
let scode: u16 = code
.parse()
.unwrap_or_else(|_| fatal("invalid status code provided"));
codes.push(scode);
}
codes
}
pub fn setparams(c: &Config) -> Params {
let mut statcodes: Vec<u16> = vec![];
let mut exclude = false;
if let Some(mcodes) = c.matchcodes.clone() {
statcodes = parsecodes(mcodes);
} else if let Some(exclcodes) = c.excludecodes.clone() {
statcodes = parsecodes(exclcodes);
exclude = true;
}
Params { statcodes, exclude }
}

35
src/common/console.rs Normal file
View File

@ -0,0 +1,35 @@
use colored::{ColoredString, Colorize};
use std::process;
use super::conf::VERSION;
pub fn fatal(msg: &str) -> ! {
println!("{}: {msg}", "fatal".red().bold());
process::exit(-1);
}
pub fn fmtcode(code: u16) -> ColoredString {
match code {
200..=299 => code.to_string().green(),
300..=399 => code.to_string().yellow(),
400..=499 => code.to_string().bright_red(),
500..=599 => code.to_string().red().bold(),
_ => code.to_string().black(),
}
}
pub fn parsehit(sc: u16, url: String, title: &str) -> String {
format!(
"{} {} {} {}{}{}",
fmtcode(sc),
"|".black().bold(),
url.white().underline(),
"[".black(),
title.trim_matches(['\n', '\t', '\r']).bright_cyan().bold(),
"]".black()
)
}
pub fn banner() {
eprintln!("{}{} {}", "speed".bright_cyan().bold(), "boat".bright_magenta().bold(), VERSION.black())
}

30
src/common/exec.rs Normal file
View File

@ -0,0 +1,30 @@
use futures::{stream, StreamExt};
use std::{fs::File, io::{BufReader, BufRead}};
use super::{
conf::{Config, Params},
console::fatal,
net::{mkclient, query},
};
pub async fn takeoff(args: Config, params: Params) {
let c = mkclient(args.follow).unwrap_or_else(|_| fatal("error instantiating http client"));
let file = File::open(args.list)
.unwrap_or_else(|e| fatal(format!("unable to read file: {e}").as_str()));
// Create a buffered reader.
let buf = BufReader::new(file);
stream::iter(buf.lines())
.for_each_concurrent(args.threads, |line| {
// call request function
let wc = c.clone();
let scodes = params.statcodes.clone();
async move {
let _ = query(wc, line.unwrap_or_else(|_| fatal("error attempting buffered read")).trim(), scodes, params.exclude).await;
}
})
.await;
}

4
src/common/mod.rs Normal file
View File

@ -0,0 +1,4 @@
pub mod conf;
pub mod console;
pub mod exec;
pub mod net;

61
src/common/net.rs Normal file
View File

@ -0,0 +1,61 @@
use reqwest::{redirect::Policy, Client};
use select::{document::Document, predicate::Name};
use std::time::Duration;
use super::console::parsehit;
pub fn mkclient(redir: bool) -> Result<Client, reqwest::Error> {
let rpolicy: Policy = if redir {
Policy::limited(5)
} else {
Policy::none()
};
Client::builder()
.user_agent("buttplug/1.0")
.redirect(rpolicy)
.timeout(Duration::from_secs(2))
.connect_timeout(Duration::from_millis(500))
.build()
}
pub async fn query(
c: Client,
url: &str,
codes: Vec<u16>,
exclude: bool,
) -> Result<(), reqwest::Error> {
let response = c.get(format!("http://{url}/")).send().await?;
let statcode = response.status().as_u16();
if codes.len() > 0 {
if codes.contains(&statcode) {
if exclude {
return Ok(());
}
} else if !exclude {
return Ok(());
}
}
let sc = response.status().as_u16();
let url: String = response.url().to_string();
let body = response.text().await?;
// Parse the HTML document
let document = Document::from(body.as_str());
// Use select to find the <title> tag
let title = document
.find(Name("title"))
.next()
.map(|n| n.text())
.unwrap_or_else(|| "".to_string());
println!(
"{}",
parsehit(sc, url, title.trim_matches(['\n', '\t', '\r']))
);
Ok(())
}

10
src/main.rs Normal file
View File

@ -0,0 +1,10 @@
mod common;
#[tokio::main]
async fn main() {
let args = common::conf::load();
let scanparams = common::conf::setparams(&args);
common::console::banner();
common::exec::takeoff(args, scanparams).await;
}