use anyhow::{bail, Context, Result};
use console::{strip_ansi_codes, style, user_attended};
use indicatif::ProgressBar;
use regex::Regex;
use reqwest::{Client, Method, Response, StatusCode, Url};
#[cfg(not(target_os = "windows"))]
use rlimit::{getrlimit, setrlimit, Resource};
use std::{
error::Error,
fs,
io::{self, BufWriter, Write},
sync::Arc,
time::Duration,
time::{SystemTime, UNIX_EPOCH},
};
use tokio::sync::{mpsc::UnboundedSender, oneshot};
use crate::{
config::Configuration,
config::OutputLevel,
event_handlers::{
Command::{self, AddError, AddStatus},
Handles,
},
progress::PROGRESS_PRINTER,
response::FeroxResponse,
send_command,
statistics::StatError::{Certificate, Connection, Other, Redirection, Request, Timeout},
traits::FeroxSerialize,
USER_AGENTS,
};
static mut USER_AGENT_CTR: usize = 0;
fn is_certificate_error(error: &reqwest::Error) -> bool {
let full_error = format!("{error:?}").to_lowercase();
let error_msg = error.to_string().to_lowercase();
if error_msg.contains("certificate verify failed")
|| error_msg.contains("self-signed certificate")
|| error_msg.contains("certificate has expired")
|| error_msg.contains("hostname mismatch")
|| error_msg.contains("certificate")
{
return true;
}
if full_error.contains("ssl routines")
|| full_error.contains("certificate verify failed")
|| full_error.contains("self-signed certificate")
|| full_error.contains("certificate has expired")
|| full_error.contains("hostname mismatch")
|| full_error.contains("tls_post_process_server_certificate")
|| full_error.contains("certificate")
|| full_error.contains("cert")
{
return true;
}
let mut source = error.source();
while let Some(err) = source {
let source_msg = err.to_string().to_lowercase();
if source_msg.contains("ssl routines")
|| source_msg.contains("certificate verify failed")
|| source_msg.contains("self-signed certificate")
|| source_msg.contains("certificate has expired")
|| source_msg.contains("hostname mismatch")
|| source_msg.contains("unable to get local issuer certificate")
|| source_msg.contains("certificate is not yet valid")
|| source_msg.contains("invalid certificate")
|| source_msg.contains("unknown ca")
|| source_msg.contains("certificate")
|| source_msg.contains("cert")
|| source_msg.contains("tls")
|| source_msg.contains("ssl")
{
return true;
}
source = err.source();
}
false
}
pub fn open_file(filename: &str) -> Result<BufWriter<fs::File>> {
log::trace!("enter: open_file({filename})");
let file = fs::OpenOptions::new() .create(true)
.append(true)
.open(filename)
.with_context(|| fmt_err(&format!("Could not open {filename}")))?;
let writer = BufWriter::new(file);
log::trace!("exit: open_file -> {writer:?}");
Ok(writer)
}
pub fn status_colorizer(status: &str) -> String {
match status.chars().next() {
Some('1') => style(status).blue().to_string(), Some('2') => style(status).green().to_string(), Some('3') => style(status).yellow().to_string(), Some('4') => style(status).red().to_string(), Some('5') => style(status).red().to_string(), Some('W') => style(status).cyan().to_string(), Some('E') => style(status).red().to_string(), _ => status.to_string(), }
}
pub fn fmt_err(msg: &str) -> String {
format!("{}: {}", status_colorizer("ERROR"), msg)
}
pub fn timestamp() -> f64 {
let since_the_epoch = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_else(|_| Duration::from_secs(0));
let secs = since_the_epoch.as_secs() as f64;
let nanos = since_the_epoch.subsec_nanos() as f64;
secs + (nanos / 1_000_000_000.0)
}
pub(crate) async fn send_try_recursion_command(
handles: Arc<Handles>,
response: FeroxResponse,
) -> Result<()> {
let mut response = response;
response.drop_text();
handles.send_scan_command(Command::TryRecursion(Box::new(response)))?;
let (tx, rx) = oneshot::channel::<bool>();
handles.send_scan_command(Command::Sync(tx))?;
rx.await?;
Ok(())
}
pub fn module_colorizer(modname: &str) -> String {
style(modname).cyan().to_string()
}
pub fn ferox_print(msg: &str, bar: &ProgressBar) {
if user_attended() {
bar.println(msg);
} else {
let stripped = strip_ansi_codes(msg);
println!("{stripped}");
}
}
pub async fn logged_request(
url: &Url,
method: &str,
data: Option<&[u8]>,
handles: Arc<Handles>,
) -> Result<Response> {
let client = &handles.config.client;
let level = handles.config.output_level;
let tx_stats = handles.stats.tx.clone();
let response = make_request(client, url, method, data, level, &handles.config, tx_stats).await;
let scans = handles.ferox_scans()?;
match response {
Ok(resp) => {
match resp.status() {
StatusCode::TOO_MANY_REQUESTS | StatusCode::FORBIDDEN => {
scans.increment_status_code(url.as_str(), resp.status());
}
_ => {}
}
Ok(resp)
}
Err(e) => {
log::warn!("err: {e:?}");
scans.increment_error(url.as_str());
bail!(e)
}
}
}
pub async fn make_request(
client: &Client,
url: &Url,
method: &str,
mut data: Option<&[u8]>,
output_level: OutputLevel,
config: &Configuration,
tx_stats: UnboundedSender<Command>,
) -> Result<Response> {
log::trace!(
"enter: make_request(Configuration::Client, {url}, {output_level:?}, {tx_stats:?})"
);
let tmp_workaround: Option<&[u8]> = Some(&[0xd_u8, 0xa]);
let mut request = client.request(Method::from_bytes(method.as_bytes())?, url.to_owned());
if (!config.proxy.is_empty() || !config.replay_proxy.is_empty())
&& data.is_none()
&& ["post", "put", "patch"].contains(&method.to_ascii_lowercase().as_str())
{
data = tmp_workaround;
}
if let Some(body_data) = data {
request = request.body(body_data.to_vec());
}
if config.random_agent {
let index = unsafe {
USER_AGENT_CTR += 1;
USER_AGENT_CTR % USER_AGENTS.len()
};
let user_agent = USER_AGENTS[index];
request = request.header("User-Agent", user_agent);
}
match request.send().await {
Err(e) => {
log::trace!("exit: make_request -> {e}");
if e.is_timeout() {
send_command!(tx_stats, AddError(Timeout));
} else if e.is_redirect() {
if let Some(last_redirect) = e.url() {
let fancy_message = format!(
"{} !=> {} ({})",
url,
last_redirect,
style("too many redirects").red(),
);
let msg_status = match e.status() {
Some(status) => status.to_string(),
None => "ERR".to_string(),
};
let report = create_report_string(
&msg_status,
method,
"-1",
"-1",
"-1",
&fancy_message,
output_level,
);
send_command!(tx_stats, AddError(Redirection));
ferox_print(&report, &PROGRESS_PRINTER)
};
} else if is_certificate_error(&e) {
log::warn!("Certificate error detected: {e}");
send_command!(tx_stats, AddError(Certificate));
bail!(":SSL: {e}");
} else if e.is_connect() {
send_command!(tx_stats, AddError(Connection));
} else if e.is_request() {
send_command!(tx_stats, AddError(Request));
} else {
send_command!(tx_stats, AddError(Other));
}
log::warn!("Error while making request: {e}");
bail!("{}", e)
}
Ok(resp) => {
log::trace!("exit: make_request -> {resp:?}");
send_command!(tx_stats, AddStatus(resp.status()));
Ok(resp)
}
}
}
pub fn create_report_string(
status: &str,
method: &str,
line_count: &str,
word_count: &str,
content_length: &str,
url: &str,
output_level: OutputLevel,
) -> String {
if matches!(output_level, OutputLevel::Silent) {
format!("{url}\n")
} else {
let color_status = status_colorizer(status);
if status.contains("MSG") {
format!(
"{color_status} {method:>8} {line_count:>9} {word_count:>9} {content_length:>9} {url}\n"
)
} else {
format!(
"{color_status} {method:>8} {line_count:>8}l {word_count:>8}w {content_length:>8}c {url}\n"
)
}
}
}
#[cfg(not(target_os = "windows"))]
pub fn set_open_file_limit(limit: u64) -> bool {
log::trace!("enter: set_open_file_limit");
if let Ok((soft, hard)) = getrlimit(Resource::NOFILE) {
if hard > limit {
if setrlimit(Resource::NOFILE, limit, hard).is_ok() {
log::debug!("set open file descriptor limit to {limit}");
log::trace!("exit: set_open_file_limit -> {}", true);
return true;
}
} else if soft != hard {
if setrlimit(Resource::NOFILE, hard, hard).is_ok() {
log::debug!("set open file descriptor limit to {limit}");
log::trace!("exit: set_open_file_limit -> {}", true);
return true;
}
}
}
log::warn!("could not set open file descriptor limit to {limit}");
log::trace!("exit: set_open_file_limit -> {}", false);
false
}
pub fn write_to<T>(
value: &T,
file: &mut io::BufWriter<fs::File>,
convert_to_json: bool,
) -> Result<()>
where
T: FeroxSerialize,
{
let contents = if convert_to_json {
value.as_json()?
} else {
value.as_str()
};
let contents = strip_ansi_codes(&contents);
let written = file.write(contents.as_bytes())?;
if written > 0 {
file.flush()?;
}
Ok(())
}
fn should_deny_absolute(url_to_test: &Url, denier: &Url, handles: Arc<Handles>) -> Result<bool> {
log::trace!(
"enter: should_deny_absolute({}, {:?})",
url_to_test.as_str(),
denier.as_str(),
);
if url_to_test == denier {
log::trace!("exit: should_deny_absolute -> true");
return Ok(true);
}
match (url_to_test.host(), denier.host()) {
(Some(normed_host), Some(denier_host)) => {
if normed_host != denier_host {
return Ok(false);
}
}
_ => {
return Ok(false);
}
}
let tested_host = url_to_test.host().unwrap();
let deny_path = denier.path();
let tested_path = url_to_test.path();
if tested_path.starts_with(deny_path) {
for ferox_scan in handles.ferox_scans()?.get_active_scans() {
let scanner = parse_url_with_raw_path(ferox_scan.url().trim_end_matches('/'))
.with_context(|| format!("Could not parse {ferox_scan} as a url"))?;
if let Some(scan_host) = scanner.host() {
if tested_host != scan_host {
continue;
}
} else {
unreachable!("should_deny_absolute: scanner.host() returned None, which shouldn't be possible");
};
let scan_path = scanner.path();
if scan_path.starts_with(deny_path) && tested_path.starts_with(scan_path) {
log::trace!("exit: should_deny_absolute -> false");
return Ok(false);
}
}
log::trace!("exit: should_deny_absolute -> true");
return Ok(true);
}
log::trace!("exit: should_deny_absolute -> false");
Ok(false)
}
fn should_deny_regex(url_to_test: &Url, denier: &Regex) -> bool {
log::trace!(
"enter: should_deny_regex({}, {})",
url_to_test.as_str(),
denier,
);
let result = denier.is_match(url_to_test.as_str());
log::trace!("exit: should_deny_regex -> {result}");
result
}
pub fn should_deny_url(url: &Url, handles: Arc<Handles>) -> Result<bool> {
log::trace!(
"enter: should_deny_url({}, {:?}, {:?})",
url.as_str(),
handles.config.url_denylist,
handles.ferox_scans()?
);
let normed_url = parse_url_with_raw_path(url.to_string().trim_end_matches('/'))?;
for denier in &handles.config.url_denylist {
if let Ok(should_deny) = should_deny_absolute(&normed_url, denier, handles.clone()) {
if should_deny {
return Ok(true);
}
}
}
for denier in &handles.config.regex_denylist {
if should_deny_regex(&normed_url, denier) {
return Ok(true);
}
}
log::trace!("exit: should_deny_url -> false");
Ok(false)
}
pub fn slugify_filename(url: &str, prefix: &str, suffix: &str) -> String {
log::trace!("enter: slugify({url:?}, {prefix:?}, {suffix:?})");
let ts = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_else(|_| Duration::from_secs(0))
.as_secs();
let altered_prefix = if !prefix.is_empty() {
format!("{prefix}-")
} else {
String::new()
};
let slug = url.replace("://", "_").replace(['/', '.', ':'], "_");
let filename = format!("{altered_prefix}{slug}-{ts}.{suffix}");
log::trace!("exit: slugify -> {filename}");
filename
}
pub fn parse_url_with_raw_path(url: &str) -> Result<Url> {
log::trace!("enter: parse_url_with_raw_path({url})");
let parsed = Url::parse(url)?;
if !parsed.has_authority() {
bail!("url to parse has no authority and is therefore invalid");
}
if parsed.host().is_none() {
bail!("url to parse doesn't have a host");
}
let farthest_right_authority_part;
if let Some(port) = parsed.port() {
farthest_right_authority_part = format!(":{port}");
} else if parsed.has_host() {
farthest_right_authority_part = parsed.host_str().unwrap().to_owned();
} else {
unreachable!("url has an authority, but has neither a port nor a host");
}
let Some((_, after_authority)) = url.split_once(&farthest_right_authority_part) else {
return Ok(parsed);
};
let after_authority = after_authority
.replacen(":80", "", 1)
.replacen(":443", "", 1);
let (path, _discarded) = after_authority
.split_once('?')
.unwrap_or_else(|| {
after_authority
.split_once('#')
.unwrap_or((&after_authority, ""))
});
let transformation_detectors = [
"..",
"%2e%2e",
"%25%32%65%25%32%65",
"%c0%ae%c0%ae",
"%e0%40%ae%e0%40%ae",
"%c0ae%c0ae",
"%uff0e%uff0e",
"%u002e%u002e",
];
let parsing_will_transform_path = transformation_detectors
.iter()
.any(|detector| path.to_lowercase().contains(detector));
if !parsing_will_transform_path {
return Ok(parsed);
}
let mut hacked_url = if path.ends_with('/') {
#[cfg(target_os = "windows")]
{
let path = format!("\\/IGNOREME{path}");
Url::from_directory_path(path).unwrap()
}
#[cfg(not(target_os = "windows"))]
Url::from_directory_path(path).unwrap()
} else {
#[cfg(target_os = "windows")]
{
let path = format!("\\/IGNOREME{path}");
Url::from_file_path(path).unwrap()
}
#[cfg(not(target_os = "windows"))]
Url::from_file_path(path).unwrap()
};
hacked_url.set_host(parsed.host_str())?;
hacked_url.set_scheme(parsed.scheme()).unwrap();
hacked_url.set_port(parsed.port()).unwrap();
hacked_url.set_username(parsed.username()).unwrap();
hacked_url.set_password(parsed.password()).unwrap();
hacked_url.set_query(parsed.query());
hacked_url.set_fragment(parsed.fragment());
log::trace!("exit: parse_url_with_raw_path -> {hacked_url}");
Ok(hacked_url)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::Configuration;
use crate::scan_manager::{FeroxScans, ScanOrder};
#[test]
fn utils_parse_url_with_raw_path_javascript() {
let url = "javascript://";
let parsed = parse_url_with_raw_path(url);
assert!(parsed.is_err());
assert!(parsed
.unwrap_err()
.to_string()
.contains("url to parse doesn't have a host"));
}
#[test]
fn utils_parse_url_with_raw_path() {
let url = "https://www.google.com/../../stuff";
let parsed = parse_url_with_raw_path(url).unwrap();
assert_eq!(parsed.as_str(), url);
let url = "https://www.google.com/../../stuff/";
let parsed = parse_url_with_raw_path(url).unwrap();
assert_eq!(parsed.as_str(), url);
let url = "https://www.google.com/stuff";
let parsed = parse_url_with_raw_path(url).unwrap();
assert_eq!(parsed.as_str(), url);
let url = "https://www.google.com/stuff/";
let parsed: Url = parse_url_with_raw_path(url).unwrap();
assert_eq!(parsed.as_str(), url);
let url = "mailto:user@example.com";
let parsed = parse_url_with_raw_path(url);
assert!(parsed.is_err());
let url = "../../stuff";
let parsed = parse_url_with_raw_path(url);
assert!(parsed.is_err());
let url = "/../../stuff";
let parsed = parse_url_with_raw_path(url);
assert!(parsed.is_err());
for url in [
"http://example.com:80/path/file.html",
"https://example.com:443/path/file.html",
] {
let parsed = parse_url_with_raw_path(url).unwrap();
assert!(parsed.port().is_none());
assert_eq!(parsed.host().unwrap().to_string().as_str(), "example.com");
}
for url in [
"http://example.com:8080/path/file.html",
"https://example.com:4433/path/file.html",
] {
let parsed = parse_url_with_raw_path(url).unwrap();
assert!(parsed.port().is_some());
assert_eq!(parsed.as_str(), url);
}
let url = "http://user:pass@example.com/%2e%2e/stuff.php";
let parsed = parse_url_with_raw_path(url).unwrap();
assert_eq!(
parsed.as_str(),
"http://user:pass@example.com/%252e%252e/stuff.php"
);
let url = "http://user:pass@example.com/%25%32%65%25%32%65/stuff.php";
let parsed = parse_url_with_raw_path(url).unwrap();
assert_eq!(parsed.username(), "user");
assert_eq!(parsed.password().unwrap(), "pass");
assert_eq!(
parsed.as_str(),
"http://user:pass@example.com/%2525%2532%2565%2525%2532%2565/stuff.php"
);
let url = "http://user:pass@example.com/%c0%ae%c0%ae/stuff.php";
let parsed = parse_url_with_raw_path(url).unwrap();
assert_eq!(parsed.username(), "user");
assert_eq!(parsed.password().unwrap(), "pass");
assert_eq!(
parsed.as_str(),
"http://user:pass@example.com/%25c0%25ae%25c0%25ae/stuff.php"
);
let url = "http://user:pass@example.com/%e0%40%ae%e0%40%ae/stuff.php";
let parsed = parse_url_with_raw_path(url).unwrap();
assert_eq!(parsed.username(), "user");
assert_eq!(parsed.password().unwrap(), "pass");
assert_eq!(
parsed.as_str(),
"http://user:pass@example.com/%25e0%2540%25ae%25e0%2540%25ae/stuff.php"
);
let url = "http://user:pass@example.com/%c0ae%c0ae/stuff.php";
let parsed = parse_url_with_raw_path(url).unwrap();
assert_eq!(parsed.username(), "user");
assert_eq!(parsed.password().unwrap(), "pass");
assert_eq!(
parsed.as_str(),
"http://user:pass@example.com/%25c0ae%25c0ae/stuff.php"
);
let url = "http://user:pass@example.com/%uff0e%uff0e/stuff.php";
let parsed = parse_url_with_raw_path(url).unwrap();
assert_eq!(parsed.username(), "user");
assert_eq!(parsed.password().unwrap(), "pass");
assert_eq!(
parsed.as_str(),
"http://user:pass@example.com/%25uff0e%25uff0e/stuff.php"
);
let url = "http://user:pass@example.com/%u002e%u002e/stuff.php";
let parsed = parse_url_with_raw_path(url).unwrap();
assert_eq!(parsed.username(), "user");
assert_eq!(parsed.password().unwrap(), "pass");
assert_eq!(
parsed.as_str(),
"http://user:pass@example.com/%25u002e%25u002e/stuff.php"
);
}
#[cfg(not(target_os = "windows"))]
mod nix_only_tests {
use super::*;
#[test]
fn utils_set_open_file_limit_with_low_requested_limit() {
let (_, hard) = getrlimit(Resource::NOFILE).unwrap();
let lower_limit = hard - 1;
assert!(set_open_file_limit(lower_limit));
}
#[test]
fn utils_set_open_file_limit_with_high_requested_limit() {
let (_, hard) = getrlimit(Resource::NOFILE).unwrap();
let higher_limit = hard + 1;
let new_soft = hard - 1;
setrlimit(Resource::NOFILE, new_soft, hard).unwrap();
assert!(set_open_file_limit(higher_limit));
}
#[test]
fn utils_set_open_file_limit_with_fails_when_both_limits_are_equal() {
let (_, hard) = getrlimit(Resource::NOFILE).unwrap();
setrlimit(Resource::NOFILE, hard, hard).unwrap();
assert!(!set_open_file_limit(hard)); }
}
#[test]
fn status_colorizer_uses_red_for_500s() {
assert_eq!(status_colorizer("500"), style("500").red().to_string());
}
#[test]
fn status_colorizer_uses_red_for_400s() {
assert_eq!(status_colorizer("400"), style("400").red().to_string());
}
#[test]
fn status_colorizer_uses_red_for_errors() {
assert_eq!(status_colorizer("ERROR"), style("ERROR").red().to_string());
}
#[test]
fn status_colorizer_uses_cyan_for_wildcards() {
assert_eq!(status_colorizer("WLD"), style("WLD").cyan().to_string());
}
#[test]
fn status_colorizer_uses_blue_for_100s() {
assert_eq!(status_colorizer("100"), style("100").blue().to_string());
}
#[test]
fn status_colorizer_uses_green_for_200s() {
assert_eq!(status_colorizer("200"), style("200").green().to_string());
}
#[test]
fn status_colorizer_uses_yellow_for_300s() {
assert_eq!(status_colorizer("300"), style("300").yellow().to_string());
}
#[test]
fn status_colorizer_returns_as_is() {
assert_eq!(status_colorizer("farfignewton"), "farfignewton".to_string());
}
#[test]
fn should_deny_url_blocks_when_denier_is_exact_match() {
let scan_url = "https://testdomain.com/";
let deny_url = "https://testdomain.com/denied";
let tested_url = Url::parse("https://testdomain.com/denied/").unwrap();
let scans = Arc::new(FeroxScans::default());
scans.add_directory_scan(
scan_url,
ScanOrder::Initial,
Arc::new(Handles::for_testing(None, None).0),
);
let mut config = Configuration::new().unwrap();
config.url_denylist = vec![Url::parse(deny_url).unwrap()];
let config = Arc::new(config);
let handles = Arc::new(Handles::for_testing(Some(scans), Some(config)).0);
assert!(should_deny_url(&tested_url, handles).unwrap());
}
#[test]
fn should_deny_url_doesnt_compare_mismatched_domains() {
let scan_url = "https://testdomain.com/";
let deny_url = "https://dev.testdomain.com/denied";
let tested_url = Url::parse("https://testdomain.com/denied/").unwrap();
let scans = Arc::new(FeroxScans::default());
scans.add_directory_scan(
scan_url,
ScanOrder::Initial,
Arc::new(Handles::for_testing(None, None).0),
);
let mut config = Configuration::new().unwrap();
config.url_denylist = vec![Url::parse(deny_url).unwrap()];
let config = Arc::new(config);
let handles = Arc::new(Handles::for_testing(Some(scans), Some(config)).0);
assert!(!should_deny_url(&tested_url, handles).unwrap());
}
#[test]
fn should_deny_url_doesnt_compare_non_domains() {
let scan_url = "https://testdomain.com/";
let deny_url = "unix:/run/foo.socket";
let tested_url = Url::parse("https://testdomain.com/denied/").unwrap();
let scans = Arc::new(FeroxScans::default());
scans.add_directory_scan(
scan_url,
ScanOrder::Initial,
Arc::new(Handles::for_testing(None, None).0),
);
let mut config = Configuration::new().unwrap();
config.url_denylist = vec![Url::parse(deny_url).unwrap()];
let config = Arc::new(config);
let handles = Arc::new(Handles::for_testing(Some(scans), Some(config)).0);
assert!(!should_deny_url(&tested_url, handles).unwrap());
}
#[test]
fn should_deny_url_doesnt_compare_mismatched_domains_in_scanned() {
let deny_url = "https://testdomain.com/";
let scan_url = "https://dev.testdomain.com/denied";
let tested_url = Url::parse("https://testdomain.com/denied/").unwrap();
let scans = Arc::new(FeroxScans::default());
scans.add_directory_scan(
scan_url,
ScanOrder::Initial,
Arc::new(Handles::for_testing(None, None).0),
);
let mut config = Configuration::new().unwrap();
config.url_denylist = vec![Url::parse(deny_url).unwrap()];
let config = Arc::new(config);
let handles = Arc::new(Handles::for_testing(Some(scans), Some(config)).0);
assert!(should_deny_url(&tested_url, handles).unwrap());
}
#[test]
fn should_deny_url_doesnt_compare_non_domains_in_scanned() {
let deny_url = "https://testdomain.com/";
let scan_url = "unix:/run/foo.socket";
let tested_url = Url::parse("https://testdomain.com/denied/").unwrap();
let scans = Arc::new(FeroxScans::default());
scans.add_directory_scan(
scan_url,
ScanOrder::Initial,
Arc::new(Handles::for_testing(None, None).0),
);
let mut config = Configuration::new().unwrap();
config.url_denylist = vec![Url::parse(deny_url).unwrap()];
let config = Arc::new(config);
let handles = Arc::new(Handles::for_testing(Some(scans), Some(config)).0);
assert!(!should_deny_url(&tested_url, handles).unwrap());
}
#[test]
fn should_deny_url_blocks_child() {
let scan_url = "https://testdomain.com/";
let deny_url = "https://testdomain.com/api";
let tested_url = Url::parse("https://testdomain.com/api/denied/").unwrap();
let scans = Arc::new(FeroxScans::default());
scans.add_directory_scan(
scan_url,
ScanOrder::Initial,
Arc::new(Handles::for_testing(None, None).0),
);
let mut config = Configuration::new().unwrap();
config.url_denylist = vec![Url::parse(deny_url).unwrap()];
let config = Arc::new(config);
let handles = Arc::new(Handles::for_testing(Some(scans), Some(config)).0);
assert!(should_deny_url(&tested_url, handles).unwrap());
}
#[test]
fn should_deny_url_doesnt_block_non_child() {
let scan_url = "https://testdomain.com/";
let deny_url = "https://testdomain.com/api";
let tested_url = Url::parse("https://testdomain.com/not-denied/").unwrap();
let scans = Arc::new(FeroxScans::default());
scans.add_directory_scan(
scan_url,
ScanOrder::Initial,
Arc::new(Handles::for_testing(None, None).0),
);
let mut config = Configuration::new().unwrap();
config.url_denylist = vec![Url::parse(deny_url).unwrap()];
let config = Arc::new(config);
let handles = Arc::new(Handles::for_testing(Some(scans), Some(config)).0);
assert!(!should_deny_url(&tested_url, handles).unwrap());
}
#[test]
fn should_deny_url_blocks_child_when_scan_url_isnt_parent() {
let scan_url = "https://testdomain.com/api";
let deny_url = "https://testdomain.com/";
let tested_url = Url::parse("https://testdomain.com/stuff/").unwrap();
let scans = Arc::new(FeroxScans::default());
scans.add_directory_scan(
scan_url,
ScanOrder::Initial,
Arc::new(Handles::for_testing(None, None).0),
);
let mut config = Configuration::new().unwrap();
config.url_denylist = vec![Url::parse(deny_url).unwrap()];
let config = Arc::new(config);
let handles = Arc::new(Handles::for_testing(Some(scans), Some(config)).0);
assert!(should_deny_url(&tested_url, handles).unwrap());
}
#[test]
fn should_deny_url_doesnt_block_child_when_scan_url_is_parent() {
let scan_url = "https://testdomain.com/api";
let deny_url = "https://testdomain.com/";
let tested_url = Url::parse("https://testdomain.com/api/not-denied/").unwrap();
let scans = Arc::new(FeroxScans::default());
scans.add_directory_scan(
scan_url,
ScanOrder::Initial,
Arc::new(Handles::for_testing(None, None).0),
);
let mut config = Configuration::new().unwrap();
config.url_denylist = vec![Url::parse(deny_url).unwrap()];
let config = Arc::new(config);
let handles = Arc::new(Handles::for_testing(Some(scans), Some(config)).0);
assert!(!should_deny_url(&tested_url, handles).unwrap());
}
#[test]
fn should_deny_url_blocks_urls_based_on_regex_in_path() {
let scan_url = "https://testdomain.com/";
let deny_pattern = "/deni.*";
let tested_url = Url::parse("https://testdomain.com/denied/").unwrap();
let scans = Arc::new(FeroxScans::default());
scans.add_directory_scan(
scan_url,
ScanOrder::Initial,
Arc::new(Handles::for_testing(None, None).0),
);
let mut config = Configuration::new().unwrap();
config.regex_denylist = vec![Regex::new(deny_pattern).unwrap()];
let config = Arc::new(config);
let handles = Arc::new(Handles::for_testing(Some(scans), Some(config)).0);
assert!(should_deny_url(&tested_url, handles).unwrap());
}
#[test]
fn should_deny_url_blocks_urls_based_on_regex_in_scheme() {
let scan_url = "https://testdomain.com/";
let deny_pattern = "http:";
let tested_http_url = Url::parse("http://testdomain.com/denied/").unwrap();
let tested_https_url = Url::parse("https://testdomain.com/denied/").unwrap();
let scans = Arc::new(FeroxScans::default());
scans.add_directory_scan(
scan_url,
ScanOrder::Initial,
Arc::new(Handles::for_testing(None, None).0),
);
let mut config = Configuration::new().unwrap();
config.regex_denylist = vec![Regex::new(deny_pattern).unwrap()];
let config = Arc::new(config);
let handles = Arc::new(Handles::for_testing(Some(scans), Some(config)).0);
assert!(!should_deny_url(&tested_https_url, handles.clone()).unwrap());
assert!(should_deny_url(&tested_http_url, handles).unwrap());
}
}