Add a new option `--header`

This commit is contained in:
theoforger 2024-11-16 16:56:51 -05:00
parent 1a13135638
commit 3971e918e9
No known key found for this signature in database
GPG Key ID: F12DFBC5ABD74A15
21 changed files with 118 additions and 23 deletions

View File

@ -1262,6 +1262,7 @@ will follow a redirection only for the second entry.
| <a href="#file-root" id="file-root"><code>--file-root &lt;DIR&gt;</code></a> | Set root directory to import files in Hurl. This is used for files in multipart form data, request body and response output.<br>When it is not explicitly defined, files are relative to the Hurl file's directory.<br><br>This is a cli-only option.<br> |
| <a href="#from-entry" id="from-entry"><code>--from-entry &lt;ENTRY_NUMBER&gt;</code></a> | Execute Hurl file from ENTRY_NUMBER (starting at 1).<br><br>This is a cli-only option.<br> |
| <a href="#glob" id="glob"><code>--glob &lt;GLOB&gt;</code></a> | Specify input files that match the given glob pattern.<br><br>Multiple glob flags may be used. This flag supports common Unix glob patterns like *, ? and [].<br>However, to avoid your shell accidentally expanding glob patterns before Hurl handles them, you must use single quotes or double quotes around each pattern.<br><br>This is a cli-only option.<br> |
| <a href="#header" id="header"><code>-H, --header &lt;HEADER&gt;</code></a> | Add an extra header to include in information sent. Can be used several times in a command<br><br>Do not add newlines or carriage returns<br><br>This is a cli-only option.<br> |
| <a href="#http10" id="http10"><code>-0, --http1.0</code></a> | Tells Hurl to use HTTP version 1.0 instead of using its internally preferred HTTP version.<br> |
| <a href="#http11" id="http11"><code>--http1.1</code></a> | Tells Hurl to use HTTP version 1.1.<br> |
| <a href="#http2" id="http2"><code>--http2</code></a> | Tells Hurl to use HTTP version 2.<br>For HTTPS, this means Hurl negotiates HTTP/2 in the TLS handshake. Hurl does this by default.<br>For HTTP, this means Hurl attempts to upgrade the request to HTTP/2 using the Upgrade: request header.<br> |

View File

