mirror of
https://github.com/Orange-OpenSource/hurl.git
synced 2024-11-23 00:44:55 +03:00
Introduces HeaderVec struct to represent a list of HTTP headers.
This commit is contained in:
parent
b543c4b522
commit
6d77de105c
@ -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)?;
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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};
|
||||
|
@ -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¶m2=¶m3=a%3Db¶m4=1%2C2%2C3", vec![], vec![])
|
||||
Request::new("GET", "http://localhost:8000/querystring-params?param1=value1¶m2=¶m3=a%3Db¶m4=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()
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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 }),
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user