diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 36ea32a..39ed26b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,4 +1,4 @@ - name: Build +name: Build on: push: diff --git a/Cargo.lock b/Cargo.lock index 71c1bef..9d5a595 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -27,12 +27,53 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" [[package]] -name = "ansi_term" -version = "0.12.1" +name = "anstream" +version = "0.6.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" +checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" dependencies = [ - "winapi", + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" + +[[package]] +name = "anstyle-parse" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" +dependencies = [ + "anstyle", + "once_cell", + "windows-sys 0.59.0", ] [[package]] @@ -79,12 +120,6 @@ version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - [[package]] name = "bitflags" version = "2.9.0" @@ -131,25 +166,56 @@ checksum = "1a48563284b67c003ba0fb7243c87fab68885e1532c605704228a80238512e31" [[package]] name = "clap" -version = "2.34.0" +version = "4.5.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" +checksum = "6088f3ae8c3608d19260cd7445411865a485688711b78b5be70d78cd96136f83" dependencies = [ - "ansi_term", - "atty", - "bitflags 1.3.2", - "strsim 0.8.0", - "textwrap", - "unicode-width", - "vec_map", + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22a7ef7f676155edfb82daa97f99441f3ebf4a58d5e32f295a56259f1b6facc8" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", ] +[[package]] +name = "clap_derive" +version = "4.5.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7" +dependencies = [ + "heck", + "proc-macro2 1.0.94", + "quote 1.0.40", + "syn 2.0.100", +] + +[[package]] +name = "clap_lex" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" + [[package]] name = "clru" version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cbd0f76e066e64fdc5631e3bb46381254deab9ef1158292f27c8c57e3bf3fe59" +[[package]] +name = "colorchoice" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" + [[package]] name = "colored" version = "3.0.0" @@ -259,7 +325,7 @@ dependencies = [ "ident_case", "proc-macro2 1.0.94", "quote 1.0.40", - "strsim 0.11.1", + "strsim", "syn 2.0.100", ] @@ -314,6 +380,12 @@ dependencies = [ "syn 2.0.100", ] +[[package]] +name = "diff" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" + [[package]] name = "dirble" version = "1.4.2" @@ -327,6 +399,7 @@ dependencies = [ "encoding", "log", "percent-encoding", + "pretty_assertions", "rand 0.9.0", "select", "serde", @@ -335,6 +408,7 @@ dependencies = [ "simple_xml_serialize", "simple_xml_serialize_macro", "simplelog", + "tempfile", "time", "url", "vergen-gix", @@ -650,7 +724,7 @@ version = "0.14.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "11365144ef93082f3403471dbaa94cfe4b5e72743bdb9560719a251d439f4cee" dependencies = [ - "bitflags 2.9.0", + "bitflags", "bstr", "gix-path", "libc", @@ -733,7 +807,7 @@ version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aaf69a6bec0a3581567484bf99a4003afcaf6c469fd4214352517ea355cf3435" dependencies = [ - "bitflags 2.9.0", + "bitflags", "bstr", "gix-features", "gix-path", @@ -766,7 +840,7 @@ version = "0.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "270645fd20556b64c8ffa1540d921b281e6994413a0ca068596f97e9367a257a" dependencies = [ - "bitflags 2.9.0", + "bitflags", "bstr", "filetime", "fnv", @@ -955,7 +1029,7 @@ version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61e1ddc474405a68d2ce8485705dd72fe6ce959f2f5fe718601ead5da2c8f9e7" dependencies = [ - "bitflags 2.9.0", + "bitflags", "bstr", "gix-commitgraph", "gix-date", @@ -988,7 +1062,7 @@ version = "0.10.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d84dae13271f4313f8d60a166bf27e54c968c7c33e2ffd31c48cafe5da649875" dependencies = [ - "bitflags 2.9.0", + "bitflags", "gix-path", "libc", "windows-sys 0.52.0", @@ -1049,7 +1123,7 @@ version = "0.43.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ed47d648619e23e93f971d2bba0d10c1100e54ef95d2981d609907a8cabac89" dependencies = [ - "bitflags 2.9.0", + "bitflags", "gix-commitgraph", "gix-date", "gix-hash", @@ -1104,6 +1178,12 @@ dependencies = [ "allocator-api2", ] +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "hermit-abi" version = "0.1.19" @@ -1281,6 +1361,12 @@ dependencies = [ "icu_properties", ] +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + [[package]] name = "itoa" version = "1.0.15" @@ -1328,7 +1414,7 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" dependencies = [ - "bitflags 2.9.0", + "bitflags", "libc", "redox_syscall", ] @@ -1458,7 +1544,7 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" dependencies = [ - "bitflags 2.9.0", + "bitflags", "cfg-if", "cfg_aliases", "libc", @@ -1640,6 +1726,16 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" +[[package]] +name = "pretty_assertions" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d" +dependencies = [ + "diff", + "yansi", +] + [[package]] name = "proc-macro2" version = "0.4.30" @@ -1772,7 +1868,7 @@ version = "0.5.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b8c0c260b63a8219631167be35e6a988e9554dbd323f8bd08439c8ed1302bd1" dependencies = [ - "bitflags 2.9.0", + "bitflags", ] [[package]] @@ -1787,7 +1883,7 @@ version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags 2.9.0", + "bitflags", "errno", "libc", "linux-raw-sys 0.4.15", @@ -1800,7 +1896,7 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f7178faa4b75a30e269c71e61c353ce2748cf3d76f0c44c393f4e60abf49b825" dependencies = [ - "bitflags 2.9.0", + "bitflags", "errno", "libc", "linux-raw-sys 0.9.2", @@ -2018,12 +2114,6 @@ dependencies = [ "quote 1.0.40", ] -[[package]] -name = "strsim" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" - [[package]] name = "strsim" version = "0.11.1" @@ -2090,9 +2180,9 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.19.0" +version = "3.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "488960f40a3fd53d72c2a29a58722561dee8afdd175bd88e3db4677d7b2ba600" +checksum = "7437ac7763b9b123ccf33c338a5cc1bac6f69b45a136c19bdd8a65e3916435bf" dependencies = [ "fastrand", "getrandom 0.3.1", @@ -2121,15 +2211,6 @@ dependencies = [ "winapi-util", ] -[[package]] -name = "textwrap" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" -dependencies = [ - "unicode-width", -] - [[package]] name = "thiserror" version = "2.0.12" @@ -2229,12 +2310,6 @@ dependencies = [ "tinyvec", ] -[[package]] -name = "unicode-width" -version = "0.1.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" - [[package]] name = "unicode-xid" version = "0.1.0" @@ -2271,16 +2346,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] -name = "vcpkg" -version = "0.2.15" +name = "utf8parse" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] -name = "vec_map" -version = "0.8.2" +name = "vcpkg" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" [[package]] name = "vergen" @@ -2534,7 +2609,7 @@ version = "0.33.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c" dependencies = [ - "bitflags 2.9.0", + "bitflags", ] [[package]] @@ -2560,6 +2635,12 @@ dependencies = [ "markup5ever", ] +[[package]] +name = "yansi" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" + [[package]] name = "yoke" version = "0.7.5" diff --git a/Cargo.toml b/Cargo.toml index 52deb2d..17ee143 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,7 +10,7 @@ description = "Fast directory scanning and scraping tool" [dependencies] curl = "0.4.19" percent-encoding = "2.1" -clap = "2.32" +clap = { version = "4.5.32", features = ["cargo", "derive"] } select = "0.6" chardet = "0.2.4" encoding = "0.2.33" @@ -33,3 +33,7 @@ vergen-gix = { version = "1.0.6", features = ["build", "si"] } [features] release_version_string = [] + +[dev-dependencies] +pretty_assertions = "1.4.1" +tempfile = "3.19.1" diff --git a/src/arg_parse.rs b/src/arg_parse.rs index 2a02b80..b87083a 100644 --- a/src/arg_parse.rs +++ b/src/arg_parse.rs @@ -17,12 +17,15 @@ use crate::wordlist::lines_from_file; use atty::Stream; -use clap::{App, AppSettings, Arg, ArgGroup, arg_enum, crate_version, value_t}; +use clap::{ + Arg, ArgAction, ArgGroup, Command, ValueEnum, builder::EnumValueParser, + crate_version, value_parser, +}; use simplelog::LevelFilter; -use std::{fmt, process::exit}; +use std::{ffi::OsString, fmt, path::PathBuf, process::exit}; use url::Url; -#[derive(Clone)] +#[derive(Clone, Debug, PartialEq, Eq)] pub struct GlobalOpts { pub hostnames: Vec, pub wordlist_files: Option>, @@ -32,7 +35,7 @@ pub struct GlobalOpts { pub max_threads: u32, pub proxy_enabled: bool, pub proxy_address: String, - #[expect(dead_code, reason = "TODO")] + #[allow(dead_code, reason = "TODO")] pub proxy_auth_enabled: bool, pub ignore_cert: bool, pub show_htaccess: bool, @@ -62,7 +65,7 @@ pub struct GlobalOpts { pub length_blacklist: LengthRanges, } -#[derive(PartialOrd, Ord, PartialEq, Eq, Clone)] +#[derive(Copy, Clone, PartialOrd, Ord, PartialEq, Eq)] pub struct LengthRange { pub start: usize, pub end: Option, @@ -90,7 +93,7 @@ impl fmt::Debug for LengthRange { } } -#[derive(Clone, Default)] +#[derive(Clone, Debug, Default, PartialEq, Eq)] pub struct LengthRanges { pub ranges: Vec, } @@ -118,19 +121,17 @@ impl fmt::Display for LengthRanges { } } -#[derive(Clone, Default)] +#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] pub struct ScanOpts { pub scan_401: bool, pub scan_403: bool, } -arg_enum! { - #[derive(Clone)] - pub enum HttpVerb { - Get, - Head, - Post - } +#[derive(Copy, Clone, Debug, PartialEq, Eq, ValueEnum)] +pub enum HttpVerb { + Get, + Head, + Post, } #[expect(clippy::derivable_impls, reason = "Interaction with arg_enum!")] @@ -141,20 +142,204 @@ impl Default for HttpVerb { } /// The supported output file types +#[derive(Copy, Clone)] enum FileTypes { Txt, Json, Xml, } +impl From for &'static str { + fn from(file: FileTypes) -> Self { + match file { + FileTypes::Txt => "txt", + FileTypes::Json => "json", + FileTypes::Xml => "xml", + } + } +} + #[allow(clippy::cognitive_complexity)] -pub fn get_args() -> GlobalOpts { +pub fn get_args(args: ArgsIter) -> GlobalOpts +where + ArgsIter: IntoIterator, + ArgsIter::Item: Into + Clone, +{ + // Mutable to allow extractors to move CLI arguments and avoid + // unnecessary clones + let mut args = app().get_matches_from(args); + + let mut hostnames: Vec = Vec::new(); + + // Get from host arguments + if let Some(host) = args.remove_one::("host") { + hostnames.push(host); + } + if let Some(host_files) = args.remove_many::("host_file") { + for host_file in host_files { + let hosts = lines_from_file(&host_file); + for hostname in hosts { + if url_is_valid(&hostname).is_ok() { + if let Ok(host) = Url::parse(hostname.as_str()) { + hostnames.push(host); + } + } else { + println!("Invalid URL: {}", hostname); + } + } + } + } + if let Some(extra_hosts) = args.remove_many::("extra_hosts") { + hostnames.extend(extra_hosts); + } + + if hostnames.is_empty() { + println!("No valid hosts were provided - exiting"); + exit(2); + } + hostnames.sort(); + hostnames.dedup(); + + // Parse wordlist file names into a vector + let wordlists: Option> = + args.remove_many("wordlist").map(Iterator::collect); + + // Check for proxy related flags + let proxy_enabled; + let proxy_address; + if let Some(proxy_addr) = args.remove_one("proxy") { + proxy_enabled = true; + proxy_address = proxy_addr; + if proxy_address == "http://localhost:8080" { + println!( + "You could use the --burp flag instead of the --proxy flag!" + ); + } + } else if args.get_flag("burp") { + proxy_enabled = true; + proxy_address = "http://localhost:8080".into(); + } else if args.get_flag("no_proxy") { + proxy_enabled = true; + proxy_address = String::new(); + } else { + proxy_enabled = false; + proxy_address = String::new(); + } + + // Read provided cookie values into a vector + let cookies: Option = args + .remove_many("cookie") + .map(Iterator::collect::>) + .map(|cookies| cookies.join("; ")); + + // Read provided headers into a vector + let headers: Option> = + args.remove_many("header").map(Iterator::collect); + + let mut whitelist = false; + let mut code_list: Vec = Vec::new(); + + if let Some(whitelist_values) = args.remove_many::("code_whitelist") { + whitelist = true; + code_list.extend(whitelist_values); + } else if let Some(blacklist_values) = + args.remove_many::("code_blacklist") + { + whitelist = false; + code_list.extend(blacklist_values); + } + + let max_recursion_depth = if args.get_flag("disable_recursion") { + Some(0) + } else { + args.remove_one("max_recursion_depth") + }; + + let mut scan_opts = ScanOpts { + scan_401: false, + scan_403: false, + }; + if args.get_flag("scan_401") || (whitelist && code_list.contains(&401)) { + scan_opts.scan_401 = true; + } + + if args.get_flag("scan_403") || (whitelist && code_list.contains(&403)) { + scan_opts.scan_403 = true; + } + + // Configure the logging level. The silent flag overrides any + // verbose flags in use. + let log_level = if args.get_flag("silent") { + LevelFilter::Warn + } else { + match args.get_count("verbose") { + 0 => LevelFilter::Info, + 1 => LevelFilter::Debug, + _ => LevelFilter::Trace, + } + }; + + // Create the GlobalOpts struct and return it + GlobalOpts { + hostnames, + wordlist_files: wordlists, + prefixes: load_modifiers(&mut args, "prefixes"), + extensions: load_modifiers(&mut args, "extensions"), + extension_substitution: args.get_flag("extension_substitution"), + max_threads: args + .remove_one("max_threads") + .expect("Max threads is set"), + proxy_enabled, + proxy_address, + proxy_auth_enabled: false, + ignore_cert: args.get_flag("ignore_cert"), + show_htaccess: args.get_flag("show_htaccess"), + throttle: args.remove_one("throttle").unwrap_or_default(), + max_recursion_depth, + user_agent: args.remove_one("user_agent"), + // Dependency between username and password is handled by Clap + username: args.remove_one("username"), + // Dependency between username and password is handled by Clap + password: args.remove_one("password"), + output_file: filename_from_args(&args, FileTypes::Txt), + json_file: filename_from_args(&args, FileTypes::Json), + xml_file: filename_from_args(&args, FileTypes::Xml), + timeout: args.remove_one("timeout").expect("Timeout is set"), + max_errors: args + .remove_one::("max_errors") + .expect("Max errors is an integer"), + wordlist_split: args + .remove_one("wordlist_split") + .expect("Wordlist split is set"), + scan_listable: args.get_flag("scan_listable"), + cookies, + headers, + scrape_listable: args.get_flag("scrape_listable"), + whitelist, + code_list, + is_terminal: atty::is(Stream::Stdout), + no_color: args.get_flag("no_color"), + disable_validator: args.get_flag("disable_validator"), + http_verb: *args.get_one("http_verb").expect("Must be valid HTTP verb"), + scan_opts, + log_level, + length_blacklist: if let Some(lengths) = + args.get_many("length_blacklist") + { + length_blacklist_parse(lengths) + } else { + Default::default() + }, + } +} + +// Defines all the command line arguments with the Clap module +fn app() -> Command { // For general compilation, include the current commit hash and // build date in the version string. When building releases via the // Makefile, only use the release number. let version_string = get_version_string(); - // Defines all the command line arguments with the Clap module - let args = App::new("Dirble") + Command::new("Dirble") .version(version_string) .author( "Developed by Izzy Whistlecroft \ @@ -176,159 +361,160 @@ EXAMPLE USE: dirble [address] -X wordlists/web.lst -U uri-list.txt\n - Providing multiple hosts to scan via command line: dirble [address] -u [address] -u [address]") - .setting(AppSettings::ArgRequiredElseHelp) - .arg(Arg::with_name("host") + .arg_required_else_help(true) + .arg(Arg::new("host") + .action(ArgAction::Set) .display_order(10) .help( "The URI of the host to scan, optionally supports basic auth with http://user:pass@host:port") .index(1) .next_line_help(true) - .takes_value(true) - .validator(url_is_valid) + .value_parser(value_parser!(Url)) .value_name("uri")) - .arg(Arg::with_name("extra_hosts") + .arg(Arg::new("extra_hosts") + .action(ArgAction::Append) .alias("host") .display_order(10) .help( "Additional hosts to scan") .long("uri") - .multiple(true) .next_line_help(true) - .short("u") - .takes_value(true) - .validator(url_is_valid) + .short('u') + .value_parser(value_parser!(Url)) .value_name("uri") .visible_alias("url")) - .arg(Arg::with_name("host_file") + .arg(Arg::new("host_file") + .action(ArgAction::Append) .alias("host-file") .display_order(10) .help( "The filename of a file containing a list of URIs to scan - cookies and headers set will be applied to all URIs") .long("uri-file") - .multiple(true) .next_line_help(true) - .short("U") - .takes_value(true) + .short('U') .value_name("uri-file") + .value_parser(value_parser!(PathBuf)) .visible_alias("url-file")) - .group(ArgGroup::with_name("hosts") - .args(&["host", "host_file", "extra_hosts"]) + .group(ArgGroup::new("hosts") + .args(["host", "host_file", "extra_hosts"]) .multiple(true) .required(true)) - .arg(Arg::with_name("http_verb") - .default_value("Get") + .arg(Arg::new("http_verb") + .action(ArgAction::Set) + .default_value("get") .display_order(11) .help( "Specify which HTTP verb to use ") // Newline is needed for the enumeration of possible values + .ignore_case(true) .long("verb") .next_line_help(true) - .possible_values(&HttpVerb::variants()) - .takes_value(true)) - .arg(Arg::with_name("wordlist") + .value_parser(EnumValueParser::::new())) + .arg(Arg::new("wordlist") + .action(ArgAction::Append) .display_order(20) .help( "Sets which wordlist to use, defaults to dirble_wordlist.txt in the same folder as the executable") .long("wordlist") - .multiple(true) .next_line_help(true) - .short("w") - .takes_value(true) + .short('w') .value_name("wordlist")) - .arg(Arg::with_name("extensions") + .arg(Arg::new("extensions") + .action(ArgAction::Append) .display_order(30) .help( "Provides comma separated extensions to extend queries with") .long("extensions") - .min_values(1) - .multiple(true) + .num_args(1..) .next_line_help(true) - .short("x") - .value_delimiter(",") + .short('x') + .value_delimiter(',') .value_name("extensions")) - .arg(Arg::with_name("extension_file") + .arg(Arg::new("extension_file") + .action(ArgAction::Append) .display_order(30) .help( "The name of a file containing extensions to extend queries with, one per line") .long("extension-file") - .multiple(true) .next_line_help(true) - .short("X") + .short('X').value_parser(value_parser!(PathBuf)) .value_name("extension-file")) - .group(ArgGroup::with_name("extension-options") - .args(&["extensions", "extension_file"]) + .group(ArgGroup::new("extension-options") + .args(["extensions", "extension_file"]) .multiple(true)) - .arg(Arg::with_name("extension_substitution") + .arg(Arg::new("extension_substitution") + .action(ArgAction::SetTrue) .display_order(31) .help( "Indicates whether the string \"%EXT%\" in a wordlist file should be substituted with the current extension") .long("ext-sub") .requires("extension-options")) - .arg(Arg::with_name("force_extension") + .arg(Arg::new("force_extension") + .action(ArgAction::SetTrue) .display_order(31) .help("Only scan with provided extensions") .requires("extension-options") - .short("f") + .short('f') .long("force-extension")) - .arg(Arg::with_name("prefixes") + .arg(Arg::new("prefixes") + .action(ArgAction::Append) .display_order(30) .help( "Provides comma separated prefixes to extend queries with") .long("prefixes") - .min_values(1) - .multiple(true) + .num_args(1..) .next_line_help(true) - .short("p") - .value_delimiter(",")) - .arg(Arg::with_name("prefix_file") + .short('p') + .value_delimiter(',')) + .arg(Arg::new("prefix_file") + .action(ArgAction::Append) .display_order(30) .help( "The name of a file containing extensions to extend queries with, one per line") .long("prefix-file") - .multiple(true) .next_line_help(true) - .short("P") + .short('P').value_parser(value_parser!(PathBuf)) .value_name("prefix-file")) - .arg(Arg::with_name("output_file") + .arg(Arg::new("output_file") + .action(ArgAction::Set) .display_order(40) .help( "Sets the file to write the report to") .long("output-file") .next_line_help(true) - .short("o") - .takes_value(true) + .short('o') .visible_alias("oN")) - .arg(Arg::with_name("json_file") + .arg(Arg::new("json_file") + .action(ArgAction::Set) .display_order(40) .help( "Sets a file to write JSON output to") .long("json-file") .next_line_help(true) - .takes_value(true) - .visible_alias("oJ")) - .arg(Arg::with_name("xml_file") + .visible_alias("oJ")) + .arg(Arg::new("xml_file") + .action(ArgAction::Set) .display_order(40) .help( "Sets a file to write XML output to") .long("xml-file") .next_line_help(true) - .takes_value(true) .visible_alias("oX")) - .arg(Arg::with_name("output_all") + .arg(Arg::new("output_all") + .action(ArgAction::Set) .display_order(41) .help( "Stores all output types respectively as .txt, .json and .xml") .long("output-all") .next_line_help(true) - .takes_value(true) - .visible_alias("oA")) - .arg(Arg::with_name("proxy") + .visible_alias("oA")) + .arg(Arg::new("proxy") .display_order(50) .help( "The proxy address to use, including type and port, can also include a @@ -336,7 +522,8 @@ username and password in the form \"http://username:password@proxy_url:proxy_port\"") .long("proxy") .value_name("proxy")) - .arg(Arg::with_name("burp") + .arg(Arg::new("burp") + .action(ArgAction::SetTrue) .conflicts_with("proxy") .display_order(50) .help( @@ -344,8 +531,9 @@ username and password in the form (http://localhost:8080)") .long("burp") .next_line_help(true) - .takes_value(false)) - .arg(Arg::with_name("no_proxy") + ) + .arg(Arg::new("no_proxy") + .action(ArgAction::SetTrue) .conflicts_with("burp") .conflicts_with("proxy") .display_order(50) @@ -353,427 +541,222 @@ username and password in the form "Disables proxy use even if there is a system proxy") .long("no-proxy") .next_line_help(true) - .takes_value(false)) - .arg(Arg::with_name("max_threads") + ) + .arg(Arg::new("max_threads") + .action(ArgAction::Set) .default_value("10") .display_order(60) .help( "Sets the maximum number of request threads that will be spawned") .long("max-threads") .next_line_help(true) - .short("t") - .takes_value(true) - .validator(positive_int_check) + .short('t') + .value_parser(value_parser!(u32).range(1..)) .value_name("max-threads")) - .arg(Arg::with_name("wordlist_split") + .arg(Arg::new("wordlist_split") .default_value("3") .display_order(60) .help( "The number of threads to run for each folder/extension combo") .long("wordlist-split") .next_line_help(true) - .short("T") - .validator(positive_int_check)) - .arg(Arg::with_name("throttle") + .short('T') + .value_parser(value_parser!(u32))) + .arg(Arg::new("throttle") + .action(ArgAction::Set) .display_order(61) .help( "Time each thread will wait between requests, given in milliseconds") .long("throttle") .next_line_help(true) - .short("z") - .takes_value(true) - .validator(positive_int_check) + .short('z') + .value_parser(value_parser!(u32)) .value_name("milliseconds")) - .arg(Arg::with_name("username") + .arg(Arg::new("username") + .action(ArgAction::Set) .display_order(70) .help( "Sets the username to authenticate with") .long("username") .next_line_help(true) .requires("password") - .takes_value(true)) - .arg(Arg::with_name("password") + ) + .arg(Arg::new("password") + .action(ArgAction::Set) .display_order(71) .help( "Sets the password to authenticate with") .long("password") .next_line_help(true) .requires("username") - .takes_value(true)) - .arg(Arg::with_name("disable_recursion") + ) + .arg(Arg::new("disable_recursion") + .action(ArgAction::SetTrue) .display_order(80) .help( "Disable discovered subdirectory scanning") .long("disable-recursion") .next_line_help(true) - .short("r")) - .arg(Arg::with_name("max_recursion_depth") + .short('r')) + .arg(Arg::new("max_recursion_depth") + .action(ArgAction::Set) .display_order(80) .help( "Sets the maximum directory depth to recurse to, 0 will disable recursion") .long("max-recursion-depth") .next_line_help(true) - .takes_value(true) - .validator(int_check)) - .arg(Arg::with_name("scan_listable") + .value_parser(value_parser!(i32))) + .arg(Arg::new("scan_listable") + .action(ArgAction::SetTrue) .display_order(80) .help( "Scan listable directories") .long("scan-listable") .next_line_help(true) - .short("l") - .takes_value(false)) - .arg(Arg::with_name("scrape_listable") + .short('l') + ) + .arg(Arg::new("scrape_listable") + .action(ArgAction::SetTrue) .display_order(80) .help( "Enable scraping of listable directories for urls, often produces large amounts of output") .long("scrape-listable") .next_line_help(true) - .takes_value(false)) - .arg(Arg::with_name("cookie") + ) + .arg(Arg::new("cookie") + .action(ArgAction::Append) .display_order(90) .help( "Provide a cookie in the form \"name=value\", can be used multiple times") .long("cookie") - .multiple(true) .next_line_help(true) - .short("c") - .takes_value(true)) - .arg(Arg::with_name("header") + .short('c') + ) + .arg(Arg::new("header") + .action(ArgAction::Append) .display_order(90) .help( "Provide an arbitrary header in the form \"header:value\" - headers with no value must end in a semicolon") .long("header") - .multiple(true) .next_line_help(true) - .short("H") - .takes_value(true)) - .arg(Arg::with_name("user_agent") + .short('H') + ) + .arg(Arg::new("user_agent") + .action(ArgAction::Set) .display_order(90) .help( "Set the user-agent provided with requests, by default it isn't set") .long("user-agent") .next_line_help(true) - .short("a") - .takes_value(true)) - .arg(Arg::with_name("verbose") + .short('a') + ) + .arg(Arg::new("verbose") + .action(ArgAction::Count) .display_order(100) .help( "Increase the verbosity level. Use twice for full verbosity.") .long("verbose") - .multiple(true) .next_line_help(true) - .short("v") - .takes_value(false) + .short('v') .conflicts_with("silent")) - .arg(Arg::with_name("silent") + .arg(Arg::new("silent") + .action(ArgAction::SetTrue) .display_order(100) .help( "Don't output information during the scan, only output the report at the end.") .long("silent") .next_line_help(true) - .short("S") - .takes_value(false)) - .arg(Arg::with_name("code_whitelist") + .short('S')) + .arg(Arg::new("code_whitelist") + .action(ArgAction::Append) .display_order(110) .help( "Provide a comma separated list of response codes to show in output, also disables detection of not found codes") .long("code-whitelist") - .min_values(1) - .multiple(true) + .num_args(1..) .next_line_help(true) - .short("W") - .validator(positive_int_check) - .value_delimiter(",")) - .arg(Arg::with_name("code_blacklist") + .short('W') + .value_delimiter(',') + .value_parser(value_parser!(u32))) + .arg(Arg::new("code_blacklist") + .action(ArgAction::Append) .conflicts_with("code_whitelist") .display_order(110) .help( "Provide a comma separated list of response codes to not show in output") .long("code-blacklist") - .min_values(1) - .multiple(true) + .num_args(1..) .next_line_help(true) - .short("B") - .validator(positive_int_check) - .value_delimiter(",")) - .arg(Arg::with_name("disable_validator") + .short('B') + .value_parser(value_parser!(u32)) + .value_delimiter(',')) + .arg(Arg::new("disable_validator") + .action(ArgAction::SetTrue) .display_order(110) .help( "Disable automatic detection of not found codes") .long("disable-validator") - .next_line_help(true) - .takes_value(false)) - .arg(Arg::with_name("scan_401") + .next_line_help(true)) + .arg(Arg::new("scan_401") + .action(ArgAction::SetTrue) .display_order(120) .help( "Scan folders even if they return 401 - Unauthorized frequently") .long("scan-401") .next_line_help(true)) - .arg(Arg::with_name("scan_403") + .arg(Arg::new("scan_403") + .action(ArgAction::SetTrue) .display_order(120) .help( "Scan folders if they return 403 - Forbidden frequently") .long("scan-403") .next_line_help(true)) - .arg(Arg::with_name("ignore_cert") + .arg(Arg::new("ignore_cert") + .action(ArgAction::SetTrue) .help( "Ignore the certificate validity for HTTPS") .long("ignore-cert") - .short("k")) - .arg(Arg::with_name("show_htaccess") + .short('k')) + .arg(Arg::new("show_htaccess") + .action(ArgAction::SetTrue) .help( "Enable display of items containing .ht when they return 403 responses") .long("show-htaccess") .next_line_help(true)) - .arg(Arg::with_name("timeout") + .arg(Arg::new("timeout") .default_value("5") .help( "Maximum time to wait for a response before giving up, given in seconds\n") .long("timeout") .next_line_help(true) - .validator(positive_int_check)) - .arg(Arg::with_name("max_errors") + .value_parser(value_parser!(u32))) + .arg(Arg::new("max_errors") .default_value("5") .help( "The number of consecutive errors a thread can have before it exits, set to 0 to disable") .long("max-errors") .next_line_help(true) - .validator(int_check)) - .arg(Arg::with_name("no_color") + .value_parser(value_parser!(u32))) + .arg(Arg::new("no_color") + .action(ArgAction::SetTrue) .alias("no-colour") .help("Disable coloring of terminal output") .long("no-color") .next_line_help(true)) - .arg(Arg::with_name("length_blacklist") + .arg(Arg::new("length_blacklist") + .action(ArgAction::Append) .help( "Specify length ranges to hide, e.g. --hide-lengths 348,500-700") .long("hide-lengths") - .min_values(1) - .multiple(true) + .num_args(1..) .next_line_help(true) - .takes_value(true) - .value_delimiter(",")) - .get_matches(); - - let mut hostnames: Vec = Vec::new(); - - // Get from host arguments - if let Some(host) = args.value_of("host") { - if let Ok(host) = Url::parse(host) { - hostnames.push(host); - } else { - println!("Invaid URL: {}", host); - } - } - if let Some(host_files) = args.values_of("host_file") { - for host_file in host_files { - let hosts = lines_from_file(&String::from(host_file)); - for hostname in hosts { - if url_is_valid(hostname.clone()).is_ok() { - if let Ok(host) = Url::parse(hostname.as_str()) { - hostnames.push(host); - } - } else { - println!("Invalid URL: {}", hostname); - } - } - } - } - if let Some(extra_hosts) = args.values_of("extra_hosts") { - for hostname in extra_hosts { - if let Ok(host) = Url::parse(hostname) { - hostnames.push(host); - } - } - } - - if hostnames.is_empty() { - println!("No valid hosts were provided - exiting"); - exit(2); - } - hostnames.sort(); - hostnames.dedup(); - - // Parse wordlist file names into a vector - let wordlists: Option> = - if let Some(wordlist_files) = args.values_of("wordlist") { - let mut wordlists_vec = Vec::new(); - for wordlist_file in wordlist_files { - wordlists_vec.push(String::from(wordlist_file)); - } - Some(wordlists_vec) - } else { - None - }; - - // Check for proxy related flags - let proxy_enabled; - let proxy_address; - if let Some(proxy_addr) = args.value_of("proxy") { - proxy_enabled = true; - proxy_address = proxy_addr; - if proxy_address == "http://localhost:8080" { - println!( - "You could use the --burp flag instead of the --proxy flag!" - ); - } - } else if args.is_present("burp") { - proxy_enabled = true; - proxy_address = "http://localhost:8080"; - } else if args.is_present("no_proxy") { - proxy_enabled = true; - proxy_address = ""; - } else { - proxy_enabled = false; - proxy_address = ""; - } - let proxy_address = String::from(proxy_address); - - // Read provided cookie values into a vector - let cookies: Option = - if let Some(cookies) = args.values_of("cookie") { - let mut temp_cookies: Vec = Vec::new(); - for cookie in cookies { - temp_cookies.push(String::from(cookie)); - } - Some(temp_cookies.join("; ")) - } else { - None - }; - - // Read provided headers into a vector - let headers: Option> = - if let Some(headers) = args.values_of("header") { - let mut temp_headers: Vec = Vec::new(); - for header in headers { - temp_headers.push(String::from(header)); - } - Some(temp_headers) - } else { - None - }; - - let mut whitelist = false; - let mut code_list: Vec = Vec::new(); - - if let Some(whitelist_values) = args.values_of("code_whitelist") { - whitelist = true; - for code in whitelist_values { - code_list.push(code.parse::().expect("Code is an integer")); - } - } else if let Some(blacklist_values) = args.values_of("code_blacklist") { - whitelist = false; - for code in blacklist_values { - code_list.push(code.parse::().expect("Code is an integer")); - } - } - - let mut max_recursion_depth = None; - if args.is_present("disable_recursion") { - max_recursion_depth = Some(0); - } else if let Some(depth) = args.value_of("max_recursion_depth") { - max_recursion_depth = - Some(depth.parse::().expect("Recursion depth is an integer")); - } - - let mut scan_opts = ScanOpts { - scan_401: false, - scan_403: false, - }; - if args.is_present("scan_401") || (whitelist && code_list.contains(&401)) { - scan_opts.scan_401 = true; - } - - if args.is_present("scan_403") || (whitelist && code_list.contains(&403)) { - scan_opts.scan_403 = true; - } - - // Configure the logging level. The silent flag overrides any - // verbose flags in use. - let log_level = if args.is_present("silent") { - LevelFilter::Warn - } else { - match args.occurrences_of("verbose") { - 0 => LevelFilter::Info, - 1 => LevelFilter::Debug, - _ => LevelFilter::Trace, - } - }; - - // Create the GlobalOpts struct and return it - GlobalOpts { - hostnames, - wordlist_files: wordlists, - prefixes: load_modifiers(&args, "prefixes"), - extensions: load_modifiers(&args, "extensions"), - extension_substitution: args.is_present("extension_substitution"), - max_threads: args - .value_of("max_threads") - .expect("Max threads is set") - .parse::() - .expect("Max threads is an integer"), - proxy_enabled, - proxy_address, - proxy_auth_enabled: false, - ignore_cert: args.is_present("ignore_cert"), - show_htaccess: args.is_present("show_htaccess"), - throttle: if let Some(throttle) = args.value_of("throttle") { - throttle.parse::().expect("Throttle is an integer") - } else { - 0 - }, - max_recursion_depth, - user_agent: args.value_of("user_agent").map(String::from), - // Dependency between username and password is handled by Clap - username: args.value_of("username").map(String::from), - // Dependency between username and password is handled by Clap - password: args.value_of("password").map(String::from), - output_file: filename_from_args(&args, FileTypes::Txt), - json_file: filename_from_args(&args, FileTypes::Json), - xml_file: filename_from_args(&args, FileTypes::Xml), - timeout: args - .value_of("timeout") - .expect("Timeout is set") - .parse::() - .expect("Timeout is an integer"), - max_errors: args - .value_of("max_errors") - .expect("Max errors is set") - .parse::() - .expect("Max errors is an integer"), - wordlist_split: args - .value_of("wordlist_split") - .expect("Wordlist split is set") - .parse::() - .expect("Wordlist split is an integer"), - scan_listable: args.is_present("scan_listable"), - cookies, - headers, - scrape_listable: args.is_present("scrape_listable"), - whitelist, - code_list, - is_terminal: atty::is(Stream::Stdout), - no_color: args.is_present("no_color"), - disable_validator: args.is_present("disable_validator"), - http_verb: value_t!(args.value_of("http_verb"), HttpVerb) - .expect("Must be valid HTTP verb"), - scan_opts, - log_level, - length_blacklist: if let Some(lengths) = - args.values_of("length_blacklist") - { - length_blacklist_parse(lengths) - } else { - Default::default() - }, - } + .value_delimiter(',')) } /// filetype is one of "txt", "json", and "xml". Returns a filename that is @@ -785,35 +768,33 @@ fn filename_from_args( args: &clap::ArgMatches, filetype: FileTypes, ) -> Option { - let extension; - use FileTypes::*; match filetype { - Txt => { - extension = "txt"; - if let Some(output_file) = args.value_of("output_file") { - return Some(String::from(output_file)); + FileTypes::Txt => { + if let Some(output_file) = args.get_one::("output_file") { + return Some(output_file.to_string()); } } - Json => { - extension = "json"; - if let Some(json_file) = args.value_of("json_file") { - return Some(String::from(json_file)); + FileTypes::Json => { + if let Some(json_file) = args.get_one::("json_file") { + return Some(json_file.to_string()); } } - Xml => { - extension = "xml"; - if let Some(xml_file) = args.value_of("xml_file") { - return Some(String::from(xml_file)); + FileTypes::Xml => { + if let Some(xml_file) = args.get_one::("xml_file") { + return Some(xml_file.to_string()); } } } - args.value_of("output_all") - .map(|output_all_prefix| format!("{}.{}", output_all_prefix, extension)) + // This function is called once for each filetype, so we don't remove + // the arg from the matcher + args.get_one::("output_all") + .map(|output_all_prefix| { + format!("{}.{}", output_all_prefix, <&'static str>::from(filetype)) + }) } -#[inline] -fn load_modifiers(args: &clap::ArgMatches, mod_type: &str) -> Vec { +fn load_modifiers(args: &mut clap::ArgMatches, mod_type: &str) -> Vec { let singular_arg; let file_arg; match mod_type { @@ -831,20 +812,16 @@ fn load_modifiers(args: &clap::ArgMatches, mod_type: &str) -> Vec { let mut modifiers = vec![]; - if !args.is_present("force_extension") || mod_type == "prefixes" { + if !args.get_flag("force_extension") || mod_type == "prefixes" { modifiers.push(String::from("")); } - if let Some(singular_args) = args.values_of(singular_arg) { - for modifier in singular_args { - modifiers.push(String::from(modifier)); - } + if let Some(singular_args) = args.remove_many(singular_arg) { + modifiers.extend(singular_args); } - if let Some(file_args) = args.values_of(file_arg) { + if let Some(file_args) = args.remove_many::(&file_arg) { for filename in file_args { - for modifier in lines_from_file(&String::from(filename)) { - modifiers.push(modifier); - } + modifiers.extend(lines_from_file(&filename)); } } @@ -879,8 +856,8 @@ pub fn get_version_string() -> &'static str { } } -fn url_is_valid(hostname: String) -> Result<(), String> { - let url = Url::parse(hostname.as_str()); +fn url_is_valid(hostname: &str) -> Result<(), String> { + let url = Url::parse(hostname); if let Ok(u) = url { if u.scheme() == "http" || u.scheme() == "https" { Ok(()) @@ -892,29 +869,9 @@ fn url_is_valid(hostname: String) -> Result<(), String> { } } -// Validator for arguments including the --max-threads flag -// Ensures that the value is a positive integer (not 0) -fn positive_int_check(value: String) -> Result<(), String> { - let int_val = value.parse::(); - if let Ok(max) = int_val { - if max > 0 { - return Ok(()); - } - } - Err(String::from("The number given must be a positive integer.")) -} - -// Validator for various arguments, ensures that value is a -// positive integer, including 0 -fn int_check(value: String) -> Result<(), String> { - let int_val = value.parse::(); - match int_val { - Ok(_) => Ok(()), - Err(_) => Err(String::from("The number given must be an integer.")), - } -} - -fn length_blacklist_parse(blacklist_inputs: clap::Values) -> LengthRanges { +fn length_blacklist_parse( + blacklist_inputs: clap::parser::ValuesRef, +) -> LengthRanges { let mut length_vector: Vec = Vec::with_capacity(blacklist_inputs.len()); @@ -957,10 +914,58 @@ fn length_blacklist_parse(blacklist_inputs: clap::Values) -> LengthRanges { #[cfg(test)] mod test { + use super::*; + use log::LevelFilter::Info; + use pretty_assertions::assert_eq; + use std::io::Write; + use tempfile::NamedTempFile; + + impl Default for GlobalOpts { + fn default() -> Self { + GlobalOpts { + hostnames: Default::default(), + wordlist_files: Default::default(), + prefixes: vec!["".into()], + extensions: vec!["".into()], + extension_substitution: false, + max_threads: 10, + proxy_enabled: Default::default(), + proxy_address: Default::default(), + proxy_auth_enabled: Default::default(), + ignore_cert: Default::default(), + show_htaccess: Default::default(), + throttle: Default::default(), + max_recursion_depth: Default::default(), + user_agent: Default::default(), + username: Default::default(), + password: Default::default(), + output_file: Default::default(), + json_file: Default::default(), + xml_file: Default::default(), + timeout: 5, + max_errors: 5, + wordlist_split: 3, + scan_listable: Default::default(), + cookies: Default::default(), + headers: Default::default(), + scrape_listable: Default::default(), + whitelist: Default::default(), + code_list: Default::default(), + is_terminal: Default::default(), + no_color: Default::default(), + disable_validator: Default::default(), + http_verb: Default::default(), + scan_opts: Default::default(), + log_level: Info, + length_blacklist: Default::default(), + } + } + } + #[test] fn argparse_length_range_contains() { // Range with start and end values - let range = crate::arg_parse::LengthRange { + let range = LengthRange { start: 3, end: Some(6), }; @@ -976,7 +981,7 @@ mod test { assert!(!range.contains(7)); // Range with just a start value - let range = crate::arg_parse::LengthRange { + let range = LengthRange { start: 5, end: None, }; @@ -991,17 +996,17 @@ mod test { #[test] fn argparse_length_ranges_contain() { // Empty range - let ranges: crate::arg_parse::LengthRanges = Default::default(); + let ranges: LengthRanges = Default::default(); assert!(!ranges.contains(4)); // Non-overlapping ranges - let ranges = crate::arg_parse::LengthRanges { + let ranges = LengthRanges { ranges: vec![ - crate::arg_parse::LengthRange { + LengthRange { start: 4, end: Some(10), }, - crate::arg_parse::LengthRange { + LengthRange { start: 15, end: Some(18), }, @@ -1018,4 +1023,1000 @@ mod test { // too large assert!(!ranges.contains(19)); } + + #[test] + fn verify_app() { + app().debug_assert(); + } + + #[track_caller] + fn assert_args(args: ArgsIter, expected: GlobalOpts) + where + ArgsIter: IntoIterator, + ArgsIter::Item: Into + Clone, + { + let mut opts = get_args(args); + // normalise the is-terminal value as it's not useful to test + opts.is_terminal = expected.is_terminal; + let info = std::panic::Location::caller(); + assert_eq!(opts, expected, "Caller: {}:{}", info.file(), info.line()); + } + + fn make_uri_file() -> NamedTempFile { + let mut file = NamedTempFile::new().unwrap(); + write!( + &mut file, + "http://some-host + https://other-host" + ) + .unwrap(); + file + } + + fn make_extensions_file() -> NamedTempFile { + let mut file = NamedTempFile::new().unwrap(); + writeln!(&mut file, "txt").unwrap(); + writeln!(&mut file, "jpg").unwrap(); + writeln!(&mut file, "png").unwrap(); + file + } + + #[test] + fn required_args() { + assert_args( + ["test", "http://some-host"], + GlobalOpts { + hostnames: vec!["http://some-host".parse().unwrap()], + ..Default::default() + }, + ); + assert_args( + ["test", "-u", "http://some-host", "-u", "https://other-host"], + GlobalOpts { + hostnames: vec![ + "http://some-host".parse().unwrap(), + "https://other-host".parse().unwrap(), + ], + ..Default::default() + }, + ); + let uri_file = make_uri_file(); + assert_args( + ["test", "-U", &uri_file.path().display().to_string()], + GlobalOpts { + hostnames: vec![ + "http://some-host".parse().unwrap(), + "https://other-host".parse().unwrap(), + ], + ..Default::default() + }, + ); + assert_args( + [ + "test", + "-U", + &uri_file.path().display().to_string(), + "-u", + "http://third-host", + ], + GlobalOpts { + hostnames: vec![ + "http://some-host".parse().unwrap(), + "http://third-host".parse().unwrap(), + "https://other-host".parse().unwrap(), + ], + ..Default::default() + }, + ); + } + + #[test] + fn http_verb() { + assert_args( + ["test", "--verb", "Get", "http://some-host"], + GlobalOpts { + hostnames: vec!["http://some-host".parse().unwrap()], + ..Default::default() + }, + ); + assert_args( + ["test", "--verb", "Post", "http://some-host"], + GlobalOpts { + hostnames: vec!["http://some-host".parse().unwrap()], + http_verb: HttpVerb::Post, + ..Default::default() + }, + ); + assert_args( + ["test", "--verb", "Head", "http://some-host"], + GlobalOpts { + hostnames: vec!["http://some-host".parse().unwrap()], + http_verb: HttpVerb::Head, + ..Default::default() + }, + ); + assert_args( + ["test", "http://some-host", "--verb", "Head"], + GlobalOpts { + hostnames: vec!["http://some-host".parse().unwrap()], + http_verb: HttpVerb::Head, + ..Default::default() + }, + ); + } + + #[test] + fn wordlist() { + assert_args( + ["test", "http://some-host", "--wordlist", "file-one"], + GlobalOpts { + hostnames: vec!["http://some-host".parse().unwrap()], + wordlist_files: Some(vec!["file-one".into()]), + ..Default::default() + }, + ); + assert_args( + [ + "test", + "http://some-host", + "--wordlist", + "file-one", + "-w", + "file-two", + ], + GlobalOpts { + hostnames: vec!["http://some-host".parse().unwrap()], + wordlist_files: Some(vec![ + "file-one".into(), + "file-two".into(), + ]), + ..Default::default() + }, + ); + } + + #[test] + fn extensions() { + assert_args( + ["test", "http://some-host", "--extensions", "txt"], + GlobalOpts { + hostnames: vec!["http://some-host".parse().unwrap()], + extensions: vec![String::new(), "txt".into()], + ..Default::default() + }, + ); + assert_args( + ["test", "http://some-host", "--extensions", "txt,jpg"], + GlobalOpts { + hostnames: vec!["http://some-host".parse().unwrap()], + extensions: vec![String::new(), "jpg".into(), "txt".into()], + ..Default::default() + }, + ); + assert_args( + [ + "test", + "http://some-host", + "--extensions", + "txt,jpg", + "-x", + "png", + ], + GlobalOpts { + hostnames: vec!["http://some-host".parse().unwrap()], + extensions: ["", "jpg", "png", "txt"] + .into_iter() + .map(Into::into) + .collect(), + ..Default::default() + }, + ); + } + + #[test] + fn extension_file() { + let exts_file = make_extensions_file(); + assert_args( + [ + "test", + "http://some-host", + "--extension-file", + &exts_file.path().display().to_string(), + ], + GlobalOpts { + hostnames: vec!["http://some-host".parse().unwrap()], + extensions: ["", "jpg", "png", "txt"] + .into_iter() + .map(Into::into) + .collect(), + ..Default::default() + }, + ); + assert_args( + [ + "test", + "http://some-host", + "-X", + &exts_file.path().display().to_string(), + ], + GlobalOpts { + hostnames: vec!["http://some-host".parse().unwrap()], + extensions: ["", "jpg", "png", "txt"] + .into_iter() + .map(Into::into) + .collect(), + ..Default::default() + }, + ); + assert_args( + [ + "test", + "http://some-host", + "-X", + &exts_file.path().display().to_string(), + "-x", + "rs", + ], + GlobalOpts { + hostnames: vec!["http://some-host".parse().unwrap()], + extensions: ["", "jpg", "png", "rs", "txt"] + .into_iter() + .map(Into::into) + .collect(), + ..Default::default() + }, + ); + } + + #[test] + fn ext_sub() { + assert_args( + [ + "test", + "http://some-host", + "--ext-sub", + "-x", + "txt,png,rs,jpg", + ], + GlobalOpts { + hostnames: vec!["http://some-host".parse().unwrap()], + extensions: ["", "jpg", "png", "rs", "txt"] + .into_iter() + .map(Into::into) + .collect(), + extension_substitution: true, + ..Default::default() + }, + ); + assert_args( + [ + "test", + "http://some-host", + "--ext-sub", + "-x", + "txt,png,rs,jpg", + "--force-extension", + ], + GlobalOpts { + hostnames: vec!["http://some-host".parse().unwrap()], + extensions: ["jpg", "png", "rs", "txt"] + .into_iter() + .map(Into::into) + .collect(), + extension_substitution: true, + ..Default::default() + }, + ); + assert_args( + [ + "test", + "http://some-host", + "--ext-sub", + "-x", + "txt,png,rs,jpg", + "-f", + ], + GlobalOpts { + hostnames: vec!["http://some-host".parse().unwrap()], + extensions: ["jpg", "png", "rs", "txt"] + .into_iter() + .map(Into::into) + .collect(), + extension_substitution: true, + ..Default::default() + }, + ); + } + + #[test] + fn prefixes() { + assert_args( + ["test", "http://some-host", "--prefixes", "txt"], + GlobalOpts { + hostnames: vec!["http://some-host".parse().unwrap()], + prefixes: vec![String::new(), "txt".into()], + ..Default::default() + }, + ); + assert_args( + ["test", "http://some-host", "--prefixes", "txt,jpg"], + GlobalOpts { + hostnames: vec!["http://some-host".parse().unwrap()], + prefixes: vec![String::new(), "jpg".into(), "txt".into()], + ..Default::default() + }, + ); + assert_args( + [ + "test", + "http://some-host", + "--prefixes", + "txt,jpg", + "-p", + "png", + ], + GlobalOpts { + hostnames: vec!["http://some-host".parse().unwrap()], + prefixes: ["", "jpg", "png", "txt"] + .into_iter() + .map(Into::into) + .collect(), + ..Default::default() + }, + ); + } + + #[test] + fn prefix_file() { + let exts_file = make_extensions_file(); + assert_args( + [ + "test", + "http://some-host", + "--prefix-file", + &exts_file.path().display().to_string(), + ], + GlobalOpts { + hostnames: vec!["http://some-host".parse().unwrap()], + prefixes: ["", "jpg", "png", "txt"] + .into_iter() + .map(Into::into) + .collect(), + ..Default::default() + }, + ); + assert_args( + [ + "test", + "http://some-host", + "-P", + &exts_file.path().display().to_string(), + ], + GlobalOpts { + hostnames: vec!["http://some-host".parse().unwrap()], + prefixes: ["", "jpg", "png", "txt"] + .into_iter() + .map(Into::into) + .collect(), + ..Default::default() + }, + ); + assert_args( + [ + "test", + "http://some-host", + "-P", + &exts_file.path().display().to_string(), + "-p", + "rs", + ], + GlobalOpts { + hostnames: vec!["http://some-host".parse().unwrap()], + prefixes: ["", "jpg", "png", "rs", "txt"] + .into_iter() + .map(Into::into) + .collect(), + ..Default::default() + }, + ); + } + + #[test] + fn output_file() { + assert_args( + ["test", "http://some-host", "-o", "some-file"], + GlobalOpts { + hostnames: vec!["http://some-host".parse().unwrap()], + output_file: Some("some-file".into()), + ..Default::default() + }, + ); + assert_args( + ["test", "http://some-host", "--oN", "some-file"], + GlobalOpts { + hostnames: vec!["http://some-host".parse().unwrap()], + output_file: Some("some-file".into()), + ..Default::default() + }, + ); + } + + #[test] + fn json_file() { + assert_args( + ["test", "http://some-host", "--json-file", "some-file"], + GlobalOpts { + hostnames: vec!["http://some-host".parse().unwrap()], + json_file: Some("some-file".into()), + ..Default::default() + }, + ); + assert_args( + ["test", "http://some-host", "--oJ", "some-file"], + GlobalOpts { + hostnames: vec!["http://some-host".parse().unwrap()], + json_file: Some("some-file".into()), + ..Default::default() + }, + ); + } + + #[test] + fn xml_file() { + assert_args( + ["test", "http://some-host", "--xml-file", "some-file"], + GlobalOpts { + hostnames: vec!["http://some-host".parse().unwrap()], + xml_file: Some("some-file".into()), + ..Default::default() + }, + ); + assert_args( + ["test", "http://some-host", "--oX", "some-file"], + GlobalOpts { + hostnames: vec!["http://some-host".parse().unwrap()], + xml_file: Some("some-file".into()), + ..Default::default() + }, + ); + } + + #[test] + fn output_all() { + assert_args( + ["test", "http://some-host", "--output-all", "some-file"], + GlobalOpts { + hostnames: vec!["http://some-host".parse().unwrap()], + output_file: Some("some-file.txt".into()), + json_file: Some("some-file.json".into()), + xml_file: Some("some-file.xml".into()), + ..Default::default() + }, + ); + assert_args( + ["test", "http://some-host", "--oA", "some-file"], + GlobalOpts { + hostnames: vec!["http://some-host".parse().unwrap()], + output_file: Some("some-file.txt".into()), + json_file: Some("some-file.json".into()), + xml_file: Some("some-file.xml".into()), + ..Default::default() + }, + ); + } + + #[test] + fn proxy() { + assert_args( + ["test", "http://some-host", "--proxy", "http://proxy:8000"], + GlobalOpts { + hostnames: vec!["http://some-host".parse().unwrap()], + proxy_enabled: true, + proxy_address: "http://proxy:8000".into(), + ..Default::default() + }, + ); + assert_args( + ["test", "http://some-host", "--burp"], + GlobalOpts { + hostnames: vec!["http://some-host".parse().unwrap()], + proxy_enabled: true, + proxy_address: "http://localhost:8080".into(), + ..Default::default() + }, + ); + assert_args( + ["test", "http://some-host", "--no-proxy"], + GlobalOpts { + hostnames: vec!["http://some-host".parse().unwrap()], + //TODO should this set proxy_enabled to false? + proxy_enabled: true, + proxy_address: String::new(), + ..Default::default() + }, + ); + } + + #[test] + fn max_threads() { + assert_args( + ["test", "http://some-host", "--max-threads", "4"], + GlobalOpts { + hostnames: vec!["http://some-host".parse().unwrap()], + max_threads: 4, + ..Default::default() + }, + ); + assert_args( + ["test", "http://some-host", "-t", "4"], + GlobalOpts { + hostnames: vec!["http://some-host".parse().unwrap()], + max_threads: 4, + ..Default::default() + }, + ); + } + + #[test] + fn wordlist_split() { + assert_args( + ["test", "http://some-host", "--wordlist-split", "4"], + GlobalOpts { + hostnames: vec!["http://some-host".parse().unwrap()], + wordlist_split: 4, + ..Default::default() + }, + ); + assert_args( + ["test", "http://some-host", "-T", "4"], + GlobalOpts { + hostnames: vec!["http://some-host".parse().unwrap()], + wordlist_split: 4, + ..Default::default() + }, + ); + } + + #[test] + fn throttle() { + assert_args( + ["test", "http://some-host", "--throttle", "4"], + GlobalOpts { + hostnames: vec!["http://some-host".parse().unwrap()], + throttle: 4, + ..Default::default() + }, + ); + assert_args( + ["test", "http://some-host", "-z", "4"], + GlobalOpts { + hostnames: vec!["http://some-host".parse().unwrap()], + throttle: 4, + ..Default::default() + }, + ); + } + + #[test] + fn username_password() { + assert_args( + [ + "test", + "http://some-host", + "--username", + "user", + "--password", + "pass", + ], + GlobalOpts { + hostnames: vec!["http://some-host".parse().unwrap()], + username: Some("user".into()), + password: Some("pass".into()), + ..Default::default() + }, + ); + } + + #[test] + fn recursion() { + assert_args( + ["test", "http://some-host", "--disable-recursion"], + GlobalOpts { + hostnames: vec!["http://some-host".parse().unwrap()], + max_recursion_depth: Some(0), + ..Default::default() + }, + ); + assert_args( + ["test", "http://some-host", "--max-recursion-depth", "4"], + GlobalOpts { + hostnames: vec!["http://some-host".parse().unwrap()], + max_recursion_depth: Some(4), + ..Default::default() + }, + ); + assert_args( + [ + "test", + "http://some-host", + "--max-recursion-depth", + "4", + "--disable-recursion", + ], + GlobalOpts { + hostnames: vec!["http://some-host".parse().unwrap()], + max_recursion_depth: Some(0), + ..Default::default() + }, + ); + } + + #[test] + fn listable() { + assert_args( + ["test", "http://some-host", "--scan-listable"], + GlobalOpts { + hostnames: vec!["http://some-host".parse().unwrap()], + scan_listable: true, + ..Default::default() + }, + ); + assert_args( + ["test", "http://some-host", "--scrape-listable"], + GlobalOpts { + hostnames: vec!["http://some-host".parse().unwrap()], + scrape_listable: true, + ..Default::default() + }, + ); + } + + #[test] + fn cookie() { + assert_args( + [ + "test", + "http://some-host", + "--cookie", + "name1=value1", + "-c", + "name2=value2", + "-c", + "name3=value3", + ], + GlobalOpts { + hostnames: vec!["http://some-host".parse().unwrap()], + cookies: Some( + "name1=value1; name2=value2; name3=value3".into(), + ), + ..Default::default() + }, + ); + } + + #[test] + fn header() { + assert_args( + [ + "test", + "http://some-host", + "--header", + "header1:value1", + "-H", + "header2: value2", + ], + GlobalOpts { + hostnames: vec!["http://some-host".parse().unwrap()], + headers: Some( + ["header1:value1", "header2: value2"] + .into_iter() + .map(Into::into) + .collect(), + ), + ..Default::default() + }, + ); + } + + #[test] + fn user_agent() { + assert_args( + [ + "test", + "http://some-host", + "--user-agent", + "some user agent", + ], + GlobalOpts { + hostnames: vec!["http://some-host".parse().unwrap()], + user_agent: Some("some user agent".into()), + ..Default::default() + }, + ); + assert_args( + ["test", "http://some-host", "-a", "some user agent"], + GlobalOpts { + hostnames: vec!["http://some-host".parse().unwrap()], + user_agent: Some("some user agent".into()), + ..Default::default() + }, + ); + } + + #[test] + fn verbosity() { + assert_args( + ["test", "http://some-host"], + GlobalOpts { + hostnames: vec!["http://some-host".parse().unwrap()], + log_level: LevelFilter::Info, + ..Default::default() + }, + ); + assert_args( + ["test", "http://some-host", "--verbose"], + GlobalOpts { + hostnames: vec!["http://some-host".parse().unwrap()], + log_level: LevelFilter::Debug, + ..Default::default() + }, + ); + assert_args( + ["test", "http://some-host", "--verbose", "--verbose"], + GlobalOpts { + hostnames: vec!["http://some-host".parse().unwrap()], + log_level: LevelFilter::Trace, + ..Default::default() + }, + ); + assert_args( + [ + "test", + "http://some-host", + "--verbose", + "--verbose", + "--verbose", + ], + GlobalOpts { + hostnames: vec!["http://some-host".parse().unwrap()], + log_level: LevelFilter::Trace, + ..Default::default() + }, + ); + assert_args( + ["test", "http://some-host", "-v"], + GlobalOpts { + hostnames: vec!["http://some-host".parse().unwrap()], + log_level: LevelFilter::Debug, + ..Default::default() + }, + ); + assert_args( + ["test", "http://some-host", "-vv"], + GlobalOpts { + hostnames: vec!["http://some-host".parse().unwrap()], + log_level: LevelFilter::Trace, + ..Default::default() + }, + ); + assert_args( + ["test", "http://some-host", "-vvv"], + GlobalOpts { + hostnames: vec!["http://some-host".parse().unwrap()], + log_level: LevelFilter::Trace, + ..Default::default() + }, + ); + } + + #[test] + fn silent() { + assert_args( + ["test", "http://some-host", "--silent"], + GlobalOpts { + hostnames: vec!["http://some-host".parse().unwrap()], + log_level: LevelFilter::Warn, + ..Default::default() + }, + ); + assert_args( + ["test", "http://some-host", "-S"], + GlobalOpts { + hostnames: vec!["http://some-host".parse().unwrap()], + log_level: LevelFilter::Warn, + ..Default::default() + }, + ); + } + + #[test] + fn code_whitelist() { + assert_args( + [ + "test", + "http://some-host", + "--code-whitelist", + "200,201,204", + ], + GlobalOpts { + hostnames: vec!["http://some-host".parse().unwrap()], + whitelist: true, + code_list: vec![200, 201, 204], + ..Default::default() + }, + ); + assert_args( + ["test", "http://some-host", "-W", "200,201,204"], + GlobalOpts { + hostnames: vec!["http://some-host".parse().unwrap()], + whitelist: true, + code_list: vec![200, 201, 204], + ..Default::default() + }, + ); + } + + #[test] + fn code_blacklist() { + assert_args( + [ + "test", + "http://some-host", + "--code-blacklist", + "200,201,204", + ], + GlobalOpts { + hostnames: vec!["http://some-host".parse().unwrap()], + whitelist: false, + code_list: vec![200, 201, 204], + ..Default::default() + }, + ); + assert_args( + ["test", "http://some-host", "-B", "200,201,204"], + GlobalOpts { + hostnames: vec!["http://some-host".parse().unwrap()], + whitelist: false, + code_list: vec![200, 201, 204], + ..Default::default() + }, + ); + } + + #[test] + fn disable_validator() { + assert_args( + ["test", "http://some-host", "--disable-validator"], + GlobalOpts { + hostnames: vec!["http://some-host".parse().unwrap()], + disable_validator: true, + ..Default::default() + }, + ); + } + + #[test] + fn scan_opts() { + assert_args( + ["test", "http://some-host", "--scan-401"], + GlobalOpts { + hostnames: vec!["http://some-host".parse().unwrap()], + scan_opts: ScanOpts { + scan_401: true, + scan_403: false, + }, + ..Default::default() + }, + ); + assert_args( + ["test", "http://some-host", "--scan-403"], + GlobalOpts { + hostnames: vec!["http://some-host".parse().unwrap()], + scan_opts: ScanOpts { + scan_401: false, + scan_403: true, + }, + ..Default::default() + }, + ); + } + + #[test] + fn ignore_cert() { + assert_args( + ["test", "http://some-host", "--ignore-cert"], + GlobalOpts { + hostnames: vec!["http://some-host".parse().unwrap()], + ignore_cert: true, + ..Default::default() + }, + ); + assert_args( + ["test", "http://some-host", "-k"], + GlobalOpts { + hostnames: vec!["http://some-host".parse().unwrap()], + ignore_cert: true, + ..Default::default() + }, + ); + } + + #[test] + fn show_htaccess() { + assert_args( + ["test", "http://some-host", "--show-htaccess"], + GlobalOpts { + hostnames: vec!["http://some-host".parse().unwrap()], + show_htaccess: true, + ..Default::default() + }, + ); + } + + #[test] + fn timeout() { + assert_args( + ["test", "http://some-host", "--timeout", "2"], + GlobalOpts { + hostnames: vec!["http://some-host".parse().unwrap()], + timeout: 2, + ..Default::default() + }, + ); + } + + #[test] + fn max_errors() { + assert_args( + ["test", "http://some-host", "--max-errors", "2"], + GlobalOpts { + hostnames: vec!["http://some-host".parse().unwrap()], + max_errors: 2, + ..Default::default() + }, + ); + } + + #[test] + fn no_colour() { + assert_args( + ["test", "http://some-host", "--no-colour"], + GlobalOpts { + hostnames: vec!["http://some-host".parse().unwrap()], + no_color: true, + ..Default::default() + }, + ); + assert_args( + ["test", "http://some-host", "--no-color"], + GlobalOpts { + hostnames: vec!["http://some-host".parse().unwrap()], + no_color: true, + ..Default::default() + }, + ); + } + + #[test] + fn hide_lengths() { + assert_args( + ["test", "http://some-host", "--hide-lengths", "400,600-700"], + GlobalOpts { + hostnames: vec!["http://some-host".parse().unwrap()], + length_blacklist: LengthRanges { + ranges: vec![ + LengthRange { + start: 400, + end: None, + }, + LengthRange { + start: 600, + end: Some(700), + }, + ], + }, + ..Default::default() + }, + ); + } } diff --git a/src/main.rs b/src/main.rs index fb82b47..2573979 100644 --- a/src/main.rs +++ b/src/main.rs @@ -44,7 +44,7 @@ mod wordlist; #[allow(clippy::cognitive_complexity)] fn main() { // Read the arguments in using the arg_parse module - let global_opts = Arc::new(arg_parse::get_args()); + let global_opts = Arc::new(arg_parse::get_args(std::env::args_os())); // Prepare the logging handler. Default to a pretty TermLogger, // but if the TermLogger initialisation fails (e.g. if we are not @@ -388,52 +388,9 @@ fn generate_end() -> request::RequestResponse { #[cfg(test)] mod test { - use crate::{arg_parse::GlobalOpts, request::RequestResponse}; - use log::LevelFilter::Info; + use crate::request::RequestResponse; use url::Url; - impl Default for GlobalOpts { - fn default() -> Self { - GlobalOpts { - hostnames: Default::default(), - wordlist_files: Default::default(), - prefixes: vec!["".into()], - extensions: vec!["".into()], - extension_substitution: false, - max_threads: Default::default(), - proxy_enabled: Default::default(), - proxy_address: Default::default(), - proxy_auth_enabled: Default::default(), - ignore_cert: Default::default(), - show_htaccess: Default::default(), - throttle: Default::default(), - max_recursion_depth: Default::default(), - user_agent: Default::default(), - username: Default::default(), - password: Default::default(), - output_file: Default::default(), - json_file: Default::default(), - xml_file: Default::default(), - timeout: Default::default(), - max_errors: Default::default(), - wordlist_split: Default::default(), - scan_listable: Default::default(), - cookies: Default::default(), - headers: Default::default(), - scrape_listable: Default::default(), - whitelist: Default::default(), - code_list: Default::default(), - is_terminal: Default::default(), - no_color: Default::default(), - disable_validator: Default::default(), - http_verb: Default::default(), - scan_opts: Default::default(), - log_level: Info, - length_blacklist: Default::default(), - } - } - } - impl Default for RequestResponse { fn default() -> Self { RequestResponse { diff --git a/src/wordlist.rs b/src/wordlist.rs index 638d4cb..83a9def 100644 --- a/src/wordlist.rs +++ b/src/wordlist.rs @@ -18,7 +18,7 @@ use crate::validator_thread::TargetValidator; use chardet::{charset2encoding, detect}; use encoding::{DecoderTrap, label::encoding_from_whatwg_label}; -use std::{fs, sync::Arc}; +use std::{fs, path::Path, sync::Arc}; use url::Url; // Struct for a UriGenerator, it needs the hostname, the suffix to @@ -105,7 +105,9 @@ impl Iterator for UriGenerator { } // Function used to read in lines from the wordlist file -pub fn lines_from_file(filename: &str) -> Vec { +pub fn lines_from_file(filename: impl AsRef) -> Vec { + let filename = filename.as_ref(); + // Read the raw file in as a vector of bytes let reader = fs::read(filename).unwrap(); @@ -127,7 +129,7 @@ pub fn lines_from_file(filename: &str) -> Vec { None => { panic!( "Error detecting file encoding of {} - is the file empty?", - filename + filename.display(), ); } }