diff --git a/src/banner.rs b/src/banner.rs index 542dbb8..e4d9977 100644 --- a/src/banner.rs +++ b/src/banner.rs @@ -3,51 +3,20 @@ use crate::{ statistics::StatCommand, utils::{make_request, status_colorizer}, }; +use anyhow::Result; use console::{style, Emoji}; use reqwest::{Client, Url}; +use serde::export::Formatter; use serde_json::Value; +use std::fmt::{self, Display}; use std::io::Write; use tokio::sync::mpsc::UnboundedSender; -/// macro helper to abstract away repetitive string formatting -macro_rules! format_banner_entry_helper { - // \u{0020} -> unicode space - // \u{2502} -> vertical box drawing character, i.e. │ - ($rune:expr, $name:expr, $value:expr, $indent:expr, $col_width:expr) => { - format!( - "\u{0020}{:\u{0020} { - format!( - "\u{0020}{:\u{0020} unicode emoji padding width - // 22 -> column width (when unicode rune is 4 bytes wide, 23 when it's 3) - // hardcoded since macros don't allow let statements - ($rune:expr, $name:expr, $value:expr) => { - format_banner_entry_helper!($rune, $name, $value, 3, 22) - }; - ($rune:expr, $name:expr, $value1:expr, $value2:expr) => { - format_banner_entry_helper!($rune, $name, $value1, $value2, 3, 22) - }; -} +/// Column width used in formatting banner entries +const COL_WIDTH: usize = 22; /// Url used to query github's api; specifically used to look for the latest tagged release name const UPDATE_URL: &str = "https://api.github.com/repos/epi052/feroxbuster/releases/latest"; @@ -65,6 +34,55 @@ enum UpdateStatus { Unknown, } +/// Represents a single line on the banner +#[derive(Default)] +struct BannerEntry { + /// emoji used in the banner entry + emoji: String, + + /// title used in the banner entry + title: String, + + /// value passed in via config/cli/defaults + value: String, +} + +/// implementation of a banner entry +impl BannerEntry { + /// Create a new banner entry from given fields + pub fn new(emoji: &str, title: &str, value: &str) -> Self { + BannerEntry { + emoji: emoji.to_string(), + title: title.to_string(), + value: value.to_string(), + } + } + + /// Simple wrapper for emoji or fallback when terminal doesn't support emoji + fn format_emoji(&self) -> String { + let width = console::measure_text_width(&self.emoji); + let pad_len = width * width; + let pad = format!("{:) -> fmt::Result { + write!( + f, + "\u{0020}{:\u{0020} String { - let width = console::measure_text_width(emoji); - let pad_len = width * width; - let pad = format!("{:( version: &str, mut writer: W, tx_stats: UnboundedSender, -) where +) -> Result<()> +where W: Write, { let artwork = format!( @@ -170,17 +181,13 @@ by Ben "epi" Risher {} ver: {}"#, let addl_section = "──────────────────────────────────────────────────"; let bottom = "───────────────────────────┴──────────────────────"; - writeln!(&mut writer, "{}", artwork).unwrap_or_default(); - writeln!(&mut writer, "{}", top).unwrap_or_default(); + writeln!(&mut writer, "{}", artwork)?; + writeln!(&mut writer, "{}", top)?; // begin with always printed items for target in targets { - writeln!( - &mut writer, - "{}", - format_banner_entry!(format_emoji("🎯"), "Target Url", target) - ) - .unwrap_or_default(); // 🎯 + let tgt = BannerEntry::new("🎯", "Target Url", target); + writeln!(&mut writer, "{}", tgt)?; } let mut codes = vec![]; @@ -189,30 +196,14 @@ by Ben "epi" Risher {} ver: {}"#, codes.push(status_colorizer(&code.to_string())) } - writeln!( - &mut writer, - "{}", - format_banner_entry!(format_emoji("🚀"), "Threads", config.threads) - ) - .unwrap_or_default(); // 🚀 + let threads = BannerEntry::new("🚀", "Threads", &config.threads.to_string()); + writeln!(&mut writer, "{}", threads)?; - writeln!( - &mut writer, - "{}", - format_banner_entry!(format_emoji("📖"), "Wordlist", config.wordlist) - ) - .unwrap_or_default(); // 📖 + let words = BannerEntry::new("📖", "Wordlist", &config.wordlist); + writeln!(&mut writer, "{}", words)?; - writeln!( - &mut writer, - "{}", - format_banner_entry!( - format_emoji("🆗"), - "Status Codes", - format!("[{}]", codes.join(", ")) - ) - ) - .unwrap_or_default(); // 🆗 + let status_codes = BannerEntry::new("👌", "Status Codes", &format!("[{}]", codes.join(", "))); + writeln!(&mut writer, "{}", status_codes)?; if !config.filter_status.is_empty() { // exception here for optional print due to me wanting the allows and denys to be printed @@ -223,49 +214,30 @@ by Ben "epi" Risher {} ver: {}"#, code_filters.push(status_colorizer(&code.to_string())) } - writeln!( - &mut writer, - "{}", - format_banner_entry!( - format_emoji("🗑"), - "Status Code Filters", - format!("[{}]", code_filters.join(", ")) - ) - ) - .unwrap_or_default(); // 🗑 + let banner_cfs = BannerEntry::new( + "🗑", + "Status Code Filters", + &format!("[{}]", code_filters.join(", ")), + ); + + writeln!(&mut writer, "{}", banner_cfs)?; } - writeln!( - &mut writer, - "{}", - format_banner_entry!(format_emoji("💥"), "Timeout (secs)", config.timeout) - ) - .unwrap_or_default(); // 💥 + let timeout = BannerEntry::new("💥", "Timeout (secs)", &config.timeout.to_string()); + writeln!(&mut writer, "{}", timeout)?; - writeln!( - &mut writer, - "{}", - format_banner_entry!(format_emoji("🦡"), "User-Agent", config.user_agent) - ) - .unwrap_or_default(); // 🦡 + let user_agent = BannerEntry::new("🦡", "User-Agent", &config.user_agent); + writeln!(&mut writer, "{}", user_agent)?; // followed by the maybe printed or variably displayed values if !config.config.is_empty() { - writeln!( - &mut writer, - "{}", - format_banner_entry!(format_emoji("💉"), "Config File", config.config) - ) - .unwrap_or_default(); // 💉 + let banner_cfg = BannerEntry::new("💉", "Config File", &config.config); + writeln!(&mut writer, "{}", banner_cfg)?; } if !config.proxy.is_empty() { - writeln!( - &mut writer, - "{}", - format_banner_entry!(format_emoji("💎"), "Proxy", config.proxy) - ) - .unwrap_or_default(); // 💎 + let proxy = BannerEntry::new("💎", "Proxy", &config.proxy); + writeln!(&mut writer, "{}", proxy)?; } if !config.replay_proxy.is_empty() { @@ -274,276 +246,166 @@ by Ben "epi" Risher {} ver: {}"#, let mut replay_codes = vec![]; - writeln!( - &mut writer, - "{}", - format_banner_entry!(format_emoji("🎥"), "Replay Proxy", config.replay_proxy) - ) - .unwrap_or_default(); // 🎥 - for code in &config.replay_codes { replay_codes.push(status_colorizer(&code.to_string())) } - writeln!( - &mut writer, - "{}", - format_banner_entry!( - format_emoji("📼"), - "Replay Proxy Codes", - format!("[{}]", replay_codes.join(", ")) - ) - ) - .unwrap_or_default(); // 📼 + let banner_rcs = BannerEntry::new( + "📼", + "Replay Proxy Codes", + &format!("[{}]", replay_codes.join(", ")), + ); + + let rproxy = BannerEntry::new("🎥", "Replay Proxy", &config.replay_proxy); + + writeln!(&mut writer, "{}", rproxy)?; + writeln!(&mut writer, "{}", banner_rcs)?; } - if !config.headers.is_empty() { - for (name, value) in &config.headers { - writeln!( - &mut writer, - "{}", - format_banner_entry!(format_emoji("🤯"), "Header", name, value) - ) - .unwrap_or_default(); // 🤯 - } + for (name, value) in &config.headers { + let header = BannerEntry::new("🤯", "Header", &format!("{}: {}", name, value)); + writeln!(&mut writer, "{}", header)?; } - if !config.filter_size.is_empty() { - for filter in &config.filter_size { - writeln!( - &mut writer, - "{}", - format_banner_entry!(format_emoji("💢"), "Size Filter", filter) - ) - .unwrap_or_default(); // 💢 - } + for filter in &config.filter_size { + let sz_filter = BannerEntry::new("💢", "Size Filter", &filter.to_string()); + writeln!(&mut writer, "{}", sz_filter)?; } - if !config.filter_similar.is_empty() { - for filter in &config.filter_similar { - writeln!( - &mut writer, - "{}", - format_banner_entry!(format_emoji("💢"), "Similarity Filter", filter) - ) - .unwrap_or_default(); // 💢 - } + for filter in &config.filter_similar { + let sim_filter = BannerEntry::new("💢", "Similarity Filter", filter); + writeln!(&mut writer, "{}", sim_filter)?; } for filter in &config.filter_word_count { - writeln!( - &mut writer, - "{}", - format_banner_entry!(format_emoji("💢"), "Word Count Filter", filter) - ) - .unwrap_or_default(); // 💢 + let wc_filter = BannerEntry::new("💢", "Word Count Filter", &filter.to_string()); + writeln!(&mut writer, "{}", wc_filter)?; } for filter in &config.filter_line_count { - writeln!( - &mut writer, - "{}", - format_banner_entry!(format_emoji("💢"), "Line Count Filter", filter) - ) - .unwrap_or_default(); // 💢 + let lc_filter = BannerEntry::new("💢", "Line Count Filter", &filter.to_string()); + writeln!(&mut writer, "{}", lc_filter)?; } for filter in &config.filter_regex { - writeln!( - &mut writer, - "{}", - format_banner_entry!(format_emoji("💢"), "Regex Filter", filter) - ) - .unwrap_or_default(); // 💢 + let reg_filter = BannerEntry::new("💢", "Regex Filter", filter); + writeln!(&mut writer, "{}", reg_filter)?; } if config.extract_links { - writeln!( - &mut writer, - "{}", - format_banner_entry!(format_emoji("🔎"), "Extract Links", config.extract_links) - ) - .unwrap_or_default(); // 🔎 + let ext_links = BannerEntry::new("🔎", "Extract Links", &config.extract_links.to_string()); + writeln!(&mut writer, "{}", ext_links)?; } if config.json { - writeln!( - &mut writer, - "{}", - format_banner_entry!(format_emoji("🧔"), "JSON Output", config.json) - ) - .unwrap_or_default(); // 🧔 + let json = BannerEntry::new("🧔", "JSON Output", &config.json.to_string()); + writeln!(&mut writer, "{}", json)?; } - if !config.queries.is_empty() { - for query in &config.queries { - writeln!( - &mut writer, - "{}", - format_banner_entry!( - format_emoji("🤔"), - "Query Parameter", - format!("{}={}", query.0, query.1) - ) - ) - .unwrap_or_default(); // 🤔 - } + for query in &config.queries { + let query = BannerEntry::new("🤔", "Query Parameter", &format!("{}={}", query.0, query.1)); + writeln!(&mut writer, "{}", query)?; } if !config.output.is_empty() { - writeln!( - &mut writer, - "{}", - format_banner_entry!(format_emoji("💾"), "Output File", config.output) - ) - .unwrap_or_default(); // 💾 + let out = BannerEntry::new("💾", "Output File", &config.output); + writeln!(&mut writer, "{}", out)?; } if !config.debug_log.is_empty() { - writeln!( - &mut writer, - "{}", - format_banner_entry!(format_emoji("🪲"), "Debugging Log", config.debug_log) - ) - .unwrap_or_default(); // 🪲 + let debug_log = BannerEntry::new("🪲", "Debugging Log", &config.debug_log); + writeln!(&mut writer, "{}", debug_log)?; } if !config.extensions.is_empty() { - writeln!( - &mut writer, - "{}", - format_banner_entry!( - format_emoji("💲"), - "Extensions", - format!("[{}]", config.extensions.join(", ")) - ) - ) - .unwrap_or_default(); // 💲 + let b_exts = BannerEntry::new( + "💲", + "Extensions", + &format!("[{}]", config.extensions.join(", ")), + ); + writeln!(&mut writer, "{}", b_exts)?; } if config.insecure { - writeln!( - &mut writer, - "{}", - format_banner_entry!(format_emoji("🔓"), "Insecure", config.insecure) - ) - .unwrap_or_default(); // 🔓 + let b_insec = BannerEntry::new("🔓", "Insecure", &config.insecure.to_string()); + writeln!(&mut writer, "{}", b_insec)?; } if config.redirects { - writeln!( - &mut writer, - "{}", - format_banner_entry!(format_emoji("📍"), "Follow Redirects", config.redirects) - ) - .unwrap_or_default(); // 📍 + let b_follow = BannerEntry::new("📍", "Follow Redirects", &config.redirects.to_string()); + writeln!(&mut writer, "{}", b_follow)?; } if config.dont_filter { - writeln!( - &mut writer, - "{}", - format_banner_entry!(format_emoji("🤪"), "Filter Wildcards", !config.dont_filter) - ) - .unwrap_or_default(); // 🤪 + let b_wild = BannerEntry::new("🤪", "Filter Wildcards", &(!config.dont_filter).to_string()); + writeln!(&mut writer, "{}", b_wild)?; } let volume = ["🔈", "🔉", "🔊", "📢"]; if let 1..=4 = config.verbosity { //speaker medium volume (increasing with verbosity to loudspeaker) - writeln!( - &mut writer, - "{}", - format_banner_entry!( - format_emoji(volume[config.verbosity as usize - 1]), - "Verbosity", - config.verbosity - ) - ) - .unwrap_or_default(); + let vol = BannerEntry::new( + volume[config.verbosity as usize - 1], + "Verbosity", + &config.verbosity.to_string(), + ); + writeln!(&mut writer, "{}", vol)?; } if config.add_slash { - writeln!( - &mut writer, - "{}", - format_banner_entry!(format_emoji("🪓"), "Add Slash", config.add_slash) - ) - .unwrap_or_default(); // 🪓 + let add = BannerEntry::new("🪓", "Add Slash", &config.add_slash.to_string()); + writeln!(&mut writer, "{}", add)?; } - if !config.no_recursion { - if config.depth == 0 { - writeln!( - &mut writer, - "{}", - format_banner_entry!(format_emoji("🔃"), "Recursion Depth", "INFINITE") - ) - .unwrap_or_default(); // 🔃 + let b_recurse = if !config.no_recursion { + let depth = if config.depth == 0 { + "INFINITE".to_string() } else { - writeln!( - &mut writer, - "{}", - format_banner_entry!(format_emoji("🔃"), "Recursion Depth", config.depth) - ) - .unwrap_or_default(); // 🔃 - } + config.depth.to_string() + }; + + BannerEntry::new("🔃", "Recursion Depth", &depth) } else { - writeln!( - &mut writer, - "{}", - format_banner_entry!(format_emoji("🚫"), "Do Not Recurse", config.no_recursion) - ) - .unwrap_or_default(); // 🚫 + BannerEntry::new("🚫", "Do Not Recurse", &config.no_recursion.to_string()) + }; + + writeln!(&mut writer, "{}", b_recurse)?; + + if config.scan_limit > 0 { + let s_lim = BannerEntry::new( + "🦥", + "Concurrent Scan Limit", + &config.scan_limit.to_string(), + ); + writeln!(&mut writer, "{}", s_lim)?; } - if CONFIGURATION.scan_limit > 0 { - writeln!( - &mut writer, - "{}", - format_banner_entry!( - format_emoji("🦥"), - "Concurrent Scan Limit", - config.scan_limit - ) - ) - .unwrap_or_default(); // 🦥 - } - - if !CONFIGURATION.time_limit.is_empty() { - writeln!( - &mut writer, - "{}", - format_banner_entry!(format_emoji("🕖"), "Time Limit", config.time_limit) - ) - .unwrap_or_default(); // 🕖 + if !config.time_limit.is_empty() { + let t_lim = BannerEntry::new("🕖", "Time Limit", &config.time_limit); + writeln!(&mut writer, "{}", t_lim)?; } if matches!(status, UpdateStatus::OutOfDate) { - writeln!( - &mut writer, - "{}", - format_banner_entry!( - format_emoji("🎉"), - "New Version Available", - "https://github.com/epi052/feroxbuster/releases/latest" - ) - ) - .unwrap_or_default(); // 🎉 + let update = BannerEntry::new( + "🎉", + "New Version Available", + "https://github.com/epi052/feroxbuster/releases/latest", + ); + writeln!(&mut writer, "{}", update)?; } - writeln!(&mut writer, "{}", bottom).unwrap_or_default(); - // ⏯ + writeln!(&mut writer, "{}", bottom)?; + writeln!( &mut writer, - " {} Press [{}] to use the {}™", - format_emoji("🏁"), + " 🏁 Press [{}] to use the {}™", style("ENTER").yellow(), style("Scan Cancel Menu").bright().yellow(), - ) - .unwrap_or_default(); + )?; - writeln!(&mut writer, "{}", addl_section).unwrap_or_default(); + writeln!(&mut writer, "{}", addl_section)?; + + Ok(()) } #[cfg(test)] @@ -564,7 +426,9 @@ mod tests { let config = Configuration::default(); let (tx, _): FeroxChannel = mpsc::unbounded_channel(); - initialize(&[], &config, VERSION, stderr(), tx).await; + initialize(&[], &config, VERSION, stderr(), tx) + .await + .unwrap(); } #[tokio::test(flavor = "multi_thread", worker_threads = 1)] @@ -584,7 +448,8 @@ mod tests { stderr(), tx, ) - .await; + .await + .unwrap(); } #[tokio::test(flavor = "multi_thread", worker_threads = 1)] @@ -604,7 +469,8 @@ mod tests { stderr(), tx, ) - .await; + .await + .unwrap(); } #[tokio::test(flavor = "multi_thread", worker_threads = 1)] @@ -624,7 +490,8 @@ mod tests { stderr(), tx, ) - .await; + .await + .unwrap(); } #[ignore] @@ -642,7 +509,8 @@ mod tests { &file, tx, ) - .await; + .await + .unwrap(); let contents = read_to_string(file.path()).unwrap(); println!("contents: {}", contents); assert!(contents.contains("New Version Available")); diff --git a/src/main.rs b/src/main.rs index 8dd8ba2..4d0c4d4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -322,7 +322,7 @@ async fn wrapped_main() -> Result<()> { std_stderr, tx_stats.clone(), ) - .await; + .await?; } // discard non-responsive targets