mod length;
mod regex;
mod status_code;
use crate::actions::Action;
use crate::metadata::AsAny;
use crate::observers::Observers;
use crate::requests::Request;
use crate::responses::Response;
use crate::state::SharedState;
use crate::std_ext::tuple::Named;
use crate::DecidersList;
pub use self::length::ContentLengthDecider;
pub use self::regex::RequestRegexDecider;
pub use self::regex::ResponseRegexDecider;
pub use self::status_code::StatusCodeDecider;
pub use crate::std_ext::ops::LogicOperation;
use dyn_clone::DynClone;
pub trait Decider<O, R>: DynClone + AsAny + Named
where
O: Observers<R>,
R: Response,
{
fn decide_with_request(&mut self, _state: &SharedState, _request: &Request) -> Option<Action> {
None
}
fn decide_with_observers(&mut self, _state: &SharedState, _observers: &O) -> Option<Action> {
None
}
}
impl<O, R> Clone for Box<dyn Decider<O, R>>
where
O: Observers<R>,
R: Response,
{
fn clone(&self) -> Self {
dyn_clone::clone_box(&**self)
}
}
impl<O, R> Clone for Box<dyn DeciderHooks<O, R>>
where
O: Observers<R>,
R: Response,
{
fn clone(&self) -> Self {
dyn_clone::clone_box(&**self)
}
}
pub trait DeciderHooks<O, R>: Decider<O, R> + DynClone + AsAny + Sync + Send
where
O: Observers<R>,
R: Response,
{
fn pre_send_hook(
&mut self,
state: &SharedState,
request: &Request,
action: Option<Action>,
operation: LogicOperation,
) -> Option<Action> {
if let Some(ref inner) = action {
match (inner, operation) {
(Action::Discard, LogicOperation::And) => return Some(Action::Discard),
(Action::Keep, LogicOperation::Or) => return Some(Action::Keep),
_ => {}
}
}
let new_action = self.decide_with_request(state, request)?;
let final_action = match (action, operation) {
(None, _) => new_action,
(Some(old_action), LogicOperation::And) => new_action & old_action,
(Some(old_action), LogicOperation::Or) => new_action | old_action,
};
Some(final_action)
}
fn post_send_hook(
&mut self,
state: &SharedState,
observers: &O,
action: Option<Action>,
operation: LogicOperation,
) -> Option<Action> {
if let Some(ref inner) = action {
match (inner, operation) {
(Action::Discard, LogicOperation::And) => return Some(Action::Discard),
(Action::Keep, LogicOperation::Or) => return Some(Action::Keep),
_ => {}
}
}
let new_action = self.decide_with_observers(state, observers)?;
let final_action = match (action, operation) {
(None, _) => new_action,
(Some(old_action), LogicOperation::And) => new_action & old_action,
(Some(old_action), LogicOperation::Or) => new_action | old_action,
};
Some(final_action)
}
}
pub trait Deciders<O, R>
where
O: Observers<R>,
R: Response,
{
fn call_pre_send_hooks(
&mut self,
_state: &SharedState,
_request: &Request,
action: Option<Action>,
_operation: LogicOperation,
) -> Option<Action> {
action
}
fn call_post_send_hooks(
&mut self,
_state: &SharedState,
_observers: &O,
action: Option<Action>,
_operation: LogicOperation,
) -> Option<Action> {
action
}
}
impl<O, R> Deciders<O, R> for ()
where
O: Observers<R>,
R: Response,
{
}
impl<Head, Tail, O, R> Deciders<O, R> for (Head, Tail)
where
Head: DeciderHooks<O, R>,
Tail: Deciders<O, R> + DecidersList,
O: Observers<R>,
R: Response,
{
fn call_pre_send_hooks(
&mut self,
state: &SharedState,
request: &Request,
action: Option<Action>,
operation: LogicOperation,
) -> Option<Action> {
let action = self.0.pre_send_hook(state, request, action, operation);
self.1
.call_pre_send_hooks(state, request, action, operation)
}
fn call_post_send_hooks(
&mut self,
state: &SharedState,
observers: &O,
action: Option<Action>,
operation: LogicOperation,
) -> Option<Action> {
let action = self.0.post_send_hook(state, observers, action, operation);
self.1
.call_post_send_hooks(state, observers, action, operation)
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::similar_names)]
use super::*;
use crate::client::{BlockingClient, BlockingRequests};
use crate::corpora::RangeCorpus;
use crate::fuzzers::BlockingFuzzer;
use crate::mutators::ReplaceKeyword;
use crate::observers::ResponseObserver;
use crate::prelude::*;
use crate::processors::ResponseProcessor;
use crate::requests::ShouldFuzz;
use crate::responses::BlockingResponse;
use crate::schedulers::OrderedScheduler;
use ::regex::Regex;
use httpmock::Method::GET;
use httpmock::MockServer;
use reqwest;
use std::time::Duration;
#[test]
fn pre_send_deciders_used_as_denylist() -> Result<(), Box<dyn std::error::Error>> {
let srv = MockServer::start();
let mock = srv.mock(|when, then| {
when.method(GET).path_matches(Regex::new("[012]").unwrap());
then.status(200).body("this is a test");
});
let range = RangeCorpus::with_stop(3).name("range").build()?;
let mut state = SharedState::with_corpus(range);
let req_client = reqwest::blocking::Client::builder()
.timeout(Duration::from_secs(1))
.build()?;
let client = BlockingClient::with_client(req_client);
let mutator = ReplaceKeyword::new(&"FUZZ", "range");
let request = Request::from_url(&srv.url("/FUZZ"), Some(&[ShouldFuzz::URLPath]))?;
let decider1 = RequestRegexDecider::new("1", |regex, request, _state| {
if regex.is_match(request.path().inner()) {
Action::Discard
} else {
Action::Keep
}
});
let decider2 = RequestRegexDecider::new("2", |regex, request, _state| {
if regex.is_match(request.path().inner()) {
Action::Discard
} else {
Action::Keep
}
});
let scheduler = OrderedScheduler::new(state.clone())?;
let response_observer: ResponseObserver<BlockingResponse> = ResponseObserver::new();
let observers = build_observers!(response_observer);
let deciders = build_deciders!(decider1, decider2);
let mutators = build_mutators!(mutator);
let mut fuzzer = BlockingFuzzer::new()
.client(client)
.request(request)
.scheduler(scheduler)
.mutators(mutators)
.observers(observers)
.deciders(deciders)
.pre_send_logic(LogicOperation::And) .build();
fuzzer.fuzz_once(&mut state)?;
mock.assert_hits(1);
Ok(())
}
#[test]
fn pre_send_deciders_used_as_allow_list() -> Result<(), Box<dyn std::error::Error>> {
let srv = MockServer::start();
let mock = srv.mock(|when, then| {
when.method(GET).path_matches(Regex::new("[012]").unwrap());
then.status(200).body("this is a test");
});
let range = RangeCorpus::with_stop(3).name("range").build()?;
let mut state = SharedState::with_corpus(range);
let req_client = reqwest::blocking::Client::builder()
.timeout(Duration::from_secs(1))
.build()?;
let client = BlockingClient::with_client(req_client);
let mutator = ReplaceKeyword::new(&"FUZZ", "range");
let request = Request::from_url(&srv.url("/FUZZ"), Some(&[ShouldFuzz::URLPath]))?;
let decider1 = RequestRegexDecider::new("1", |regex, request, _state| {
if regex.is_match(request.path().inner()) {
Action::Keep
} else {
Action::Discard
}
});
let decider2 = RequestRegexDecider::new("2", |regex, request, _state| {
if regex.is_match(request.path().inner()) {
Action::Keep
} else {
Action::Discard
}
});
let scheduler = OrderedScheduler::new(state.clone())?;
let response_observer: ResponseObserver<BlockingResponse> = ResponseObserver::new();
let observers = build_observers!(response_observer);
let deciders = build_deciders!(decider1, decider2);
let mutators = build_mutators!(mutator);
let mut fuzzer = BlockingFuzzer::new()
.client(client)
.request(request)
.scheduler(scheduler)
.mutators(mutators)
.observers(observers)
.deciders(deciders)
.build();
fuzzer.fuzz_once(&mut state)?;
mock.assert_hits(2);
Ok(())
}
#[test]
fn post_send_deciders_used_as_denylist() -> Result<(), Box<dyn std::error::Error>> {
let srv = MockServer::start();
let mock_200s = srv.mock(|when, then| {
when.method(GET).path("/0");
then.status(200).body("this is a test");
});
let mock_401s = srv.mock(|when, then| {
when.method(GET).path("/1");
then.status(401).body("this is a test");
});
let mock_404s = srv.mock(|when, then| {
when.method(GET).path("/2");
then.status(404).body("this is a test");
});
let mock_tracked_side_effects = srv.mock(|when, then| {
when.method(GET).path("/side-effect");
then.status(301).body("this is a test");
});
let range = RangeCorpus::with_stop(3).name("range").build()?;
let mut state = SharedState::with_corpus(range);
let req_client = reqwest::blocking::Client::builder()
.timeout(Duration::from_secs(1))
.build()?;
let client = BlockingClient::with_client(req_client);
let side_effect_generator = client.clone();
let mutator = ReplaceKeyword::new(&"FUZZ", "range");
let request = Request::from_url(&srv.url("/FUZZ"), Some(&[ShouldFuzz::URLPath]))?;
let decider1 = StatusCodeDecider::new(401, |status, observed, _state| {
if status == observed {
Action::Discard
} else {
Action::Keep
}
});
let decider2 = StatusCodeDecider::new(404, |status, observed, _state| {
if status == observed {
Action::Discard
} else {
Action::Keep
}
});
let side_effect_url = srv.url("/side-effect");
let processor = ResponseProcessor::new(
move |observer: &ResponseObserver<BlockingResponse>, action, _state| {
if let Some(action) = action {
if matches!(action, Action::Discard) {
assert!([401, 404].contains(&observer.status_code()));
let req = Request::from_url(&side_effect_url, None).unwrap();
side_effect_generator.send(req).unwrap();
}
}
},
);
let scheduler = OrderedScheduler::new(state.clone())?;
let response_observer: ResponseObserver<BlockingResponse> = ResponseObserver::new();
let observers = build_observers!(response_observer);
let deciders = build_deciders!(decider1, decider2);
let mutators = build_mutators!(mutator);
let processors = build_processors!(processor);
let mut fuzzer = BlockingFuzzer::new()
.client(client)
.request(request)
.scheduler(scheduler)
.mutators(mutators)
.observers(observers)
.processors(processors)
.deciders(deciders)
.post_send_logic(LogicOperation::And) .build();
fuzzer.fuzz_once(&mut state)?;
mock_200s.assert_hits(1);
mock_401s.assert_hits(1);
mock_404s.assert_hits(1);
mock_tracked_side_effects.assert_hits(2);
Ok(())
}
#[test]
fn post_send_deciders_used_as_allow_list() -> Result<(), Box<dyn std::error::Error>> {
let srv = MockServer::start();
let mock_200s = srv.mock(|when, then| {
when.method(GET).path("/0");
then.status(200).body("this is a test");
});
let mock_403s = srv.mock(|when, then| {
when.method(GET).path("/1");
then.status(403).body("this is a test");
});
let mock_404s = srv.mock(|when, then| {
when.method(GET).path("/2");
then.status(404).body("this is a test");
});
let mock_tracked_side_effects = srv.mock(|when, then| {
when.method(GET).path("/side-effect");
then.status(301).body("this is a test");
});
let range = RangeCorpus::with_stop(3).name("range").build()?;
let mut state = SharedState::with_corpus(range);
let req_client = reqwest::blocking::Client::builder()
.timeout(Duration::from_secs(1))
.build()?;
let client = BlockingClient::with_client(req_client);
let side_effect_generator = client.clone();
let mutator = ReplaceKeyword::new(&"FUZZ", "range");
let request = Request::from_url(&srv.url("/FUZZ"), Some(&[ShouldFuzz::URLPath]))?;
let decider1 = StatusCodeDecider::new(200, |status, observed, _state| {
if status == observed {
Action::Keep
} else {
Action::Discard
}
});
let decider2 = StatusCodeDecider::new(403, |status, observed, _state| {
if status == observed {
Action::Keep
} else {
Action::Discard
}
});
let side_effect_url = srv.url("/side-effect");
let processor = ResponseProcessor::new(
move |observer: &ResponseObserver<BlockingResponse>, action, _state| {
if let Some(action) = action {
if matches!(action, Action::Keep) {
assert!([200, 403].contains(&observer.status_code()));
let req = Request::from_url(&side_effect_url, None).unwrap();
side_effect_generator.send(req).unwrap();
}
}
},
);
let scheduler = OrderedScheduler::new(state.clone())?;
let response_observer: ResponseObserver<BlockingResponse> = ResponseObserver::new();
let observers = build_observers!(response_observer);
let deciders = build_deciders!(decider1, decider2);
let mutators = build_mutators!(mutator);
let processors = build_processors!(processor);
let mut fuzzer = BlockingFuzzer::new()
.client(client)
.request(request)
.scheduler(scheduler)
.mutators(mutators)
.observers(observers)
.processors(processors)
.deciders(deciders)
.build();
fuzzer.fuzz_once(&mut state)?;
mock_200s.assert_hits(1);
mock_403s.assert_hits(1);
mock_404s.assert_hits(1);
mock_tracked_side_effects.assert_hits(2);
Ok(())
}
}