remove dependencies reqwest, percent-encoding and cookie

This commit is contained in:
Fabrice Reix 2020-09-17 19:55:25 +02:00
parent 2f84c178a8
commit 5b7f59dc7b
16 changed files with 9 additions and 3259 deletions

955
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -16,7 +16,6 @@ strict = []
[dependencies] [dependencies]
clap = "2.33.0" clap = "2.33.0"
structopt = "0.2.10" structopt = "0.2.10"
reqwest = "0.9.20"
libxml = "0.2.12" libxml = "0.2.12"
regex = "1.1.0" regex = "1.1.0"
serde_json = "1.0.40" serde_json = "1.0.40"
@ -27,8 +26,6 @@ atty = "0.2.13"
url = "2.1.0" url = "2.1.0"
sxd-document = "0.3.2" sxd-document = "0.3.2"
serde = "1.0.104" serde = "1.0.104"
percent-encoding = "2.1.0"
cookie = "0.12.0"
base64 = "0.11.0" base64 = "0.11.0"
float-cmp = "0.6.0" float-cmp = "0.6.0"
encoding = "0.2" encoding = "0.2"

View File

@ -15,9 +15,6 @@
* limitations under the License. * limitations under the License.
* *
*/ */
use std::fs;
use crate::http;
use super::Error; use super::Error;
@ -32,34 +29,6 @@ pub fn cookies_output_file(filename: String, n: usize) -> Result<std::path::Path
} }
} }
pub fn cookies(filename: &str) -> Result<Vec<http::cookie::Cookie>, Error> {
let path = std::path::Path::new(filename);
if !path.exists() {
return Err(Error {
message: format!("file {} does not exist", filename)
});
}
let s = fs::read_to_string(filename).expect("Something went wrong reading the file");
let lines: Vec<&str> = regex::Regex::new(r"\n|\r\n")
.unwrap()
.split(&s)
.collect();
let mut cookies = vec![];
for line in lines {
if line.starts_with('#') || line.is_empty() {
continue;
}
if let Some(cookie) = http::cookie::Cookie::from_netscape(line) {
cookies.push(cookie);
} else {
return Err(Error {
message: format!("Cookie {} can not be parsed", line)
});
};
}
Ok(cookies)
}
pub fn output_color(color_present: bool, no_color_present: bool, stdout: bool) -> bool { pub fn output_color(color_present: bool, no_color_present: bool, stdout: bool) -> bool {
if color_present { if color_present {
@ -71,55 +40,8 @@ pub fn output_color(color_present: bool, no_color_present: bool, stdout: bool) -
} }
} }
pub fn redirect(redirect_present: bool, max_redirect: &str) -> Result<http::client::Redirect, Error> {
if redirect_present {
if max_redirect == "-1" {
Ok(http::client::Redirect::Unlimited)
} else if let Ok(n) = max_redirect.parse::<usize>() {
Ok(http::client::Redirect::Limited(n))
} else {
Err(Error { message: "Invalid value for option --max-redirs".to_string() })
}
} else {
Ok(http::client::Redirect::None)
}
}
pub fn validate_proxy(url: String) -> Result<String, Error> {
// validate proxy value at parsing
// use code from reqwest for the timebeing
let url = if url.starts_with("http") {
url
} else {
format!("http://{}", url)
};
match reqwest::Proxy::http(url.as_str()) {
Ok(_) => Ok(url),
Err(_) => Err(Error { message: format!("Invalid proxy url <{}>", url) })
}
}
pub fn proxy(option_value: Option<&str>, env_value: Option<String>) -> Result<Option<String>, Error> {
match option_value {
Some(url) => if url.is_empty() {
Ok(None)
} else {
let url = validate_proxy(url.to_string())?;
Ok(Some(url))
},
None => match env_value {
Some(url) => if url.is_empty() {
Ok(None)
} else {
let url = validate_proxy(url)?;
Ok(Some(url))
},
None => Ok(None),
}
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
@ -131,20 +53,5 @@ mod tests {
assert_eq!(output_color(false, false, true), true); assert_eq!(output_color(false, false, true), true);
} }
#[test]
fn test_redirect() {
assert_eq!(redirect(false, "10").unwrap(), http::client::Redirect::None);
assert_eq!(redirect(true, "10").unwrap(), http::client::Redirect::Limited(10));
assert_eq!(redirect(true, "-1").unwrap(), http::client::Redirect::Unlimited);
assert_eq!(redirect(true, "A").err().unwrap().message, "Invalid value for option --max-redirs");
}
#[test]
fn test_http_proxy() {
assert_eq!(proxy(None, None).unwrap(), None);
assert_eq!(proxy(Some("http://localhost:8001"), None).unwrap(), Some("http://localhost:8001".to_string()));
assert_eq!(proxy(Some("http://localhost:8001"), Some("http://localhost:8002".to_string())).unwrap(), Some("http://localhost:8001".to_string()));
assert_eq!(proxy(Some(""), Some("http://localhost:8002".to_string())).unwrap(), None);
assert_eq!(proxy(None, Some("http://localhost:8002".to_string())).unwrap(), Some("http://localhost:8002".to_string()));
}
} }

View File

