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