From 65ecb0f5c22621e268b7797cca527cb0a9243619 Mon Sep 17 00:00:00 2001 From: baerwang Date: Fri, 1 Mar 2024 13:45:26 +0800 Subject: [PATCH] feat: File size limits --- packages/hurl/src/cli/options/commands.rs | 10 ++++ packages/hurl/src/cli/options/matches.rs | 4 ++ packages/hurl/src/cli/options/mod.rs | 6 +++ packages/hurl/src/http/options.rs | 3 ++ packages/hurl/src/main.rs | 1 + packages/hurl/src/report/junit/mod.rs | 2 + packages/hurl/src/report/junit/testcase.rs | 2 + packages/hurl/src/runner/entry.rs | 6 +++ packages/hurl/src/runner/result.rs | 57 ++++++++++++---------- packages/hurl/src/runner/runner_options.rs | 10 ++++ 10 files changed, 76 insertions(+), 25 deletions(-) diff --git a/packages/hurl/src/cli/options/commands.rs b/packages/hurl/src/cli/options/commands.rs index 10feb5ad18e..e5401e816c3 100644 --- a/packages/hurl/src/cli/options/commands.rs +++ b/packages/hurl/src/cli/options/commands.rs @@ -284,6 +284,16 @@ pub fn max_time() -> clap::Arg { .num_args(1) } +pub fn max_filesize() -> clap::Arg { + clap::Arg::new("max_filesize") + .long("max-filesize") + .value_name("KB") + .default_value("0") + .value_parser(clap::value_parser!(u64)) + .help("File size limits") + .num_args(1) +} + pub fn netrc() -> clap::Arg { clap::Arg::new("netrc") .long("netrc") diff --git a/packages/hurl/src/cli/options/matches.rs b/packages/hurl/src/cli/options/matches.rs index 49dbb8e6c8a..ba64d226adf 100644 --- a/packages/hurl/src/cli/options/matches.rs +++ b/packages/hurl/src/cli/options/matches.rs @@ -426,6 +426,10 @@ pub fn very_verbose(arg_matches: &ArgMatches) -> bool { has_flag(arg_matches, "very_verbose") } +pub fn max_filesize(arg_matches: &ArgMatches) -> usize { + get::(arg_matches, "max_filesize").unwrap() as usize +} + /// Returns a list of path names from the command line options `matches`. fn glob_files(matches: &ArgMatches) -> Result, OptionsError> { let mut filenames = vec![]; diff --git a/packages/hurl/src/cli/options/mod.rs b/packages/hurl/src/cli/options/mod.rs index a531889fe8b..64929352f09 100644 --- a/packages/hurl/src/cli/options/mod.rs +++ b/packages/hurl/src/cli/options/mod.rs @@ -86,6 +86,7 @@ pub struct Options { pub variables: HashMap, pub verbose: bool, pub very_verbose: bool, + pub max_filesize: usize, } #[derive(Clone, Debug, PartialEq, Eq)] @@ -202,6 +203,7 @@ pub fn parse() -> Result { .arg(commands::json()) .arg(commands::max_redirects()) .arg(commands::max_time()) + .arg(commands::max_filesize()) .arg(commands::netrc()) .arg(commands::netrc_file()) .arg(commands::netrc_optional()) @@ -295,6 +297,7 @@ fn parse_matches(arg_matches: &ArgMatches) -> Result { let variables = matches::variables(arg_matches)?; let verbose = matches::verbose(arg_matches); let very_verbose = matches::very_verbose(arg_matches); + let max_filesize = matches::max_filesize(arg_matches); Ok(Options { aws_sigv4, cacert_file, @@ -345,6 +348,7 @@ fn parse_matches(arg_matches: &ArgMatches) -> Result { variables, verbose, very_verbose, + max_filesize, }) } @@ -448,6 +452,7 @@ impl Options { let unix_socket = self.unix_socket.clone(); let user = self.user.clone(); let user_agent = self.user_agent.clone(); + let max_filesize = self.max_filesize; RunnerOptionsBuilder::new() .aws_sigv4(aws_sigv4) @@ -486,6 +491,7 @@ impl Options { .unix_socket(unix_socket) .user(user) .user_agent(user_agent) + .max_filesize(max_filesize) .build() } diff --git a/packages/hurl/src/http/options.rs b/packages/hurl/src/http/options.rs index f57567a4922..c7b0eb9dedb 100644 --- a/packages/hurl/src/http/options.rs +++ b/packages/hurl/src/http/options.rs @@ -52,6 +52,7 @@ pub struct ClientOptions { pub user: Option, pub user_agent: Option, pub verbosity: Option, + pub max_filesize: usize, } // FIXME/ we could implement copy here @@ -92,6 +93,7 @@ impl Default for ClientOptions { user: None, user_agent: None, verbosity: None, + max_filesize: 0, } } } @@ -242,6 +244,7 @@ mod tests { user: Some("user:password".to_string()), user_agent: Some("my-useragent".to_string()), verbosity: None, + max_filesize: 0, } .curl_args(), [ diff --git a/packages/hurl/src/main.rs b/packages/hurl/src/main.rs index e6113aceec4..8f0b8ebe241 100644 --- a/packages/hurl/src/main.rs +++ b/packages/hurl/src/main.rs @@ -345,6 +345,7 @@ pub mod tests { errors: vec![], time_in_ms: 0, compressed: false, + max_filesize: 0, }; HurlRun { content: String::new(), diff --git a/packages/hurl/src/report/junit/mod.rs b/packages/hurl/src/report/junit/mod.rs index 5078c8a8e29..4b9602f37bf 100644 --- a/packages/hurl/src/report/junit/mod.rs +++ b/packages/hurl/src/report/junit/mod.rs @@ -166,6 +166,7 @@ mod tests { )], time_in_ms: 0, compressed: false, + max_filesize: 0, }], time_in_ms: 230, success: true, @@ -189,6 +190,7 @@ mod tests { )], time_in_ms: 0, compressed: false, + max_filesize: 0, }], time_in_ms: 230, success: true, diff --git a/packages/hurl/src/report/junit/testcase.rs b/packages/hurl/src/report/junit/testcase.rs index 19ba4c0b401..643bc4a0fe7 100644 --- a/packages/hurl/src/report/junit/testcase.rs +++ b/packages/hurl/src/report/junit/testcase.rs @@ -133,6 +133,7 @@ HTTP/1.0 200 )], time_in_ms: 0, compressed: false, + max_filesize: 0, }], time_in_ms: 230, success: true, @@ -172,6 +173,7 @@ HTTP/1.0 200 )], time_in_ms: 0, compressed: false, + max_filesize: 0, }], time_in_ms: 230, success: true, diff --git a/packages/hurl/src/runner/entry.rs b/packages/hurl/src/runner/entry.rs index f89ae36ae26..411c3483eea 100644 --- a/packages/hurl/src/runner/entry.rs +++ b/packages/hurl/src/runner/entry.rs @@ -117,6 +117,8 @@ pub fn run( // 3. finally, run the remaining asserts let mut asserts = vec![]; + let max_filesize = client_options.max_filesize; + if !runner_options.ignore_asserts { if let Some(response_spec) = &entry.response { let mut status_asserts = eval_version_status_asserts(response_spec, http_response); @@ -133,6 +135,7 @@ pub fn run( errors, time_in_ms, compressed, + max_filesize, }; } } @@ -152,6 +155,7 @@ pub fn run( errors: vec![e], time_in_ms, compressed, + max_filesize, }; } }, @@ -179,6 +183,7 @@ pub fn run( errors, time_in_ms, compressed, + max_filesize, } } @@ -230,6 +235,7 @@ impl ClientOptions { Some(Verbosity::VeryVerbose) => Some(http::Verbosity::VeryVerbose), _ => None, }, + max_filesize: runner_options.max_filesize, } } } diff --git a/packages/hurl/src/runner/result.rs b/packages/hurl/src/runner/result.rs index 3e469dd3743..60a43aa198c 100644 --- a/packages/hurl/src/runner/result.rs +++ b/packages/hurl/src/runner/result.rs @@ -75,6 +75,7 @@ pub struct EntryResult { // server is requested to send compressed response, and the response should be uncompressed // when outputted on stdout. pub compressed: bool, + pub max_filesize: usize, } impl Default for EntryResult { @@ -88,6 +89,7 @@ impl Default for EntryResult { errors: vec![], time_in_ms: 0, compressed: false, + max_filesize: 0, } } } @@ -139,34 +141,39 @@ impl EntryResult { context_dir: &ContextDir, source_info: SourceInfo, ) -> Result<(), Error> { - let Some(call) = self.calls.last() else { - return Ok(()); - }; - // We check file access authorization for file output when a context dir has been given - if let Output::File(filename) = output { - if !context_dir.is_access_allowed(filename) { - let inner = RunnerError::UnauthorizedFileAccess { - path: PathBuf::from(filename.clone()), - }; - return Err(Error::new(source_info, inner, false)); + if let Some(call) = self.calls.last() { + if let Output::File(filename) = output { + check_file_access(filename, context_dir, &source_info)?; } - } - let response = &call.response; - if self.compressed { - let bytes = match response.uncompress_body() { - Ok(bytes) => bytes, - Err(e) => { - // TODO: pass a [`SourceInfo`] in case of error - // We may pass a [`SourceInfo`] as a parameter of this method to make - // a more accurate error (for instance a [`SourceInfo`] pointing at - // `output: foo.bin` - let source_info = SourceInfo::new(Pos::new(0, 0), Pos::new(0, 0)); - return Err(Error::new(source_info, e.into(), false)); + + let response = &call.response; + let body = if self.compressed { + match response.uncompress_body() { + Ok(bytes) => bytes, + Err(e) => return Err(Error::new(source_info, e.into(), false)), } + } else { + response.body.clone() }; - output.write(&bytes, Some(context_dir)) - } else { - output.write(&response.body, Some(context_dir)) + + if body.len() > self.max_filesize { + output.write(&body, Some(context_dir))?; + } } + Ok(()) + } +} + +fn check_file_access( + filename: &str, + context_dir: &ContextDir, + source_info: &SourceInfo, +) -> Result<(), Error> { + if !context_dir.is_access_allowed(filename) { + let inner = RunnerError::UnauthorizedFileAccess { + path: PathBuf::from(filename), + }; + return Err(Error::new(source_info.clone(), inner, false)); } + Ok(()) } diff --git a/packages/hurl/src/runner/runner_options.rs b/packages/hurl/src/runner/runner_options.rs index a8e40eee4ac..4ccb2cf88ad 100644 --- a/packages/hurl/src/runner/runner_options.rs +++ b/packages/hurl/src/runner/runner_options.rs @@ -61,6 +61,7 @@ pub struct RunnerOptionsBuilder { unix_socket: Option, user: Option, user_agent: Option, + max_filesize: usize, } impl Default for RunnerOptionsBuilder { @@ -103,6 +104,7 @@ impl Default for RunnerOptionsBuilder { unix_socket: None, user: None, user_agent: None, + max_filesize: 0, } } } @@ -367,6 +369,12 @@ impl RunnerOptionsBuilder { self } + /// Set the file size limit + pub fn max_filesize(&mut self, max_filesize: usize) -> &mut Self { + self.max_filesize = max_filesize; + self + } + /// Create an instance of [`RunnerOptions`]. pub fn build(&self) -> RunnerOptions { RunnerOptions { @@ -407,6 +415,7 @@ impl RunnerOptionsBuilder { unix_socket: self.unix_socket.clone(), user: self.user.clone(), user_agent: self.user_agent.clone(), + max_filesize: self.max_filesize, } } } @@ -450,6 +459,7 @@ pub struct RunnerOptions { pub(crate) unix_socket: Option, pub(crate) user: Option, pub(crate) user_agent: Option, + pub(crate) max_filesize: usize, } impl Default for RunnerOptions {