@ -17,7 +17,7 @@
*/ */
use std::fmt; use std::fmt;
use serde::{Deserialize, Serialize}; use serde::Serialize;
use serde::ser::Serializer; use serde::ser::Serializer;
#[derive(Clone, Debug, PartialEq, Eq)] #[derive(Clone, Debug, PartialEq, Eq)]
@ -31,7 +31,7 @@ pub enum DeprecatedValue {
ListInt(Vec<i32>), ListInt(Vec<i32>),
} }
#[derive(Clone, Debug, PartialEq, Eq, Deserialize)] #[derive(Clone, Debug, PartialEq, Eq)]
//#[derive(Clone, Debug, PartialEq, PartialOrd)] //#[derive(Clone, Debug, PartialEq, PartialOrd)]
pub enum Value { pub enum Value {
Bool(bool), Bool(bool),
@ -130,13 +130,13 @@ impl Value {
} }
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[derive(Clone, Debug, PartialEq, Eq)]
pub struct Pos { pub struct Pos {
pub line: usize, pub line: usize,
pub column: usize, pub column: usize,
} }
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[derive(Clone, Debug, PartialEq, Eq)]
pub struct SourceInfo { pub struct SourceInfo {
pub start: Pos, pub start: Pos,
pub end: Pos, pub end: Pos,

View File

@ -1,216 +0,0 @@
/*
* hurl (https://hurl.dev)
* Copyright (C) 2020 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::path::Path;
use super::core::*;
use super::request::*;
use super::response::*;
pub struct Client {
_inner_client: reqwest::Client,
options: ClientOptions,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ClientOptions {
pub noproxy_hosts: Vec<String>,
pub insecure: bool,
pub redirect: Redirect,
pub http_proxy: Option<String>,
pub https_proxy: Option<String>,
pub all_proxy: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Redirect {
None,
Limited(usize),
Unlimited,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct HttpError {
pub url: String,
pub message: String,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Proxy {
pub protocol: Option<ProxyProtocol>,
pub host: String,
pub port: Option<u16>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum ProxyProtocol {
Http,
Https,
}
pub fn get_redirect_policy(redirect: Redirect) -> reqwest::RedirectPolicy {
match redirect {
Redirect::None => reqwest::RedirectPolicy::none(),
Redirect::Limited(max) => reqwest::RedirectPolicy::limited(max),
Redirect::Unlimited {} => reqwest::RedirectPolicy::custom(|attempt| { attempt.follow() }),
}
}
impl Client {
pub fn init(options: ClientOptions) -> Client {
let client_builder = reqwest::Client::builder()
.redirect(get_redirect_policy(options.redirect.clone()))
.use_sys_proxy()
.danger_accept_invalid_hostnames(options.insecure)
.danger_accept_invalid_certs(options.insecure)
.cookie_store(false);
Client {
_inner_client: client_builder.build().unwrap(),
options,
}
}
pub fn execute(&self, request: &Request) -> Result<Response, HttpError> {
let mut headers = reqwest::header::HeaderMap::new();
for header in request.clone().headers() {
headers.append(
reqwest::header::HeaderName::from_lowercase(
header.name.to_lowercase().as_str().as_bytes(),
)
.unwrap(),
reqwest::header::HeaderValue::from_str(header.value.as_str()).unwrap(),
);
}
let client_builder = reqwest::Client::builder()
.redirect(get_redirect_policy(self.options.redirect.clone()))
.danger_accept_invalid_hostnames(self.options.insecure)
.danger_accept_invalid_certs(self.options.insecure)
.cookie_store(false);
let client_builder = if let Some(url) = self.options.http_proxy.clone() {
let proxy = reqwest::Proxy::http(url.as_str()).unwrap();
client_builder.proxy(proxy)
} else {
client_builder
};
let client_builder = if let Some(url) = self.options.https_proxy.clone() {
let proxy = reqwest::Proxy::https(url.as_str()).unwrap();
client_builder.proxy(proxy)
} else {
client_builder
};
let client_builder = if let Some(url) = self.options.all_proxy.clone() {
let proxy = reqwest::Proxy::all(url.as_str()).unwrap();
client_builder.proxy(proxy)
} else {
client_builder
};
let client_builder = if self.options.noproxy_hosts.contains(&request.url.host.clone()) {
client_builder.no_proxy()
} else {
client_builder
};
let client = client_builder.build().unwrap();
let req = if request.multipart.is_empty() {
client
.request(
request.clone().method.to_reqwest(),
reqwest::Url::parse(request.clone().url().as_str()).unwrap(),
)
.headers(headers)
.body(request.clone().body)
.build()
.unwrap()
} else {
let mut form = reqwest::multipart::Form::new();
for param in request.multipart.clone() {
match param {
MultipartParam::TextParam { name, value } => {
form = form.text(name, value)
}
MultipartParam::FileParam { name, filename, content_type } => {
if let Some(content_type) = content_type {
let path = Path::new(filename.as_str());
let part = reqwest::multipart::Part::file(path).unwrap()
.mime_str(content_type.as_str())
.unwrap();
form = form.part(name, part);
} else {
form = form.file(name, filename).unwrap();
}
}
}
}
client
.request(
request.clone().method.to_reqwest(),
reqwest::Url::parse(request.clone().url().as_str()).unwrap(),
)
.headers(headers)
.multipart(form)
.build()
.unwrap()
};
match client.execute(req) {
Ok(mut resp) => {
let mut headers = vec![];
//eprintln!(">>> response headers {:?}", resp.headers().clone());
for (name, value) in resp.headers() {
headers.push(Header {
name: name.as_str().to_string(),
value: value.to_str().unwrap().to_string(),
})
}
let version = match resp.version() {
reqwest::Version::HTTP_10 => Version::Http10,
reqwest::Version::HTTP_11 => Version::Http11,
reqwest::Version::HTTP_2 => Version::Http2,
v => panic!("Version {:?} not supported!", v),
};
let mut buf: Vec<u8> = vec![];
resp.copy_to(&mut buf).unwrap(); // TODO Test error case
resp.content_length(); // dirty hack to prevent error "connection closed before message completed"?
Ok(Response {
version,
status: resp.status().as_u16(),
headers,
body: buf,
})
}
Err(e) => {
Err(HttpError {
message: format!("{:?}", e.to_string()),
url: request.clone().url(),
})
}
}
}
}

View File

@ -1,472 +0,0 @@
/*
* hurl (https://hurl.dev)
* Copyright (C) 2020 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 core::fmt;
use chrono::NaiveDateTime;
use cookie::Cookie as ExternalCookie;
use super::core::*;
//use std::collections::HashMap;
// cookies
// keep cookies same name different domains
// send the most specific?? send the 2 of them?
// more flexible to keep list of cookies internally
pub type Domain = String;
pub type Name = String;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ResponseCookie {
pub name: String,
pub value: String,
pub max_age: Option<i64>,
pub domain: Option<String>,
pub path: Option<String>,
pub secure: Option<bool>,
pub http_only: Option<bool>,
pub expires: Option<String>,
pub same_site: Option<String>,
}
pub struct ParseCookieError {}
impl std::str::FromStr for ResponseCookie {
type Err = ParseCookieError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let c = ExternalCookie::parse(s).unwrap();
let name = c.name().to_string();
let value = c.value().to_string();
let max_age = match c.max_age() {
None => None,
Some(d) => Some(d.num_seconds())
};
let domain = match c.domain() {
None => None,
Some(v) => Some(v.to_string())
};
let path = match c.path() {
None => None,
Some(v) => Some(v.to_string())
};
let secure = match c.secure() {
None => None,
Some(value) => Some(value)
};
let http_only = match c.http_only() {
None => None,
Some(value) => Some(value)
};
let expires = match c.expires() {
None => None,
Some(time) => Some(time.rfc822().to_string())
};
let same_site = match c.same_site() {
None => None,
Some(s) => Some(s.to_string())
};
Ok(ResponseCookie { name, value, max_age, domain, path, secure, expires, http_only, same_site })
}
}
impl fmt::Display for ResponseCookie {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let max_age = match self.clone().max_age {
None => String::from(""),
Some(v) => format!("; Max-Age:{}", v)
};
let domain = match self.clone().domain {
None => String::from(""),
Some(v) => format!("; Domain:{}", v)
};
let path = match self.clone().path {
None => String::from(""),
Some(v) => format!("; Path:{}", v)
};
write!(f, "{}={}{}{}{}",
self.name,
self.value,
max_age,
domain,
path)
}
}
impl fmt::Display for Cookie {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}={}; domain={}; path={}",
self.name,
self.value,
self.domain,
self.path,
)
}
}
impl ResponseCookie {
// pub fn to_header(&self) -> Header {
// return Header {
// name: String::from("Cookie"),
// value: format!("{}={}", self.name, self.value),
// };
// //format!("Cookie: {}", self.to_string());
// }
pub fn encode_cookie(header_name: String, header_value: String) -> Header {
let name = String::from("Cookie");
let value = format!("{}={};", header_name, header_value);
Header { name, value }
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct CookieJar {
inner: Vec<Cookie>
}
impl CookieJar {
pub fn init(cookies: Vec<Cookie>) -> CookieJar {
CookieJar { inner: cookies }
}
pub fn cookies(self) -> Vec<Cookie> {
self.inner
}
pub fn get_cookies(self, domain: String, path: String) -> Vec<ResponseCookie> {
self.inner
.iter()
.filter(|c| c.is_usable(domain.clone(), path.clone()))
.map(|c| ResponseCookie {
name: c.clone().name,
value: c.clone().value,
max_age: None,
domain: Some(c.domain.clone()),
path: Some(c.path.clone()),
secure: Some(c.secure),
http_only: None,
expires: None,
same_site: None,
})
.collect()
}
pub fn update_cookies(&mut self, default_domain: String, _default_path: String, cookie: ResponseCookie) {
match cookie.max_age {
Some(0) => {
//eprintln!("delete cookie {:?}", cookie);
self.inner.retain(|c| c.name != cookie.name);
}
_ => {
// replace value if same name+domain
let domain = match cookie.clone().domain {
None => default_domain,
Some(d) => d,
};
let path = match cookie.clone().path {
None => String::from("/"), // do not use default path for the time-beingdefault_path,
Some(p) => p,
};
// find existing cookie
for c in self.inner.iter_mut() {
if c.name == cookie.name && c.domain == domain {
c.value = cookie.value;
return;
}
}
let secure = if let Some(v) = cookie.secure { v } else { false };
// push new cookie
self.inner.push(Cookie {
name: cookie.clone().name,
value: cookie.clone().value,
domain,
path,
subdomains: cookie.domain.is_some(),
secure,
expires: None,
});
}
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Cookie {
pub name: String,
pub value: String,
pub domain: String,
pub path: String,
pub subdomains: bool,
pub secure: bool,
pub expires: Option<NaiveDateTime>,
}
impl Cookie {
fn is_usable(&self, domain: String, path: String) -> bool {
// domain
if !is_subdomain(self.clone().domain, domain.clone()) {
return false;
}
if !self.subdomains && domain != self.clone().domain {
return false;
}
// path
if !is_subpath(self.clone().path, path) {
return false;
}
true
}
}
fn is_subdomain(domain: String, subdomain: String) -> bool {
if domain.as_str() == "" {
return false;
}
let mut domain_segments: Vec<&str> = domain.split('.').collect();
if domain_segments.get(0).unwrap() == &"" {
domain_segments.remove(0);
}
domain_segments.reverse();
let mut subdomain_segments: Vec<&str> = subdomain.split('.').collect();
if subdomain_segments.get(0).unwrap() == &"" {
subdomain_segments.remove(0);
}
subdomain_segments.reverse();
if domain_segments.len() > subdomain_segments.len() {
return false;
}
for i in 0..domain_segments.len() {
if domain_segments.get(i).unwrap() != subdomain_segments.get(i).unwrap() {
return false;
}
}
true
}
fn is_subpath(path: String, subpath: String) -> bool {
if path.as_str() == "" {
return false;
}
let mut path_segments: Vec<&str> = path.split('/').collect();
if path_segments.get(0).unwrap() == &"" {
path_segments.remove(0);
}
path_segments.reverse();
if path_segments.get(0).unwrap() == &"" {
path_segments.remove(0);
}
let mut subpath_segments: Vec<&str> = subpath.split('/').collect();
if subpath_segments.get(0).unwrap() == &"" {
subpath_segments.remove(0);
}
subpath_segments.reverse();
if path_segments.len() > subpath_segments.len() {
return false;
}
for i in 0..path_segments.len() {
if path_segments.get(i).unwrap() != subpath_segments.get(i).unwrap() {
return false;
}
}
true
}
impl Cookie {
pub fn to_netscape(&self) -> String {
let domain_name = self.domain.to_string();
let include_domains = if self.subdomains { "TRUE" } else { "FALSE" }.to_string();
let path = self.path.clone();
let https_only = if self.secure { "TRUE" } else { "FALSE" }.to_string();
let expires = if let Some(expires) = self.expires {
expires.timestamp().to_string()
} else {
"0".to_string()
};
let name = self.name.clone();
let value = self.value.clone();
format!("{}\t{}\t{}\t{}\t{}\t{}\t{}",
domain_name,
include_domains,
path,
https_only,
expires,
name,
value
)
}
pub fn from_netscape(s: &str) -> Option<Cookie> {
let tokens = s.split('\t').collect::<Vec<&str>>();
if tokens.len() != 7 { return None; }
let domain = (*tokens.get(0).unwrap()).to_string();
let subdomains = (*tokens.get(1).unwrap()).to_string().as_str() == "TRUE";
let path = (*tokens.get(2).unwrap()).to_string();
let secure = (*tokens.get(3).unwrap()).to_string().as_str() == "TRUE";
let expires = None;
let name = (*tokens.get(5).unwrap()).to_string();
let value = (*tokens.get(6).unwrap()).to_string();
Some(Cookie { name, value, domain, path, subdomains, secure, expires })
}
}
#[cfg(test)]
mod tests {
use super::*;
fn cookie_lsid() -> Cookie {
Cookie {
name: String::from("LSID"),
value: String::from("DQAAAK…Eaem_vYg"),
domain: String::from("docs.foo.com"),
path: String::from("/accounts"),
subdomains: false,
secure: false,
expires: None,
}
}
fn cookie_hsid() -> Cookie {
Cookie {
name: String::from("HSID"),
value: String::from("AYQEVn…DKrdst"),
domain: String::from(".foo.com"),
path: String::from("/"),
subdomains: true,
secure: false,
expires: None,
}
}
fn cookie_ssid() -> Cookie {
Cookie {
name: String::from("SSID"),
value: String::from("Ap4P…GTEq"),
domain: String::from("foo.com"),
path: String::from("/"),
subdomains: true,
secure: false,
expires: None,
}
}
fn sample_cookiejar() -> CookieJar {
CookieJar {
inner: vec![
cookie_lsid(),
cookie_hsid(),
cookie_ssid(),
]
}
}
#[test]
fn test_is_usable() {
let domain = String::from("example.org");
let path = String::from("/");
assert_eq!(cookie_lsid().is_usable(domain.clone(), path.clone()), false);
assert_eq!(cookie_hsid().is_usable(domain.clone(), path.clone()), false);
assert_eq!(cookie_ssid().is_usable(domain, path.clone()), false);
let domain = String::from("foo.com");
let path = String::from("/");
assert_eq!(cookie_lsid().is_usable(domain.clone(), path.clone()), false);
assert_eq!(cookie_hsid().is_usable(domain.clone(), path.clone()), true);
assert_eq!(cookie_ssid().is_usable(domain, path.clone()), true);
let domain = String::from("foo.com");
let path = String::from("/accounts");
assert_eq!(cookie_lsid().is_usable(domain.clone(), path.clone()), false);
assert_eq!(cookie_hsid().is_usable(domain.clone(), path.clone()), true);
assert_eq!(cookie_ssid().is_usable(domain, path), true);
let domain = String::from("docs.foo.com");
let path = String::from("/accounts");
assert_eq!(cookie_lsid().is_usable(domain.clone(), path.clone()), true);
assert_eq!(cookie_hsid().is_usable(domain.clone(), path.clone()), true);
assert_eq!(cookie_ssid().is_usable(domain, path), true);
}
#[test]
fn test_get_cookies() {
let domain = String::from("docs.foo.com");
let path = String::from("/accounts");
assert_eq!(sample_cookiejar().get_cookies(domain, path).len(), 3);
let domain = String::from("toto.docs.foo.com");
let path = String::from("/accounts");
assert_eq!(sample_cookiejar().get_cookies(domain, path).len(), 2);
}
#[test]
fn test_is_subdomain() {
assert_eq!(is_subdomain(String::from("foo.example.org"), String::from("example.org")), false);
assert_eq!(is_subdomain(String::from("example.org"), String::from("toto.org")), false);
assert_eq!(is_subdomain(String::from("example.org"), String::from("example.org")), true);
assert_eq!(is_subdomain(String::from("example.org"), String::from("foo.example.org")), true);
assert_eq!(is_subdomain(String::from(".example.org"), String::from("foo.example.org")), true);
}
#[test]
fn test_is_subpath() {
assert_eq!(is_subpath(String::from("/toto"), String::from("/toto")), true);
assert_eq!(is_subpath(String::from("/"), String::from("/toto")), true);
assert_eq!(is_subpath(String::from("/to"), String::from("/toto")), false);
}
#[test]
fn test_from_netscape() {
assert_eq!(Cookie::from_netscape("localhost\tFALSE\t/\tFALSE\t0\tcookie2\tvalueA").unwrap(),
Cookie {
name: "cookie2".to_string(),
value: "valueA".to_string(),
domain: "localhost".to_string(),
path: "/".to_string(),
subdomains: false,
secure: false,
expires: None,
}
);
}
}

View File

@ -1,128 +0,0 @@
/*
* hurl (https://hurl.dev)
* Copyright (C) 2020 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.
*
*/
extern crate reqwest;
use std::fmt;
use serde::{Deserialize, Serialize};
pub enum Encoding {
Utf8,
Latin1,
}
impl fmt::Display for Encoding {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", match self {
Encoding::Utf8 => "utf8",
Encoding::Latin1 => "iso8859-1"
})
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Url {
pub scheme: String,
pub host: String,
pub port: Option<u16>,
pub path: String,
pub query_string: String,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Header {
pub name: String,
pub value: String,
}
pub fn get_header_value(headers: Vec<Header>, name: &str) -> Option<String> {
for header in headers {
if header.name.as_str() == name {
return Some(header.value);
}
}
None
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Param {
pub name: String,
pub value: String,
}
pub fn encode_form_params(params: Vec<Param>) -> Vec<u8> {
params
.iter()
//.map(|p| format!("{}={}", p.name, utf8_percent_encode(p.value.as_str(), FRAGMENT)))
.map(|p| format!("{}={}", p.name, url_encode(p.value.clone())))
.collect::<Vec<_>>()
.join("&")
.into_bytes()
}
fn url_encode(s: String) -> String {
const MAX_CHAR_VAL: u32 = std::char::MAX as u32;
let mut buff = [0; 4];
s.chars()
.map(|ch| {
match ch as u32 {
0..=47 | 58..=64 | 91..=96 | 123..=MAX_CHAR_VAL => {
ch.encode_utf8(&mut buff);
buff[0..ch.len_utf8()].iter().map(|&byte| format!("%{:x}", byte)).collect::<String>()
}
_ => ch.to_string(),
}
})
.collect::<String>()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_encode_form_params() {
assert_eq!(
encode_form_params(vec![
Param {
name: String::from("param1"),
value: String::from("value1"),
},
Param {
name: String::from("param2"),
value: String::from(""),
}
]),
vec![
112, 97, 114, 97, 109, 49, 61, 118, 97, 108, 117, 101, 49, 38, 112, 97, 114, 97, 109,
50, 61
]
);
assert_eq!(
std::str::from_utf8(&encode_form_params(vec![
Param { name: String::from("param1"), value: String::from("value1") },
Param { name: String::from("param2"), value: String::from("") },
Param { name: String::from("param3"), value: String::from("a=b") },
Param { name: String::from("param4"), value: String::from("a%3db") },
])).unwrap(),
"param1=value1&param2=&param3=a%3db&param4=a%253db"
);
}
}

View File

@ -1,119 +0,0 @@
/*
* hurl (https://hurl.dev)
* Copyright (C) 2020 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 serde::ser::{Serializer, SerializeStruct};
use serde::Serialize;
use super::cookie::*;
use super::request::*;
use super::response::*;
impl Serialize for Request {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
// 3 is the number of fields in the struct.
let mut state = serializer.serialize_struct("??", 3)?;
state.serialize_field("method", &self.clone().method.to_text())?;
state.serialize_field("url", &self.clone().url())?;
state.serialize_field("queryString", &self.clone().querystring)?;
state.serialize_field("headers", &self.clone().headers())?;
state.serialize_field("cookies", &self.clone().cookies)?;
if let Some(params) = self.clone().form_params() {
state.serialize_field("format_params", &params)?;
}
state.serialize_field("body", &base64::encode(&self.body))?;
state.end()
}
}
impl Serialize for Response {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
// 3 is the number of fields in the struct.
let mut state = serializer.serialize_struct("??", 3)?;
state.serialize_field("httpVersion", &self.clone().version)?;
state.serialize_field("status", &self.clone().status)?;
state.serialize_field("cookies", &self.clone().cookies())?;
state.serialize_field("headers", &self.clone().headers)?;
// WIP - Serialize response body only for json for the timebeing
let content_type = self.get_header("content_type", true);
if let Some(value) = content_type.first() {
if value.as_str() == "application/json; charset=UTF-8" {
let s = String::from_utf8(self.body.clone()).expect("Found invalid UTF-8");
let result: Result<serde_json::Value, serde_json::Error> = serde_json::from_str(s.as_str());
if let Ok(v) = result {
state.serialize_field("json", &v)?;
}
}
}
state.end()
}
}
impl Serialize for ResponseCookie {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
// 3 is the number of fields in the struct.
let mut state = serializer.serialize_struct("??", 3)?;
state.serialize_field("name", &self.clone().name)?;
state.serialize_field("value", &self.clone().value)?;
if let Some(value) = self.clone().domain {
state.serialize_field("domain", &value)?;
}
state.end()
}
}
impl Serialize for Cookie {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut state = serializer.serialize_struct("InternalCookie", 3)?;
state.serialize_field("name", &self.clone().name)?;
state.serialize_field("value", &self.clone().value)?;
state.serialize_field("domain", &self.clone().domain)?;
state.serialize_field("path", &self.clone().path)?;
state.serialize_field("include_subdomain", &self.clone().subdomains)?;
state.end()
}
}
impl Serialize for Version {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
match self {
Version::Http10 => serializer.serialize_str("HTTP/1.0"),
Version::Http11 => serializer.serialize_str("HTTP/1.1"),
Version::Http2 => serializer.serialize_str("HTTP/2"),
}
}
}

View File

@ -1,391 +0,0 @@
/*
* hurl (https://hurl.dev)
* Copyright (C) 2020 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.
*
*/
extern crate url as external_url;
use chrono::DateTime;
use super::cookie::*;
use super::core::*;
use super::request::*;
use super::response::*;
type ParseError = String;
pub fn parse_request(value: serde_json::Value) -> Result<Request, ParseError> {
if let serde_json::Value::Object(map) = value {
let method = match map.get("method") {
Some(serde_json::Value::String(s)) => parse_method(s.clone())?,
_ => return Err("expecting a string for the method".to_string()),
};
let url = match map.get("url") {
Some(serde_json::Value::String(s)) => parse_url(s.clone())?,
_ => return Err("expecting a string for the url".to_string()),
};
let headers = match map.get("headers") {
Some(serde_json::Value::Array(values)) => {
let mut headers = vec![];
for value in values {
let header = parse_header(value.clone())?;
headers.push(header);
}
headers
}
_ => vec![],
};
let cookies = match map.get("cookies") {
Some(serde_json::Value::Array(values)) => {
let mut headers = vec![];
for value in values {
let header = parse_response_cookie(value.clone())?;
headers.push(header);
}
headers
}
_ => vec![],
};
let multipart = vec![];
Ok(Request {
method,
url,
querystring: vec![],
headers,
cookies,
body: vec![],
multipart,
})
} else {
Err("expecting an object for the request".to_string())
}
}
pub fn parse_response(value: serde_json::Value) -> Result<Response, ParseError> {
if let serde_json::Value::Object(map) = value {
let status = match map.get("status") {
Some(serde_json::Value::Number(x)) => if let Some(x) = x.as_u64() {
x as u16
} else {
return Err("expecting a integer for the status".to_string());
},
_ => return Err("expecting a number for the status".to_string()),
};
let version = match map.get("httpVersion") {
Some(serde_json::Value::String(s)) => parse_version(s.clone())?,
_ => return Err("expecting a string for the version".to_string()),
};
let headers = match map.get("headers") {
Some(serde_json::Value::Array(values)) => {
let mut headers = vec![];
for value in values {
let header = parse_header(value.clone())?;
headers.push(header);
}
headers
}
_ => vec![],
};
Ok(Response {
version,
status,
headers,
body: vec![],
})
} else {
Err("expecting an object for the response".to_string())
}
}
fn parse_method(s: String) -> Result<Method, ParseError> {
match s.as_str() {
"GET" => Ok(Method::Get),
"HEAD" => Ok(Method::Head),
"POST" => Ok(Method::Post),
"PUT" => Ok(Method::Put),
"DELETE" => Ok(Method::Delete),
"CONNECT" => Ok(Method::Connect),
"OPTIONS" => Ok(Method::Options),
"TRACE" => Ok(Method::Trace),
"PATCH" => Ok(Method::Patch),
_ => Err(format!("Invalid method <{}>", s))
}
}
fn parse_url(s: String) -> Result<Url, ParseError> {
match external_url::Url::parse(s.as_str()) {
Err(_) => Err(format!("Invalid url <{}>", s)),
Ok(u) => Ok(Url {
scheme: u.scheme().to_string(),
host: u.host_str().unwrap().to_string(),
port: u.port(),
path: u.path().to_string(),
query_string: if let Some(s) = u.query() { s.to_string() } else { "".to_string() },
})
}
}
fn parse_header(value: serde_json::Value) -> Result<Header, ParseError> {
if let serde_json::Value::Object(map) = value {
let name = match map.get("name") {
Some(serde_json::Value::String(s)) => s.to_string(),
_ => return Err("expecting a string for the header name".to_string()),
};
let value = match map.get("value") {
Some(serde_json::Value::String(s)) => s.to_string(),
_ => return Err("expecting a string for the header value".to_string()),
};
Ok(Header { name, value })
} else {
Err("Expecting object for one header".to_string())
}
}
pub fn parse_response_cookie(value: serde_json::Value) -> Result<ResponseCookie, ParseError> {
if let serde_json::Value::Object(map) = value {
let name = match map.get("name") {
Some(serde_json::Value::String(s)) => s.to_string(),
_ => return Err("expecting a string for the cookie name".to_string()),
};
let value = match map.get("value") {
Some(serde_json::Value::String(s)) => s.to_string(),
_ => return Err("expecting a string for the cookie value".to_string()),
};
let domain = match map.get("domain") {
None => None,
Some(serde_json::Value::String(s)) => Some(s.to_string()),
_ => return Err("expecting a string for the cookie domain".to_string()),
};
let path = match map.get("path") {
None => None,
Some(serde_json::Value::String(s)) => Some(s.to_string()),
_ => return Err("expecting a string for the cookie path".to_string()),
};
let expires = match map.get("expires") {
None => None,
Some(serde_json::Value::String(s)) => Some(s.to_string()),
_ => return Err("expecting a string for the cookie expires".to_string()),
};
let secure = match map.get("secure") {
None => None,
Some(serde_json::Value::Bool(value)) => Some(*value),
_ => return Err("expecting a bool for the cookie secure flag".to_string()),
};
let http_only = match map.get("http_only") {
None => None,
Some(serde_json::Value::Bool(value)) => Some(*value),
_ => return Err("expecting a bool for the cookie http_only flag".to_string()),
};
let same_site = match map.get("same_site") {
None => None,
Some(serde_json::Value::String(s)) => Some(s.to_string()),
_ => return Err("expecting a string for the cookie same_site".to_string()),
};
Ok(ResponseCookie { name, value, max_age: None, domain, path, secure, http_only, expires, same_site })
} else {
Err("Expecting object for one cookie".to_string())
}
}
pub fn parse_cookie(value: serde_json::Value) -> Result<Cookie, ParseError> {
if let serde_json::Value::Object(map) = value {
let name = match map.get("name") {
Some(serde_json::Value::String(s)) => s.to_string(),
_ => return Err("expecting a string for the cookie name".to_string()),
};
let value = match map.get("value") {
Some(serde_json::Value::String(s)) => s.to_string(),
_ => return Err("expecting a string for the cookie value".to_string()),
};
let domain = match map.get("domain") {
Some(serde_json::Value::String(s)) => s.to_string(),
_ => return Err("expecting a string for the cookie domain".to_string()),
};
let path = match map.get("path") {
Some(serde_json::Value::String(s)) => s.to_string(),
_ => return Err("expecting a string for the cookie path".to_string()),
};
let subdomains = match map.get("include_subdomain") {
Some(serde_json::Value::Bool(v)) => *v,
_ => return Err("expecting a bool for the include_subdomain".to_string()),
};
let secure = match map.get("secure") {
None => false,
Some(serde_json::Value::Bool(value)) => *value,
_ => return Err("expecting a bool for the secure flag".to_string()),
};
let expires = match map.get("expired") {
None => None,
Some(serde_json::Value::String(v)) => {
match DateTime::parse_from_rfc3339(v.as_str()) {
Ok(v) => Some(v.naive_utc()),
Err(_) => return Err("expecting a String (date) for the expired fieldate can be parsed".to_string()),
}
}
_ => return Err("expecting a String (date) for the expired field".to_string()),
};
Ok(Cookie { name, value, domain, path, subdomains, secure, expires })
} else {
Err("Expecting object for one cookie".to_string())
}
}
fn parse_version(s: String) -> Result<Version, ParseError> {
match s.as_str() {
"HTTP/1.0" => Ok(Version::Http10),
"HTTP/1.1" => Ok(Version::Http11),
"HTTP/2" => Ok(Version::Http2),
_ => Err("Expecting version HTTP/1.0, HTTP/1.2 or HTTP/2".to_string())
}
}
#[cfg(test)]
mod tests {
use super::*;
use super::super::request::tests::*;
#[test]
fn test_parse_request() {
let v: serde_json::Value = serde_json::from_str(r#"{
"method": "GET",
"url": "http://localhost:8000/hello",
"headers": []
}"#).unwrap();
assert_eq!(parse_request(v).unwrap(), hello_http_request());
let v: serde_json::Value = serde_json::from_str(r#"{
"method": "GET",
"url": "http://localhost:8000/querystring-params?param1=value1&param2=a%20b",
"headers": []
}"#).unwrap();
assert_eq!(parse_request(v).unwrap(), Request {
method: Method::Get,
url: Url {
scheme: "http".to_string(),
host: "localhost".to_string(),
port: Some(8000),
path: "/querystring-params".to_string(),
query_string: "param1=value1&param2=a%20b".to_string(),
},
querystring: vec![],
headers: vec![],
cookies: vec![],
body: vec![],
multipart: vec![],
});
let v: serde_json::Value = serde_json::from_str(r#"{
"method": "GET",
"url": "http://localhost/custom",
"headers": [
{"name": "User-Agent", "value": "iPhone"},
{"name": "Foo", "value": "Bar"}
],
"cookies": [
{"name": "theme", "value": "light"},
{"name": "sessionToken", "value": "abc123"}
]
}"#).unwrap();
assert_eq!(parse_request(v).unwrap(), custom_http_request());
}
#[test]
fn test_parse_response() {
let v: serde_json::Value = serde_json::from_str(r#"{
"status": 200,
"httpVersion": "HTTP/1.0",
"headers": [
{"name": "Content-Type", "value": "text/html; charset=utf-8" },
{"name": "Content-Length", "value": "12" }
]
}"#).unwrap();
assert_eq!(parse_response(v).unwrap(), Response {
version: Version::Http10,
status: 200,
headers: vec![
Header { name: String::from("Content-Type"), value: String::from("text/html; charset=utf-8") },
Header { name: String::from("Content-Length"), value: String::from("12") },
],
body: vec![],
});
}
#[test]
fn test_parse_method() {
assert_eq!(parse_method("GET".to_string()).unwrap(), Method::Get);
let error = parse_method("x".to_string()).err().unwrap();
assert_eq!(error, "Invalid method <x>");
}
#[test]
fn test_parse_url() {
assert_eq!(
parse_url("http://localhost:8000/query?param1=value1".to_string()).unwrap(),
Url {
scheme: "http".to_string(),
host: "localhost".to_string(),
port: Some(8000),
path: "/query".to_string(),
query_string: "param1=value1".to_string(),
});
}
#[test]
fn test_parse_header() {
let v: serde_json::Value = serde_json::from_str(r#"{
"name": "name1",
"value": "value1"
}"#).unwrap();
assert_eq!(parse_header(v).unwrap(), Header { name: "name1".to_string(), value: "value1".to_string() });
}
#[test]
fn test_parse_response_cookie() {
let v: serde_json::Value = serde_json::from_str(r#"{
"name": "name1",
"value": "value1"
}"#).unwrap();
assert_eq!(parse_response_cookie(v).unwrap(),
ResponseCookie {
name: "name1".to_string(),
value: "value1".to_string(),
max_age: None,
domain: None,
path: None,
secure: None,
http_only: None,
expires: None,
same_site: None,
}
);
}
#[test]
fn test_parse_version() {
assert_eq!(parse_version("HTTP/1.0".to_string()).unwrap(), Version::Http10);
}
}

View File

@ -15,11 +15,5 @@
* limitations under the License. * limitations under the License.
* *
*/ */
pub mod client;
pub mod core;
pub mod cookie;
pub mod request;
pub mod response;
pub mod import;
pub mod export;
pub mod libcurl; pub mod libcurl;

View File

@ -1,413 +0,0 @@
/*
* hurl (https://hurl.dev)
* Copyright (C) 2020 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::fmt;
use percent_encoding::{AsciiSet, CONTROLS, percent_decode, utf8_percent_encode};
use serde::{Deserialize, Serialize};
use super::cookie::*;
use super::core::*;
const FRAGMENT: &AsciiSet = &CONTROLS
.add(b' ')
.add(b'"')
.add(b':')
.add(b'/')
.add(b'<')
.add(b'>')
.add(b'+')
.add(b'=')
.add(b'?')
.add(b'%')
.add(b'`');
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Request {
pub method: Method,
pub url: Url,
pub querystring: Vec<Param>,
pub headers: Vec<Header>,
pub cookies: Vec<ResponseCookie>,
pub body: Vec<u8>,
pub multipart: Vec<MultipartParam>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum MultipartParam {
TextParam { name: String, value: String },
FileParam { name: String, filename: String, content_type: Option<String> },
}
fn has_header(headers: &[Header], name: String) -> bool {
for header in headers {
if header.name.as_str() == name {
return true;
}
}
false
}
pub enum RequestEncoding {
Utf8,
Latin1,
}
impl fmt::Display for RequestEncoding {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", match self {
RequestEncoding::Utf8 => "utf8",
RequestEncoding::Latin1 => "iso8859-1"
})
}
}
impl Request {
pub fn host(self) -> String {
self.url.host
}
pub fn url(self) -> String {
let port = match self.url.port {
None => String::from(""),
Some(p) => format!(":{}", p)
};
let querystring = self.url.query_string.clone();
// add params
let querystring = if self.querystring.is_empty() {
querystring
} else {
let mut buf = querystring.clone();
if !querystring.is_empty() {
buf.push('&');
}
for param in self.querystring {
if !buf.is_empty() {
buf.push('&');
}
let encoded = utf8_percent_encode(param.value.as_str(), FRAGMENT).to_string();
buf.push_str(format!("{}={}", param.name, encoded).as_str());
}
buf
};
let querystring = if querystring.is_empty() {
"".to_string()
} else {
format!("?{}", querystring)
};
return format!("{}://{}{}{}{}",
self.url.scheme,
self.url.host,
port,
self.url.path,
querystring
);
}
pub fn headers(self) -> Vec<Header> {
let mut headers: Vec<Header> = self.headers.clone();
let user_agent = format!("hurl/{}", clap::crate_version!());
let default_headers = vec![
(String::from("User-Agent"), user_agent),
(String::from("Host"), self.url.clone().host)
];
for (name, value) in default_headers {
if !has_header(&self.headers, name.clone()) {
headers.push(Header { name, value });
}
}
if !self.cookies.is_empty() {
headers.push(Header {
name: String::from("Cookie"),
value: self.cookies
.iter()
.map(|c| format!("{}={}", c.name, c.value))
.collect::<Vec<String>>()
.join("; "),
});
}
headers
}
pub fn content_type(self) -> Option<String> {
for Header { name, value } in self.headers {
if name.as_str() == "Content-Type" {
return Some(value);
}
}
None
}
pub fn add_session_cookies(&mut self, cookies: Vec<ResponseCookie>) {
//eprintln!("add session cookies {:?}", cookies);
for cookie in cookies {
// TBC: both request and session cookies should have a domain => should not be an Option
let session_domain = cookie.clone().domain.unwrap();
match self.clone().get_cookie(cookie.clone().name) {
Some(ResponseCookie { domain: Some(domain), .. }) => {
if session_domain != domain {
self.cookies.push(cookie.clone());
}
}
_ => {
self.cookies.push(cookie.clone());
}
}
}
}
pub fn get_cookie(self, name: String) -> Option<ResponseCookie> {
for cookie in self.cookies {
if cookie.name == name {
return Some(cookie);
}
}
None
}
pub fn form_params(self) -> Option<Vec<Param>> {
if self.clone().content_type() != Some(String::from("application/x-www-form-urlencoded")) {
return None;
}
let decoded = percent_decode(&self.body);
let params = match decoded.decode_utf8() {
Ok(v) => {
let params: Vec<&str> = v.split('&').collect();
params.iter().map(|s| Param::parse(s)).collect()
}
_ => vec![]
};
Some(params)
}
pub fn encoding(&self) -> Encoding {
if let Some(v) = self.get_header("content-type", true) {
if v.contains("charset=ISO-8859-1") {
return Encoding::Latin1;
}
}
Encoding::Utf8
}
pub fn get_header(&self, name: &str, case_sensitive: bool) -> Option<String> {
for header in self.headers.clone() {
if header.name == name
|| !case_sensitive && header.name.to_lowercase() == name.to_lowercase()
{
return Some(header.value);
}
}
None
}
}
impl Param {
fn parse(s: &str) -> Param {
match s.find('=') {
None => Param { name: s.to_string(), value: String::from("") },
Some(i) => {
let (name, value) = s.split_at(i);
Param { name: name.to_string(), value: value[1..].to_string() }
}
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum Method {
Get,
Head,
Post,
Put,
Delete,
Connect,
Options,
Trace,
Patch,
}
impl Method {
pub fn to_reqwest(&self) -> reqwest::Method {
match self {
Method::Get => reqwest::Method::GET,
Method::Head => reqwest::Method::HEAD,
Method::Post => reqwest::Method::POST,
Method::Put => reqwest::Method::PUT,
Method::Delete => reqwest::Method::DELETE,
Method::Connect => reqwest::Method::CONNECT,
Method::Options => reqwest::Method::OPTIONS,
Method::Trace => reqwest::Method::TRACE,
Method::Patch => reqwest::Method::PATCH,
}
}
}
#[cfg(test)]
pub mod tests {
use super::*;
pub fn hello_http_request() -> Request {
Request {
method: Method::Get,
url: Url {
scheme: "http".to_string(),
host: "localhost".to_string(),
port: Some(8000),
path: "/hello".to_string(),
query_string: "".to_string(),
},
querystring: vec![],
headers: vec![],
cookies: vec![],
body: vec![],
multipart: vec![],
}
}
// GET http://localhost:8000/querystring-params?param1=value1&param2
pub fn query_http_request() -> Request {
Request {
method: Method::Get,
url: Url {
scheme: "http".to_string(),
host: "localhost".to_string(),
port: Some(8000),
path: "/querystring-params".to_string(),
query_string: "".to_string(),
},
querystring: vec![
Param { name: String::from("param1"), value: String::from("value1") },
Param { name: String::from("param2"), value: String::from("a b") },
],
headers: vec![],
cookies: vec![],
body: vec![],
multipart: vec![],
}
}
pub fn custom_http_request() -> Request {
Request {
method: Method::Get,
url: Url {
scheme: "http".to_string(),
host: "localhost".to_string(),
port: None,
path: "/custom".to_string(),
query_string: "".to_string(),
},
querystring: vec![],
headers: vec![
Header { name: String::from("User-Agent"), value: String::from("iPhone") },
Header { name: String::from("Foo"), value: String::from("Bar") },
],
cookies: vec![
ResponseCookie {
name: String::from("theme"),
value: String::from("light"),
max_age: None,
domain: None,
path: None,
secure: None,
http_only: None,
expires: None,
same_site: None,
},
ResponseCookie {
name: String::from("sessionToken"),
value: String::from("abc123"),
max_age: None,
domain: None,
path: None,
secure: None,
http_only: None,
expires: None,
same_site: None,
}
],
body: vec![],
multipart: vec![],
}
}
pub fn form_http_request() -> Request {
Request {
method: Method::Post,
url: Url {
scheme: "http".to_string(),
host: "localhost".to_string(),
port: None,
path: "/form-params".to_string(),
query_string: "".to_string(),
},
querystring: vec![],
headers: vec![
Header { name: String::from("Content-Type"), value: String::from("application/x-www-form-urlencoded") },
],
cookies: vec![],
body: "param1=value1&param2=&param3=a%3db&param4=a%253db".to_string().into_bytes(),
multipart: vec![],
}
}
#[test]
pub fn test_headers() {
assert_eq!(hello_http_request().headers(), vec![
Header { name: String::from("User-Agent"), value: format!("hurl/{}", clap::crate_version!()) },
Header { name: String::from("Host"), value: String::from("localhost") }
]);
assert_eq!(custom_http_request().headers(), vec![
Header { name: String::from("User-Agent"), value: String::from("iPhone") },
Header { name: String::from("Foo"), value: String::from("Bar") },
Header { name: String::from("Host"), value: String::from("localhost") },
Header { name: String::from("Cookie"), value: String::from("theme=light; sessionToken=abc123") },
]);
}
#[test]
pub fn test_url() {
assert_eq!(hello_http_request().url(), String::from("http://localhost:8000/hello"));
assert_eq!(query_http_request().url(), String::from("http://localhost:8000/querystring-params?param1=value1&param2=a%20b"));
}
#[test]
pub fn test_form_params() {
assert_eq!(hello_http_request().form_params(), None);
assert_eq!(form_http_request().form_params().unwrap(), vec![
Param { name: String::from("param1"), value: String::from("value1") },
Param { name: String::from("param2"), value: String::from("") },
Param { name: String::from("param3"), value: String::from("a=b") },
Param { name: String::from("param4"), value: String::from("a%3db") },
]);
}
}

View File

@ -1,268 +0,0 @@
/*
* hurl (https://hurl.dev)
* Copyright (C) 2020 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::fmt;
use super::cookie::*;
use super::core::*;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Response {
pub version: Version,
pub status: u16,
pub headers: Vec<Header>,
pub body: Vec<u8>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Version {
Http10,
Http11,
Http2,
}
impl fmt::Display for Version {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let s = match self {
Version::Http10 => "1.0",
Version::Http11 => "1.1",
Version::Http2 => "2",
};
write!(f, "{}", s)
}
}
impl Response {
pub fn get_header(&self, name: &str, case_sensitive: bool) -> Vec<String> {
let mut values = vec![];
for header in self.headers.clone() {
if header.name == name
|| !case_sensitive && header.name.to_lowercase() == name.to_lowercase()
{
values.push(header.value);
}
}
values
}
pub fn get_cookie(&self, name: &str) -> Option<ResponseCookie> {
for cookie in self.cookies() {
if cookie.name.as_str() == name
{
return Some(cookie);
}
}
None
}
pub fn cookies(&self) -> Vec<ResponseCookie> {
let mut cookies = vec![];
for Header { name, value } in self.clone().headers {
if name.to_lowercase() == "set-cookie" {
let c = cookie::Cookie::parse(value.as_str()).unwrap();
// eprintln!(">>> parse set-cookie header");
// eprintln!(">>> c = {:?}", c);
//
// let fields = value.split(";").collect::<Vec<&str>>();
// let name_value = fields.get(0).unwrap().split("=").collect::<Vec<&str>>();
// let name = name_value.get(0).unwrap().to_string();
// let value = name_value.get(1).unwrap().to_string();
let name = c.name().to_string();
let value = c.value().to_string();
let max_age = match c.max_age() {
None => None,
Some(d) => Some(d.num_seconds())
};
let expires = match c.expires() {
None => None,
Some(time) => Some(time.rfc822().to_string())
};
let domain = match c.domain() {
None => None,
Some(v) => Some(v.to_string())
};
let path = match c.path() {
None => None,
Some(v) => Some(v.to_string())
};
let secure = if let Some(value) = c.secure() {
Some(value)
} else {
None
};
let http_only = if let Some(value) = c.http_only() {
Some(value)
} else {
None
};
let same_site = match c.same_site() {
None => None,
Some(v) => Some(v.to_string())
};
cookies.push(ResponseCookie { name, value, max_age, expires, domain, path, secure, http_only, same_site });
}
}
cookies
}
}
impl Response {
pub fn content_type(&self) -> Option<String> {
let values = self.get_header("content-type", true);
if let Some(value) = values.first() {
Some(value.clone())
} else {
None
}
}
pub fn encoding(&self) -> Encoding {
if let Some(value) = self.content_type() {
if value.contains("charset=ISO-8859-1") {
return Encoding::Latin1;
}
}
Encoding::Utf8
}
pub fn has_utf8_body(&self) -> bool {
if let Some(value) = self.content_type() {
value.contains("charset=utf-8")
} else {
false
}
}
pub fn is_html(&self) -> bool {
if let Some(value) = self.content_type() {
value.contains("html")
} else {
false
}
}
}
#[cfg(test)]
pub mod tests {
use super::*;
pub fn hello_http_response() -> Response {
Response {
version: Version::Http10,
status: 200,
headers: vec![
Header { name: String::from("Content-Type"), value: String::from("text/html; charset=utf-8") },
Header { name: String::from("Content-Length"), value: String::from("12") },
],
body: String::into_bytes(String::from("Hello World!")),
}
}
pub fn html_http_response() -> Response {
Response {
version: Version::Http10,
status: 200,
headers: vec![
Header { name: String::from("Content-Type"), value: String::from("text/html; charset=utf-8") },
],
body: String::into_bytes(String::from("<html><head><meta charset=\"UTF-8\"></head><body><br></body></html>")),
}
}
pub fn xml_invalid_response() -> Response {
Response {
version: Version::Http10,
status: 200,
headers: vec![
Header { name: String::from("Content-Type"), value: String::from("text/html; charset=utf-8") },
Header { name: String::from("Content-Length"), value: String::from("12") },
],
body: String::into_bytes(r#"
xxx
"#.to_string()),
}
}
pub fn xml_two_users_http_response() -> Response {
Response {
version: Version::Http10,
status: 200,
headers: vec![
Header { name: String::from("Content-Type"), value: String::from("text/html; charset=utf-8") },
Header { name: String::from("Content-Length"), value: String::from("12") },
],
body: String::into_bytes(r#"
<?xml version="1.0"?>
<users>
<user id="1">Bob</user>
<user id="2">Bill</user>
</users>
"#.to_string()),
}
}
pub fn xml_three_users_http_response() -> Response {
Response {
version: Version::Http10,
status: 200,
headers: vec![
Header { name: String::from("Content-Type"), value: String::from("text/html; charset=utf-8") },
Header { name: String::from("Content-Length"), value: String::from("12") },
],
body: String::into_bytes(r#"
<?xml version="1.0"?>
<users>
<user id="1">Bob</user>
<user id="2">Bill</user>
<user id="3">Bruce</user>
</users>
"#.to_string()),
}
}
pub fn json_http_response() -> Response {
Response {
version: Version::Http10,
status: 0,
headers: vec![],
body: String::into_bytes(r#"
{
"success":false,
"errors": [
{ "id": "error1"},
{"id": "error2"}
],
"duration": 1.5
}
"#.to_string()),
}
}
pub fn bytes_http_response() -> Response {
Response {
version: Version::Http10,
status: 200,
headers: vec![
Header { name: String::from("Content-Type"), value: String::from("application/octet-stream") },
Header { name: String::from("Content-Length"), value: String::from("1") },
],
body: vec![255],
}
}
}

View File

@ -17,7 +17,6 @@
*/ */
use std::collections::HashMap; use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use crate::core::common::{FormatError, SourceInfo, Value}; use crate::core::common::{FormatError, SourceInfo, Value};
use crate::http::libcurl; use crate::http::libcurl;
@ -82,14 +81,14 @@ pub type PredicateResult = Result<(), Error>;
// region error // region error
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[derive(Clone, Debug, PartialEq, Eq)]
pub struct Error { pub struct Error {
pub source_info: SourceInfo, pub source_info: SourceInfo,
pub inner: RunnerError, pub inner: RunnerError,
pub assert: bool, pub assert: bool,
} }
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[derive(Clone, Debug, PartialEq, Eq)]
pub enum RunnerError { pub enum RunnerError {
TemplateVariableNotDefined { name: String }, TemplateVariableNotDefined { name: String },
VariableNotDefined { name: String }, VariableNotDefined { name: String },

View File

@ -64,8 +64,8 @@ impl Serialize for AssertResult {
S: Serializer, S: Serializer,
{ {
let mut state = serializer.serialize_struct("??", 3)?; let mut state = serializer.serialize_struct("??", 3)?;
if let AssertResult::Version { source_info, actual, expected } = self { if let AssertResult::Version { actual, expected,.. } = self {
state.serialize_field("source_info", source_info)?; //state.serialize_field("source_info", source_info)?;
state.serialize_field("actual", actual)?; state.serialize_field("actual", actual)?;
state.serialize_field("expected", expected)?; state.serialize_field("expected", expected)?;
}; };

View File

@ -40,7 +40,6 @@ mod query;
pub mod request; pub mod request;
mod response; mod response;
mod template; mod template;
mod text;
mod xpath; mod xpath;
mod expr; mod expr;

View File

@ -1,184 +0,0 @@
/*
* hurl (https://hurl.dev)
* Copyright (C) 2020 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::fmt;
use crate::http::core::*;
use crate::http::request::*;
use crate::http::response::*;
impl Request {
pub fn to_text(&self) -> String {
let mut s = format!("{} {}\n",
self.clone().method.to_text(),
self.clone().url()
);
for header in self.clone().headers() {
s.push_str(header.to_text().as_str());
}
s.push_str("\n");
let body = match self.clone().form_params() {
None => body_text(self.clone().body, self.clone().content_type()),
Some(params) => {
let mut buf = String::from("[Form Params]");
for param in params {
buf.push_str(format!("\n{}={}", param.name, param.value).as_str())
}
buf
}
};
s.push_str(body.as_str());
s.push_str("\n");
s
}
}
impl Response {
pub fn to_text(&self, limit_body: usize) -> String {
let mut s = format!("HTTP/{} {}\n", self.version.to_text(), self.status);
for header in self.headers.clone() {
s.push_str(header.to_text().as_str());
}
s.push_str("");
// shoudl use number of char, not a number of bytes!!
//let limit_body = 400; // TODO should be explicitly pass as a command-line argument
if !self.body.is_empty() {
let body = body_text(self.clone().body, get_header_value(self.clone().headers, "content-type"));
s.push_str(substring(body.as_str(), 0, limit_body));
}
s
}
}
impl Method {
pub fn to_text(&self) -> String {
match self {
Method::Get => String::from("GET"),
Method::Head => String::from("HEAD"),
Method::Post => String::from("POST"),
Method::Put => String::from("PUT"),
Method::Delete => String::from("DELETE"),
Method::Connect => String::from("CONNECT"),
Method::Options => String::from("OPTIONS"),
Method::Trace => String::from("TRACE"),
Method::Patch => String::from("PATCH"),
}
}
}
impl Version {
pub fn to_text(&self) -> String {
match self {
Version::Http10 => String::from("1.0"),
Version::Http11 => String::from("1.1"),
Version::Http2 => String::from("2"),
}
}
}
impl Header {
fn to_text(&self) -> String {
return format!("{}: {}\n", self.name, self.value);
}
}
impl fmt::Display for Header {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}: {}", self.name, self.value)
}
}
fn body_text(bytes: Vec<u8>, content_type: Option<String>) -> String {
match content_type {
Some(content_type) =>
if is_text(content_type.as_str()) {
match String::from_utf8(bytes.clone()) {
Ok(v) => v,
Err(_) => format!("{:?}", bytes)
}
} else {
format!("{:?}", bytes)
}
_ => {
if bytes.is_empty() {
String::from("")
} else {
format!("{:?}", bytes)
}
}
}
}
fn is_text(content_type: &str) -> bool {
for s in &[
"application/json",
"text/html",
"charset=utf-8",
"application/x-www-form-urlencoded"
] {
if content_type.contains(s) {
return true;
}
}
false
}
fn substring(s: &str, start: usize, len: usize) -> &str {
let mut char_pos = 0;
let mut byte_start = 0;
let mut it = s.chars();
loop {
if char_pos == start { break; }
if let Some(c) = it.next() {
char_pos += 1;
byte_start += c.len_utf8();
} else { break; }
}
char_pos = 0;
let mut byte_end = byte_start;
loop {
if char_pos == len { break; }
if let Some(c) = it.next() {
char_pos += 1;
byte_end += c.len_utf8();
} else { break; }
}
&s[byte_start..byte_end]
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_is_text() {
assert_eq!(is_text("application/json"), true);
assert_eq!(is_text("application/json;charset=utf-8"), true);
}
#[test]
fn test_substring() {
assert_eq!(substring("", 0, 0), "");
assert_eq!(substring("hello world!", 0, 5), "hello");
assert_eq!(substring("hello world!", 0, 15), "hello world!");
}
}