mirror of
https://github.com/Orange-OpenSource/hurl.git
synced 2024-11-22 15:42:20 +03:00
Move curl command construction in its own struct.
This commit is contained in:
parent
9774c8c450
commit
7fc2d56aa7
@ -31,6 +31,7 @@ use hurl_core::typing::Count;
|
||||
|
||||
use crate::http::certificate::Certificate;
|
||||
use crate::http::core::*;
|
||||
use crate::http::curl_cmd::CurlCmd;
|
||||
use crate::http::debug::log_body;
|
||||
use crate::http::header::{
|
||||
HeaderVec, ACCEPT_ENCODING, AUTHORIZATION, CONTENT_TYPE, EXPECT, LOCATION, USER_AGENT,
|
||||
@ -762,43 +763,9 @@ impl Client {
|
||||
output: Option<&Output>,
|
||||
options: &ClientOptions,
|
||||
) -> String {
|
||||
let mut arguments = vec!["curl".to_string()];
|
||||
arguments.append(&mut request_spec.curl_args(context_dir));
|
||||
|
||||
// We extract the last part of the arguments (the url) to insert it
|
||||
// after all the options
|
||||
let url = arguments.pop().unwrap();
|
||||
|
||||
let cookies = all_cookies(&self.cookie_storage(), request_spec);
|
||||
if !cookies.is_empty() {
|
||||
arguments.push("--cookie".to_string());
|
||||
arguments.push(format!(
|
||||
"'{}'",
|
||||
cookies
|
||||
.iter()
|
||||
.map(|c| c.to_string())
|
||||
.collect::<Vec<String>>()
|
||||
.join("; ")
|
||||
));
|
||||
}
|
||||
arguments.append(&mut options.curl_args());
|
||||
|
||||
// --output is not an option of the HTTP client, we deal with it here:
|
||||
match output {
|
||||
Some(Output::File(filename)) => {
|
||||
let filename = context_dir.resolved_path(filename);
|
||||
arguments.push("--output".to_string());
|
||||
arguments.push(filename.to_string_lossy().to_string());
|
||||
}
|
||||
Some(Output::Stdout) => {
|
||||
arguments.push("--output".to_string());
|
||||
arguments.push("-".to_string());
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
|
||||
arguments.push(url);
|
||||
arguments.join(" ")
|
||||
let cookies = self.cookie_storage();
|
||||
let cmd = CurlCmd::new(request_spec, &cookies, context_dir, output, options);
|
||||
cmd.to_string()
|
||||
}
|
||||
|
||||
/// Returns the SSL certificates information associated to this call.
|
||||
|
947
packages/hurl/src/http/curl_cmd.rs
Normal file
947
packages/hurl/src/http/curl_cmd.rs
Normal file
@ -0,0 +1,947 @@
|
||||
/*
|
||||
* Hurl (https://hurl.dev)
|
||||
* Copyright (C) 2024 Orange
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
use crate::http::client::all_cookies;
|
||||
use crate::http::{
|
||||
Body, ClientOptions, Cookie, FileParam, Header, IpResolve, Method, MultipartParam, Param,
|
||||
RequestSpec, RequestedHttpVersion, CONTENT_TYPE,
|
||||
};
|
||||
use crate::runner::Output;
|
||||
use crate::util::path::ContextDir;
|
||||
use core::fmt;
|
||||
use hurl_core::typing::Count;
|
||||
use std::collections::HashMap;
|
||||
use std::path::Path;
|
||||
|
||||
/// Represents a curl command, with arguments.
|
||||
pub struct CurlCmd {
|
||||
/// The args of this command.
|
||||
args: Vec<String>,
|
||||
}
|
||||
|
||||
impl fmt::Display for CurlCmd {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "{}", self.args.join(" "))
|
||||
}
|
||||
}
|
||||
|
||||
impl CurlCmd {
|
||||
/// Creates a new curl command, based on an HTTP request, cookies, a context directory, output
|
||||
/// and runner options.
|
||||
pub fn new(
|
||||
request_spec: &RequestSpec,
|
||||
cookies: &[Cookie],
|
||||
context_dir: &ContextDir,
|
||||
output: Option<&Output>,
|
||||
options: &ClientOptions,
|
||||
) -> Self {
|
||||
let mut args = vec!["curl".to_string()];
|
||||
|
||||
let mut params = method_params(request_spec);
|
||||
args.append(&mut params);
|
||||
|
||||
let mut params = headers_params(request_spec);
|
||||
args.append(&mut params);
|
||||
|
||||
let mut params = body_params(request_spec, context_dir);
|
||||
args.append(&mut params);
|
||||
|
||||
let mut params = cookies_params(request_spec, cookies);
|
||||
args.append(&mut params);
|
||||
|
||||
let mut params = other_options_params(context_dir, output, options);
|
||||
args.append(&mut params);
|
||||
|
||||
let mut params = url_param(request_spec);
|
||||
args.append(&mut params);
|
||||
|
||||
CurlCmd { args }
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the curl args corresponding to the HTTP method, from a request spec.
|
||||
fn method_params(request_spec: &RequestSpec) -> Vec<String> {
|
||||
let has_body = !request_spec.multipart.is_empty()
|
||||
|| !request_spec.form.is_empty()
|
||||
|| !request_spec.body.bytes().is_empty();
|
||||
request_spec.method.curl_args(has_body)
|
||||
}
|
||||
|
||||
/// Returns the curl args corresponding to the HTTP headers, from a request spec.
|
||||
fn headers_params(request_spec: &RequestSpec) -> Vec<String> {
|
||||
let mut args = vec![];
|
||||
|
||||
for header in request_spec.headers.iter() {
|
||||
args.append(&mut header.curl_args());
|
||||
}
|
||||
|
||||
let has_explicit_content_type = request_spec.headers.contains_key(CONTENT_TYPE);
|
||||
if has_explicit_content_type {
|
||||
return args;
|
||||
}
|
||||
|
||||
if let Some(content_type) = &request_spec.implicit_content_type {
|
||||
if content_type != "application/x-www-form-urlencoded"
|
||||
&& content_type != "multipart/form-data"
|
||||
{
|
||||
args.push("--header".to_string());
|
||||
args.push(format!("'{}: {content_type}'", CONTENT_TYPE));
|
||||
}
|
||||
} else if !request_spec.body.bytes().is_empty() {
|
||||
match request_spec.body {
|
||||
Body::Text(_) => {
|
||||
args.push("--header".to_string());
|
||||
args.push(format!("'{}:'", CONTENT_TYPE));
|
||||
}
|
||||
Body::Binary(_) => {
|
||||
args.push("--header".to_string());
|
||||
args.push(format!("'{}: application/octet-stream'", CONTENT_TYPE));
|
||||
}
|
||||
Body::File(_, _) => {
|
||||
args.push("--header".to_string());
|
||||
args.push(format!("'{}:'", CONTENT_TYPE));
|
||||
}
|
||||
}
|
||||
}
|
||||
args
|
||||
}
|
||||
|
||||
/// Returns the curl args corresponding to the request body, from a request spec.
|
||||
fn body_params(request_spec: &RequestSpec, context_dir: &ContextDir) -> Vec<String> {
|
||||
let mut args = vec![];
|
||||
|
||||
for param in request_spec.form.iter() {
|
||||
args.push("--data".to_string());
|
||||
args.push(format!("'{}'", param.curl_arg_escape()));
|
||||
}
|
||||
for param in request_spec.multipart.iter() {
|
||||
args.push("--form".to_string());
|
||||
args.push(format!("'{}'", param.curl_arg(context_dir)));
|
||||
}
|
||||
|
||||
if request_spec.body.bytes().is_empty() {
|
||||
return args;
|
||||
}
|
||||
|
||||
// See <https://curl.se/docs/manpage.html#-d> and <https://curl.se/docs/manpage.html#--data-binary>:
|
||||
//
|
||||
// > -d, --data <data>
|
||||
// > ...
|
||||
// > If you start the data with the letter @, the rest should be a file name to read the
|
||||
// > data from, or - if you want curl to read the data from stdin. Posting data from a
|
||||
// > file named 'foobar' would thus be done with -d, --data @foobar. When -d, --data is
|
||||
// > told to read from a file like that, carriage returns and newlines will be stripped
|
||||
// > out. If you do not want the @ character to have a special interpretation use
|
||||
// > --data-raw instead.
|
||||
// > ...
|
||||
// > --data-binary <data>
|
||||
// >
|
||||
// > (HTTP) This posts data exactly as specified with no extra processing whatsoever.
|
||||
//
|
||||
// In summary: if the payload is a file (@foo.bin), we must use --data-binary option in
|
||||
// order to curl to not process the data sent.
|
||||
let param = match request_spec.body {
|
||||
Body::File(_, _) => "--data-binary",
|
||||
_ => "--data",
|
||||
};
|
||||
args.push(param.to_string());
|
||||
args.push(request_spec.body.curl_arg(context_dir));
|
||||
|
||||
args
|
||||
}
|
||||
|
||||
/// Returns the curl args corresponding to a list of cookies.
|
||||
fn cookies_params(request_spec: &RequestSpec, cookies: &[Cookie]) -> Vec<String> {
|
||||
let mut args = vec![];
|
||||
|
||||
let cookies = all_cookies(cookies, request_spec);
|
||||
if !cookies.is_empty() {
|
||||
args.push("--cookie".to_string());
|
||||
args.push(format!(
|
||||
"'{}'",
|
||||
cookies
|
||||
.iter()
|
||||
.map(|c| c.to_string())
|
||||
.collect::<Vec<String>>()
|
||||
.join("; ")
|
||||
));
|
||||
}
|
||||
args
|
||||
}
|
||||
|
||||
/// Returns the curl args corresponding to run options.
|
||||
fn other_options_params(
|
||||
context_dir: &ContextDir,
|
||||
output: Option<&Output>,
|
||||
options: &ClientOptions,
|
||||
) -> Vec<String> {
|
||||
let mut args = options.curl_args();
|
||||
|
||||
// --output is not an option of the HTTP client, we deal with it here:
|
||||
match output {
|
||||
Some(Output::File(filename)) => {
|
||||
let filename = context_dir.resolved_path(filename);
|
||||
args.push("--output".to_string());
|
||||
args.push(filename.to_string_lossy().to_string());
|
||||
}
|
||||
Some(Output::Stdout) => {
|
||||
args.push("--output".to_string());
|
||||
args.push("-".to_string());
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
args
|
||||
}
|
||||
|
||||
/// Returns the curl args corresponding to the URL, from a request spec.
|
||||
fn url_param(request_spec: &RequestSpec) -> Vec<String> {
|
||||
let mut args = vec![];
|
||||
|
||||
let querystring = if request_spec.querystring.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
let params = request_spec
|
||||
.querystring
|
||||
.iter()
|
||||
.map(|p| p.curl_arg_escape())
|
||||
.collect::<Vec<String>>();
|
||||
params.join("&")
|
||||
};
|
||||
let url = if querystring.as_str() == "" {
|
||||
request_spec.url.raw()
|
||||
} else if request_spec.url.raw().contains('?') {
|
||||
format!("{}&{}", request_spec.url.raw(), querystring)
|
||||
} else {
|
||||
format!("{}?{}", request_spec.url.raw(), querystring)
|
||||
};
|
||||
let url = format!("'{url}'");
|
||||
|
||||
// curl support "globbing" <https://everything.curl.dev/cmdline/urls/globbing.html>
|
||||
// {,},[,] have special meaning to curl, in order to support templating.
|
||||
// We have two options:
|
||||
// - either we encode {,},[,] to %7b,%7d,%5b,%%5d
|
||||
// - or we let the url "as-it" and use curl [`--globoff`](https://curl.se/docs/manpage.html#-g) option.
|
||||
// We're going with the second one!
|
||||
if url.contains('{') || url.contains('}') || url.contains('[') || url.contains(']') {
|
||||
args.push("--globoff".to_string());
|
||||
}
|
||||
args.push(url);
|
||||
args
|
||||
}
|
||||
|
||||
fn encode_byte(b: u8) -> String {
|
||||
format!("\\x{b:02x}")
|
||||
}
|
||||
|
||||
/// Encode bytes to a shell string.
|
||||
fn encode_bytes(bytes: &[u8]) -> String {
|
||||
bytes.iter().map(|b| encode_byte(*b)).collect()
|
||||
}
|
||||
|
||||
impl Method {
|
||||
/// Returns the curl args for HTTP method, given the request has a body or not.
|
||||
fn curl_args(&self, has_body: bool) -> Vec<String> {
|
||||
match self.0.as_str() {
|
||||
"GET" => {
|
||||
if has_body {
|
||||
vec!["--request".to_string(), "GET".to_string()]
|
||||
} else {
|
||||
vec![]
|
||||
}
|
||||
}
|
||||
"HEAD" => vec!["--head".to_string()],
|
||||
"POST" => {
|
||||
if has_body {
|
||||
vec![]
|
||||
} else {
|
||||
vec!["--request".to_string(), "POST".to_string()]
|
||||
}
|
||||
}
|
||||
s => vec!["--request".to_string(), s.to_string()],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Header {
|
||||
fn curl_args(&self) -> Vec<String> {
|
||||
let name = &self.name;
|
||||
let value = &self.value;
|
||||
vec![
|
||||
"--header".to_string(),
|
||||
encode_shell_string(&format!("{name}: {value}")),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
impl Param {
|
||||
fn curl_arg_escape(&self) -> String {
|
||||
let name = &self.name;
|
||||
let value = escape_url(&self.value);
|
||||
format!("{name}={value}")
|
||||
}
|
||||
|
||||
fn curl_arg(&self) -> String {
|
||||
let name = &self.name;
|
||||
let value = &self.value;
|
||||
format!("{name}={value}")
|
||||
}
|
||||
}
|
||||
|
||||
impl MultipartParam {
|
||||
fn curl_arg(&self, context_dir: &ContextDir) -> String {
|
||||
match self {
|
||||
MultipartParam::Param(param) => param.curl_arg(),
|
||||
MultipartParam::FileParam(FileParam {
|
||||
name,
|
||||
filename,
|
||||
content_type,
|
||||
..
|
||||
}) => {
|
||||
let path = context_dir.resolved_path(Path::new(filename));
|
||||
let value = format!("@{};type={}", path.to_string_lossy(), content_type);
|
||||
format!("{name}={value}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Body {
|
||||
fn curl_arg(&self, context_dir: &ContextDir) -> String {
|
||||
match self {
|
||||
Body::Text(s) => encode_shell_string(s),
|
||||
Body::Binary(bytes) => format!("$'{}'", encode_bytes(bytes)),
|
||||
Body::File(_, filename) => {
|
||||
let path = context_dir.resolved_path(Path::new(filename));
|
||||
format!("'@{}'", path.to_string_lossy())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ClientOptions {
|
||||
/// Returns the list of options for the curl command line equivalent to this [`ClientOptions`].
|
||||
fn curl_args(&self) -> Vec<String> {
|
||||
let mut arguments = vec![];
|
||||
|
||||
if let Some(ref aws_sigv4) = self.aws_sigv4 {
|
||||
arguments.push("--aws-sigv4".to_string());
|
||||
arguments.push(aws_sigv4.clone());
|
||||
}
|
||||
if let Some(ref cacert_file) = self.cacert_file {
|
||||
arguments.push("--cacert".to_string());
|
||||
arguments.push(cacert_file.clone());
|
||||
}
|
||||
if let Some(ref client_cert_file) = self.client_cert_file {
|
||||
arguments.push("--cert".to_string());
|
||||
arguments.push(client_cert_file.clone());
|
||||
}
|
||||
if let Some(ref client_key_file) = self.client_key_file {
|
||||
arguments.push("--key".to_string());
|
||||
arguments.push(client_key_file.clone());
|
||||
}
|
||||
if self.compressed {
|
||||
arguments.push("--compressed".to_string());
|
||||
}
|
||||
if self.connect_timeout != ClientOptions::default().connect_timeout {
|
||||
arguments.push("--connect-timeout".to_string());
|
||||
arguments.push(self.connect_timeout.as_secs().to_string());
|
||||
}
|
||||
for connect in self.connects_to.iter() {
|
||||
arguments.push("--connect-to".to_string());
|
||||
arguments.push(connect.clone());
|
||||
}
|
||||
if let Some(ref cookie_file) = self.cookie_input_file {
|
||||
arguments.push("--cookie".to_string());
|
||||
arguments.push(cookie_file.clone());
|
||||
}
|
||||
match self.http_version {
|
||||
RequestedHttpVersion::Default => {}
|
||||
RequestedHttpVersion::Http10 => arguments.push("--http1.0".to_string()),
|
||||
RequestedHttpVersion::Http11 => arguments.push("--http1.1".to_string()),
|
||||
RequestedHttpVersion::Http2 => arguments.push("--http2".to_string()),
|
||||
RequestedHttpVersion::Http3 => arguments.push("--http3".to_string()),
|
||||
}
|
||||
if self.insecure {
|
||||
arguments.push("--insecure".to_string());
|
||||
}
|
||||
match self.ip_resolve {
|
||||
IpResolve::Default => {}
|
||||
IpResolve::IpV4 => arguments.push("--ipv4".to_string()),
|
||||
IpResolve::IpV6 => arguments.push("--ipv6".to_string()),
|
||||
}
|
||||
if self.follow_location_trusted {
|
||||
arguments.push("--location-trusted".to_string());
|
||||
} else if self.follow_location {
|
||||
arguments.push("--location".to_string());
|
||||
}
|
||||
if let Some(max_filesize) = self.max_filesize {
|
||||
arguments.push("--max-filesize".to_string());
|
||||
arguments.push(max_filesize.to_string());
|
||||
}
|
||||
if let Some(max_speed) = self.max_recv_speed {
|
||||
arguments.push("--limit-rate".to_string());
|
||||
arguments.push(max_speed.to_string());
|
||||
}
|
||||
// We don't implement --limit-rate for self.max_send_speed as curl limit-rate seems
|
||||
// to limit both upload and download speed. There is no distinct option..
|
||||
if self.max_redirect != ClientOptions::default().max_redirect {
|
||||
let max_redirect = match self.max_redirect {
|
||||
Count::Finite(n) => n as i32,
|
||||
Count::Infinite => -1,
|
||||
};
|
||||
arguments.push("--max-redirs".to_string());
|
||||
arguments.push(max_redirect.to_string());
|
||||
}
|
||||
if let Some(filename) = &self.netrc_file {
|
||||
arguments.push("--netrc-file".to_string());
|
||||
arguments.push(format!("'{filename}'"));
|
||||
}
|
||||
if self.netrc_optional {
|
||||
arguments.push("--netrc-optional".to_string());
|
||||
}
|
||||
if self.netrc {
|
||||
arguments.push("--netrc".to_string());
|
||||
}
|
||||
if self.path_as_is {
|
||||
arguments.push("--path-as-is".to_string());
|
||||
}
|
||||
if let Some(ref proxy) = self.proxy {
|
||||
arguments.push("--proxy".to_string());
|
||||
arguments.push(format!("'{proxy}'"));
|
||||
}
|
||||
for resolve in self.resolves.iter() {
|
||||
arguments.push("--resolve".to_string());
|
||||
arguments.push(resolve.clone());
|
||||
}
|
||||
if self.timeout != ClientOptions::default().timeout {
|
||||
arguments.push("--timeout".to_string());
|
||||
arguments.push(self.timeout.as_secs().to_string());
|
||||
}
|
||||
if let Some(ref unix_socket) = self.unix_socket {
|
||||
arguments.push("--unix-socket".to_string());
|
||||
arguments.push(format!("'{unix_socket}'"));
|
||||
}
|
||||
if let Some(ref user) = self.user {
|
||||
arguments.push("--user".to_string());
|
||||
arguments.push(format!("'{user}'"));
|
||||
}
|
||||
if let Some(ref user_agent) = self.user_agent {
|
||||
arguments.push("--user-agent".to_string());
|
||||
arguments.push(format!("'{user_agent}'"));
|
||||
}
|
||||
arguments
|
||||
}
|
||||
}
|
||||
|
||||
fn escape_url(s: &str) -> String {
|
||||
percent_encoding::percent_encode(s.as_bytes(), percent_encoding::NON_ALPHANUMERIC).to_string()
|
||||
}
|
||||
|
||||
fn encode_shell_string(s: &str) -> String {
|
||||
// $'...' form will be used to encode escaped sequence
|
||||
if escape_mode(s) {
|
||||
let escaped = escape_string(s);
|
||||
format!("$'{escaped}'")
|
||||
} else {
|
||||
format!("'{s}'")
|
||||
}
|
||||
}
|
||||
|
||||
// the shell string must be in escaped mode ($'...')
|
||||
// if it contains \n, \t or '
|
||||
fn escape_mode(s: &str) -> bool {
|
||||
for c in s.chars() {
|
||||
if c == '\n' || c == '\t' || c == '\'' {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn escape_string(s: &str) -> String {
|
||||
let mut escaped_sequences = HashMap::new();
|
||||
escaped_sequences.insert('\n', "\\n");
|
||||
escaped_sequences.insert('\t', "\\t");
|
||||
escaped_sequences.insert('\'', "\\'");
|
||||
escaped_sequences.insert('\\', "\\\\");
|
||||
|
||||
let mut escaped = String::new();
|
||||
for c in s.chars() {
|
||||
match escaped_sequences.get(&c) {
|
||||
None => escaped.push(c),
|
||||
Some(escaped_seq) => escaped.push_str(escaped_seq),
|
||||
}
|
||||
}
|
||||
escaped
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::http::{HeaderVec, Url};
|
||||
use hurl_core::typing::BytesPerSec;
|
||||
use std::path::Path;
|
||||
use std::str::FromStr;
|
||||
use std::time::Duration;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn hello_request_with_default_options() {
|
||||
let mut request = RequestSpec {
|
||||
method: Method("GET".to_string()),
|
||||
url: Url::from_str("http://localhost:8000/hello").unwrap(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let context_dir = &ContextDir::default();
|
||||
let cookies = vec![];
|
||||
let options = ClientOptions::default();
|
||||
let output = None;
|
||||
|
||||
let cmd = CurlCmd::new(&request, &cookies, &context_dir, output.as_ref(), &options);
|
||||
assert_eq!(cmd.to_string(), "curl 'http://localhost:8000/hello'");
|
||||
|
||||
// Same requests with some output:
|
||||
let output = Some(Output::new("foo.out"));
|
||||
let cmd = CurlCmd::new(&request, &cookies, &context_dir, output.as_ref(), &options);
|
||||
assert_eq!(
|
||||
cmd.to_string(),
|
||||
"curl \
|
||||
--output foo.out \
|
||||
'http://localhost:8000/hello'"
|
||||
);
|
||||
|
||||
// With some headers
|
||||
let mut headers = HeaderVec::new();
|
||||
headers.push(Header::new("User-Agent", "iPhone"));
|
||||
headers.push(Header::new("Foo", "Bar"));
|
||||
request.headers = headers;
|
||||
let cmd = CurlCmd::new(&request, &cookies, &context_dir, output.as_ref(), &options);
|
||||
assert_eq!(
|
||||
cmd.to_string(),
|
||||
"curl \
|
||||
--header 'User-Agent: iPhone' \
|
||||
--header 'Foo: Bar' \
|
||||
--output foo.out \
|
||||
'http://localhost:8000/hello'"
|
||||
);
|
||||
|
||||
// With some cookies:
|
||||
let cookies = vec![
|
||||
Cookie {
|
||||
domain: "localhost".to_string(),
|
||||
include_subdomain: "TRUE".to_string(),
|
||||
path: "/".to_string(),
|
||||
https: "FALSE".to_string(),
|
||||
expires: "0".to_string(),
|
||||
name: "cookie1".to_string(),
|
||||
value: "valueA".to_string(),
|
||||
http_only: false,
|
||||
},
|
||||
Cookie {
|
||||
domain: "localhost".to_string(),
|
||||
include_subdomain: "FALSE".to_string(),
|
||||
path: "/".to_string(),
|
||||
https: "FALSE".to_string(),
|
||||
expires: "1".to_string(),
|
||||
name: "cookie2".to_string(),
|
||||
value: String::new(),
|
||||
http_only: true,
|
||||
},
|
||||
];
|
||||
let cmd = CurlCmd::new(&request, &cookies, &context_dir, output.as_ref(), &options);
|
||||
assert_eq!(
|
||||
cmd.to_string(),
|
||||
"curl \
|
||||
--header 'User-Agent: iPhone' \
|
||||
--header 'Foo: Bar' \
|
||||
--cookie 'cookie1=valueA' \
|
||||
--output foo.out \
|
||||
'http://localhost:8000/hello'"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hello_request_with_options() {
|
||||
let request = RequestSpec {
|
||||
method: Method("GET".to_string()),
|
||||
url: Url::from_str("http://localhost:8000/hello").unwrap(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let context_dir = &ContextDir::default();
|
||||
let cookies = vec![];
|
||||
let options = ClientOptions {
|
||||
aws_sigv4: None,
|
||||
cacert_file: None,
|
||||
client_cert_file: None,
|
||||
client_key_file: None,
|
||||
compressed: true,
|
||||
connect_timeout: Duration::from_secs(20),
|
||||
connects_to: vec!["example.com:443:host-47.example.com:443".to_string()],
|
||||
cookie_input_file: Some("cookie_file".to_string()),
|
||||
follow_location: true,
|
||||
follow_location_trusted: false,
|
||||
http_version: RequestedHttpVersion::Http10,
|
||||
insecure: true,
|
||||
ip_resolve: IpResolve::IpV6,
|
||||
max_filesize: None,
|
||||
max_recv_speed: Some(BytesPerSec(8000)),
|
||||
max_redirect: Count::Finite(10),
|
||||
max_send_speed: Some(BytesPerSec(8000)),
|
||||
netrc: false,
|
||||
netrc_file: Some("/var/run/netrc".to_string()),
|
||||
netrc_optional: true,
|
||||
path_as_is: true,
|
||||
proxy: Some("localhost:3128".to_string()),
|
||||
no_proxy: None,
|
||||
resolves: vec![
|
||||
"foo.com:80:192.168.0.1".to_string(),
|
||||
"bar.com:443:127.0.0.1".to_string(),
|
||||
],
|
||||
ssl_no_revoke: false,
|
||||
timeout: Duration::from_secs(10),
|
||||
unix_socket: Some("/var/run/example.sock".to_string()),
|
||||
user: Some("user:password".to_string()),
|
||||
user_agent: Some("my-useragent".to_string()),
|
||||
verbosity: None,
|
||||
};
|
||||
|
||||
let cmd = CurlCmd::new(&request, &cookies, &context_dir, None, &options);
|
||||
assert_eq!(
|
||||
cmd.to_string(),
|
||||
"curl \
|
||||
--compressed \
|
||||
--connect-timeout 20 \
|
||||
--connect-to example.com:443:host-47.example.com:443 \
|
||||
--cookie cookie_file \
|
||||
--http1.0 \
|
||||
--insecure \
|
||||
--ipv6 \
|
||||
--location \
|
||||
--limit-rate 8000 \
|
||||
--max-redirs 10 \
|
||||
--netrc-file '/var/run/netrc' \
|
||||
--netrc-optional \
|
||||
--path-as-is \
|
||||
--proxy 'localhost:3128' \
|
||||
--resolve foo.com:80:192.168.0.1 \
|
||||
--resolve bar.com:443:127.0.0.1 \
|
||||
--timeout 10 \
|
||||
--unix-socket '/var/run/example.sock' \
|
||||
--user 'user:password' \
|
||||
--user-agent 'my-useragent' \
|
||||
'http://localhost:8000/hello'"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn url_with_dot() {
|
||||
let request = RequestSpec {
|
||||
method: Method("GET".to_string()),
|
||||
url: Url::from_str("https://example.org/hello/../to/../your/../file").unwrap(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let context_dir = &ContextDir::default();
|
||||
let cookies = vec![];
|
||||
let options = ClientOptions::default();
|
||||
let output = None;
|
||||
|
||||
let cmd = CurlCmd::new(&request, &cookies, &context_dir, output.as_ref(), &options);
|
||||
assert_eq!(
|
||||
cmd.to_string(),
|
||||
"curl 'https://example.org/hello/../to/../your/../file'"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn url_with_curl_glob() {
|
||||
let request = RequestSpec {
|
||||
method: Method("GET".to_string()),
|
||||
url: Url::from_str("http://foo.com?param1=value1¶m2={bar}").unwrap(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let context_dir = &ContextDir::default();
|
||||
let cookies = vec![];
|
||||
let options = ClientOptions::default();
|
||||
let output = None;
|
||||
|
||||
let cmd = CurlCmd::new(&request, &cookies, &context_dir, output.as_ref(), &options);
|
||||
assert_eq!(
|
||||
cmd.to_string(),
|
||||
"curl \
|
||||
--globoff \
|
||||
'http://foo.com?param1=value1¶m2={bar}'"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn query_request() {
|
||||
let mut request = RequestSpec {
|
||||
method: Method("GET".to_string()),
|
||||
url: Url::from_str("http://localhost:8000/querystring-params").unwrap(),
|
||||
querystring: vec![
|
||||
Param {
|
||||
name: String::from("param1"),
|
||||
value: String::from("value1"),
|
||||
},
|
||||
Param {
|
||||
name: String::from("param2"),
|
||||
value: String::from("a b"),
|
||||
},
|
||||
],
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let context_dir = &ContextDir::default();
|
||||
let cookies = vec![];
|
||||
let options = ClientOptions::default();
|
||||
let output = None;
|
||||
|
||||
let cmd = CurlCmd::new(&request, &cookies, &context_dir, output.as_ref(), &options);
|
||||
assert_eq!(
|
||||
cmd.to_string(),
|
||||
"curl 'http://localhost:8000/querystring-params?param1=value1¶m2=a%20b'",
|
||||
);
|
||||
|
||||
// Add som query param in the URL
|
||||
request.url =
|
||||
Url::from_str("http://localhost:8000/querystring-params?param3=foo¶m4=bar")
|
||||
.unwrap();
|
||||
let cmd = CurlCmd::new(&request, &cookies, &context_dir, output.as_ref(), &options);
|
||||
assert_eq!(
|
||||
cmd.to_string(),
|
||||
"curl 'http://localhost:8000/querystring-params?param3=foo¶m4=bar¶m1=value1¶m2=a%20b'",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn form_request() {
|
||||
let mut headers = HeaderVec::new();
|
||||
headers.push(Header::new(
|
||||
"Content-Type",
|
||||
"application/x-www-form-urlencoded",
|
||||
));
|
||||
|
||||
let request = RequestSpec {
|
||||
method: Method("POST".to_string()),
|
||||
url: Url::from_str("http://localhost/form-params").unwrap(),
|
||||
headers,
|
||||
form: vec![Param::new("param1", "value1"), Param::new("param2", "a b")],
|
||||
implicit_content_type: Some("multipart/form-data".to_string()),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let context_dir = &ContextDir::default();
|
||||
let cookies = vec![];
|
||||
let options = ClientOptions::default();
|
||||
let output = None;
|
||||
|
||||
let cmd = CurlCmd::new(&request, &cookies, &context_dir, output.as_ref(), &options);
|
||||
assert_eq!(
|
||||
cmd.to_string(),
|
||||
"curl \
|
||||
--header 'Content-Type: application/x-www-form-urlencoded' \
|
||||
--data 'param1=value1' \
|
||||
--data 'param2=a%20b' \
|
||||
'http://localhost/form-params'"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn json_request() {
|
||||
let mut headers = HeaderVec::new();
|
||||
headers.push(Header::new("content-type", "application/vnd.api+json"));
|
||||
let mut request = RequestSpec {
|
||||
method: Method("POST".to_string()),
|
||||
url: Url::from_str("http://localhost/json").unwrap(),
|
||||
headers,
|
||||
body: Body::Text("".to_string()),
|
||||
implicit_content_type: Some("application/json".to_string()),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let context_dir = &ContextDir::default();
|
||||
let cookies = vec![];
|
||||
let options = ClientOptions::default();
|
||||
let output = None;
|
||||
|
||||
let cmd = CurlCmd::new(&request, &cookies, &context_dir, output.as_ref(), &options);
|
||||
assert_eq!(
|
||||
cmd.to_string(),
|
||||
"curl \
|
||||
--request POST \
|
||||
--header 'content-type: application/vnd.api+json' \
|
||||
'http://localhost/json'"
|
||||
);
|
||||
|
||||
// Add a non-empty body
|
||||
request.body = Body::Text("{\"foo\":\"bar\"}".to_string());
|
||||
let cmd = CurlCmd::new(&request, &cookies, &context_dir, output.as_ref(), &options);
|
||||
assert_eq!(
|
||||
cmd.to_string(),
|
||||
"curl \
|
||||
--header 'content-type: application/vnd.api+json' \
|
||||
--data '{\"foo\":\"bar\"}' \
|
||||
'http://localhost/json'"
|
||||
);
|
||||
|
||||
// Change method
|
||||
request.method = Method("PUT".to_string());
|
||||
let cmd = CurlCmd::new(&request, &cookies, &context_dir, output.as_ref(), &options);
|
||||
assert_eq!(
|
||||
cmd.to_string(),
|
||||
"curl \
|
||||
--request PUT \
|
||||
--header 'content-type: application/vnd.api+json' \
|
||||
--data '{\"foo\":\"bar\"}' \
|
||||
'http://localhost/json'"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn post_binary_file() {
|
||||
let request = RequestSpec {
|
||||
method: Method("POST".to_string()),
|
||||
url: Url::from_str("http://localhost:8000/hello").unwrap(),
|
||||
body: Body::File(b"Hello World!".to_vec(), "foo.bin".to_string()),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let context_dir = &ContextDir::default();
|
||||
let cookies = vec![];
|
||||
let options = ClientOptions::default();
|
||||
let output = None;
|
||||
|
||||
let cmd = CurlCmd::new(&request, &cookies, &context_dir, output.as_ref(), &options);
|
||||
assert_eq!(
|
||||
cmd.to_string(),
|
||||
"curl \
|
||||
--header 'Content-Type:' \
|
||||
--data-binary '@foo.bin' \
|
||||
'http://localhost:8000/hello'"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_encode_byte() {
|
||||
assert_eq!(encode_byte(1), "\\x01".to_string());
|
||||
assert_eq!(encode_byte(32), "\\x20".to_string());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn header_curl_args() {
|
||||
assert_eq!(
|
||||
Header::new("Host", "example.com").curl_args(),
|
||||
vec!["--header".to_string(), "'Host: example.com'".to_string()]
|
||||
);
|
||||
assert_eq!(
|
||||
Header::new("If-Match", "\"e0023aa4e\"").curl_args(),
|
||||
vec![
|
||||
"--header".to_string(),
|
||||
"'If-Match: \"e0023aa4e\"'".to_string()
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn param_curl_args() {
|
||||
assert_eq!(
|
||||
Param {
|
||||
name: "param1".to_string(),
|
||||
value: "value1".to_string(),
|
||||
}
|
||||
.curl_arg(),
|
||||
"param1=value1".to_string()
|
||||
);
|
||||
assert_eq!(
|
||||
Param {
|
||||
name: "param2".to_string(),
|
||||
value: String::new(),
|
||||
}
|
||||
.curl_arg(),
|
||||
"param2=".to_string()
|
||||
);
|
||||
assert_eq!(
|
||||
Param {
|
||||
name: "param3".to_string(),
|
||||
value: "a=b".to_string(),
|
||||
}
|
||||
.curl_arg_escape(),
|
||||
"param3=a%3Db".to_string()
|
||||
);
|
||||
assert_eq!(
|
||||
Param {
|
||||
name: "param4".to_string(),
|
||||
value: "1,2,3".to_string(),
|
||||
}
|
||||
.curl_arg_escape(),
|
||||
"param4=1%2C2%2C3".to_string()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_encode_body() {
|
||||
let current_dir = Path::new("/tmp");
|
||||
let file_root = Path::new("/tmp");
|
||||
let context_dir = ContextDir::new(current_dir, file_root);
|
||||
assert_eq!(
|
||||
Body::Text("hello".to_string()).curl_arg(&context_dir),
|
||||
"'hello'".to_string()
|
||||
);
|
||||
|
||||
if cfg!(unix) {
|
||||
assert_eq!(
|
||||
Body::File(vec![], "filename".to_string()).curl_arg(&context_dir),
|
||||
"'@/tmp/filename'".to_string()
|
||||
);
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
Body::Binary(vec![1, 2, 3]).curl_arg(&context_dir),
|
||||
"$'\\x01\\x02\\x03'".to_string()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_encode_shell_string() {
|
||||
assert_eq!(encode_shell_string("hello"), "'hello'");
|
||||
assert_eq!(encode_shell_string("\\n"), "'\\n'");
|
||||
assert_eq!(encode_shell_string("'"), "$'\\''");
|
||||
assert_eq!(encode_shell_string("\\'"), "$'\\\\\\''");
|
||||
assert_eq!(encode_shell_string("\n"), "$'\\n'");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_escape_string() {
|
||||
assert_eq!(escape_string("hello"), "hello");
|
||||
assert_eq!(escape_string("\\n"), "\\\\n");
|
||||
assert_eq!(escape_string("'"), "\\'");
|
||||
assert_eq!(escape_string("\\'"), "\\\\\\'");
|
||||
assert_eq!(escape_string("\n"), "\\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_escape_mode() {
|
||||
assert!(!escape_mode("hello"));
|
||||
assert!(!escape_mode("\\"));
|
||||
assert!(escape_mode("'"));
|
||||
assert!(escape_mode("\n"));
|
||||
}
|
||||
}
|
@ -44,6 +44,7 @@ mod certificate;
|
||||
mod client;
|
||||
mod cookie;
|
||||
mod core;
|
||||
mod curl_cmd;
|
||||
mod debug;
|
||||
mod easy_ext;
|
||||
mod error;
|
||||
@ -53,7 +54,6 @@ mod mimetype;
|
||||
mod options;
|
||||
mod request;
|
||||
mod request_spec;
|
||||
mod request_spec_curl_args;
|
||||
mod response;
|
||||
mod response_cookie;
|
||||
mod response_debug;
|
||||
|
@ -98,203 +98,3 @@ impl Default for ClientOptions {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ClientOptions {
|
||||
/// Returns the list of options for the curl command line equivalent to this [`ClientOptions`].
|
||||
pub fn curl_args(&self) -> Vec<String> {
|
||||
let mut arguments = vec![];
|
||||
|
||||
if let Some(ref aws_sigv4) = self.aws_sigv4 {
|
||||
arguments.push("--aws-sigv4".to_string());
|
||||
arguments.push(aws_sigv4.clone());
|
||||
}
|
||||
if let Some(ref cacert_file) = self.cacert_file {
|
||||
arguments.push("--cacert".to_string());
|
||||
arguments.push(cacert_file.clone());
|
||||
}
|
||||
if let Some(ref client_cert_file) = self.client_cert_file {
|
||||
arguments.push("--cert".to_string());
|
||||
arguments.push(client_cert_file.clone());
|
||||
}
|
||||
if let Some(ref client_key_file) = self.client_key_file {
|
||||
arguments.push("--key".to_string());
|
||||
arguments.push(client_key_file.clone());
|
||||
}
|
||||
if self.compressed {
|
||||
arguments.push("--compressed".to_string());
|
||||
}
|
||||
if self.connect_timeout != ClientOptions::default().connect_timeout {
|
||||
arguments.push("--connect-timeout".to_string());
|
||||
arguments.push(self.connect_timeout.as_secs().to_string());
|
||||
}
|
||||
for connect in self.connects_to.iter() {
|
||||
arguments.push("--connect-to".to_string());
|
||||
arguments.push(connect.clone());
|
||||
}
|
||||
if let Some(ref cookie_file) = self.cookie_input_file {
|
||||
arguments.push("--cookie".to_string());
|
||||
arguments.push(cookie_file.clone());
|
||||
}
|
||||
match self.http_version {
|
||||
RequestedHttpVersion::Default => {}
|
||||
RequestedHttpVersion::Http10 => arguments.push("--http1.0".to_string()),
|
||||
RequestedHttpVersion::Http11 => arguments.push("--http1.1".to_string()),
|
||||
RequestedHttpVersion::Http2 => arguments.push("--http2".to_string()),
|
||||
RequestedHttpVersion::Http3 => arguments.push("--http3".to_string()),
|
||||
}
|
||||
if self.insecure {
|
||||
arguments.push("--insecure".to_string());
|
||||
}
|
||||
match self.ip_resolve {
|
||||
IpResolve::Default => {}
|
||||
IpResolve::IpV4 => arguments.push("--ipv4".to_string()),
|
||||
IpResolve::IpV6 => arguments.push("--ipv6".to_string()),
|
||||
}
|
||||
if self.follow_location_trusted {
|
||||
arguments.push("--location-trusted".to_string());
|
||||
} else if self.follow_location {
|
||||
arguments.push("--location".to_string());
|
||||
}
|
||||
if let Some(max_filesize) = self.max_filesize {
|
||||
arguments.push("--max-filesize".to_string());
|
||||
arguments.push(max_filesize.to_string());
|
||||
}
|
||||
if let Some(max_speed) = self.max_recv_speed {
|
||||
arguments.push("--limit-rate".to_string());
|
||||
arguments.push(max_speed.to_string());
|
||||
}
|
||||
// We don't implement --limit-rate for self.max_send_speed as curl limit-rate seems
|
||||
// to limit both upload and download speed. There is no distinct option..
|
||||
if self.max_redirect != ClientOptions::default().max_redirect {
|
||||
let max_redirect = match self.max_redirect {
|
||||
Count::Finite(n) => n as i32,
|
||||
Count::Infinite => -1,
|
||||
};
|
||||
arguments.push("--max-redirs".to_string());
|
||||
arguments.push(max_redirect.to_string());
|
||||
}
|
||||
if let Some(filename) = &self.netrc_file {
|
||||
arguments.push("--netrc-file".to_string());
|
||||
arguments.push(format!("'{filename}'"));
|
||||
}
|
||||
if self.netrc_optional {
|
||||
arguments.push("--netrc-optional".to_string());
|
||||
}
|
||||
if self.netrc {
|
||||
arguments.push("--netrc".to_string());
|
||||
}
|
||||
if self.path_as_is {
|
||||
arguments.push("--path-as-is".to_string());
|
||||
}
|
||||
if let Some(ref proxy) = self.proxy {
|
||||
arguments.push("--proxy".to_string());
|
||||
arguments.push(format!("'{proxy}'"));
|
||||
}
|
||||
for resolve in self.resolves.iter() {
|
||||
arguments.push("--resolve".to_string());
|
||||
arguments.push(resolve.clone());
|
||||
}
|
||||
if self.timeout != ClientOptions::default().timeout {
|
||||
arguments.push("--timeout".to_string());
|
||||
arguments.push(self.timeout.as_secs().to_string());
|
||||
}
|
||||
if let Some(ref unix_socket) = self.unix_socket {
|
||||
arguments.push("--unix-socket".to_string());
|
||||
arguments.push(format!("'{unix_socket}'"));
|
||||
}
|
||||
if let Some(ref user) = self.user {
|
||||
arguments.push("--user".to_string());
|
||||
arguments.push(format!("'{user}'"));
|
||||
}
|
||||
if let Some(ref user_agent) = self.user_agent {
|
||||
arguments.push("--user-agent".to_string());
|
||||
arguments.push(format!("'{user_agent}'"));
|
||||
}
|
||||
arguments
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_curl_args() {
|
||||
assert!(ClientOptions::default().curl_args().is_empty());
|
||||
|
||||
assert_eq!(
|
||||
ClientOptions {
|
||||
aws_sigv4: None,
|
||||
cacert_file: None,
|
||||
client_cert_file: None,
|
||||
client_key_file: None,
|
||||
compressed: true,
|
||||
connect_timeout: Duration::from_secs(20),
|
||||
connects_to: vec!["example.com:443:host-47.example.com:443".to_string()],
|
||||
cookie_input_file: Some("cookie_file".to_string()),
|
||||
follow_location: true,
|
||||
follow_location_trusted: false,
|
||||
http_version: RequestedHttpVersion::Http10,
|
||||
insecure: true,
|
||||
ip_resolve: IpResolve::IpV6,
|
||||
max_filesize: None,
|
||||
max_recv_speed: Some(BytesPerSec(8000)),
|
||||
max_redirect: Count::Finite(10),
|
||||
max_send_speed: Some(BytesPerSec(8000)),
|
||||
netrc: false,
|
||||
netrc_file: Some("/var/run/netrc".to_string()),
|
||||
netrc_optional: true,
|
||||
path_as_is: true,
|
||||
proxy: Some("localhost:3128".to_string()),
|
||||
no_proxy: None,
|
||||
resolves: vec![
|
||||
"foo.com:80:192.168.0.1".to_string(),
|
||||
"bar.com:443:127.0.0.1".to_string(),
|
||||
],
|
||||
ssl_no_revoke: false,
|
||||
timeout: Duration::from_secs(10),
|
||||
unix_socket: Some("/var/run/example.sock".to_string()),
|
||||
user: Some("user:password".to_string()),
|
||||
user_agent: Some("my-useragent".to_string()),
|
||||
verbosity: None,
|
||||
}
|
||||
.curl_args(),
|
||||
[
|
||||
"--compressed",
|
||||
"--connect-timeout",
|
||||
"20",
|
||||
"--connect-to",
|
||||
"example.com:443:host-47.example.com:443",
|
||||
"--cookie",
|
||||
"cookie_file",
|
||||
"--http1.0",
|
||||
"--insecure",
|
||||
"--ipv6",
|
||||
"--location",
|
||||
"--limit-rate",
|
||||
"8000",
|
||||
"--max-redirs",
|
||||
"10",
|
||||
"--netrc-file",
|
||||
"'/var/run/netrc'",
|
||||
"--netrc-optional",
|
||||
"--path-as-is",
|
||||
"--proxy",
|
||||
"'localhost:3128'",
|
||||
"--resolve",
|
||||
"foo.com:80:192.168.0.1",
|
||||
"--resolve",
|
||||
"bar.com:443:127.0.0.1",
|
||||
"--timeout",
|
||||
"10",
|
||||
"--unix-socket",
|
||||
"'/var/run/example.sock'",
|
||||
"--user",
|
||||
"'user:password'",
|
||||
"--user-agent",
|
||||
"'my-useragent'",
|
||||
]
|
||||
.map(|a| a.to_string())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,524 +0,0 @@
|
||||
/*
|
||||
* Hurl (https://hurl.dev)
|
||||
* Copyright (C) 2024 Orange
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
use std::collections::HashMap;
|
||||
use std::path::Path;
|
||||
|
||||
use crate::http::core::*;
|
||||
use crate::http::*;
|
||||
use crate::util::path::ContextDir;
|
||||
|
||||
impl RequestSpec {
|
||||
/// Returns this request as curl arguments.
|
||||
/// It does not contain the requests cookies (they will be accessed from the client)
|
||||
pub fn curl_args(&self, context_dir: &ContextDir) -> Vec<String> {
|
||||
let mut arguments = vec![];
|
||||
|
||||
let data =
|
||||
!self.multipart.is_empty() || !self.form.is_empty() || !self.body.bytes().is_empty();
|
||||
arguments.append(&mut self.method.curl_args(data));
|
||||
|
||||
for header in self.headers.iter() {
|
||||
arguments.append(&mut header.curl_args());
|
||||
}
|
||||
|
||||
let has_explicit_content_type = self.headers.contains_key(CONTENT_TYPE);
|
||||
if !has_explicit_content_type {
|
||||
if let Some(content_type) = &self.implicit_content_type {
|
||||
if content_type != "application/x-www-form-urlencoded"
|
||||
&& content_type != "multipart/form-data"
|
||||
{
|
||||
arguments.push("--header".to_string());
|
||||
arguments.push(format!("'{}: {content_type}'", CONTENT_TYPE));
|
||||
}
|
||||
} else if !self.body.bytes().is_empty() {
|
||||
match self.body {
|
||||
Body::Text(_) => {
|
||||
arguments.push("--header".to_string());
|
||||
arguments.push(format!("'{}:'", CONTENT_TYPE));
|
||||
}
|
||||
Body::Binary(_) => {
|
||||
arguments.push("--header".to_string());
|
||||
arguments.push(format!("'{}: application/octet-stream'", CONTENT_TYPE));
|
||||
}
|
||||
Body::File(_, _) => {
|
||||
arguments.push("--header".to_string());
|
||||
arguments.push(format!("'{}:'", CONTENT_TYPE));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for param in self.form.iter() {
|
||||
arguments.push("--data".to_string());
|
||||
arguments.push(format!("'{}'", param.curl_arg_escape()));
|
||||
}
|
||||
for param in self.multipart.iter() {
|
||||
arguments.push("--form".to_string());
|
||||
arguments.push(format!("'{}'", param.curl_arg(context_dir)));
|
||||
}
|
||||
|
||||
if !self.body.bytes().is_empty() {
|
||||
// See <https://curl.se/docs/manpage.html#-d> and <https://curl.se/docs/manpage.html#--data-binary>:
|
||||
//
|
||||
// > -d, --data <data>
|
||||
// > ...
|
||||
// > If you start the data with the letter @, the rest should be a file name to read the
|
||||
// > data from, or - if you want curl to read the data from stdin. Posting data from a
|
||||
// > file named 'foobar' would thus be done with -d, --data @foobar. When -d, --data is
|
||||
// > told to read from a file like that, carriage returns and newlines will be stripped
|
||||
// > out. If you do not want the @ character to have a special interpretation use
|
||||
// > --data-raw instead.
|
||||
// > ...
|
||||
// > --data-binary <data>
|
||||
// >
|
||||
// > (HTTP) This posts data exactly as specified with no extra processing whatsoever.
|
||||
//
|
||||
// In summary: if the payload is a file (@foo.bin), we must use --data-binary option in
|
||||
// order to curl to not process the data sent.
|
||||
let param = match self.body {
|
||||
Body::File(_, _) => "--data-binary",
|
||||
_ => "--data",
|
||||
};
|
||||
arguments.push(param.to_string());
|
||||
arguments.push(self.body.curl_arg(context_dir));
|
||||
}
|
||||
|
||||
let querystring = if self.querystring.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
let params = self
|
||||
.querystring
|
||||
.iter()
|
||||
.map(|p| p.curl_arg_escape())
|
||||
.collect::<Vec<String>>();
|
||||
params.join("&")
|
||||
};
|
||||
let url = if querystring.as_str() == "" {
|
||||
self.url.raw()
|
||||
} else if self.url.raw().contains('?') {
|
||||
format!("{}&{}", self.url.raw(), querystring)
|
||||
} else {
|
||||
format!("{}?{}", self.url.raw(), querystring)
|
||||
};
|
||||
arguments.push(format!("'{url}'"));
|
||||
|
||||
arguments
|
||||
}
|
||||
}
|
||||
|
||||
fn encode_byte(b: u8) -> String {
|
||||
format!("\\x{b:02x}")
|
||||
}
|
||||
|
||||
fn encode_bytes(bytes: &[u8]) -> String {
|
||||
bytes.iter().map(|b| encode_byte(*b)).collect()
|
||||
}
|
||||
|
||||
impl Method {
|
||||
pub fn curl_args(&self, data: bool) -> Vec<String> {
|
||||
match self.0.as_str() {
|
||||
"GET" => {
|
||||
if data {
|
||||
vec!["--request".to_string(), "GET".to_string()]
|
||||
} else {
|
||||
vec![]
|
||||
}
|
||||
}
|
||||
"HEAD" => vec!["--head".to_string()],
|
||||
"POST" => {
|
||||
if data {
|
||||
vec![]
|
||||
} else {
|
||||
vec!["--request".to_string(), "POST".to_string()]
|
||||
}
|
||||
}
|
||||
s => vec!["--request".to_string(), s.to_string()],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Header {
|
||||
pub fn curl_args(&self) -> Vec<String> {
|
||||
let name = &self.name;
|
||||
let value = &self.value;
|
||||
vec![
|
||||
"--header".to_string(),
|
||||
encode_shell_string(&format!("{name}: {value}")),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
impl Param {
|
||||
pub fn curl_arg_escape(&self) -> String {
|
||||
let name = &self.name;
|
||||
let value = escape_url(&self.value);
|
||||
format!("{name}={value}")
|
||||
}
|
||||
|
||||
pub fn curl_arg(&self) -> String {
|
||||
let name = &self.name;
|
||||
let value = &self.value;
|
||||
format!("{name}={value}")
|
||||
}
|
||||
}
|
||||
|
||||
impl MultipartParam {
|
||||
pub fn curl_arg(&self, context_dir: &ContextDir) -> String {
|
||||
match self {
|
||||
MultipartParam::Param(param) => param.curl_arg(),
|
||||
MultipartParam::FileParam(FileParam {
|
||||
name,
|
||||
filename,
|
||||
content_type,
|
||||
..
|
||||
}) => {
|
||||
let path = context_dir.resolved_path(Path::new(filename));
|
||||
let value = format!("@{};type={}", path.to_string_lossy(), content_type);
|
||||
format!("{name}={value}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Body {
|
||||
pub fn curl_arg(&self, context_dir: &ContextDir) -> String {
|
||||
match self {
|
||||
Body::Text(s) => encode_shell_string(s),
|
||||
Body::Binary(bytes) => format!("$'{}'", encode_bytes(bytes)),
|
||||
Body::File(_, filename) => {
|
||||
let path = context_dir.resolved_path(Path::new(filename));
|
||||
format!("'@{}'", path.to_string_lossy())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn escape_url(s: &str) -> String {
|
||||
percent_encoding::percent_encode(s.as_bytes(), percent_encoding::NON_ALPHANUMERIC).to_string()
|
||||
}
|
||||
|
||||
fn encode_shell_string(s: &str) -> String {
|
||||
// $'...' form will be used to encode escaped sequence
|
||||
if escape_mode(s) {
|
||||
let escaped = escape_string(s);
|
||||
format!("$'{escaped}'")
|
||||
} else {
|
||||
format!("'{s}'")
|
||||
}
|
||||
}
|
||||
|
||||
// the shell string must be in escaped mode ($'...')
|
||||
// if it contains \n, \t or '
|
||||
fn escape_mode(s: &str) -> bool {
|
||||
for c in s.chars() {
|
||||
if c == '\n' || c == '\t' || c == '\'' {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn escape_string(s: &str) -> String {
|
||||
let mut escaped_sequences = HashMap::new();
|
||||
escaped_sequences.insert('\n', "\\n");
|
||||
escaped_sequences.insert('\t', "\\t");
|
||||
escaped_sequences.insert('\'', "\\'");
|
||||
escaped_sequences.insert('\\', "\\\\");
|
||||
|
||||
let mut escaped = String::new();
|
||||
for c in s.chars() {
|
||||
match escaped_sequences.get(&c) {
|
||||
None => escaped.push(c),
|
||||
Some(escaped_seq) => escaped.push_str(escaped_seq),
|
||||
}
|
||||
}
|
||||
escaped
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod tests {
|
||||
use std::path::Path;
|
||||
use std::str::FromStr;
|
||||
|
||||
use super::*;
|
||||
|
||||
fn form_http_request() -> RequestSpec {
|
||||
let mut headers = HeaderVec::new();
|
||||
headers.push(Header::new(
|
||||
"Content-Type",
|
||||
"application/x-www-form-urlencoded",
|
||||
));
|
||||
|
||||
RequestSpec {
|
||||
method: Method("POST".to_string()),
|
||||
url: Url::from_str("http://localhost/form-params").unwrap(),
|
||||
headers,
|
||||
form: vec![
|
||||
Param {
|
||||
name: String::from("param1"),
|
||||
value: String::from("value1"),
|
||||
},
|
||||
Param {
|
||||
name: String::from("param2"),
|
||||
value: String::from("a b"),
|
||||
},
|
||||
],
|
||||
implicit_content_type: Some("multipart/form-data".to_string()),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn json_request() -> RequestSpec {
|
||||
let mut headers = HeaderVec::new();
|
||||
headers.push(Header::new("content-type", "application/vnd.api+json"));
|
||||
RequestSpec {
|
||||
method: Method("POST".to_string()),
|
||||
url: Url::from_str("http://localhost/json").unwrap(),
|
||||
headers,
|
||||
body: Body::Text("{\"foo\":\"bar\"}".to_string()),
|
||||
implicit_content_type: Some("application/json".to_string()),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_encode_byte() {
|
||||
assert_eq!(encode_byte(1), "\\x01".to_string());
|
||||
assert_eq!(encode_byte(32), "\\x20".to_string());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn method_curl_args() {
|
||||
assert!(Method("GET".to_string()).curl_args(false).is_empty());
|
||||
assert_eq!(
|
||||
Method("GET".to_string()).curl_args(true),
|
||||
vec!["--request".to_string(), "GET".to_string()]
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
Method("POST".to_string()).curl_args(false),
|
||||
vec!["--request".to_string(), "POST".to_string()]
|
||||
);
|
||||
assert!(Method("POST".to_string()).curl_args(true).is_empty());
|
||||
|
||||
assert_eq!(
|
||||
Method("PUT".to_string()).curl_args(false),
|
||||
vec!["--request".to_string(), "PUT".to_string()]
|
||||
);
|
||||
assert_eq!(
|
||||
Method("PUT".to_string()).curl_args(true),
|
||||
vec!["--request".to_string(), "PUT".to_string()]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn header_curl_args() {
|
||||
assert_eq!(
|
||||
Header::new("Host", "example.com").curl_args(),
|
||||
vec!["--header".to_string(), "'Host: example.com'".to_string()]
|
||||
);
|
||||
assert_eq!(
|
||||
Header::new("If-Match", "\"e0023aa4e\"").curl_args(),
|
||||
vec![
|
||||
"--header".to_string(),
|
||||
"'If-Match: \"e0023aa4e\"'".to_string()
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn param_curl_args() {
|
||||
assert_eq!(
|
||||
Param {
|
||||
name: "param1".to_string(),
|
||||
value: "value1".to_string(),
|
||||
}
|
||||
.curl_arg(),
|
||||
"param1=value1".to_string()
|
||||
);
|
||||
assert_eq!(
|
||||
Param {
|
||||
name: "param2".to_string(),
|
||||
value: String::new(),
|
||||
}
|
||||
.curl_arg(),
|
||||
"param2=".to_string()
|
||||
);
|
||||
assert_eq!(
|
||||
Param {
|
||||
name: "param3".to_string(),
|
||||
value: "a=b".to_string(),
|
||||
}
|
||||
.curl_arg_escape(),
|
||||
"param3=a%3Db".to_string()
|
||||
);
|
||||
assert_eq!(
|
||||
Param {
|
||||
name: "param4".to_string(),
|
||||
value: "1,2,3".to_string(),
|
||||
}
|
||||
.curl_arg_escape(),
|
||||
"param4=1%2C2%2C3".to_string()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn requests_curl_args() {
|
||||
let context_dir = &ContextDir::default();
|
||||
assert_eq!(
|
||||
hello_http_request().curl_args(context_dir),
|
||||
vec!["'http://localhost:8000/hello'".to_string()]
|
||||
);
|
||||
assert_eq!(
|
||||
custom_http_request().curl_args(context_dir),
|
||||
vec![
|
||||
"--header".to_string(),
|
||||
"'User-Agent: iPhone'".to_string(),
|
||||
"--header".to_string(),
|
||||
"'Foo: Bar'".to_string(),
|
||||
"'http://localhost/custom'".to_string(),
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
query_http_request().curl_args(context_dir),
|
||||
vec![
|
||||
"'http://localhost:8000/querystring-params?param1=value1¶m2=a%20b'".to_string()
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
form_http_request().curl_args(context_dir),
|
||||
vec![
|
||||
"--header".to_string(),
|
||||
"'Content-Type: application/x-www-form-urlencoded'".to_string(),
|
||||
"--data".to_string(),
|
||||
"'param1=value1'".to_string(),
|
||||
"--data".to_string(),
|
||||
"'param2=a%20b'".to_string(),
|
||||
"'http://localhost/form-params'".to_string(),
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
json_request().curl_args(context_dir),
|
||||
vec![
|
||||
"--header".to_string(),
|
||||
"'content-type: application/vnd.api+json'".to_string(),
|
||||
"--data".to_string(),
|
||||
"'{\"foo\":\"bar\"}'".to_string(),
|
||||
"'http://localhost/json'".to_string(),
|
||||
]
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
RequestSpec {
|
||||
method: Method("GET".to_string()),
|
||||
url: Url::from_str("http://localhost:8000/").unwrap(),
|
||||
..Default::default()
|
||||
}
|
||||
.curl_args(context_dir),
|
||||
vec!["'http://localhost:8000/'".to_string(),]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn post_data_curl_args() {
|
||||
let context_dir = &ContextDir::default();
|
||||
let req = RequestSpec {
|
||||
method: Method("POST".to_string()),
|
||||
url: Url::from_str("http://localhost:8000/hello").unwrap(),
|
||||
body: Body::Text("foo".to_string()),
|
||||
..Default::default()
|
||||
};
|
||||
assert_eq!(
|
||||
req.curl_args(context_dir),
|
||||
vec![
|
||||
"--header",
|
||||
"'Content-Type:'",
|
||||
"--data",
|
||||
"'foo'",
|
||||
"'http://localhost:8000/hello'"
|
||||
]
|
||||
);
|
||||
|
||||
let context_dir = &ContextDir::default();
|
||||
let req = RequestSpec {
|
||||
method: Method("POST".to_string()),
|
||||
url: Url::from_str("http://localhost:8000/hello").unwrap(),
|
||||
body: Body::File(b"Hello World!".to_vec(), "foo.bin".to_string()),
|
||||
..Default::default()
|
||||
};
|
||||
assert_eq!(
|
||||
req.curl_args(context_dir),
|
||||
vec![
|
||||
"--header",
|
||||
"'Content-Type:'",
|
||||
"--data-binary",
|
||||
"'@foo.bin'",
|
||||
"'http://localhost:8000/hello'"
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_encode_body() {
|
||||
let current_dir = Path::new("/tmp");
|
||||
let file_root = Path::new("/tmp");
|
||||
let context_dir = ContextDir::new(current_dir, file_root);
|
||||
assert_eq!(
|
||||
Body::Text("hello".to_string()).curl_arg(&context_dir),
|
||||
"'hello'".to_string()
|
||||
);
|
||||
|
||||
if cfg!(unix) {
|
||||
assert_eq!(
|
||||
Body::File(vec![], "filename".to_string()).curl_arg(&context_dir),
|
||||
"'@/tmp/filename'".to_string()
|
||||
);
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
Body::Binary(vec![1, 2, 3]).curl_arg(&context_dir),
|
||||
"$'\\x01\\x02\\x03'".to_string()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_encode_shell_string() {
|
||||
assert_eq!(encode_shell_string("hello"), "'hello'");
|
||||
assert_eq!(encode_shell_string("\\n"), "'\\n'");
|
||||
assert_eq!(encode_shell_string("'"), "$'\\''");
|
||||
assert_eq!(encode_shell_string("\\'"), "$'\\\\\\''");
|
||||
assert_eq!(encode_shell_string("\n"), "$'\\n'");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_escape_string() {
|
||||
assert_eq!(escape_string("hello"), "hello");
|
||||
assert_eq!(escape_string("\\n"), "\\\\n");
|
||||
assert_eq!(escape_string("'"), "\\'");
|
||||
assert_eq!(escape_string("\\'"), "\\\\\\'");
|
||||
assert_eq!(escape_string("\n"), "\\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_escape_mode() {
|
||||
assert!(!escape_mode("hello"));
|
||||
assert!(!escape_mode("\\"));
|
||||
assert!(escape_mode("'"));
|
||||
assert!(escape_mode("\n"));
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user