mirror of
https://github.com/epi052/feroxbuster.git
synced 2026-06-01 13:01:19 -03:00
added terminal input event handlers
This commit is contained in:
147
src/event_handlers/inputs.rs
Normal file
147
src/event_handlers/inputs.rs
Normal file
@@ -0,0 +1,147 @@
|
||||
use super::*;
|
||||
use crate::{
|
||||
config::PROGRESS_PRINTER,
|
||||
scan_manager::FeroxState,
|
||||
scan_manager::PAUSE_SCAN,
|
||||
scanner::RESPONSES,
|
||||
utils::{open_file, write_to},
|
||||
SLEEP_DURATION,
|
||||
};
|
||||
use anyhow::Result;
|
||||
use console::style;
|
||||
use crossterm::event::{self, Event, KeyCode};
|
||||
use std::{
|
||||
sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
Arc,
|
||||
},
|
||||
thread::sleep,
|
||||
time::Duration,
|
||||
time::{SystemTime, UNIX_EPOCH},
|
||||
};
|
||||
|
||||
/// Atomic boolean flag, used to determine whether or not the terminal input handler should exit
|
||||
pub static SCAN_COMPLETE: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct TerminalInputHandle {}
|
||||
|
||||
#[derive(Debug)]
|
||||
/// Container for filters transmitter and FeroxFilters object
|
||||
pub struct TermInputHandler {
|
||||
/// handles to other handlers
|
||||
handles: Arc<Handles>,
|
||||
}
|
||||
|
||||
/// implementation of event handler for terminal input (used for cancel scan menu) todo update for correctness /// Initialize the ctrl+c handler that saves scan state to disk
|
||||
impl TermInputHandler {
|
||||
/// Create new event handler
|
||||
pub fn new(handles: Arc<Handles>) -> Self {
|
||||
Self { handles }
|
||||
}
|
||||
|
||||
/// Initialize the sc side of an mpsc channel that is responsible for updates todo update for correctness
|
||||
pub fn initialize(handles: Arc<Handles>) {
|
||||
log::trace!("enter: initialize({:?})", handles);
|
||||
|
||||
let handler = Self::new(handles);
|
||||
handler.start();
|
||||
|
||||
log::trace!("exit: initialize");
|
||||
}
|
||||
|
||||
fn start(&self) {
|
||||
tokio::task::spawn_blocking(Self::start_enter_handler);
|
||||
|
||||
let cloned = self.handles.clone();
|
||||
|
||||
if self.handles.config.save_state {
|
||||
// start the ctrl+c handler
|
||||
let result = ctrlc::set_handler(move || {
|
||||
let _ = Self::sigint_handler(cloned.clone());
|
||||
});
|
||||
|
||||
if result.is_err() {
|
||||
log::error!("Could not set Ctrl+c handler");
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Writes the current state of the program to disk (if save_state is true) and then exits
|
||||
pub fn sigint_handler(handles: Arc<Handles>) -> Result<()> {
|
||||
log::trace!("enter: sigint_handler({:?})", handles);
|
||||
|
||||
let ts = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs();
|
||||
|
||||
let slug = if !handles.config.target_url.is_empty() {
|
||||
// target url populated
|
||||
handles
|
||||
.config
|
||||
.target_url
|
||||
.replace("://", "_")
|
||||
.replace("/", "_")
|
||||
.replace(".", "_")
|
||||
} else {
|
||||
// stdin used
|
||||
"stdin".to_string()
|
||||
};
|
||||
|
||||
let filename = format!("ferox-{}-{}.state", slug, ts);
|
||||
let warning = format!(
|
||||
"🚨 Caught {} 🚨 saving scan state to {} ...",
|
||||
style("ctrl+c").yellow(),
|
||||
filename
|
||||
);
|
||||
|
||||
PROGRESS_PRINTER.println(warning);
|
||||
|
||||
let state = FeroxState::new(
|
||||
handles.ferox_scans()?,
|
||||
handles.config.clone(),
|
||||
&RESPONSES,
|
||||
handles.stats.data.clone(),
|
||||
);
|
||||
|
||||
let state_file = open_file(&filename);
|
||||
|
||||
let mut buffered_file = state_file?;
|
||||
write_to(&state, &mut buffered_file, true)?;
|
||||
|
||||
log::trace!("exit: sigint_handler (end of program)");
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
/// Handles specific key events triggered by the user over stdin
|
||||
fn start_enter_handler() {
|
||||
// todo eventually move away from atomics, the blocking recv is the problem
|
||||
log::trace!("enter: start");
|
||||
|
||||
loop {
|
||||
if PAUSE_SCAN.load(Ordering::Relaxed) {
|
||||
// if the scan is already paused, we don't want this event poller fighting the user
|
||||
// over stdin
|
||||
sleep(Duration::from_millis(SLEEP_DURATION));
|
||||
} else if event::poll(Duration::from_millis(SLEEP_DURATION)).unwrap_or(false) {
|
||||
// It's guaranteed that the `read()` won't block when the `poll()`
|
||||
// function returns `true`
|
||||
|
||||
if let Ok(key_pressed) = event::read() {
|
||||
// ignore any other keys
|
||||
if key_pressed == Event::Key(KeyCode::Enter.into()) {
|
||||
// if the user presses Enter, set PAUSE_SCAN to true. The interactive menu
|
||||
// will be triggered and will handle setting PAUSE_SCAN to false
|
||||
PAUSE_SCAN.store(true, Ordering::Release);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Timeout expired and no `Event` is available; use the timeout to check SCAN_COMPLETE
|
||||
if SCAN_COMPLETE.load(Ordering::Relaxed) {
|
||||
// scan has been marked complete by main, time to exit the loop
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
log::trace!("exit: start");
|
||||
}
|
||||
}
|
||||
@@ -5,10 +5,12 @@ mod container;
|
||||
mod command;
|
||||
mod outputs;
|
||||
mod scans;
|
||||
mod inputs;
|
||||
|
||||
pub use self::command::Command;
|
||||
pub use self::container::{Handles, Tasks};
|
||||
pub use self::filters::{FiltersHandle, FiltersHandler};
|
||||
pub use self::inputs::{TermInputHandler, SCAN_COMPLETE};
|
||||
pub use self::outputs::{TermOutHandle, TermOutHandler};
|
||||
pub use self::scans::{ScanHandle, ScanHandler};
|
||||
pub use self::statistics::{StatsHandle, StatsHandler};
|
||||
|
||||
57
src/main.rs
57
src/main.rs
@@ -2,16 +2,10 @@ use std::{
|
||||
collections::HashSet,
|
||||
fs::File,
|
||||
io::{stderr, BufRead, BufReader},
|
||||
sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
Arc,
|
||||
},
|
||||
thread::sleep,
|
||||
time::Duration,
|
||||
sync::{atomic::Ordering, Arc},
|
||||
};
|
||||
|
||||
use anyhow::{bail, Context, Result};
|
||||
use crossterm::event::{self, Event, KeyCode};
|
||||
use futures::StreamExt;
|
||||
use tokio::{io, sync::oneshot};
|
||||
use tokio_util::codec::{FramedRead, LinesCodec};
|
||||
@@ -21,52 +15,17 @@ use feroxbuster::{
|
||||
config::{Configuration, PROGRESS_BAR, PROGRESS_PRINTER},
|
||||
event_handlers::{
|
||||
Command::{CreateBar, Exit, JoinTasks, LoadStats, ScanInitialUrls, UpdateWordlist},
|
||||
FiltersHandler, Handles, ScanHandler, StatsHandler, Tasks, TermOutHandler,
|
||||
FiltersHandler, Handles, ScanHandler, StatsHandler, Tasks, TermInputHandler,
|
||||
TermOutHandler, SCAN_COMPLETE,
|
||||
},
|
||||
heuristics, logger,
|
||||
scan_manager::{self, PAUSE_SCAN},
|
||||
scan_manager::{self},
|
||||
scanner,
|
||||
utils::fmt_err,
|
||||
SLEEP_DURATION,
|
||||
};
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
use feroxbuster::{utils::set_open_file_limit, DEFAULT_OPEN_FILE_LIMIT};
|
||||
|
||||
/// Atomic boolean flag, used to determine whether or not the terminal input handler should exit
|
||||
static SCAN_COMPLETE: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
/// Handles specific key events triggered by the user over stdin
|
||||
fn terminal_input_handler() {
|
||||
log::trace!("enter: terminal_input_handler");
|
||||
|
||||
loop {
|
||||
if PAUSE_SCAN.load(Ordering::Relaxed) {
|
||||
// if the scan is already paused, we don't want this event poller fighting the user
|
||||
// over stdin
|
||||
sleep(Duration::from_millis(SLEEP_DURATION));
|
||||
} else if event::poll(Duration::from_millis(SLEEP_DURATION)).unwrap_or(false) {
|
||||
// It's guaranteed that the `read()` won't block when the `poll()`
|
||||
// function returns `true`
|
||||
|
||||
if let Ok(key_pressed) = event::read() {
|
||||
// ignore any other keys
|
||||
if key_pressed == Event::Key(KeyCode::Enter.into()) {
|
||||
// if the user presses Enter, set PAUSE_SCAN to true. The interactive menu
|
||||
// will be triggered and will handle setting PAUSE_SCAN to false
|
||||
PAUSE_SCAN.store(true, Ordering::Release);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Timeout expired and no `Event` is available; use the timeout to check SCAN_COMPLETE
|
||||
if SCAN_COMPLETE.load(Ordering::Relaxed) {
|
||||
// scan has been marked complete by main, time to exit the loop
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
log::trace!("exit: terminal_input_handler");
|
||||
}
|
||||
|
||||
/// Create a HashSet of Strings from the given wordlist then stores it inside an Arc
|
||||
fn get_unique_words_from_wordlist(path: &str) -> Result<Arc<HashSet<String>>> {
|
||||
log::trace!("enter: get_unique_words_from_wordlist({})", path);
|
||||
@@ -236,12 +195,8 @@ async fn wrapped_main(config: Arc<Configuration>) -> Result<()> {
|
||||
// spawn a thread that listens for keyboard input on stdin, when a user presses enter
|
||||
// the input handler will toggle PAUSE_SCAN, which in turn is used to pause and resume
|
||||
// scans that are already running
|
||||
tokio::task::spawn_blocking(terminal_input_handler);
|
||||
|
||||
if config.save_state {
|
||||
// start the ctrl+c handler
|
||||
scan_manager::initialize(handles.clone());
|
||||
}
|
||||
// also starts ctrl+c handler
|
||||
TermInputHandler::initialize(handles.clone());
|
||||
|
||||
if config.resumed {
|
||||
let scanned_urls = handles.ferox_scans()?;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use std::{
|
||||
cmp::PartialEq,
|
||||
collections::HashMap,
|
||||
convert::TryInto,
|
||||
fmt,
|
||||
fs::File,
|
||||
io::BufReader,
|
||||
@@ -8,7 +9,6 @@ use std::{
|
||||
sync::atomic::{AtomicBool, AtomicUsize, Ordering},
|
||||
sync::{Arc, Mutex, RwLock},
|
||||
thread::sleep,
|
||||
time::{SystemTime, UNIX_EPOCH},
|
||||
};
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
@@ -26,19 +26,16 @@ use tokio::{
|
||||
};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::event_handlers::Handles;
|
||||
use crate::utils::fmt_err;
|
||||
use crate::utils::write_to;
|
||||
use crate::{
|
||||
config::{Configuration, CONFIGURATION, PROGRESS_BAR, PROGRESS_PRINTER},
|
||||
config::{Configuration, PROGRESS_BAR, PROGRESS_PRINTER},
|
||||
event_handlers::Handles,
|
||||
parser::TIMESPEC_REGEX,
|
||||
progress::{add_bar, BarType},
|
||||
scanner::RESPONSES,
|
||||
statistics::Stats,
|
||||
utils::open_file,
|
||||
utils::fmt_err,
|
||||
FeroxResponse, FeroxSerialize, SLEEP_DURATION,
|
||||
};
|
||||
use std::convert::TryInto;
|
||||
|
||||
/// Single atomic number that gets incremented once, used to track first thread to interact with
|
||||
/// when pausing a scan
|
||||
@@ -939,7 +936,7 @@ pub struct FeroxState {
|
||||
scans: Arc<FeroxScans>,
|
||||
|
||||
/// Current running config
|
||||
config: &'static Configuration,
|
||||
config: Arc<Configuration>,
|
||||
|
||||
/// Known responses
|
||||
responses: &'static FeroxResponses,
|
||||
@@ -948,6 +945,24 @@ pub struct FeroxState {
|
||||
statistics: Arc<Stats>,
|
||||
}
|
||||
|
||||
/// implementation of FeroxState
|
||||
impl FeroxState {
|
||||
/// create new FeroxState object
|
||||
pub fn new(
|
||||
scans: Arc<FeroxScans>,
|
||||
config: Arc<Configuration>,
|
||||
responses: &'static FeroxResponses,
|
||||
statistics: Arc<Stats>,
|
||||
) -> Self {
|
||||
Self {
|
||||
scans,
|
||||
config,
|
||||
responses,
|
||||
statistics,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// FeroxSerialize implementation for FeroxState
|
||||
impl FeroxSerialize for FeroxState {
|
||||
/// Simply return debug format of FeroxState to satisfy as_str
|
||||
@@ -998,7 +1013,7 @@ pub async fn start_max_time_thread(handles: Arc<Handles>) {
|
||||
#[cfg(test)]
|
||||
panic!(handles);
|
||||
#[cfg(not(test))]
|
||||
let _ = sigint_handler(handles.clone());
|
||||
let _ = crate::event_handlers::TermInputHandler::sigint_handler(handles.clone());
|
||||
}
|
||||
|
||||
log::error!(
|
||||
@@ -1007,65 +1022,6 @@ pub async fn start_max_time_thread(handles: Arc<Handles>) {
|
||||
);
|
||||
}
|
||||
|
||||
/// Writes the current state of the program to disk (if save_state is true) and then exits
|
||||
fn sigint_handler(handles: Arc<Handles>) -> Result<()> {
|
||||
log::trace!("enter: sigint_handler({:?})", handles);
|
||||
|
||||
let ts = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs();
|
||||
|
||||
let slug = if !CONFIGURATION.target_url.is_empty() {
|
||||
// target url populated
|
||||
CONFIGURATION
|
||||
.target_url
|
||||
.replace("://", "_")
|
||||
.replace("/", "_")
|
||||
.replace(".", "_")
|
||||
} else {
|
||||
// stdin used
|
||||
"stdin".to_string()
|
||||
};
|
||||
|
||||
let filename = format!("ferox-{}-{}.state", slug, ts);
|
||||
let warning = format!(
|
||||
"🚨 Caught {} 🚨 saving scan state to {} ...",
|
||||
style("ctrl+c").yellow(),
|
||||
filename
|
||||
);
|
||||
|
||||
PROGRESS_PRINTER.println(warning);
|
||||
|
||||
let state = FeroxState {
|
||||
config: &CONFIGURATION,
|
||||
scans: handles.ferox_scans()?,
|
||||
responses: &RESPONSES,
|
||||
statistics: handles.stats.data.clone(),
|
||||
};
|
||||
|
||||
let state_file = open_file(&filename);
|
||||
|
||||
let mut buffered_file = state_file?;
|
||||
write_to(&state, &mut buffered_file, true)?;
|
||||
|
||||
log::trace!("exit: sigint_handler (end of program)");
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
/// Initialize the ctrl+c handler that saves scan state to disk
|
||||
pub fn initialize(handles: Arc<Handles>) {
|
||||
log::trace!("enter: initialize({:?})", handles);
|
||||
|
||||
let result = ctrlc::set_handler(move || {
|
||||
let _ = sigint_handler(handles.clone());
|
||||
});
|
||||
|
||||
if result.is_err() {
|
||||
log::error!("Could not set Ctrl+c handler");
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
log::trace!("exit: initialize");
|
||||
}
|
||||
|
||||
/// Primary logic used to load a Configuration from disk and populate the appropriate data
|
||||
/// structures
|
||||
pub fn resume_scan(filename: &str) -> Configuration {
|
||||
@@ -1433,12 +1389,12 @@ mod tests {
|
||||
let response: FeroxResponse = serde_json::from_str(json_response).unwrap();
|
||||
RESPONSES.insert(response);
|
||||
|
||||
let ferox_state = FeroxState {
|
||||
scans: Arc::new(ferox_scans),
|
||||
responses: &RESPONSES,
|
||||
config: &CONFIGURATION,
|
||||
statistics: stats,
|
||||
};
|
||||
let ferox_state = FeroxState::new(
|
||||
Arc::new(ferox_scans),
|
||||
Arc::new(Configuration::new().unwrap()),
|
||||
&RESPONSES,
|
||||
stats,
|
||||
);
|
||||
|
||||
let expected_strs = predicates::str::contains("scans: FeroxScans").and(
|
||||
predicate::str::contains("config: Configuration")
|
||||
|
||||
@@ -458,7 +458,7 @@ fn scanner_single_request_scan_with_debug_logging() {
|
||||
assert!(contents.contains("DBG"));
|
||||
assert!(contents.contains("INF"));
|
||||
assert!(contents.contains("feroxbuster All scans complete!"));
|
||||
assert!(contents.contains("feroxbuster exit: terminal_input_handler"));
|
||||
assert!(contents.contains("feroxbuster::event_handlers::inputs exit: start"));
|
||||
|
||||
assert_eq!(mock.hits(), 1);
|
||||
teardown_tmp_directory(tmp_dir);
|
||||
@@ -497,8 +497,8 @@ fn scanner_single_request_scan_with_debug_logging_as_json() {
|
||||
assert!(contents.contains("\"level\":\"INFO\""));
|
||||
assert!(contents.contains("time_offset"));
|
||||
assert!(contents.contains("\"module\":\"feroxbuster::scanner\""));
|
||||
assert!(contents.contains("\"module\":\"feroxbuster::event_handlers::inputs\""));
|
||||
assert!(contents.contains("All scans complete!"));
|
||||
assert!(contents.contains("exit: terminal_input_handler"));
|
||||
|
||||
assert_eq!(mock.hits(), 1);
|
||||
teardown_tmp_directory(tmp_dir);
|
||||
|
||||
Reference in New Issue
Block a user