@ -34,6 +34,7 @@ _hurl() {
'--location-trusted[Follow redirects but allows sending the name + password to all hosts that the site may redirect to]' \
'--from-entry[Execute Hurl file from ENTRY_NUMBER (starting at 1)]: :' \
'*--glob[Specify input files that match the given GLOB. Multiple glob flags may be used]: :' \
'*(-H --header)'{-H,--header}'[Pass custom header(s) to server]: :' \
'(-0 --http1.0)'{-0,--http1.0}'[Tell Hurl to use HTTP version 1.0]' \
'--http1.1[Tell Hurl to use HTTP version 1.1]' \
'--http2[Tell Hurl to use HTTP version 2]' \

View File

@ -39,6 +39,7 @@ Register-ArgumentCompleter -Native -CommandName 'hurl' -ScriptBlock {
[CompletionResult]::new('--location-trusted', 'location-trusted', [CompletionResultType]::ParameterName, 'Follow redirects but allows sending the name + password to all hosts that the site may redirect to')
[CompletionResult]::new('--from-entry', 'from-entry', [CompletionResultType]::ParameterName, 'Execute Hurl file from ENTRY_NUMBER (starting at 1)')
[CompletionResult]::new('--glob', 'glob', [CompletionResultType]::ParameterName, 'Specify input files that match the given GLOB. Multiple glob flags may be used')
[CompletionResult]::new('--header', 'header', [CompletionResultType]::ParameterName, 'Pass custom header(s) to server')
[CompletionResult]::new('--http1.0', 'http1.0', [CompletionResultType]::ParameterName, 'Tell Hurl to use HTTP version 1.0')
[CompletionResult]::new('--http1.1', 'http1.1', [CompletionResultType]::ParameterName, 'Tell Hurl to use HTTP version 1.1')
[CompletionResult]::new('--http2', 'http2', [CompletionResultType]::ParameterName, 'Tell Hurl to use HTTP version 2')

View File

@ -5,7 +5,7 @@ _hurl()
_init_completion || return
if [[ $cur == -* ]]; then
COMPREPLY=($(compgen -W '--aws-sigv4 --cacert --cert --key --color --compressed --connect-timeout --connect-to --continue-on-error --cookie --cookie-jar --curl --delay --error-format --file-root --location --location-trusted --from-entry --glob --http1.0 --http1.1 --http2 --http3 --ignore-asserts --include --insecure --interactive --ipv4 --ipv6 --jobs --json --limit-rate --max-filesize --max-redirs --max-time --netrc --netrc-file --netrc-optional --no-color --no-output --noproxy --output --parallel --path-as-is --proxy --repeat --report-html --report-json --report-junit --report-tap --resolve --retry --retry-interval --ssl-no-revoke --test --to-entry --unix-socket --user --user-agent --variable --variables-file --verbose --very-verbose --help --version' -- "$cur"))
COMPREPLY=($(compgen -W '--aws-sigv4 --cacert --cert --key --color --compressed --connect-timeout --connect-to --continue-on-error --cookie --cookie-jar --curl --delay --error-format --file-root --location --location-trusted --from-entry --glob --header --http1.0 --http1.1 --http2 --http3 --ignore-asserts --include --insecure --interactive --ipv4 --ipv6 --jobs --json --limit-rate --max-filesize --max-redirs --max-time --netrc --netrc-file --netrc-optional --no-color --no-output --noproxy --output --parallel --path-as-is --proxy --repeat --report-html --report-json --report-junit --report-tap --resolve --retry --retry-interval --ssl-no-revoke --test --to-entry --unix-socket --user --user-agent --variable --variables-file --verbose --very-verbose --help --version' -- "$cur"))
return
fi

View File

@ -17,6 +17,7 @@ complete -c hurl -l location -d 'Follow redirects'
complete -c hurl -l location-trusted -d 'Follow redirects but allows sending the name + password to all hosts that the site may redirect to'
complete -c hurl -l from-entry -d 'Execute Hurl file from ENTRY_NUMBER (starting at 1)'
complete -c hurl -l glob -d 'Specify input files that match the given GLOB. Multiple glob flags may be used'
complete -c hurl -l header -d 'Pass custom header(s) to server'
complete -c hurl -l http1.0 -d 'Tell Hurl to use HTTP version 1.0'
complete -c hurl -l http1.1 -d 'Tell Hurl to use HTTP version 1.1'
complete -c hurl -l http2 -d 'Tell Hurl to use HTTP version 2'

View File

@ -165,6 +165,7 @@ will follow a redirection only for the second entry.
| <a href="#file-root" id="file-root"><code>--file-root &lt;DIR&gt;</code></a> | Set root directory to import files in Hurl. This is used for files in multipart form data, request body and response output.<br>When it is not explicitly defined, files are relative to the Hurl file's directory.<br><br>This is a cli-only option.<br> |
| <a href="#from-entry" id="from-entry"><code>--from-entry &lt;ENTRY_NUMBER&gt;</code></a> | Execute Hurl file from ENTRY_NUMBER (starting at 1).<br><br>This is a cli-only option.<br> |
| <a href="#glob" id="glob"><code>--glob &lt;GLOB&gt;</code></a> | Specify input files that match the given glob pattern.<br><br>Multiple glob flags may be used. This flag supports common Unix glob patterns like *, ? and [].<br>However, to avoid your shell accidentally expanding glob patterns before Hurl handles them, you must use single quotes or double quotes around each pattern.<br><br>This is a cli-only option.<br> |
| <a href="#header" id="header"><code>-H, --header &lt;HEADER&gt;</code></a> | Add an extra header to include in information sent. Can be used several times in a command<br><br>Do not add newlines or carriage returns<br><br>This is a cli-only option.<br> |
| <a href="#http10" id="http10"><code>-0, --http1.0</code></a> | Tells Hurl to use HTTP version 1.0 instead of using its internally preferred HTTP version.<br> |
| <a href="#http11" id="http11"><code>--http1.1</code></a> | Tells Hurl to use HTTP version 1.1.<br> |
| <a href="#http2" id="http2"><code>--http2</code></a> | Tells Hurl to use HTTP version 2.<br>For HTTPS, this means Hurl negotiates HTTP/2 in the TLS handshake. Hurl does this by default.<br>For HTTP, this means Hurl attempts to upgrade the request to HTTP/2 using the Upgrade: request header.<br> |

View File

@ -1,4 +1,4 @@
.TH hurl 1 "04 Dec 2024" "hurl 6.1.0-SNAPSHOT" " Hurl Manual"
.TH hurl 1 "05 Dec 2024" "hurl 6.1.0-SNAPSHOT" " Hurl Manual"
.SH NAME
hurl - run and test HTTP requests.
@ -237,6 +237,14 @@ However, to avoid your shell accidentally expanding glob patterns before Hurl ha
This is a cli-only option.
.IP "-H, --header <HEADER> "
Add an extra header to include in information sent. Can be used several times in a command
Do not add newlines or carriage returns
This is a cli-only option.
.IP "-0, --http1.0 "
Tells Hurl to use HTTP version 1.0 instead of using its internally preferred HTTP version.

View File

@ -256,6 +256,14 @@ However, to avoid your shell accidentally expanding glob patterns before Hurl ha
This is a cli-only option.
### -H, --header <HEADER> {#header}
Add an extra header to include in information sent. Can be used several times in a command
Do not add newlines or carriage returns
This is a cli-only option.
### -0, --http1.0 {#http10}
Tells Hurl to use HTTP version 1.0 instead of using its internally preferred HTTP version.

View File

@ -1,4 +1,4 @@
.TH hurl 1 "04 Dec 2024" "hurl 6.1.0-SNAPSHOT" " Hurl Manual"
.TH hurl 1 "05 Dec 2024" "hurl 6.1.0-SNAPSHOT" " Hurl Manual"
.SH NAME
hurlfmt - format Hurl files

View File

@ -0,0 +1,12 @@
name: header
long: header
short: H
value: HEADER
help: Pass custom header(s) to server
help_heading: HTTP options
multi: append
cli_only: true
---
Add an extra header to include in information sent. Can be used several times in a command
Do not add newlines or carriage returns

View File

@ -22,6 +22,8 @@ HTTP options:
Maximum time allowed for connection [default: 300]
--connect-to <HOST1:PORT1:HOST2:PORT2>
For a request to the given HOST1:PORT1 pair, connect to HOST2:PORT2 instead
-H, --header <HEADER>
Pass custom header(s) to server
-0, --http1.0
Tell Hurl to use HTTP version 1.0
--http1.1

View File

@ -1262,6 +1262,7 @@ will follow a redirection only for the second entry.
| <a href="#file-root" id="file-root"><code>--file-root &lt;DIR&gt;</code></a> | Set root directory to import files in Hurl. This is used for files in multipart form data, request body and response output.<br>When it is not explicitly defined, files are relative to the Hurl file's directory.<br><br>This is a cli-only option.<br> |
| <a href="#from-entry" id="from-entry"><code>--from-entry &lt;ENTRY_NUMBER&gt;</code></a> | Execute Hurl file from ENTRY_NUMBER (starting at 1).<br><br>This is a cli-only option.<br> |
| <a href="#glob" id="glob"><code>--glob &lt;GLOB&gt;</code></a> | Specify input files that match the given glob pattern.<br><br>Multiple glob flags may be used. This flag supports common Unix glob patterns like *, ? and [].<br>However, to avoid your shell accidentally expanding glob patterns before Hurl handles them, you must use single quotes or double quotes around each pattern.<br><br>This is a cli-only option.<br> |
| <a href="#header" id="header"><code>-H, --header &lt;HEADER&gt;</code></a> | Add an extra header to include in information sent. Can be used several times in a command<br><br>Do not add newlines or carriage returns<br><br>This is a cli-only option.<br> |
| <a href="#http10" id="http10"><code>-0, --http1.0</code></a> | Tells Hurl to use HTTP version 1.0 instead of using its internally preferred HTTP version.<br> |
| <a href="#http11" id="http11"><code>--http1.1</code></a> | Tells Hurl to use HTTP version 1.1.<br> |
| <a href="#http2" id="http2"><code>--http2</code></a> | Tells Hurl to use HTTP version 2.<br>For HTTPS, this means Hurl negotiates HTTP/2 in the TLS handshake. Hurl does this by default.<br>For HTTP, this means Hurl attempts to upgrade the request to HTTP/2 using the Upgrade: request header.<br> |

View File

@ -205,6 +205,17 @@ pub fn glob() -> clap::Arg {
.action(clap::ArgAction::Append)
}
pub fn header() -> clap::Arg {
clap::Arg::new("header")
.long("header")
.short('H')
.value_name("HEADER")
.help("Pass custom header(s) to server")
.help_heading("HTTP options")
.num_args(1)
.action(clap::ArgAction::Append)
}
pub fn http10() -> clap::Arg {
clap::Arg::new("http10")
.long("http1.0")

View File

@ -155,6 +155,10 @@ pub fn from_entry(arg_matches: &ArgMatches) -> Option<usize> {
get::<u32>(arg_matches, "from_entry").map(|x| x as usize)
}
pub fn headers(arg_matches: &ArgMatches) -> Vec<String> {
get_strings(arg_matches, "header").unwrap_or_default()
}
pub fn html_dir(arg_matches: &ArgMatches) -> Result<Option<PathBuf>, CliOptionsError> {
if let Some(dir) = get::<String>(arg_matches, "report_html") {
let path = Path::new(&dir);

View File

@ -61,6 +61,7 @@ pub struct CliOptions {
pub follow_location: bool,
pub follow_location_trusted: bool,
pub from_entry: Option<usize>,
pub headers: Vec<String>,
pub html_dir: Option<PathBuf>,
pub http_version: Option<HttpVersion>,
pub ignore_asserts: bool,
@ -177,6 +178,7 @@ pub fn parse() -> Result<CliOptions, CliOptionsError> {
.arg(commands::compressed())
.arg(commands::connect_timeout())
.arg(commands::connect_to())
.arg(commands::header())
.arg(commands::http10())
.arg(commands::http11())
.arg(commands::http2())
@ -282,6 +284,7 @@ fn parse_matches(arg_matches: &ArgMatches) -> Result<CliOptions, CliOptionsError
let file_root = matches::file_root(arg_matches);
let (follow_location, follow_location_trusted) = matches::follow_location(arg_matches);
let from_entry = matches::from_entry(arg_matches);
let headers = matches::headers(arg_matches);
let html_dir = matches::html_dir(arg_matches)?;
let http_version = matches::http_version(arg_matches);
let ignore_asserts = matches::ignore_asserts(arg_matches);
@ -340,6 +343,7 @@ fn parse_matches(arg_matches: &ArgMatches) -> Result<CliOptions, CliOptionsError
follow_location,
follow_location_trusted,
from_entry,
headers,
html_dir,
http_version,
ignore_asserts,
@ -415,6 +419,7 @@ impl CliOptions {
let follow_location = self.follow_location;
let follow_location_trusted = self.follow_location_trusted;
let from_entry = self.from_entry;
let headers = &self.headers;
let http_version = match self.http_version {
Some(version) => version.into(),
None => RequestedHttpVersion::default(),
@ -473,6 +478,7 @@ impl CliOptions {
.follow_location(follow_location)
.follow_location_trusted(follow_location_trusted)
.from_entry(from_entry)
.headers(headers)
.http_version(http_version)
.ignore_asserts(ignore_asserts)
.insecure(insecure)

View File

@ -488,9 +488,14 @@ impl Client {
self.set_multipart(&request_spec.multipart)?;
let request_spec_body = &request_spec.body.bytes();
self.set_body(request_spec_body)?;
let headers_spec = &request_spec.headers;
let options_headers = options
.headers
.iter()
.map(|h| h.as_str())
.collect::<Vec<&str>>();
let headers = &request_spec.headers.aggregate_raw_headers(&options_headers);
self.set_headers(
headers_spec,
headers,
request_spec.implicit_content_type.as_deref(),
options,
)?;
@ -538,19 +543,19 @@ impl Client {
/// Sets HTTP headers.
fn set_headers(
&mut self,
headers_spec: &HeaderVec,
headers: &HeaderVec,
implicit_content_type: Option<&str>,
options: &ClientOptions,
) -> Result<(), HttpError> {
let mut list = List::new();
for header in headers_spec {
for header in headers {
list.append(&format!("{}: {}", header.name, header.value))?;
}
// If request has no Content-Type header, we set it if the content type has been set
// implicitly on this request.
if !headers_spec.contains_key(CONTENT_TYPE) {
if !headers.contains_key(CONTENT_TYPE) {
if let Some(s) = implicit_content_type {
list.append(&format!("{}: {s}", CONTENT_TYPE))?;
} else {
@ -566,13 +571,13 @@ impl Client {
// libcurl will generate `SignedHeaders` that include `expect` even though the header is not
// present, causing some APIs to reject the request.
// Therefore, we only remove this header when not in aws_sigv4 mode.
if !headers_spec.contains_key(EXPECT) && options.aws_sigv4.is_none() {
if !headers.contains_key(EXPECT) && options.aws_sigv4.is_none() {
// We remove default Expect headers added by curl because we want
// to explicitly manage this header.
list.append(&format!("{}:", EXPECT))?;
}
if !headers_spec.contains_key(USER_AGENT) {
if !headers.contains_key(USER_AGENT) {
let user_agent = match options.user_agent {
Some(ref u) => u.clone(),
None => format!("hurl/{}", clap::crate_version!()),
@ -592,12 +597,12 @@ impl Client {
} else {
let user = user.as_bytes();
let authorization = general_purpose::STANDARD.encode(user);
if !headers_spec.contains_key(AUTHORIZATION) {
if !headers.contains_key(AUTHORIZATION) {
list.append(&format!("{}: Basic {authorization}", AUTHORIZATION))?;
}
}
}
if options.compressed && !headers_spec.contains_key(ACCEPT_ENCODING) {
if options.compressed && !headers.contains_key(ACCEPT_ENCODING) {
list.append(&format!("{}: gzip, deflate, br", ACCEPT_ENCODING))?;
}

View File

@ -17,8 +17,8 @@
*/
use crate::http::client::all_cookies;
use crate::http::{
Body, ClientOptions, Cookie, FileParam, Header, IpResolve, Method, MultipartParam, Param,
RequestSpec, RequestedHttpVersion, CONTENT_TYPE,
Body, ClientOptions, Cookie, FileParam, Header, HeaderVec, IpResolve, Method, MultipartParam,
Param, RequestSpec, RequestedHttpVersion, CONTENT_TYPE,
};
use crate::runner::Output;
use crate::util::path::ContextDir;
@ -63,7 +63,17 @@ impl CurlCmd {
let mut params = method_params(request_spec);
args.append(&mut params);
let mut params = headers_params(request_spec);
let options_headers = options
.headers
.iter()
.map(|h| h.as_str())
.collect::<Vec<&str>>();
let headers = &request_spec.headers.aggregate_raw_headers(&options_headers);
let mut params = headers_params(
headers,
request_spec.implicit_content_type.as_deref(),
&request_spec.body,
);
args.append(&mut params);
let mut params = body_params(request_spec, context_dir);
@ -90,28 +100,33 @@ fn method_params(request_spec: &RequestSpec) -> Vec<String> {
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> {
/// Returns the curl args corresponding to the HTTP headers, from a list of headers,
/// an optional implicit content type, and the request body.
fn headers_params(
headers: &HeaderVec,
implicit_content_type: Option<&str>,
body: &Body,
) -> Vec<String> {
let mut args = vec![];
for header in request_spec.headers.iter() {
for header in headers.iter() {
args.append(&mut header.curl_args());
}
let has_explicit_content_type = request_spec.headers.contains_key(CONTENT_TYPE);
let has_explicit_content_type = headers.contains_key(CONTENT_TYPE);
if has_explicit_content_type {
return args;
}
if let Some(content_type) = &request_spec.implicit_content_type {
if let Some(content_type) = 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 {
} else if !body.bytes().is_empty() {
match body {
Body::Text(_) => {
args.push("--header".to_string());
args.push(format!("'{}:'", CONTENT_TYPE));
@ -608,6 +623,10 @@ mod tests {
cookie_input_file: Some("cookie_file".to_string()),
follow_location: true,
follow_location_trusted: false,
headers: vec![
"Test-Header-1: content-1".to_string(),
"Test-Header-2: content-2".to_string(),
],
http_version: RequestedHttpVersion::Http10,
insecure: true,
ip_resolve: IpResolve::IpV6,
@ -637,6 +656,8 @@ mod tests {
assert_eq!(
cmd.to_string(),
"curl \
--header 'Test-Header-1: content-1' \
--header 'Test-Header-2: content-2' \
--compressed \
--connect-timeout 20 \
--connect-to example.com:443:host-47.example.com:443 \

View File

@ -64,7 +64,6 @@ impl HeaderVec {
/// Aggregates the headers from `self` and `raw_headers`
///
/// Returns the aggregated `HeaderVec`
#[allow(dead_code)]
pub fn aggregate_raw_headers(&self, raw_headers: &[&str]) -> HeaderVec {
let mut headers = self.clone();
let to_aggregate = raw_headers.iter().filter_map(|h| Header::parse(h));

View File

@ -33,6 +33,7 @@ pub struct ClientOptions {
pub cookie_input_file: Option<String>,
pub follow_location: bool,
pub follow_location_trusted: bool,
pub headers: Vec<String>,
pub http_version: RequestedHttpVersion,
pub insecure: bool,
pub ip_resolve: IpResolve,
@ -75,6 +76,7 @@ impl Default for ClientOptions {
cookie_input_file: None,
follow_location: false,
follow_location_trusted: false,
headers: vec![],
http_version: RequestedHttpVersion::default(),
insecure: false,
ip_resolve: IpResolve::default(),

View File

@ -219,6 +219,7 @@ impl ClientOptions {
cookie_input_file: runner_options.cookie_input_file.clone(),
follow_location: runner_options.follow_location,
follow_location_trusted: runner_options.follow_location_trusted,
headers: runner_options.headers.clone(),
http_version: runner_options.http_version,
ip_resolve: runner_options.ip_resolve,
max_filesize: runner_options.max_filesize,

View File

@ -39,6 +39,7 @@ pub struct RunnerOptionsBuilder {
follow_location: bool,
follow_location_trusted: bool,
from_entry: Option<usize>,
headers: Vec<String>,
http_version: RequestedHttpVersion,
ignore_asserts: bool,
insecure: bool,
@ -86,6 +87,7 @@ impl Default for RunnerOptionsBuilder {
follow_location: false,
follow_location_trusted: false,
from_entry: None,
headers: vec![],
http_version: RequestedHttpVersion::default(),
ignore_asserts: false,
insecure: false,
@ -235,6 +237,12 @@ impl RunnerOptionsBuilder {
self
}
/// Sets additional headers (overrides if a header already exists).
pub fn headers(&mut self, header: &[String]) -> &mut Self {
self.headers = header.to_vec();
self
}
/// Set requested HTTP version (can be different of the effective HTTP version).
pub fn http_version(&mut self, version: RequestedHttpVersion) -> &mut Self {
self.http_version = version;
@ -425,6 +433,7 @@ impl RunnerOptionsBuilder {
follow_location: self.follow_location,
follow_location_trusted: self.follow_location_trusted,
from_entry: self.from_entry,
headers: self.headers.clone(),
http_version: self.http_version,
ignore_asserts: self.ignore_asserts,
insecure: self.insecure,
@ -473,6 +482,7 @@ pub struct RunnerOptions {
pub(crate) follow_location: bool,
pub(crate) follow_location_trusted: bool,
pub(crate) from_entry: Option<usize>,
pub(crate) headers: Vec<String>,
pub(crate) http_version: RequestedHttpVersion,
pub(crate) ignore_asserts: bool,
pub(crate) ip_resolve: IpResolve,