1use hurl_core::ast::{Entry, PredicateFuncValue, Response, SourceInfo};
19
20use crate::http;
21use crate::http::{ClientOptions, CurlCmd};
22use crate::runner::cache::BodyCache;
23use crate::runner::error::RunnerError;
24use crate::runner::result::{AssertResult, EntryResult};
25use crate::runner::runner_options::RunnerOptions;
26use crate::runner::{request, response, CaptureResult, RunnerErrorKind, VariableSet};
27use crate::util::logger::{Logger, Verbosity};
28use crate::util::term::WriteMode;
29
30pub fn run(
37 entry: &Entry,
38 entry_index: usize,
39 http_client: &mut http::Client,
40 variables: &mut VariableSet,
41 runner_options: &RunnerOptions,
42 logger: &mut Logger,
43) -> EntryResult {
44 let compressed = runner_options.compressed;
45 let source_info = entry.source_info();
46 let context_dir = &runner_options.context_dir;
47
48 if let Some(response_spec) = &entry.response {
52 let immediate_logs =
53 matches!(logger.stderr.mode(), WriteMode::Immediate) && logger.verbosity.is_some();
54 if immediate_logs {
55 let redacted = response_spec.captures().iter().find(|c| c.redacted);
56 if let Some(redacted) = redacted {
57 let source_info = redacted.name.source_info;
58 let error =
59 RunnerError::new(source_info, RunnerErrorKind::PossibleLoggedSecret, false);
60 return EntryResult {
61 entry_index,
62 source_info,
63 errors: vec![error],
64 compressed,
65 ..Default::default()
66 };
67 }
68 }
69 }
70
71 let http_request = match request::eval_request(&entry.request, variables, context_dir) {
73 Ok(r) => r,
74 Err(error) => {
75 return EntryResult {
76 entry_index,
77 source_info,
78 errors: vec![error],
79 compressed,
80 ..Default::default()
81 };
82 }
83 };
84
85 let client_options = ClientOptions::from(runner_options, logger.verbosity);
86
87 use std::str::FromStr;
89 if let Some(s) = request::cookie_storage_set(&entry.request) {
90 if let Ok(cookie) = http::Cookie::from_str(s.as_str()) {
91 http_client.add_cookie(&cookie, logger);
92 } else {
93 logger.warning(&format!("Cookie string can not be parsed: '{s}'"));
94 }
95 }
96 if request::cookie_storage_clear(&entry.request) {
97 http_client.clear_cookie_storage(logger);
98 }
99
100 let curl_cmd = http_client.curl_command_line(
101 &http_request,
102 context_dir,
103 runner_options.output.as_ref(),
104 &client_options,
105 logger,
106 );
107
108 log_request(http_client, &curl_cmd, &http_request, logger);
109
110 let calls = match http_client.execute_with_redirect(&http_request, &client_options, logger) {
112 Ok(calls) => calls,
113 Err(http_error) => {
114 let start = entry.request.url.source_info.start;
115 let end = entry.request.url.source_info.end;
116 let error_source_info = SourceInfo::new(start, end);
117 let error =
118 RunnerError::new(error_source_info, RunnerErrorKind::Http(http_error), false);
119 return EntryResult {
120 entry_index,
121 source_info,
122 errors: vec![error],
123 compressed,
124 curl_cmd,
125 ..Default::default()
126 };
127 }
128 };
129
130 let responses = calls.iter().map(|c| &c.response).collect::<Vec<_>>();
132 let http_response = responses.last().unwrap();
133
134 let transfer_duration = calls.iter().map(|call| call.timings.total).sum();
136
137 let mut cache = BodyCache::new();
142 let mut asserts = vec![];
143
144 if !runner_options.ignore_asserts {
145 if let Some(response_spec) = &entry.response {
146 let mut status_asserts =
147 response::eval_version_status_asserts(response_spec, http_response);
148 let errors = asserts_to_errors(&status_asserts);
149 asserts.append(&mut status_asserts);
150 if !errors.is_empty() {
151 logger.debug("");
152 return EntryResult {
153 entry_index,
154 source_info,
155 calls,
156 captures: vec![],
157 asserts,
158 errors,
159 transfer_duration,
160 compressed,
161 curl_cmd,
162 };
163 }
164 }
165 };
166
167 let captures = match &entry.response {
168 None => vec![],
169 Some(response_spec) => {
170 match response::eval_captures(response_spec, &responses, &mut cache, variables) {
171 Ok(captures) => captures,
172 Err(e) => {
173 return EntryResult {
174 entry_index,
175 source_info,
176 calls,
177 captures: vec![],
178 asserts,
179 errors: vec![e],
180 transfer_duration,
181 compressed,
182 curl_cmd,
183 };
184 }
185 }
186 }
187 };
188
189 logger.set_secrets(variables.secrets());
192
193 log_captures(&captures, logger);
194 logger.debug("");
195
196 if !runner_options.ignore_asserts {
198 if let Some(response_spec) = &entry.response {
199 warn_deprecated(response_spec, logger);
200 let mut other_asserts = response::eval_asserts(
201 response_spec,
202 variables,
203 &responses,
204 &mut cache,
205 context_dir,
206 );
207 asserts.append(&mut other_asserts);
208 }
209 };
210
211 let errors = asserts_to_errors(&asserts);
212
213 EntryResult {
214 entry_index,
215 source_info,
216 calls,
217 captures,
218 asserts,
219 errors,
220 transfer_duration,
221 compressed,
222 curl_cmd,
223 }
224}
225
226fn asserts_to_errors(asserts: &[AssertResult]) -> Vec<RunnerError> {
228 asserts
229 .iter()
230 .filter_map(|assert| assert.to_runner_error())
231 .map(
232 |RunnerError {
233 source_info,
234 kind: inner,
235 ..
236 }| RunnerError::new(source_info, inner, true),
237 )
238 .collect()
239}
240
241impl ClientOptions {
242 fn from(runner_options: &RunnerOptions, verbosity: Option<Verbosity>) -> Self {
243 ClientOptions {
244 allow_reuse: runner_options.allow_reuse,
245 aws_sigv4: runner_options.aws_sigv4.clone(),
246 cacert_file: runner_options.cacert_file.clone(),
247 client_cert_file: runner_options.client_cert_file.clone(),
248 client_key_file: runner_options.client_key_file.clone(),
249 compressed: runner_options.compressed,
250 connect_timeout: runner_options.connect_timeout,
251 connects_to: runner_options.connects_to.clone(),
252 cookie_input_file: runner_options.cookie_input_file.clone(),
253 follow_location: runner_options.follow_location,
254 follow_location_trusted: runner_options.follow_location_trusted,
255 headers: runner_options.headers.clone(),
256 http_version: runner_options.http_version,
257 ip_resolve: runner_options.ip_resolve,
258 max_filesize: runner_options.max_filesize,
259 max_recv_speed: runner_options.max_recv_speed,
260 max_redirect: runner_options.max_redirect,
261 max_send_speed: runner_options.max_send_speed,
262 negotiate: runner_options.negotiate,
263 netrc: runner_options.netrc,
264 netrc_file: runner_options.netrc_file.clone(),
265 netrc_optional: runner_options.netrc_optional,
266 ntlm: runner_options.ntlm,
267 path_as_is: runner_options.path_as_is,
268 pinned_pub_key: runner_options.pinned_pub_key.clone(),
269 proxy: runner_options.proxy.clone(),
270 no_proxy: runner_options.no_proxy.clone(),
271 insecure: runner_options.insecure,
272 resolves: runner_options.resolves.clone(),
273 ssl_no_revoke: runner_options.ssl_no_revoke,
274 timeout: runner_options.timeout,
275 unix_socket: runner_options.unix_socket.clone(),
276 user: runner_options.user.clone(),
277 user_agent: runner_options.user_agent.clone(),
278 verbosity: match verbosity {
279 Some(Verbosity::Verbose) => Some(http::Verbosity::Verbose),
280 Some(Verbosity::VeryVerbose) => Some(http::Verbosity::VeryVerbose),
281 _ => None,
282 },
283 }
284 }
285}
286
287fn log_request(
289 http_client: &mut http::Client,
290 curl_cmd: &CurlCmd,
291 request: &http::RequestSpec,
292 logger: &mut Logger,
293) {
294 logger.debug("");
295 logger.debug_important("Cookie store:");
296 for cookie in &http_client.cookie_storage(logger) {
297 logger.debug(&cookie.to_string());
298 }
299
300 logger.debug("");
301 logger.debug_important("Request:");
302 logger.debug(&format!("{} {}", request.method, request.url.raw()));
303 for header in &request.headers {
304 logger.debug(&header.to_string());
305 }
306 if !request.querystring.is_empty() {
307 logger.debug("[QueryStringParams]");
308 for param in &request.querystring {
309 logger.debug(¶m.to_string());
310 }
311 }
312 if !request.form.is_empty() {
313 logger.debug("[FormParams]");
314 for param in &request.form {
315 logger.debug(¶m.to_string());
316 }
317 }
318 if !request.multipart.is_empty() {
319 logger.debug("[MultipartFormData]");
320 for param in &request.multipart {
321 logger.debug(¶m.to_string());
322 }
323 }
324 if !request.cookies.is_empty() {
325 logger.debug("[Cookies]");
326 for cookie in &request.cookies {
327 logger.debug(&cookie.to_string());
328 }
329 }
330 logger.debug("");
331 logger.debug("Request can be run with the following curl command:");
332 logger.debug(&curl_cmd.to_string());
333 logger.debug("");
334}
335
336fn log_captures(captures: &[CaptureResult], logger: &mut Logger) {
338 if captures.is_empty() {
339 return;
340 }
341 logger.debug_important("Captures:");
342 for c in captures.iter() {
343 logger.capture(&c.name, &c.value);
344 }
345}
346
347fn warn_deprecated(response_spec: &Response, logger: &mut Logger) {
349 if response_spec.asserts().iter().any(|a| {
350 matches!(
351 &a.predicate.predicate_func.value,
352 PredicateFuncValue::Include { .. }
353 )
354 }) {
355 logger.warning("<includes> predicate is now deprecated in favor of <contains> predicate");
356 }
357}