mirror of
https://github.com/Orange-OpenSource/hurl.git
synced 2024-11-13 06:54:54 +03:00
remove dependencies reqwest, percent-encoding and cookie
This commit is contained in:
parent
2f84c178a8
commit
5b7f59dc7b
955
Cargo.lock
generated
955
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -16,7 +16,6 @@ strict = []
|
||||
[dependencies]
|
||||
clap = "2.33.0"
|
||||
structopt = "0.2.10"
|
||||
reqwest = "0.9.20"
|
||||
libxml = "0.2.12"
|
||||
regex = "1.1.0"
|
||||
serde_json = "1.0.40"
|
||||
@ -27,8 +26,6 @@ atty = "0.2.13"
|
||||
url = "2.1.0"
|
||||
sxd-document = "0.3.2"
|
||||
serde = "1.0.104"
|
||||
percent-encoding = "2.1.0"
|
||||
cookie = "0.12.0"
|
||||
base64 = "0.11.0"
|
||||
float-cmp = "0.6.0"
|
||||
encoding = "0.2"
|
||||
|
@ -15,9 +15,6 @@
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
use std::fs;
|
||||
|
||||
use crate::http;
|
||||
|
||||
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 {
|
||||
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)]
|
||||
mod tests {
|
||||
@ -131,20 +53,5 @@ mod tests {
|
||||
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()));
|
||||
}
|
||||
}
|
||||
|
@ -17,7 +17,7 @@
|
||||
*/
|
||||
use std::fmt;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde::Serialize;
|
||||
use serde::ser::Serializer;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
@ -31,7 +31,7 @@ pub enum DeprecatedValue {
|
||||
ListInt(Vec<i32>),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Deserialize)]
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
//#[derive(Clone, Debug, PartialEq, PartialOrd)]
|
||||
pub enum Value {
|
||||
Bool(bool),
|
||||
@ -130,13 +130,13 @@ impl Value {
|
||||
}
|
||||
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct Pos {
|
||||
pub line: usize,
|
||||
pub column: usize,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct SourceInfo {
|
||||
pub start: Pos,
|
||||
pub end: Pos,
|
||||
|
@ -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(),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
128
src/http/core.rs
128
src/http/core.rs
@ -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¶m2=¶m3=a%3db¶m4=a%253db"
|
||||
);
|
||||
}
|
||||
}
|
@ -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", ¶ms)?;
|
||||
}
|
||||
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"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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¶m2=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¶m2=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);
|
||||
}
|
||||
}
|
@ -15,11 +15,5 @@
|
||||
* 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;
|
@ -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¶m2
|
||||
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¶m2=¶m3=a%3db¶m4=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¶m2=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") },
|
||||
]);
|
||||
}
|
||||
}
|
@ -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],
|
||||
}
|
||||
}
|
||||
}
|
@ -17,7 +17,6 @@
|
||||
*/
|
||||
use std::collections::HashMap;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::core::common::{FormatError, SourceInfo, Value};
|
||||
use crate::http::libcurl;
|
||||
@ -82,14 +81,14 @@ pub type PredicateResult = Result<(), Error>;
|
||||
|
||||
// region error
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct Error {
|
||||
pub source_info: SourceInfo,
|
||||
pub inner: RunnerError,
|
||||
pub assert: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum RunnerError {
|
||||
TemplateVariableNotDefined { name: String },
|
||||
VariableNotDefined { name: String },
|
||||
|
@ -64,8 +64,8 @@ impl Serialize for AssertResult {
|
||||
S: Serializer,
|
||||
{
|
||||
let mut state = serializer.serialize_struct("??", 3)?;
|
||||
if let AssertResult::Version { source_info, actual, expected } = self {
|
||||
state.serialize_field("source_info", source_info)?;
|
||||
if let AssertResult::Version { actual, expected,.. } = self {
|
||||
//state.serialize_field("source_info", source_info)?;
|
||||
state.serialize_field("actual", actual)?;
|
||||
state.serialize_field("expected", expected)?;
|
||||
};
|
||||
|
@ -40,7 +40,6 @@ mod query;
|
||||
pub mod request;
|
||||
mod response;
|
||||
mod template;
|
||||
mod text;
|
||||
mod xpath;
|
||||
mod expr;
|
||||
|
||||
|
@ -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!");
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user