Introduces HeaderVec struct to represent a list of HTTP headers.

This commit is contained in:
jcamiel 2024-02-09 17:48:24 +01:00
parent b543c4b522
commit 6d77de105c
No known key found for this signature in database
GPG Key ID: 07FF11CFD55356CC
9 changed files with 182 additions and 64 deletions

View File

@ -29,6 +29,9 @@ use url::Url;
use crate::http::certificate::Certificate;
use crate::http::core::*;
use crate::http::header::{
HeaderVec, ACCEPT_ENCODING, AUTHORIZATION, CONTENT_TYPE, EXPECT, USER_AGENT,
};
use crate::http::options::ClientOptions;
use crate::http::request::*;
use crate::http::request_spec::*;
@ -164,7 +167,7 @@ impl Client {
let start = Utc::now();
let verbose = options.verbosity.is_some();
let very_verbose = options.verbosity == Some(Verbosity::VeryVerbose);
let mut request_headers: Vec<Header> = vec![];
let mut request_headers = HeaderVec::new();
let mut status_lines = vec![];
let mut response_headers = vec![];
let has_body_data = !request_spec.body.bytes().is_empty()
@ -523,15 +526,15 @@ impl Client {
}
// If request has no Content-Type header, we set it if the content type has been set explicitly on this request.
if request.get_header_values(Header::CONTENT_TYPE).is_empty() {
if request.get_header_values(CONTENT_TYPE).is_empty() {
if let Some(s) = &request.content_type {
list.append(&format!("{}: {s}", Header::CONTENT_TYPE))?;
list.append(&format!("{}: {s}", CONTENT_TYPE))?;
} else {
// We remove default Content-Type headers added by curl because we want
// to explicitly manage this header.
// For instance, with --data option, curl will send a 'Content-type: application/x-www-form-urlencoded'
// header.
list.append(&format!("{}:", Header::CONTENT_TYPE))?;
list.append(&format!("{}:", CONTENT_TYPE))?;
}
}
@ -539,18 +542,18 @@ 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 request.get_header_values(Header::EXPECT).is_empty() && options.aws_sigv4.is_none() {
if request.get_header_values(EXPECT).is_empty() && options.aws_sigv4.is_none() {
// We remove default Expect headers added by curl because we want
// to explicitly manage this header.
list.append(&format!("{}:", Header::EXPECT))?;
list.append(&format!("{}:", EXPECT))?;
}
if request.get_header_values(Header::USER_AGENT).is_empty() {
if request.get_header_values(USER_AGENT).is_empty() {
let user_agent = match options.user_agent {
Some(ref u) => u.clone(),
None => format!("hurl/{}", clap::crate_version!()),
};
list.append(&format!("{}: {user_agent}", Header::USER_AGENT))?;
list.append(&format!("{}: {user_agent}", USER_AGENT))?;
}
if let Some(ref user) = options.user {
@ -565,17 +568,13 @@ impl Client {
} else {
let user = user.as_bytes();
let authorization = general_purpose::STANDARD.encode(user);
if request.get_header_values(Header::AUTHORIZATION).is_empty() {
list.append(&format!("{}: Basic {authorization}", Header::AUTHORIZATION))?;
if request.get_header_values(AUTHORIZATION).is_empty() {
list.append(&format!("{}: Basic {authorization}", AUTHORIZATION))?;
}
}
}
if options.compressed
&& request
.get_header_values(Header::ACCEPT_ENCODING)
.is_empty()
{
list.append(&format!("{}: gzip, deflate, br", Header::ACCEPT_ENCODING))?;
if options.compressed && request.get_header_values(ACCEPT_ENCODING).is_empty() {
list.append(&format!("{}: gzip, deflate, br", ACCEPT_ENCODING))?;
}
self.handle.http_headers(list)?;

View File

@ -16,6 +16,14 @@
*
*/
use core::fmt;
use std::slice::Iter;
pub const ACCEPT_ENCODING: &str = "Accept-Encoding";
pub const AUTHORIZATION: &str = "Authorization";
pub const COOKIE: &str = "Cookie";
pub const CONTENT_TYPE: &str = "Content-Type";
pub const EXPECT: &str = "Expect";
pub const USER_AGENT: &str = "User-Agent";
/// Represents an HTTP header
#[derive(Clone, Debug, PartialEq, Eq)]
@ -31,12 +39,6 @@ impl fmt::Display for Header {
}
impl Header {
pub const ACCEPT_ENCODING: &'static str = "Accept-Encoding";
pub const AUTHORIZATION: &'static str = "Authorization";
pub const CONTENT_TYPE: &'static str = "Content-Type";
pub const EXPECT: &'static str = "Expect";
pub const USER_AGENT: &'static str = "User-Agent";
pub fn new(name: &str, value: &str) -> Self {
Header {
name: name.to_string(),
@ -58,3 +60,118 @@ pub fn get_values(headers: &[Header], name: &str) -> Vec<String> {
})
.collect()
}
/// Represents an ordered list of [`Header`].
/// The headers are sorted by insertion order.
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct HeaderVec {
headers: Vec<Header>,
}
impl HeaderVec {
/// Creates an empty [`HeaderVec`].
pub fn new() -> Self {
HeaderVec::default()
}
/// Returns a reference to the header associated with `name`.
///
/// If there are multiple headers associated with `name`, then the first one is returned.
/// Use [`get_all`] to get all values associated with a given key.
pub fn get(&self, name: &str) -> Option<&Header> {
self.headers
.iter()
.find(|h| h.name.to_lowercase() == name.to_lowercase())
}
/// Returns a list of header associated with `name`.
pub fn get_all(&self, name: &str) -> Vec<&Header> {
self.headers
.iter()
.filter(|h| h.name.to_lowercase() == name.to_lowercase())
.collect()
}
/// Returns an iterator over all the headers.
pub fn iter(&self) -> impl Iterator<Item = &Header> {
self.headers.iter()
}
/// Returns the number of headers stored in the list.
///
/// This number represents the total numbers of header, including header with the same name and
/// different values.
pub fn len(&self) -> usize {
self.headers.len()
}
/// Push a new `header` into the headers list.
pub fn push(&mut self, header: Header) {
self.headers.push(header)
}
}
impl<'a> IntoIterator for &'a HeaderVec {
type Item = &'a Header;
type IntoIter = Iter<'a, Header>;
fn into_iter(self) -> Self::IntoIter {
self.headers.iter()
}
}
#[cfg(test)]
mod tests {
use crate::http::header::HeaderVec;
use crate::http::Header;
#[test]
fn test_simple_header_map() {
let mut headers = HeaderVec::new();
headers.push(Header::new("foo", "xxx"));
headers.push(Header::new("bar", "yyy0"));
headers.push(Header::new("bar", "yyy1"));
headers.push(Header::new("bar", "yyy2"));
headers.push(Header::new("baz", "zzz"));
assert_eq!(headers.len(), 5);
assert_eq!(headers.get("foo"), Some(&Header::new("foo", "xxx")));
assert_eq!(headers.get("FOO"), Some(&Header::new("foo", "xxx")));
assert_eq!(headers.get("bar"), Some(&Header::new("bar", "yyy0")));
assert_eq!(headers.get("qux"), None);
assert_eq!(
headers.get_all("bar"),
vec![
&Header::new("bar", "yyy0"),
&Header::new("bar", "yyy1"),
&Header::new("bar", "yyy2"),
]
);
assert_eq!(headers.get_all("BAZ"), vec![&Header::new("baz", "zzz")]);
assert_eq!(headers.get_all("qux"), Vec::<&Header>::new());
}
#[test]
fn test_iter() {
let data = vec![("foo", "xxx"), ("bar", "yyy0"), ("baz", "yyy1")];
let mut headers = HeaderVec::new();
data.iter()
.for_each(|(name, value)| headers.push(Header::new(name, value)));
// Test iter()
for (i, h) in headers.iter().enumerate() {
assert_eq!(h.name, data[i].0);
assert_eq!(h.value, data[i].1)
}
// Test into_iter()
let mut i = 0;
for h in &headers {
assert_eq!(h.name, data[i].0);
assert_eq!(h.value, data[i].1);
i += 1;
}
}
}

View File

@ -21,7 +21,9 @@ pub(crate) use self::client::Client;
pub use self::cookie::{CookieAttribute, ResponseCookie};
pub(crate) use self::core::{Cookie, Param, RequestCookie};
pub(crate) use self::error::HttpError;
pub use self::header::Header;
pub use self::header::{
Header, ACCEPT_ENCODING, AUTHORIZATION, CONTENT_TYPE, COOKIE, EXPECT, USER_AGENT,
};
pub(crate) use self::options::{ClientOptions, Verbosity};
pub use self::request::{IpResolve, Request, RequestedHttpVersion};
pub(crate) use self::request_spec::{Body, FileParam, Method, MultipartParam, RequestSpec};

View File

@ -20,7 +20,8 @@ use std::fmt;
use url::Url;
use crate::http::core::*;
use crate::http::{header, Header, HttpError};
use crate::http::header::{HeaderVec, CONTENT_TYPE, COOKIE};
use crate::http::HttpError;
/// Represents a runtime HTTP request.
/// This is a real request, that has been executed by our HTTP client.
@ -31,7 +32,7 @@ use crate::http::{header, Header, HttpError};
pub struct Request {
pub url: String,
pub method: String,
pub headers: Vec<Header>,
pub headers: HeaderVec,
pub body: Vec<u8>,
}
@ -68,7 +69,7 @@ pub enum IpResolve {
impl Request {
/// Creates a new request.
pub fn new(method: &str, url: &str, headers: Vec<Header>, body: Vec<u8>) -> Self {
pub fn new(method: &str, url: &str, headers: HeaderVec, body: Vec<u8>) -> Self {
Request {
url: url.to_string(),
method: method.to_string(),
@ -96,17 +97,15 @@ impl Request {
/// see <https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cookie>
pub fn cookies(&self) -> Vec<RequestCookie> {
self.headers
.get_all(COOKIE)
.iter()
.filter(|h| h.name.as_str() == "Cookie")
.flat_map(|h| parse_cookies(h.value.as_str().trim()))
.collect()
}
/// Returns optional Content-type header value.
pub fn content_type(&self) -> Option<String> {
header::get_values(&self.headers, Header::CONTENT_TYPE)
.first()
.cloned()
pub fn content_type(&self) -> Option<&str> {
self.headers.get(CONTENT_TYPE).map(|h| h.value.as_str())
}
/// Returns the base url http(s)://host(:port)
@ -152,32 +151,26 @@ fn parse_cookie(s: &str) -> RequestCookie {
#[cfg(test)]
mod tests {
use super::*;
use crate::http::RequestCookie;
use crate::http::{Header, RequestCookie};
fn hello_request() -> Request {
Request::new(
"GET",
"http://localhost:8000/hello",
vec![
Header::new("Host", "localhost:8000"),
Header::new("Accept", "*/*"),
Header::new("User-Agent", "hurl/1.0"),
],
vec![],
)
let mut headers = HeaderVec::new();
headers.push(Header::new("Host", "localhost:8000"));
headers.push(Header::new("Accept", "*/*"));
headers.push(Header::new("User-Agent", "hurl/1.0"));
headers.push(Header::new("content-type", "application/json"));
Request::new("GET", "http://localhost:8000/hello", headers, vec![])
}
fn query_string_request() -> Request {
Request::new("GET", "http://localhost:8000/querystring-params?param1=value1&param2=&param3=a%3Db&param4=1%2C2%2C3", vec![], vec![])
Request::new("GET", "http://localhost:8000/querystring-params?param1=value1&param2=&param3=a%3Db&param4=1%2C2%2C3", HeaderVec::new(), vec![])
}
fn cookies_request() -> Request {
Request::new(
"GET",
"http://localhost:8000/cookies",
vec![Header::new("Cookie", "cookie1=value1; cookie2=value2")],
vec![],
)
let mut headers = HeaderVec::new();
headers.push(Header::new("Cookie", "cookie1=value1; cookie2=value2"));
Request::new("GET", "http://localhost:8000/cookies", headers, vec![])
}
#[test]
@ -206,6 +199,13 @@ mod tests {
)
}
#[test]
fn test_content_type() {
assert_eq!(hello_request().content_type(), Some("application/json"));
assert_eq!(query_string_request().content_type(), None);
assert_eq!(cookies_request().content_type(), None);
}
#[test]
fn test_cookies() {
assert!(hello_request().cookies().is_empty());
@ -255,7 +255,7 @@ mod tests {
#[test]
fn test_base_url() {
assert_eq!(
Request::new("", "http://localhost", vec![], vec![])
Request::new("", "http://localhost", HeaderVec::new(), vec![])
.base_url()
.unwrap(),
"http://localhost".to_string()
@ -264,7 +264,7 @@ mod tests {
Request::new(
"",
"http://localhost:8000/redirect-relative",
vec![],
HeaderVec::new(),
vec![]
)
.base_url()
@ -272,7 +272,7 @@ mod tests {
"http://localhost:8000".to_string()
);
assert_eq!(
Request::new("", "https://localhost:8000", vec![], vec![])
Request::new("", "https://localhost:8000", HeaderVec::new(), vec![])
.base_url()
.unwrap(),
"https://localhost:8000".to_string()

View File

@ -27,7 +27,7 @@ impl Request {
// If it ok, we print each line of the body in debug format. Otherwise, we
// print the body first 64 bytes.
if let Some(content_type) = self.content_type() {
if !mimetype::is_kind_of_text(&content_type) {
if !mimetype::is_kind_of_text(content_type) {
debug::log_bytes(&self.body, 64, debug, logger);
return;
}

View File

@ -23,7 +23,7 @@ impl Request {
/// Returns character encoding of the HTTP request.
fn character_encoding(&self) -> Result<EncodingRef, HttpError> {
match self.content_type() {
Some(content_type) => match mimetype::charset(&content_type) {
Some(content_type) => match mimetype::charset(content_type) {
Some(charset) => {
match encoding::label::encoding_from_whatwg_label(charset.as_str()) {
None => Err(HttpError::InvalidCharset { charset }),

View File

@ -18,6 +18,7 @@
use std::collections::HashMap;
use crate::http::core::*;
use crate::http::header::CONTENT_TYPE;
use crate::http::{RequestSpec, *};
use crate::util::path::ContextDir;
@ -39,31 +40,28 @@ impl RequestSpec {
.headers
.iter()
.map(|h| &h.name)
.any(|n| n.as_str() == Header::CONTENT_TYPE);
.any(|n| n.as_str() == CONTENT_TYPE);
if !has_explicit_content_type {
if let Some(content_type) = &self.content_type {
if content_type.as_str() != "application/x-www-form-urlencoded"
&& content_type.as_str() != "multipart/form-data"
{
arguments.push("--header".to_string());
arguments.push(format!("'{}: {content_type}'", Header::CONTENT_TYPE));
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!("'{}:'", Header::CONTENT_TYPE))
arguments.push(format!("'{}:'", CONTENT_TYPE))
}
Body::Binary(_) => {
arguments.push("--header".to_string());
arguments.push(format!(
"'{}: application/octet-stream'",
Header::CONTENT_TYPE
))
arguments.push(format!("'{}: application/octet-stream'", CONTENT_TYPE))
}
Body::File(_, _) => {
arguments.push("--header".to_string());
arguments.push(format!("'{}:'", Header::CONTENT_TYPE))
arguments.push(format!("'{}:'", CONTENT_TYPE))
}
}
}

View File

@ -19,6 +19,7 @@ use std::fmt;
use std::time::Duration;
use crate::http::certificate::Certificate;
use crate::http::header::CONTENT_TYPE;
use crate::http::{header, Header};
/// Represents a runtime HTTP response.
@ -78,7 +79,7 @@ impl Response {
/// Returns optional Content-type header value.
pub fn content_type(&self) -> Option<String> {
header::get_values(&self.headers, Header::CONTENT_TYPE)
header::get_values(&self.headers, CONTENT_TYPE)
.first()
.cloned()
}

View File

@ -22,6 +22,7 @@ use base64::Engine;
use hurl_core::ast::*;
use crate::http;
use crate::http::AUTHORIZATION;
use crate::runner::body::eval_body;
use crate::runner::error::Error;
use crate::runner::multipart::eval_multipart_param;
@ -55,7 +56,7 @@ pub fn eval_request(
let user_password = user_password.as_bytes();
let authorization = general_purpose::STANDARD.encode(user_password);
let value = format!("Basic {authorization}");
let header = http::Header::new(http::Header::AUTHORIZATION, &value);
let header = http::Header::new(AUTHORIZATION, &value);
headers.push(header);
}