use hurl_core::ast::{
Assert, Capture, Entry, FilterValue, PredicateFuncValue, Response, SourceInfo,
};
use hurl_core::types::Index;
use crate::http;
use crate::http::{ClientOptions, CurlCmd};
use crate::util::logger::{Logger, Verbosity};
use crate::util::term::WriteMode;
use super::cache::BodyCache;
use super::error::{RunnerError, RunnerErrorKind};
use super::request;
use super::response;
use super::result::{AssertResult, CaptureResult, EntryResult};
use super::runner_options::RunnerOptions;
use super::variable::VariableSet;
pub fn run(
entry: &Entry,
entry_index: Index,
http_client: &mut http::Client,
variables: &mut VariableSet,
runner_options: &RunnerOptions,
logger: &mut Logger,
) -> EntryResult {
let compressed = runner_options.compressed;
let source_info = entry.source_info();
let context_dir = &runner_options.context_dir;
if let Some(response_spec) = &entry.response {
let immediate_logs =
matches!(logger.stderr.mode(), WriteMode::Immediate) && logger.verbosity.is_some();
if immediate_logs {
let redacted = response_spec.captures().iter().find(|c| c.redacted);
if let Some(redacted) = redacted {
let source_info = redacted.name.source_info;
let error =
RunnerError::new(source_info, RunnerErrorKind::PossibleLoggedSecret, false);
return EntryResult {
entry_index,
source_info,
errors: vec![error],
compressed,
..Default::default()
};
}
}
}
let http_request = match request::eval_request(&entry.request, variables, context_dir) {
Ok(r) => r,
Err(error) => {
return EntryResult {
entry_index,
source_info,
errors: vec![error],
compressed,
..Default::default()
};
}
};
let client_options = ClientOptions::from(runner_options, logger.verbosity);
if let Some(s) = request::get_cmd_cookie_storage_set(&entry.request) {
if let Ok(cookie) = http::Cookie::from_netscape(&s) {
http_client.add_cookie(&cookie, logger);
} else {
logger.warning(&format!("Cookie string can not be parsed: '{s}'"));
}
}
if request::get_cmd_cookie_storage_clear(&entry.request) {
http_client.clear_cookie_storage(logger);
}
let curl_cmd = http_client.curl_command_line(
&http_request,
context_dir,
runner_options.output.as_ref(),
&client_options,
logger,
);
log_request(http_client, &curl_cmd, &http_request, logger);
let calls = match http_client.execute_with_redirect(&http_request, &client_options, logger) {
Ok(calls) => calls,
Err(http_error) => {
let start = entry.request.url.source_info.start;
let end = entry.request.url.source_info.end;
let error_source_info = SourceInfo::new(start, end);
let error =
RunnerError::new(error_source_info, RunnerErrorKind::Http(http_error), false);
return EntryResult {
entry_index,
source_info,
errors: vec![error],
compressed,
curl_cmd,
..Default::default()
};
}
};
let responses = calls.iter().map(|c| &c.response).collect::<Vec<_>>();
let http_response = responses.last().unwrap();
let transfer_duration = calls.iter().map(|call| call.timings.total).sum();
let mut cache = BodyCache::new();
let mut asserts = vec![];
if !runner_options.no_assert
&& let Some(response_spec) = &entry.response
{
let mut status_asserts =
response::eval_version_status_asserts(response_spec, http_response);
let errors = asserts_to_errors(&status_asserts);
asserts.append(&mut status_asserts);
if !errors.is_empty() {
logger.debug("");
return EntryResult {
entry_index,
source_info,
calls,
captures: vec![],
asserts,
errors,
transfer_duration,
compressed,
curl_cmd,
};
}
};
let captures = match &entry.response {
None => vec![],
Some(response_spec) => {
match response::eval_captures(response_spec, &responses, &mut cache, variables) {
Ok(captures) => captures,
Err(e) => {
return EntryResult {
entry_index,
source_info,
calls,
captures: vec![],
asserts,
errors: vec![e],
transfer_duration,
compressed,
curl_cmd,
};
}
}
}
};
logger.set_secrets(variables.secrets());
log_captures(&captures, logger);
logger.debug("");
if !runner_options.no_assert
&& let Some(response_spec) = &entry.response
{
warn_deprecated(response_spec, logger);
let mut other_asserts = response::eval_asserts(
response_spec,
variables,
&responses,
&mut cache,
context_dir,
);
asserts.append(&mut other_asserts);
};
let errors = asserts_to_errors(&asserts);
EntryResult {
entry_index,
source_info,
calls,
captures,
asserts,
errors,
transfer_duration,
compressed,
curl_cmd,
}
}
fn asserts_to_errors(asserts: &[AssertResult]) -> Vec<RunnerError> {
asserts
.iter()
.filter_map(|assert| assert.to_runner_error())
.map(
|RunnerError {
source_info,
kind: inner,
..
}| RunnerError::new(source_info, inner, true),
)
.collect()
}
impl ClientOptions {
fn from(runner_options: &RunnerOptions, verbosity: Option<Verbosity>) -> Self {
ClientOptions {
allow_reuse: runner_options.allow_reuse,
aws_sigv4: runner_options.aws_sigv4.clone(),
cacert_file: runner_options.cacert_file.clone(),
client_cert_file: runner_options.client_cert_file.clone(),
client_key_file: runner_options.client_key_file.clone(),
compressed: runner_options.compressed,
connect_timeout: runner_options.connect_timeout,
connects_to: runner_options.connects_to.clone(),
cookie_input_file: runner_options.cookie_input_file.clone(),
digest: runner_options.digest,
follow_location: runner_options.follow_location,
headers: runner_options.headers.clone(),
http_version: runner_options.http_version,
ip_resolve: runner_options.ip_resolve,
max_filesize: runner_options.max_filesize,
max_recv_speed: runner_options.max_recv_speed,
max_redirect: runner_options.max_redirect,
max_send_speed: runner_options.max_send_speed,
negotiate: runner_options.negotiate,
netrc: runner_options.netrc,
netrc_file: runner_options.netrc_file.clone(),
netrc_optional: runner_options.netrc_optional,
ntlm: runner_options.ntlm,
path_as_is: runner_options.path_as_is,
pinned_pub_key: runner_options.pinned_pub_key.clone(),
proxy: runner_options.proxy.clone(),
no_proxy: runner_options.no_proxy.clone(),
insecure: runner_options.insecure,
resolves: runner_options.resolves.clone(),
ssl_no_revoke: runner_options.ssl_no_revoke,
timeout: runner_options.timeout,
unix_socket: runner_options.unix_socket.clone(),
use_cookie_store: runner_options.use_cookie_store,
user: runner_options.user.clone(),
user_agent: runner_options.user_agent.clone(),
verbosity: verbosity.map(|v| match v {
Verbosity::LowVerbose => http::Verbosity::LowVerbose,
Verbosity::Verbose => http::Verbosity::Verbose,
Verbosity::VeryVerbose => http::Verbosity::VeryVerbose,
}),
}
}
}
fn log_request(
http_client: &mut http::Client,
curl_cmd: &CurlCmd,
request: &http::RequestSpec,
logger: &mut Logger,
) {
let cookie_store = http_client.cookie_store(logger);
logger.debug("");
logger.debug_important("Cookie store:");
for cookie in cookie_store.cookies() {
logger.debug(&cookie.to_string());
}
logger.debug("");
logger.debug_important("Request:");
logger.debug(&format!("{} {}", request.method, request.url.raw()));
for header in &request.headers {
logger.debug(&header.to_string());
}
if !request.querystring.is_empty() {
logger.debug("[Query]");
for param in &request.querystring {
logger.debug(¶m.to_string());
}
}
if !request.form.is_empty() {
logger.debug("[Form]");
for param in &request.form {
logger.debug(¶m.to_string());
}
}
if !request.multipart.is_empty() {
logger.debug("[Multipart]");
for param in &request.multipart {
logger.debug(¶m.to_string());
}
}
if !request.cookies.is_empty() {
logger.debug("[Cookies]");
for cookie in &request.cookies {
logger.debug(&cookie.to_string());
}
}
logger.debug("");
logger.debug("Request can be run with the following curl command:");
logger.debug(&curl_cmd.to_string());
logger.debug("");
}
fn log_captures(captures: &[CaptureResult], logger: &mut Logger) {
if captures.is_empty() {
return;
}
logger.debug_important("Captures:");
for c in captures.iter() {
logger.capture(&c.name, &c.value);
}
}
fn warn_deprecated(response_spec: &Response, logger: &mut Logger) {
if response_spec.asserts().iter().any(|a| {
matches!(
&a.predicate.predicate_func.value,
PredicateFuncValue::Include { .. }
)
}) {
logger.warning("<includes> predicate is now deprecated in favor of <contains> predicate");
}
if response_spec
.captures()
.iter()
.any(captures_has_format_filter)
{
logger.warning(
"<format> filter in captures is now deprecated in favor of <dateFormat> filter",
);
}
if response_spec
.captures()
.iter()
.any(captures_has_decode_filter)
{
logger.warning(
"<decode> filter in captures is now deprecated in favor of <charsetDecode> filter",
);
}
if response_spec
.asserts()
.iter()
.any(asserts_has_format_filter)
{
logger.warning(
"<format> filter in asserts is now deprecated in favor of <dateFormat> filter",
);
}
if response_spec
.asserts()
.iter()
.any(asserts_has_decode_filter)
{
logger.warning(
"<decode> filter in asserts is now deprecated in favor of <charsetDecode> filter",
);
}
}
fn asserts_has_format_filter(a: &Assert) -> bool {
a.filters
.iter()
.any(|(_, f)| matches!(f.value, FilterValue::Format { .. }))
}
fn captures_has_format_filter(a: &Capture) -> bool {
a.filters
.iter()
.any(|(_, f)| matches!(f.value, FilterValue::Format { .. }))
}
fn asserts_has_decode_filter(a: &Assert) -> bool {
a.filters
.iter()
.any(|(_, f)| matches!(f.value, FilterValue::Decode { .. }))
}
fn captures_has_decode_filter(a: &Capture) -> bool {
a.filters
.iter()
.any(|(_, f)| matches!(f.value, FilterValue::Decode { .. }))
}