mirror of
https://github.com/epi052/feroxbuster
synced 2024-07-08 19:45:45 +00:00
added progress bars to output
This commit is contained in:
parent
a9f7eb3dbc
commit
b84c8cbdf4
|
@ -17,6 +17,7 @@ toml = "0.5"
|
|||
serde = { version = "1.0", features = ["derive"] }
|
||||
uuid = { version = "0.8", features = ["v4"] }
|
||||
ansi_term = "0.12"
|
||||
indicatif = "0.15"
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3.1"
|
||||
|
|
|
@ -2,6 +2,7 @@ use crate::utils::status_colorizer;
|
|||
use crate::{client, parser};
|
||||
use crate::{DEFAULT_CONFIG_NAME, DEFAULT_STATUS_CODES, DEFAULT_WORDLIST, VERSION};
|
||||
use clap::value_t;
|
||||
use indicatif::MultiProgress;
|
||||
use lazy_static::lazy_static;
|
||||
use reqwest::{Client, StatusCode};
|
||||
use serde::Deserialize;
|
||||
|
@ -14,6 +15,7 @@ use std::process::exit;
|
|||
lazy_static! {
|
||||
/// Global configuration variable.
|
||||
pub static ref CONFIGURATION: Configuration = Configuration::new();
|
||||
pub static ref PROGRESS_BAR: MultiProgress = MultiProgress::new();
|
||||
}
|
||||
|
||||
/// Represents the final, global configuration of the program.
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
use crate::config::CONFIGURATION;
|
||||
use crate::progress;
|
||||
use crate::scanner::{format_url, make_request};
|
||||
use crate::utils::{get_url_path_length, status_colorizer};
|
||||
use reqwest::Response;
|
||||
use std::process;
|
||||
use uuid::Uuid;
|
||||
use indicatif::ProgressBar;
|
||||
|
||||
const UUID_LENGTH: u64 = 32;
|
||||
|
||||
|
@ -44,7 +46,7 @@ fn unique_string(length: usize) -> String {
|
|||
///
|
||||
/// In the event that url returns a wildcard response, a
|
||||
/// [WildcardFilter](struct.WildcardFilter.html) is created and returned to the caller.
|
||||
pub async fn wildcard_test(target_url: &str) -> Option<WildcardFilter> {
|
||||
pub async fn wildcard_test(target_url: &str, bar: ProgressBar) -> Option<WildcardFilter> {
|
||||
log::trace!("enter: wildcard_test({:?})", target_url);
|
||||
|
||||
if CONFIGURATION.dontfilter {
|
||||
|
@ -53,7 +55,9 @@ pub async fn wildcard_test(target_url: &str) -> Option<WildcardFilter> {
|
|||
return None;
|
||||
}
|
||||
|
||||
if let Some(resp_one) = make_wildcard_request(&target_url, 1).await {
|
||||
if let Some(resp_one) = make_wildcard_request(&target_url, 1, bar.clone()).await {
|
||||
bar.inc(1);
|
||||
|
||||
// found a wildcard response
|
||||
let mut wildcard = WildcardFilter::default();
|
||||
|
||||
|
@ -66,7 +70,9 @@ pub async fn wildcard_test(target_url: &str) -> Option<WildcardFilter> {
|
|||
|
||||
// content length of wildcard is non-zero, perform additional tests:
|
||||
// make a second request, with a known-sized (64) longer request
|
||||
if let Some(resp_two) = make_wildcard_request(&target_url, 3).await {
|
||||
if let Some(resp_two) = make_wildcard_request(&target_url, 3, bar.clone()).await {
|
||||
bar.inc(1);
|
||||
|
||||
let wc2_length = resp_two.content_length().unwrap_or(0);
|
||||
|
||||
if wc2_length == wc_length + (UUID_LENGTH * 2) {
|
||||
|
@ -74,22 +80,30 @@ pub async fn wildcard_test(target_url: &str) -> Option<WildcardFilter> {
|
|||
// reflected in the response along with some static content; aka custom 404
|
||||
let url_len = get_url_path_length(&resp_one.url());
|
||||
|
||||
println!(
|
||||
bar.println(format!(
|
||||
"[{}] - Url is being reflected in wildcard response, i.e. a dynamic wildcard",
|
||||
status_colorizer("WILDCARD")
|
||||
);
|
||||
println!(
|
||||
"[{}] - Auto-filtering out responses that are [({} + url length) bytes] long; this behavior can be turned off by using --dontfilter",
|
||||
status_colorizer("WILDCARD"),
|
||||
wc_length - url_len,
|
||||
));
|
||||
bar.println(
|
||||
format!(
|
||||
"[{}] - Auto-filtering out responses that are [({} + url length) bytes] long; this behavior can be turned off by using --dontfilter",
|
||||
status_colorizer("WILDCARD"),
|
||||
wc_length - url_len,
|
||||
)
|
||||
);
|
||||
|
||||
wildcard.dynamic = wc_length - url_len;
|
||||
} else if wc_length == wc2_length {
|
||||
println!("[{}] - Wildcard response is a static size; auto-filtering out responses of size [{} bytes]; this behavior can be turned off by using --dontfilter", status_colorizer("WILDCARD"), wc_length);
|
||||
bar.println(format!(
|
||||
"[{}] - Wildcard response is a static size; auto-filtering out responses of size [{} bytes]; this behavior can be turned off by using --dontfilter",
|
||||
status_colorizer("WILDCARD"),
|
||||
wc_length
|
||||
));
|
||||
|
||||
wildcard.size = wc_length;
|
||||
}
|
||||
} else {
|
||||
bar.inc(2);
|
||||
}
|
||||
|
||||
log::trace!("exit: wildcard_test -> Some({:?})", wildcard);
|
||||
|
@ -106,7 +120,7 @@ pub async fn wildcard_test(target_url: &str) -> Option<WildcardFilter> {
|
|||
/// Once the unique url is created, the request is sent to the server. If the server responds
|
||||
/// back with a valid status code, the response is considered to be a wildcard response. If that
|
||||
/// wildcard response has a 3xx status code, that redirection location is displayed to the user.
|
||||
async fn make_wildcard_request(target_url: &str, length: usize) -> Option<Response> {
|
||||
async fn make_wildcard_request(target_url: &str, length: usize, bar: ProgressBar) -> Option<Response> {
|
||||
log::trace!("enter: make_wildcard_request({}, {})", target_url, length);
|
||||
|
||||
let unique_str = unique_string(length);
|
||||
|
@ -137,31 +151,37 @@ async fn make_wildcard_request(target_url: &str, length: usize) -> Option<Respon
|
|||
// found a wildcard response
|
||||
let url_len = get_url_path_length(&response.url());
|
||||
|
||||
println!(
|
||||
"[{}] - Received [{}] for {} (content: {} bytes, url length: {})",
|
||||
wildcard,
|
||||
status_colorizer(&response.status().to_string()),
|
||||
response.url(),
|
||||
response.content_length().unwrap_or(0),
|
||||
url_len
|
||||
bar.println(
|
||||
format!(
|
||||
"[{}] - Received [{}] for {} (content: {} bytes, url length: {})",
|
||||
wildcard,
|
||||
status_colorizer(&response.status().to_string()),
|
||||
response.url(),
|
||||
response.content_length().unwrap_or(0),
|
||||
url_len
|
||||
)
|
||||
);
|
||||
|
||||
if response.status().is_redirection() {
|
||||
// show where it goes, if possible
|
||||
if let Some(next_loc) = response.headers().get("Location") {
|
||||
if let Ok(next_loc_str) = next_loc.to_str() {
|
||||
println!(
|
||||
"[{}] {} redirects to => {}",
|
||||
wildcard,
|
||||
response.url(),
|
||||
next_loc_str
|
||||
bar.println(
|
||||
format!(
|
||||
"[{}] {} redirects to => {}",
|
||||
wildcard,
|
||||
response.url(),
|
||||
next_loc_str
|
||||
)
|
||||
);
|
||||
} else {
|
||||
println!(
|
||||
"[{}] {} redirects to => {:?}",
|
||||
wildcard,
|
||||
response.url(),
|
||||
next_loc
|
||||
bar.println(
|
||||
format!(
|
||||
"[{}] {} redirects to => {:?}",
|
||||
wildcard,
|
||||
response.url(),
|
||||
next_loc
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -190,6 +210,9 @@ pub async fn connectivity_test(target_urls: &[String]) -> Vec<String> {
|
|||
|
||||
let mut good_urls = vec![];
|
||||
|
||||
// hidden bar just to get ProgressBar::println functionality
|
||||
let bar = progress::add_bar("", 1, true);
|
||||
|
||||
for target_url in target_urls {
|
||||
let request = match format_url(
|
||||
target_url,
|
||||
|
@ -201,6 +224,7 @@ pub async fn connectivity_test(target_urls: &[String]) -> Vec<String> {
|
|||
Ok(url) => url,
|
||||
Err(e) => {
|
||||
log::error!("{}", e);
|
||||
bar.inc(1);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
@ -210,12 +234,14 @@ pub async fn connectivity_test(target_urls: &[String]) -> Vec<String> {
|
|||
good_urls.push(target_url.to_owned());
|
||||
}
|
||||
Err(e) => {
|
||||
println!("Could not connect to {}, skipping...", target_url);
|
||||
bar.println(format!("Could not connect to {}, skipping...", target_url));
|
||||
log::error!("{}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bar.finish();
|
||||
|
||||
if good_urls.is_empty() {
|
||||
log::error!("Could not connect to any target provided, exiting.");
|
||||
log::trace!("exit: connectivity_test");
|
||||
|
|
|
@ -4,6 +4,7 @@ pub mod config;
|
|||
pub mod heuristics;
|
||||
pub mod logger;
|
||||
pub mod parser;
|
||||
pub mod progress;
|
||||
pub mod scanner;
|
||||
pub mod utils;
|
||||
|
||||
|
|
|
@ -71,7 +71,6 @@ async fn scan(targets: Vec<String>) -> FeroxResult<()> {
|
|||
|
||||
// drive execution of all accumulated futures
|
||||
futures::future::join_all(tasks).await;
|
||||
|
||||
log::trace!("exit: scan");
|
||||
|
||||
Ok(())
|
||||
|
@ -120,13 +119,16 @@ async fn main() {
|
|||
if !CONFIGURATION.quiet {
|
||||
// only print banner if -q isn't used
|
||||
banner::initialize(&targets);
|
||||
// progress::initialize();
|
||||
}
|
||||
|
||||
// discard non-responsive targets
|
||||
let live_targets = heuristics::connectivity_test(&targets).await;
|
||||
|
||||
match scan(live_targets).await {
|
||||
Ok(_) => log::info!("Done"),
|
||||
Ok(_) => {
|
||||
log::info!("Done");
|
||||
}
|
||||
Err(e) => log::error!("An error occurred: {}", e),
|
||||
};
|
||||
|
||||
|
|
21
src/progress.rs
Normal file
21
src/progress.rs
Normal file
|
@ -0,0 +1,21 @@
|
|||
use crate::config::PROGRESS_BAR;
|
||||
use indicatif::{ProgressBar, ProgressStyle};
|
||||
|
||||
pub fn add_bar(prefix: &str, length: u64, hidden: bool) -> ProgressBar {
|
||||
let style = ProgressStyle::default_bar()
|
||||
.template("[{bar:.cyan/blue}] - {elapsed:<4} {pos:>7}/{len:7} {per_sec:7} {prefix}")
|
||||
.progress_chars("#>-");
|
||||
|
||||
let progress_bar = if hidden {
|
||||
// PROGRESS_BAR.add(ProgressBar::hidden())
|
||||
ProgressBar::hidden()
|
||||
} else {
|
||||
PROGRESS_BAR.add(ProgressBar::new(length))
|
||||
};
|
||||
|
||||
progress_bar.set_style(style);
|
||||
|
||||
progress_bar.set_prefix(&prefix);
|
||||
|
||||
progress_bar
|
||||
}
|
|
@ -1,9 +1,10 @@
|
|||
use crate::config::CONFIGURATION;
|
||||
use crate::config::{CONFIGURATION, PROGRESS_BAR};
|
||||
use crate::heuristics::WildcardFilter;
|
||||
use crate::utils::{get_current_depth, get_url_path_length, status_colorizer};
|
||||
use crate::{heuristics, FeroxResult};
|
||||
use crate::{heuristics, progress, FeroxResult};
|
||||
use futures::future::{BoxFuture, FutureExt};
|
||||
use futures::{stream, StreamExt};
|
||||
use indicatif::ProgressBar;
|
||||
use reqwest::{Client, Response, Url};
|
||||
use std::collections::HashSet;
|
||||
use std::fs::OpenOptions;
|
||||
|
@ -12,6 +13,7 @@ use std::ops::Deref;
|
|||
use std::sync::Arc;
|
||||
use tokio::sync::mpsc::{self, UnboundedReceiver, UnboundedSender};
|
||||
use tokio::task::JoinHandle;
|
||||
use std::convert::TryInto;
|
||||
|
||||
/// Simple helper to generate a `Url`
|
||||
///
|
||||
|
@ -164,23 +166,27 @@ async fn spawn_file_reporter(mut report_channel: UnboundedReceiver<Response>) {
|
|||
///
|
||||
/// The consumer simply receives responses and prints them if they meet the given
|
||||
/// reporting criteria
|
||||
async fn spawn_terminal_reporter(mut report_channel: UnboundedReceiver<Response>) {
|
||||
async fn spawn_terminal_reporter(
|
||||
mut report_channel: UnboundedReceiver<Response>,
|
||||
bar: ProgressBar,
|
||||
) {
|
||||
log::trace!("enter: spawn_terminal_reporter({:?})", report_channel);
|
||||
//todo trace
|
||||
|
||||
while let Some(resp) = report_channel.recv().await {
|
||||
log::debug!("received {} on reporting channel", resp.url());
|
||||
|
||||
if CONFIGURATION.statuscodes.contains(&resp.status().as_u16()) {
|
||||
if CONFIGURATION.quiet {
|
||||
println!("{}", resp.url());
|
||||
bar.println(format!("{}", resp.url()));
|
||||
} else {
|
||||
let status = status_colorizer(&resp.status().to_string());
|
||||
println!(
|
||||
bar.println(format!(
|
||||
"[{}] - {} - [{} bytes]",
|
||||
status,
|
||||
resp.url(),
|
||||
resp.content_length().unwrap_or(0)
|
||||
);
|
||||
));
|
||||
}
|
||||
}
|
||||
log::debug!("report complete: {}", resp.url());
|
||||
|
@ -492,11 +498,26 @@ pub async fn scan_url(target_url: &str, wordlist: Arc<HashSet<String>>, base_dep
|
|||
let (tx_dir, rx_dir): (UnboundedSender<String>, UnboundedReceiver<String>) =
|
||||
mpsc::unbounded_channel();
|
||||
|
||||
let num_reqs_expected: u64 = if CONFIGURATION.extensions.is_empty() {
|
||||
wordlist.len().try_into().unwrap()
|
||||
} else {
|
||||
let total = wordlist.len() * (CONFIGURATION.extensions.len() + 1);
|
||||
total.try_into().unwrap()
|
||||
};
|
||||
|
||||
let progress_bar = progress::add_bar(&target_url, num_reqs_expected, false);
|
||||
progress_bar.reset_elapsed();
|
||||
|
||||
let bar_future = tokio::task::spawn_blocking(move || PROGRESS_BAR.join().unwrap());
|
||||
|
||||
let reporter_bar = progress_bar.clone();
|
||||
let wildcard_bar = progress_bar.clone();
|
||||
|
||||
let reporter = if !CONFIGURATION.output.is_empty() {
|
||||
// output file defined
|
||||
tokio::spawn(async move { spawn_file_reporter(rx_rpt).await })
|
||||
} else {
|
||||
tokio::spawn(async move { spawn_terminal_reporter(rx_rpt).await })
|
||||
tokio::spawn(async move { spawn_terminal_reporter(rx_rpt, reporter_bar).await })
|
||||
};
|
||||
|
||||
// lifetime satisfiers, as it's an Arc, clones are cheap anyway
|
||||
|
@ -508,7 +529,7 @@ pub async fn scan_url(target_url: &str, wordlist: Arc<HashSet<String>>, base_dep
|
|||
async move { spawn_recursion_handler(rx_dir, recurser_words, base_depth).await },
|
||||
);
|
||||
|
||||
let filter = match heuristics::wildcard_test(&target_url).await {
|
||||
let filter = match heuristics::wildcard_test(&target_url, wildcard_bar).await {
|
||||
Some(f) => {
|
||||
if CONFIGURATION.dontfilter {
|
||||
// don't auto filter, i.e. use the defaults
|
||||
|
@ -526,14 +547,20 @@ pub async fn scan_url(target_url: &str, wordlist: Arc<HashSet<String>>, base_dep
|
|||
let wc_filter = filter.clone();
|
||||
let txd = tx_dir.clone();
|
||||
let txr = tx_rpt.clone();
|
||||
let pb = progress_bar.clone(); // progress bar is an Arc around internal state
|
||||
let tgt = target_url.to_string(); // done to satisfy 'static lifetime below
|
||||
tokio::spawn(async move {
|
||||
make_requests(&tgt, &word, base_depth, wc_filter, txd, txr).await
|
||||
})
|
||||
(
|
||||
tokio::spawn(async move {
|
||||
make_requests(&tgt, &word, base_depth, wc_filter, txd, txr).await
|
||||
}),
|
||||
pb,
|
||||
)
|
||||
})
|
||||
.for_each_concurrent(CONFIGURATION.threads, |resp| async move {
|
||||
.for_each_concurrent(CONFIGURATION.threads, |(resp, bar)| async move {
|
||||
match resp.await {
|
||||
Ok(_) => {}
|
||||
Ok(_) => {
|
||||
bar.inc(1);
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("error awaiting a response: {}", e);
|
||||
}
|
||||
|
@ -545,6 +572,8 @@ pub async fn scan_url(target_url: &str, wordlist: Arc<HashSet<String>>, base_dep
|
|||
producers.await;
|
||||
log::trace!("done awaiting scan producers");
|
||||
|
||||
progress_bar.finish();
|
||||
|
||||
// manually drop tx in order for the rx task's while loops to eval to false
|
||||
log::trace!("dropped recursion handler's transmitter");
|
||||
drop(tx_dir);
|
||||
|
@ -567,6 +596,8 @@ pub async fn scan_url(target_url: &str, wordlist: Arc<HashSet<String>>, base_dep
|
|||
}
|
||||
log::trace!("done awaiting report receiver");
|
||||
|
||||
bar_future.await.unwrap();
|
||||
|
||||
log::trace!("exit: scan_url");
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue
Block a user