initial
This commit is contained in:
commit
4578bfe896
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
/target
|
1701
Cargo.lock
generated
Normal file
1701
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
14
Cargo.toml
Normal file
14
Cargo.toml
Normal 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
3
README.md
Normal 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
69
src/common/conf.rs
Normal 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
35
src/common/console.rs
Normal 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
30
src/common/exec.rs
Normal 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
4
src/common/mod.rs
Normal file
@ -0,0 +1,4 @@
|
||||
pub mod conf;
|
||||
pub mod console;
|
||||
pub mod exec;
|
||||
pub mod net;
|
61
src/common/net.rs
Normal file
61
src/common/net.rs
Normal 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
10
src/main.rs
Normal 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;
|
||||
}
|
Loading…
Reference in New Issue
Block a user