From e66354a9faaf4f3d86dbe629b3911a4acc44785f Mon Sep 17 00:00:00 2001 From: Fabrice Reix Date: Thu, 17 Sep 2020 21:41:17 +0200 Subject: [PATCH] Clean/Refactor http module --- src/bin/hurl.rs | 6 +- src/http/{libcurl => }/client.rs | 186 ++++++++------ src/http/core.rs | 74 ++++++ src/http/libcurl/core.rs | 423 ------------------------------- src/http/libcurl/mod.rs | 20 -- src/http/mod.rs | 14 +- src/http/request.rs | 214 ++++++++++++++++ src/http/response.rs | 251 ++++++++++++++++++ src/runner/assert.rs | 6 +- src/runner/capture.rs | 10 +- src/runner/cookie.rs | 11 - src/runner/core.rs | 8 +- src/runner/entry.rs | 8 +- src/runner/file.rs | 10 +- src/runner/http_response.rs | 55 ++-- src/runner/log_deserialize.rs | 5 +- src/runner/log_serialize.rs | 3 +- src/runner/multipart.rs | 14 +- src/runner/query.rs | 74 ++---- src/runner/request.rs | 90 +++---- src/runner/response.rs | 20 +- tests/libcurl.rs | 64 ++--- tests/runner.rs | 10 +- 23 files changed, 834 insertions(+), 742 deletions(-) rename src/http/{libcurl => }/client.rs (79%) create mode 100644 src/http/core.rs delete mode 100644 src/http/libcurl/core.rs delete mode 100644 src/http/libcurl/mod.rs create mode 100644 src/http/request.rs create mode 100644 src/http/response.rs diff --git a/src/bin/hurl.rs b/src/bin/hurl.rs index 79e649b90..b3813de8a 100644 --- a/src/bin/hurl.rs +++ b/src/bin/hurl.rs @@ -30,7 +30,7 @@ use clap::{AppSettings, ArgMatches}; use hurl::cli; use hurl::core::common::FormatError; use hurl::html; -use hurl::http::libcurl; +use hurl::http; use hurl::parser; use hurl::runner; use hurl::runner::core::*; @@ -109,7 +109,7 @@ fn execute(filename: &str, let proxy = cli_options.proxy; let no_proxy = cli_options.no_proxy; let cookie_input_file = cli_options.cookie_input_file; - let options = libcurl::client::ClientOptions { + let options = http::ClientOptions { follow_location, max_redirect, cookie_input_file, @@ -118,7 +118,7 @@ fn execute(filename: &str, verbose, insecure, }; - let mut client = libcurl::client::Client::init(options); + let mut client = http::Client::init(options); let context_dir = match file_root { diff --git a/src/http/libcurl/client.rs b/src/http/client.rs similarity index 79% rename from src/http/libcurl/client.rs rename to src/http/client.rs index 2d51196ef..2042bc02b 100644 --- a/src/http/libcurl/client.rs +++ b/src/http/client.rs @@ -16,15 +16,26 @@ * */ +use std::io::Read; use std::str; use curl::easy; +use encoding::all::ISO_8859_1; +use encoding::{DecoderTrap, Encoding}; use super::core::*; -use std::io::Read; -use encoding::{Encoding, DecoderTrap}; -use encoding::all::ISO_8859_1; +use super::request::*; +use super::response::*; +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum HttpError { + CouldNotResolveProxyName, + CouldNotResolveHost, + FailToConnect, + TooManyRedirect, + CouldNotParseResponse, + SSLCertificate, +} #[derive(Debug)] pub struct Client { @@ -50,9 +61,7 @@ pub struct ClientOptions { pub insecure: bool, } - impl Client { - /// /// Init HTTP hurl client /// @@ -61,7 +70,13 @@ impl Client { // Activate cookie storage // with or without persistence (empty string) - h.cookie_file(options.cookie_input_file.unwrap_or_else(|| "".to_string()).as_str()).unwrap(); + h.cookie_file( + options + .cookie_input_file + .unwrap_or_else(|| "".to_string()) + .as_str(), + ) + .unwrap(); if let Some(proxy) = options.proxy { h.proxy(proxy.as_str()).unwrap(); @@ -93,7 +108,11 @@ impl Client { /// /// Execute an http request /// - pub fn execute(&mut self, request: &Request, redirect_count: usize) -> Result { + pub fn execute( + &mut self, + request: &Request, + redirect_count: usize, + ) -> Result { self.set_url(&request.url, &request.querystring); self.set_method(&request.method); @@ -101,16 +120,13 @@ impl Client { self.set_form(&request.form); self.set_multipart(&request.multipart); - let b = request.body.clone(); let mut data: &[u8] = b.as_ref(); self.set_body(data); self.set_headers(&request); - - self.handle.debug_function(|info_type, data| - match info_type { - + self.handle + .debug_function(|info_type, data| match info_type { // return all request headers (not one by one) easy::InfoType::HeaderOut => { let lines = split_lines(data); @@ -124,30 +140,34 @@ impl Client { } } _ => {} - } - ).unwrap(); + }) + .unwrap(); let mut lines = vec![]; let mut body = Vec::::new(); { let mut transfer = self.handle.transfer(); if !data.is_empty() { - transfer.read_function(|buf| { - Ok(data.read(buf).unwrap_or(0)) - }).unwrap(); + transfer + .read_function(|buf| Ok(data.read(buf).unwrap_or(0))) + .unwrap(); } - transfer.header_function(|h| { - if let Some(s) = decode_header(h) { - lines.push(s) - } - true - }).unwrap(); + transfer + .header_function(|h| { + if let Some(s) = decode_header(h) { + lines.push(s) + } + true + }) + .unwrap(); - transfer.write_function(|data| { - body.extend(data); - Ok(data.len()) - }).unwrap(); + transfer + .write_function(|data| { + body.extend(data); + Ok(data.len()) + }) + .unwrap(); if let Err(e) = transfer.perform() { match e.code() { @@ -161,11 +181,10 @@ impl Client { } let status = self.handle.response_code().unwrap(); - let first_line = lines.remove(0); // remove the status line + let first_line = lines.remove(0); // remove the status line let version = self.parse_response_version(first_line)?; let headers = self.parse_response_headers(&mut lines); - if let Some(url) = self.get_follow_location(headers.clone()) { let request = Request { method: Method::Get, @@ -219,7 +238,6 @@ impl Client { self.handle.url(url.as_str()).unwrap(); } - /// /// set method /// @@ -237,7 +255,6 @@ impl Client { } } - /// /// set request headers /// @@ -245,29 +262,31 @@ impl Client { let mut list = easy::List::new(); for header in request.headers.clone() { - list.append(format!("{}: {}", header.name, header.value).as_str()).unwrap(); + list.append(format!("{}: {}", header.name, header.value).as_str()) + .unwrap(); } if get_header_values(request.headers.clone(), "Content-Type".to_string()).is_empty() { if let Some(s) = request.content_type.clone() { - list.append(format!("Content-Type: {}", s).as_str()).unwrap(); + list.append(format!("Content-Type: {}", s).as_str()) + .unwrap(); } else { - list.append("Content-Type:").unwrap(); // remove header Content-Type + list.append("Content-Type:").unwrap(); // remove header Content-Type } } -// if request.form.is_empty() && request.multipart.is_empty() && request.body.is_empty() { -// list.append("Content-Length:").unwrap(); -// } + // if request.form.is_empty() && request.multipart.is_empty() && request.body.is_empty() { + // list.append("Content-Length:").unwrap(); + // } if get_header_values(request.headers.clone(), "User-Agent".to_string()).is_empty() { - list.append(format!("User-Agent: hurl/{}", clap::crate_version!()).as_str()).unwrap(); + list.append(format!("User-Agent: hurl/{}", clap::crate_version!()).as_str()) + .unwrap(); } self.handle.http_headers(list).unwrap(); } - /// /// set request cookies /// @@ -277,7 +296,6 @@ impl Client { //} } - /// /// set form /// @@ -289,7 +307,6 @@ impl Client { } } - /// /// set form /// @@ -299,18 +316,19 @@ impl Client { for param in params { match param { MultipartParam::Param(Param { name, value }) => { - form.part(name) - .contents(value.as_bytes()) - .add() - .unwrap() - } - MultipartParam::FileParam(FileParam { name, filename, data, content_type }) => { - form.part(name) - .buffer(filename, data.clone()) - .content_type(content_type) - .add() - .unwrap() + form.part(name).contents(value.as_bytes()).add().unwrap() } + MultipartParam::FileParam(FileParam { + name, + filename, + data, + content_type, + }) => form + .part(name) + .buffer(filename, data.clone()) + .content_type(content_type) + .add() + .unwrap(), } } self.handle.httppost(form).unwrap(); @@ -341,7 +359,6 @@ impl Client { .join("&") } - /// /// parse response version /// @@ -357,13 +374,12 @@ impl Client { } } - /// /// parse headers from libcurl responses /// fn parse_response_headers(&mut self, lines: &mut Vec) -> Vec
{ let mut headers: Vec
= vec![]; - lines.pop(); // remove the blank line between headers and body + lines.pop(); // remove the blank line between headers and body for line in lines { if let Some(header) = Header::parse(line.to_string()) { headers.push(header); @@ -372,7 +388,6 @@ impl Client { headers } - /// /// retrieve an optional location to follow /// You need: @@ -402,7 +417,6 @@ impl Client { } } - /// /// get cookie storage /// @@ -420,7 +434,15 @@ impl Client { let expires = fields.get(4).unwrap().to_string(); let name = fields.get(5).unwrap().to_string(); let value = fields.get(6).unwrap().to_string(); - cookies.push(Cookie { domain, include_subdomain, path, https, expires, name, value }); + cookies.push(Cookie { + domain, + include_subdomain, + path, + https, + expires, + name, + value, + }); } cookies } @@ -433,11 +455,12 @@ impl Client { eprintln!("* add to cookie store: {}", cookie); //self.handle.cookie_list(format!("Set-Cookie: {}={}", cookie.name, cookie.value).as_str()).unwrap(); } - self.handle.cookie_list(cookie.to_string().as_str()).unwrap(); + self.handle + .cookie_list(cookie.to_string().as_str()) + .unwrap(); } } - impl Header { /// /// Parse an http header line received from the server @@ -457,7 +480,6 @@ impl Header { } } - /// /// Split an array of bytes into http lines (\r\n separator) /// @@ -479,42 +501,42 @@ fn split_lines(data: &[u8]) -> Vec { lines } - /// /// Decode optionally header value as text with utf8 or iso-8859-1 encoding /// pub fn decode_header(data: &[u8]) -> Option { match str::from_utf8(data) { Ok(s) => Some(s.to_string()), - Err(_) => { - match ISO_8859_1.decode(data, DecoderTrap::Strict) { - Ok(s) => Some(s), - Err(_) => { - println!("Error decoding header both utf8 and iso-8859-1 {:?}", data); - None - } + Err(_) => match ISO_8859_1.decode(data, DecoderTrap::Strict) { + Ok(s) => Some(s), + Err(_) => { + println!("Error decoding header both utf8 and iso-8859-1 {:?}", data); + None } - } + }, } } - #[cfg(test)] mod tests { use super::*; #[test] fn test_parse_header() { - assert_eq!(Header::parse("Foo: Bar\r\n".to_string()).unwrap(), - Header { - name: "Foo".to_string(), - value: "Bar".to_string(), - }); - assert_eq!(Header::parse("Location: http://localhost:8000/redirected\r\n".to_string()).unwrap(), - Header { - name: "Location".to_string(), - value: "http://localhost:8000/redirected".to_string(), - }); + assert_eq!( + Header::parse("Foo: Bar\r\n".to_string()).unwrap(), + Header { + name: "Foo".to_string(), + value: "Bar".to_string(), + } + ); + assert_eq!( + Header::parse("Location: http://localhost:8000/redirected\r\n".to_string()).unwrap(), + Header { + name: "Location".to_string(), + value: "http://localhost:8000/redirected".to_string(), + } + ); assert!(Header::parse("Foo".to_string()).is_none()); } @@ -527,4 +549,4 @@ mod tests { assert_eq!(lines.get(1).unwrap().as_str(), "Host: localhost:8000"); assert_eq!(lines.get(2).unwrap().as_str(), ""); } -} \ No newline at end of file +} diff --git a/src/http/core.rs b/src/http/core.rs new file mode 100644 index 000000000..0c7300304 --- /dev/null +++ b/src/http/core.rs @@ -0,0 +1,74 @@ +/* + * 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; + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Header { + pub name: String, + pub value: String, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Cookie { + pub domain: String, + pub include_subdomain: String, + pub path: String, + pub https: String, + pub expires: String, + pub name: String, + pub value: String, +} + +impl fmt::Display for Header { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}: {}", self.name, self.value) + } +} + +impl fmt::Display for Cookie { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "{}\t{}\t{}\t{}\t{}\t{}\t{}", + self.domain, + self.include_subdomain, + self.path, + self.https, + self.expires, + self.name, + self.value + ) + } +} + +/// +/// return a list of headers values for the given header name +/// +pub fn get_header_values(headers: Vec
, expected_name: String) -> Vec { + headers + .iter() + .filter_map(|Header { name, value }| { + if name.clone() == expected_name { + Some(value.to_string()) + } else { + None + } + }) + .collect() +} diff --git a/src/http/libcurl/core.rs b/src/http/libcurl/core.rs deleted file mode 100644 index 91d5fa244..000000000 --- a/src/http/libcurl/core.rs +++ /dev/null @@ -1,423 +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; - -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct Request { - pub method: Method, - pub url: String, - pub headers: Vec
, - pub querystring: Vec, - pub form: Vec, - pub multipart: Vec, - pub cookies: Vec, - pub body: Vec, - pub content_type: Option, -} - - -//impl Request { -// -// /// -// /// Get implicit content-type from request -// /// Note that for multipart, the content-type is not returned because it is generated at runtime by the client -// /// -// pub fn content_type(&self) -> Option { -// if self.form.is_empty() { -// Some("application/x-www-form-urlencoded".to_string()) -// // } else if self..mform.is_empty() { -// // Some("application/x-www-form-urlencoded".to_string()) -// } else { -// None -// } -// } -//} - - -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct Response { - pub version: Version, - pub status: u32, - pub headers: Vec
, - pub body: Vec, -} - - -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum Method { - Get, - Head, - Post, - Put, - Delete, - Connect, - Options, - Trace, - Patch, -} - -impl fmt::Display for Method { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - let value = match self { - Method::Get => "GET", - Method::Head => "HEAD", - Method::Post => "POST", - Method::Put => "PUT", - Method::Delete => "DELETE", - Method::Connect => "CONNECT", - Method::Options => "OPTIONS", - Method::Trace => "TRACE", - Method::Patch => "PATCH" - }; - write!(f, "{}", value) - } -} - -#[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 value = match self { - Version::Http10 => "1.0", - Version::Http11 => "1.1", - Version::Http2 => "2", - }; - write!(f, "{}", value) - } -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct Header { - pub name: String, - pub value: String, -} - -impl fmt::Display for Header { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}: {}", self.name, self.value) - } -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct Param { - pub name: String, - pub value: String, -} - -impl fmt::Display for Param { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}: {}", self.name, self.value) - } -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum MultipartParam { - Param(Param), - FileParam(FileParam), -} - -impl fmt::Display for MultipartParam { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self { - MultipartParam::Param(param) => write!(f, "{}", param.to_string()), - MultipartParam::FileParam(param) => write!(f, "{}", param.to_string()), - } - } -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct FileParam { - pub name: String, - pub filename: String, - pub data: Vec, - pub content_type: String, -} - -impl fmt::Display for FileParam { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}: file,{}; {}", self.name, self.filename, self.content_type) - } -} - - -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct RequestCookie { - pub name: String, - pub value: String, -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct Cookie { - pub domain: String, - pub include_subdomain: String, - pub path: String, - pub https: String, - pub expires: String, - pub name: String, - pub value: String, -} - -impl fmt::Display for Cookie { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}\t{}\t{}\t{}\t{}\t{}\t{}", self.domain, self.include_subdomain, self.path, self.https, self.expires, self.name, self.value) - } -} - - -impl fmt::Display for RequestCookie { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}={}", self.name, self.value) - } -} - - -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum HttpError { - CouldNotResolveProxyName, - CouldNotResolveHost, - FailToConnect, - TooManyRedirect, - CouldNotParseResponse, - SSLCertificate, -} - - -impl Response { - /// - /// return a list of headers values for the given header name - /// - pub fn get_header_values(&self, expected_name: String) -> Vec { - get_header_values(self.headers.clone(), expected_name) - } -} - - -/// -/// return a list of headers values for the given header name -/// -pub fn get_header_values(headers: Vec
, expected_name: String) -> Vec { - headers - .iter() - .filter_map(|Header { name, value }| if name.clone() == expected_name { Some(value.to_string()) } else { None }) - .collect() -} - -#[cfg(test)] -pub mod tests { - use super::*; - - - #[test] - fn get_header_values() { - let response = Response { - version: Version::Http10, - status: 200, - headers: vec![ - Header { name: "Content-Length".to_string(), value: "12".to_string() } - ], - body: vec![], - }; - assert_eq!(response.get_header_values("Content-Length".to_string()), vec!["12".to_string()]); - assert!(response.get_header_values("Unknown".to_string()).is_empty()); - } - - pub fn hello_http_request() -> Request { - Request { - method: Method::Get, - url: "http://localhost:8000/hello".to_string(), - querystring: vec![], - headers: vec![], - cookies: vec![], - body: vec![], - multipart: vec![], - form: vec![], - content_type: None, - } - } - - pub fn custom_http_request() -> Request { - Request { - method: Method::Get, - url: "http://localhost/custom".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![ - RequestCookie { - name: String::from("theme"), - value: String::from("light"), - }, - RequestCookie { - name: String::from("sessionToken"), - value: String::from("abc123"), - } - ], - body: vec![], - multipart: vec![], - form: vec![], - content_type: None, - } - } - - // GET http://localhost:8000/querystring-params?param1=value1¶m2 - pub fn query_http_request() -> Request { - Request { - method: Method::Get, - url: "http://localhost:8000/querystring-params".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![], - form: vec![], - content_type: None, - } - } - - - pub fn form_http_request() -> Request { - Request { - method: Method::Post, - url: "http://localhost/form-params".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![], - form: vec![], - content_type: Some("multipart/form-data".to_string()), - } - } - - 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("
")), - } - } - - 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#" - - - Bob - Bill - -"#.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#" - - - Bob - Bill - Bruce - -"#.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], - } - } -} \ No newline at end of file diff --git a/src/http/libcurl/mod.rs b/src/http/libcurl/mod.rs deleted file mode 100644 index 54b9d22a1..000000000 --- a/src/http/libcurl/mod.rs +++ /dev/null @@ -1,20 +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. - * - */ - -pub mod client; -pub mod core; \ No newline at end of file diff --git a/src/http/mod.rs b/src/http/mod.rs index bc639b207..8eab0e517 100644 --- a/src/http/mod.rs +++ b/src/http/mod.rs @@ -16,4 +16,16 @@ * */ -pub mod libcurl; \ No newline at end of file +pub use self::client::{Client, ClientOptions, HttpError}; +pub use self::core::{Cookie, Header}; +#[cfg(test)] +pub use self::request::tests::*; +pub use self::request::{FileParam, Method, MultipartParam, Param, Request, RequestCookie}; +#[cfg(test)] +pub use self::response::tests::*; +pub use self::response::{Response, Version}; + +mod client; +mod core; +mod request; +mod response; diff --git a/src/http/request.rs b/src/http/request.rs new file mode 100644 index 000000000..bbe43b9d7 --- /dev/null +++ b/src/http/request.rs @@ -0,0 +1,214 @@ +/* + * 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 super::core::*; + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Request { + pub method: Method, + pub url: String, + pub headers: Vec
, + pub querystring: Vec, + pub form: Vec, + pub multipart: Vec, + pub cookies: Vec, + pub body: Vec, + pub content_type: Option, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum Method { + Get, + Head, + Post, + Put, + Delete, + Connect, + Options, + Trace, + Patch, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Param { + pub name: String, + pub value: String, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum MultipartParam { + Param(Param), + FileParam(FileParam), +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct FileParam { + pub name: String, + pub filename: String, + pub data: Vec, + pub content_type: String, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct RequestCookie { + pub name: String, + pub value: String, +} + +impl fmt::Display for Method { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let value = match self { + Method::Get => "GET", + Method::Head => "HEAD", + Method::Post => "POST", + Method::Put => "PUT", + Method::Delete => "DELETE", + Method::Connect => "CONNECT", + Method::Options => "OPTIONS", + Method::Trace => "TRACE", + Method::Patch => "PATCH", + }; + write!(f, "{}", value) + } +} + +impl fmt::Display for Param { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}: {}", self.name, self.value) + } +} + +impl fmt::Display for MultipartParam { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + MultipartParam::Param(param) => write!(f, "{}", param.to_string()), + MultipartParam::FileParam(param) => write!(f, "{}", param.to_string()), + } + } +} + +impl fmt::Display for FileParam { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "{}: file,{}; {}", + self.name, self.filename, self.content_type + ) + } +} + +impl fmt::Display for RequestCookie { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}={}", self.name, self.value) + } +} + +#[cfg(test)] +pub mod tests { + use super::*; + + pub fn hello_http_request() -> Request { + Request { + method: Method::Get, + url: "http://localhost:8000/hello".to_string(), + querystring: vec![], + headers: vec![], + cookies: vec![], + body: vec![], + multipart: vec![], + form: vec![], + content_type: None, + } + } + + pub fn custom_http_request() -> Request { + Request { + method: Method::Get, + url: "http://localhost/custom".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![ + RequestCookie { + name: String::from("theme"), + value: String::from("light"), + }, + RequestCookie { + name: String::from("sessionToken"), + value: String::from("abc123"), + }, + ], + body: vec![], + multipart: vec![], + form: vec![], + content_type: None, + } + } + + pub fn query_http_request() -> Request { + Request { + method: Method::Get, + url: "http://localhost:8000/querystring-params".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![], + form: vec![], + content_type: None, + } + } + + pub fn form_http_request() -> Request { + Request { + method: Method::Post, + url: "http://localhost/form-params".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![], + form: vec![], + content_type: Some("multipart/form-data".to_string()), + } + } +} diff --git a/src/http/response.rs b/src/http/response.rs new file mode 100644 index 000000000..6e3112ae5 --- /dev/null +++ b/src/http/response.rs @@ -0,0 +1,251 @@ +/* + * 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 super::core::*; + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Response { + pub version: Version, + pub status: u32, + pub headers: Vec
, + pub body: Vec, +} + +#[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 value = match self { + Version::Http10 => "1.0", + Version::Http11 => "1.1", + Version::Http2 => "2", + }; + write!(f, "{}", value) + } +} + +impl Response { + /// + /// return a list of headers values for the given header name + /// + pub fn get_header_values(&self, expected_name: String) -> Vec { + get_header_values(self.headers.clone(), expected_name) + } + + /// + /// + /// + pub fn get_header(&self, name: String) -> Vec { + self.headers + .iter() + .filter(|&h| h.name.to_lowercase() == name.to_lowercase()) + .map(|h| h.value.clone()) + .collect() + } + + /// + /// Return optional Content-type header value + /// + pub fn content_type(&self) -> Option { + for header in self.headers.clone() { + if header.name.to_lowercase().as_str() == "content-type" { + return Some(header.value); + } + } + None + } +} + +#[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( + "
", + )), + } + } + + 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#" + + + Bob + Bill + +"# + .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#" + + + Bob + Bill + Bruce + +"# + .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], + } + } + + #[test] + fn get_header_values() { + let response = Response { + version: Version::Http10, + status: 200, + headers: vec![Header { + name: "Content-Length".to_string(), + value: "12".to_string(), + }], + body: vec![], + }; + assert_eq!( + response.get_header_values("Content-Length".to_string()), + vec!["12".to_string()] + ); + assert!(response.get_header_values("Unknown".to_string()).is_empty()); + } +} diff --git a/src/runner/assert.rs b/src/runner/assert.rs index 37e6b1313..073de284a 100644 --- a/src/runner/assert.rs +++ b/src/runner/assert.rs @@ -19,7 +19,7 @@ use std::collections::HashMap; use crate::core::common::Value; -use crate::http::libcurl; +use crate::http; use super::core::{Error, RunnerError}; use super::core::*; @@ -107,7 +107,7 @@ impl AssertResult { } impl Assert { - pub fn eval(self, http_response: libcurl::core::Response, variables: &HashMap) -> AssertResult { + pub fn eval(self, http_response: http::Response, variables: &HashMap) -> AssertResult { let actual = self.query.eval(variables, http_response); let source_info = self.predicate.clone().predicate_func.source_info; let predicate_result = match actual.clone() { @@ -162,7 +162,7 @@ pub mod tests { fn test_eval() { let variables = HashMap::new(); assert_eq!( - assert_count_user().eval(libcurl::core::tests::xml_three_users_http_response(), &variables), + assert_count_user().eval(http::xml_three_users_http_response(), &variables), AssertResult::Explicit { actual: Ok(Some(Value::Nodeset(3))), source_info: SourceInfo::init(1, 14, 1, 27), diff --git a/src/runner/capture.rs b/src/runner/capture.rs index d91e21c3f..78fde2899 100644 --- a/src/runner/capture.rs +++ b/src/runner/capture.rs @@ -20,7 +20,7 @@ use std::collections::HashMap; use regex::Regex; use crate::core::common::Value; -use crate::http::libcurl; +use crate::http; use super::core::{CaptureResult, Error}; use super::core::RunnerError; @@ -28,7 +28,7 @@ use super::super::core::ast::*; impl Capture { - pub fn eval(self, variables: &HashMap, http_response: libcurl::core::Response) -> Result { + pub fn eval(self, variables: &HashMap, http_response: http::Response) -> Result { let name = self.name.value; let value = self.query.clone().eval(variables, http_response)?; let value = match value { @@ -184,7 +184,7 @@ pub mod tests { }, }; - let error = capture.eval(&variables, libcurl::core::tests::xml_three_users_http_response()).err().unwrap(); + let error = capture.eval(&variables, http::xml_three_users_http_response()).err().unwrap(); assert_eq!(error.source_info.start, Pos { line: 1, column: 7 }); assert_eq!(error.inner, RunnerError::QueryInvalidXpathEval) } @@ -239,13 +239,13 @@ pub mod tests { #[test] fn test_capture() { let variables = HashMap::new(); - assert_eq!(user_count_capture().eval(&variables, libcurl::core::tests::xml_three_users_http_response()).unwrap(), + assert_eq!(user_count_capture().eval(&variables, http::xml_three_users_http_response()).unwrap(), CaptureResult { name: "UserCount".to_string(), value: Value::from_f64(3.0), }); - assert_eq!(duration_capture().eval(&variables, libcurl::core::tests::json_http_response()).unwrap(), + assert_eq!(duration_capture().eval(&variables, http::json_http_response()).unwrap(), CaptureResult { name: "duration".to_string(), value: Value::from_f64(1.5), diff --git a/src/runner/cookie.rs b/src/runner/cookie.rs index eb054e8ba..0e480277c 100644 --- a/src/runner/cookie.rs +++ b/src/runner/cookie.rs @@ -25,17 +25,6 @@ /// and not by the http client. /// -use crate::http::libcurl::core::Response; - -impl Response { - pub fn cookies(&self) -> Vec { - self.headers - .iter() - .filter(|&h| h.name.to_lowercase() == "set-cookie") - .filter_map(|h| ResponseCookie::parse(h.value.clone())) - .collect() - } -} /// /// Cookie return from HTTP Response diff --git a/src/runner/core.rs b/src/runner/core.rs index a362226c7..9642da7a9 100644 --- a/src/runner/core.rs +++ b/src/runner/core.rs @@ -19,7 +19,7 @@ use std::collections::HashMap; use crate::core::common::{FormatError, SourceInfo, Value}; -use crate::http::libcurl; +use crate::http; #[derive(Clone, Debug, PartialEq, Eq)] pub struct RunnerOptions { @@ -35,7 +35,7 @@ pub struct HurlResult { pub entries: Vec, pub time_in_ms: u128, pub success: bool, - pub cookies: Vec, + pub cookies: Vec, } impl HurlResult { @@ -50,8 +50,8 @@ impl HurlResult { #[derive(Clone, Debug, PartialEq, Eq)] pub struct EntryResult { - pub request: Option, - pub response: Option, + pub request: Option, + pub response: Option, //pub captures: Vec<(String, Value)>, pub captures: Vec, pub asserts: Vec, diff --git a/src/runner/entry.rs b/src/runner/entry.rs index f473cde9b..2bcb2bdb1 100644 --- a/src/runner/entry.rs +++ b/src/runner/entry.rs @@ -22,7 +22,7 @@ use std::time::Instant; use crate::core::ast::*; use crate::core::common::SourceInfo; use crate::core::common::Value; -use crate::http::libcurl; +use crate::http; use super::core::*; @@ -49,7 +49,7 @@ use crate::format::logger::Logger; //// }); /// ``` pub fn run(entry: Entry, - http_client: &mut libcurl::client::Client, + http_client: &mut http::Client, entry_index: usize, variables: &mut HashMap, context_dir: String, @@ -80,7 +80,7 @@ pub fn run(entry: Entry, use url::Url; if let Ok(url) = Url::parse(http_request.url.as_str()) { for c in http_request.cookies.clone() { - let cookie = libcurl::core::Cookie { + let cookie = http::Cookie { domain: url.host_str().unwrap().to_string(), include_subdomain: "FALSE".to_string(), path: "/".to_string(), @@ -182,7 +182,7 @@ pub fn run(entry: Entry, } -pub fn log_request(logger: &Logger, request: &libcurl::core::Request) { +pub fn log_request(logger: &Logger, request: &http::Request) { logger.verbose("Request"); logger.verbose(format!("{} {}", request.method, request.url).as_str()); for header in request.headers.clone() { diff --git a/src/runner/file.rs b/src/runner/file.rs index 6874266b9..bd4eac3cc 100644 --- a/src/runner/file.rs +++ b/src/runner/file.rs @@ -20,7 +20,7 @@ use std::time::Instant; use crate::core::ast::*; use crate::core::common::Value; -use crate::http::libcurl; +use crate::http; use super::core::*; @@ -35,7 +35,7 @@ use crate::core::common::FormatError; /// # Example /// /// ``` -/// use hurl::http::libcurl; +/// use hurl::http; /// use hurl::runner; /// use hurl::format; /// @@ -48,7 +48,7 @@ use crate::core::common::FormatError; /// let hurl_file = hurl::parser::parse_hurl_file(s).unwrap(); /// /// // Create an http client -/// let options = libcurl::client::ClientOptions { +/// let options = http::ClientOptions { /// follow_location: false, /// max_redirect: None, /// cookie_input_file: None, @@ -57,7 +57,7 @@ use crate::core::common::FormatError; /// verbose: false, /// insecure: false, /// }; -/// let mut client = libcurl::client::Client::init(options); +/// let mut client = http::Client::init(options); /// /// // Define runner options /// let variables = std::collections::HashMap::new(); @@ -92,7 +92,7 @@ use crate::core::common::FormatError; /// ``` pub fn run( hurl_file: HurlFile, - http_client: &mut libcurl::client::Client, + http_client: &mut http::Client, filename: String, context_dir: String, options: RunnerOptions, diff --git a/src/runner/http_response.rs b/src/runner/http_response.rs index f2d57cf92..6bfc623e9 100644 --- a/src/runner/http_response.rs +++ b/src/runner/http_response.rs @@ -16,33 +16,25 @@ * */ -use crate::http::libcurl::core::Response; +use crate::http::Response; use super::core::RunnerError; +use super::cookie::ResponseCookie; use encoding::{EncodingRef, DecoderTrap}; -/// -/// get body content as text from http response -/// used by query -/// -#[allow(dead_code)] + impl Response { - /// - /// Return optional Content-type header value - /// - fn content_type(&self) -> Option { - for header in self.headers.clone() { - if header.name.to_lowercase() == "content-type" { - return Some(header.value); - } - } - None + pub fn cookies(&self) -> Vec { + self.headers + .iter() + .filter(|&h| h.name.to_lowercase().as_str() == "set-cookie") + .filter_map(|h| ResponseCookie::parse(h.value.clone())) + .collect() } - /// /// Return encoding of the response /// @@ -61,6 +53,9 @@ impl Response { } } + /// + /// return response body as text + /// pub fn text(&self) -> Result { let encoding = self.encoding()?; match encoding.decode(&self.body, DecoderTrap::Strict) { @@ -68,6 +63,30 @@ impl Response { Err(_) => Err(RunnerError::InvalidDecoding { charset: encoding.name().to_string() }) } } + + /// + /// return true if response is an html response + /// + pub fn is_html(&self) -> bool { + match self.content_type() { + None => false, + Some(s) => s.starts_with("text/html") + } + } + + + /// + /// Return option cookie from response + /// + pub fn get_cookie(&self, name: String) -> Option { + for cookie in self.cookies() { + if cookie.name == name { + return Some(cookie); + } + } + None + } + } /// @@ -86,7 +105,7 @@ fn mime_charset(mime_type: String) -> Option { #[cfg(test)] pub mod tests { use super::*; - use crate::http::libcurl::core::{Version, Header}; + use crate::http::*; #[test] pub fn test_charset() { diff --git a/src/runner/log_deserialize.rs b/src/runner/log_deserialize.rs index 401d55402..132f872ab 100644 --- a/src/runner/log_deserialize.rs +++ b/src/runner/log_deserialize.rs @@ -17,8 +17,7 @@ */ -use crate::http::libcurl::core::*; - +use crate::http::*; use super::cookie::*; use super::core::*; @@ -350,8 +349,6 @@ fn parse_version(s: String) -> Result { #[cfg(test)] mod tests { - use crate::http::libcurl::core::tests::*; - use super::*; #[test] diff --git a/src/runner/log_serialize.rs b/src/runner/log_serialize.rs index 2f9b6d69b..2f458f7ad 100644 --- a/src/runner/log_serialize.rs +++ b/src/runner/log_serialize.rs @@ -19,7 +19,8 @@ use serde::ser::{Serializer, SerializeStruct}; use serde::Serialize; -use crate::http::libcurl::core::*; +use crate::http::*; + use super::cookie::*; use super::core::*; diff --git a/src/runner/multipart.rs b/src/runner/multipart.rs index c8c3bce3d..4dffdf6f0 100644 --- a/src/runner/multipart.rs +++ b/src/runner/multipart.rs @@ -28,7 +28,7 @@ use std::path::Path; use crate::core::ast::*; use crate::core::common::Value; -use crate::http::libcurl; +use crate::http; use super::core::{Error, RunnerError}; @@ -38,23 +38,23 @@ impl MultipartParam { pub fn eval(self, variables: &HashMap, context_dir: String, - ) -> Result { + ) -> Result { match self { MultipartParam::Param(KeyValue { key, value, .. }) => { let name = key.value; let value = value.eval(variables)?; - Ok(libcurl::core::MultipartParam::Param(libcurl::core::Param { name, value })) + Ok(http::MultipartParam::Param(http::Param { name, value })) } MultipartParam::FileParam(param) => { let file_param = param.eval(context_dir)?; - Ok(libcurl::core::MultipartParam::FileParam(file_param)) + Ok(http::MultipartParam::FileParam(file_param)) } } } } impl FileParam { - pub fn eval(self, context_dir: String) -> Result { + pub fn eval(self, context_dir: String) -> Result { let name = self.key.value; let filename = self.value.filename.clone(); @@ -93,7 +93,7 @@ impl FileParam { } let content_type = self.value.content_type(); - Ok(libcurl::core::FileParam { + Ok(http::FileParam { name, filename: filename.value, data, @@ -166,7 +166,7 @@ mod tests { }, line_terminator0: line_terminator, }.eval("integration/tests".to_string()).unwrap(), - libcurl::core::FileParam { + http::FileParam { name: "upload1".to_string(), filename: "hello.txt".to_string(), data: b"Hello World!".to_vec(), diff --git a/src/runner/query.rs b/src/runner/query.rs index 0089fd213..50e877cf2 100644 --- a/src/runner/query.rs +++ b/src/runner/query.rs @@ -21,7 +21,7 @@ use std::collections::HashMap; use regex::Regex; use crate::core::common::Value; -use crate::http::libcurl; +use crate::http; use crate::jsonpath; use super::cookie; @@ -32,29 +32,8 @@ use super::xpath; pub type QueryResult = Result, Error>; -impl libcurl::core::Response { - pub fn is_html(&self) -> bool { - for header in self.headers.clone() { - if header.name.to_lowercase() == "content-type" { - return header.value.contains("html"); - } - } - false - } - - pub fn get_cookie(&self, name: String) -> Option { - for cookie in self.cookies() { - if cookie.name == name { - return Some(cookie); - } - } - None - } -} - - impl Query { - pub fn eval(self, variables: &HashMap, http_response: libcurl::core::Response) -> QueryResult { + pub fn eval(self, variables: &HashMap, http_response: http::Response) -> QueryResult { match self.value { QueryValue::Status {} => Ok(Some(Value::Integer(i64::from(http_response.status)))), QueryValue::Header { name, .. } => { @@ -224,7 +203,6 @@ impl CookieAttributeName { #[cfg(test)] pub mod tests { use crate::core::common::{Pos, SourceInfo}; - use super::*; pub fn xpath_invalid_query() -> Query { @@ -297,9 +275,9 @@ pub mod tests { } } - pub fn json_http_response() -> libcurl::core::Response { - libcurl::core::Response { - version: libcurl::core::Version::Http10, + pub fn json_http_response() -> http::Response { + http::Response { + version: http::Version::Http10, status: 0, headers: vec![], body: String::into_bytes(r#" @@ -436,7 +414,7 @@ pub mod tests { fn test_query_status() { let variables = HashMap::new(); assert_eq!( - Query { source_info: SourceInfo::init(0, 0, 0, 0), value: QueryValue::Status {} }.eval(&variables, libcurl::core::tests::hello_http_response()).unwrap().unwrap(), + Query { source_info: SourceInfo::init(0, 0, 0, 0), value: QueryValue::Status {} }.eval(&variables, http::hello_http_response()).unwrap().unwrap(), Value::Integer(200) ); } @@ -464,7 +442,7 @@ pub mod tests { // let error = query_header.eval(http::hello_http_response()).err().unwrap(); // assert_eq!(error.source_info.start, Pos { line: 1, column: 8 }); // assert_eq!(error.inner, RunnerError::QueryHeaderNotFound); - assert_eq!(query_header.eval(&variables, libcurl::core::tests::hello_http_response()).unwrap(), None); + assert_eq!(query_header.eval(&variables, http::hello_http_response()).unwrap(), None); } #[test] @@ -488,7 +466,7 @@ pub mod tests { }, }; assert_eq!( - query_header.eval(&variables, libcurl::core::tests::hello_http_response()).unwrap().unwrap(), + query_header.eval(&variables, http::hello_http_response()).unwrap().unwrap(), Value::String(String::from("text/html; charset=utf-8")) ); } @@ -500,11 +478,11 @@ pub mod tests { value: String::from(""), source_info: SourceInfo::init(0, 0, 0, 0), }; - let response = libcurl::core::Response { - version: libcurl::core::Version::Http10, + let response = http::Response { + version: http::Version::Http10, status: 0, headers: vec![ - libcurl::core::Header { + http::Header { name: "Set-Cookie".to_string(), value: "LSID=DQAAAKEaem_vYg; Path=/accounts; Expires=Wed, 13 Jan 2021 22:23:01 GMT; Secure; HttpOnly".to_string(), } @@ -649,22 +627,22 @@ pub mod tests { Query { source_info: SourceInfo::init(0, 0, 0, 0), value: QueryValue::Body {}, - }.eval(&variables, libcurl::core::tests::hello_http_response()).unwrap().unwrap(), + }.eval(&variables, http::hello_http_response()).unwrap().unwrap(), Value::String(String::from("Hello World!")) ); let error = Query { source_info: SourceInfo::init(1, 1, 1, 2), value: QueryValue::Body {}, - }.eval(&variables, libcurl::core::tests::bytes_http_response()).err().unwrap(); + }.eval(&variables, http::bytes_http_response()).err().unwrap(); assert_eq!(error.source_info, SourceInfo::init(1, 1, 1, 2)); - assert_eq!(error.inner, RunnerError::InvalidDecoding { charset: "utf-8".to_string()}); + assert_eq!(error.inner, RunnerError::InvalidDecoding { charset: "utf-8".to_string() }); } #[test] fn test_query_invalid_utf8() { let variables = HashMap::new(); - let http_response = libcurl::core::Response { - version: libcurl::core::Version::Http10, + let http_response = http::Response { + version: http::Version::Http10, status: 0, headers: vec![], body: vec![200], @@ -697,7 +675,7 @@ pub mod tests { }, }, }; - let error = query.eval(&variables, libcurl::core::tests::xml_two_users_http_response()).err().unwrap(); + let error = query.eval(&variables, http::xml_two_users_http_response()).err().unwrap(); assert_eq!(error.inner, RunnerError::QueryInvalidXpathEval); assert_eq!(error.source_info.start, Pos { line: 1, column: 7 }); } @@ -706,8 +684,8 @@ pub mod tests { fn test_query_xpath() { let variables = HashMap::new(); - assert_eq!(xpath_users().eval(&variables, libcurl::core::tests::xml_two_users_http_response()).unwrap().unwrap(), Value::Nodeset(2)); - assert_eq!(xpath_count_user_query().eval(&variables, libcurl::core::tests::xml_two_users_http_response()).unwrap().unwrap(), Value::Float(2, 0)); + assert_eq!(xpath_users().eval(&variables, http::xml_two_users_http_response()).unwrap().unwrap(), Value::Nodeset(2)); + assert_eq!(xpath_count_user_query().eval(&variables, http::xml_two_users_http_response()).unwrap().unwrap(), Value::Float(2, 0)); } #[cfg(test)] @@ -738,7 +716,7 @@ pub mod tests { #[test] fn test_query_xpath_with_html() { let variables = HashMap::new(); - assert_eq!(xpath_html_charset().eval(&variables, libcurl::core::tests::html_http_response()).unwrap().unwrap(), Value::String(String::from("UTF-8"))); + assert_eq!(xpath_html_charset().eval(&variables, http::html_http_response()).unwrap().unwrap(), Value::String(String::from("UTF-8"))); } #[test] @@ -775,8 +753,8 @@ pub mod tests { #[test] fn test_query_invalid_json() { let variables = HashMap::new(); - let http_response = libcurl::core::Response { - version: libcurl::core::Version::Http10, + let http_response = http::Response { + version: http::Version::Http10, status: 0, headers: vec![], body: String::into_bytes(String::from("xxx")), @@ -789,8 +767,8 @@ pub mod tests { #[test] fn test_query_json_not_found() { let variables = HashMap::new(); - let http_response = libcurl::core::Response { - version: libcurl::core::Version::Http10, + let http_response = http::Response { + version: http::Version::Http10, status: 0, headers: vec![], body: String::into_bytes(String::from("{}")), @@ -819,11 +797,11 @@ pub mod tests { fn test_query_regex() { let variables = HashMap::new(); assert_eq!( - regex_name().eval(&variables, libcurl::core::tests::hello_http_response()).unwrap().unwrap(), + regex_name().eval(&variables, http::hello_http_response()).unwrap().unwrap(), Value::String("World".to_string()) ); - let error = regex_invalid().eval(&variables, libcurl::core::tests::hello_http_response()).err().unwrap(); + let error = regex_invalid().eval(&variables, http::hello_http_response()).err().unwrap(); assert_eq!(error.source_info, SourceInfo::init(1, 7, 1, 10)); assert_eq!(error.inner, RunnerError::InvalidRegex()); } diff --git a/src/runner/request.rs b/src/runner/request.rs index e20ab50c7..7d3ae6af5 100644 --- a/src/runner/request.rs +++ b/src/runner/request.rs @@ -25,7 +25,7 @@ use std::io::prelude::*; use crate::core::ast::*; use crate::core::common::Value; -use crate::http::libcurl; +use crate::http; use super::core::Error; @@ -33,38 +33,38 @@ impl Request { pub fn eval(self, variables: &HashMap, context_dir: String, - ) -> Result { + ) -> Result { let method = self.method.clone().eval(); let url = self.clone().url.eval(&variables)?; // headers - let mut headers: Vec = vec![]; + let mut headers: Vec = vec![]; for header in self.clone().headers { let name = header.key.value; let value = header.value.eval(variables)?; - headers.push(libcurl::core::Header { + headers.push(http::Header { name, value, }); } - let mut querystring: Vec = vec![]; + let mut querystring: Vec = vec![]; for param in self.clone().querystring_params() { let name = param.key.value; let value = param.value.eval(variables)?; - querystring.push(libcurl::core::Param { name, value }); + querystring.push(http::Param { name, value }); } - let mut form: Vec = vec![]; + let mut form: Vec = vec![]; for param in self.clone().form_params() { let name = param.key.value; let value = param.value.eval(variables)?; - form.push(libcurl::core::Param { name, value }); + form.push(http::Param { name, value }); } // if !self.clone().form_params().is_empty() { -// headers.push(libcurl::core::Header { +// headers.push(http::core::Header { // name: String::from("Content-Type"), // value: String::from("application/x-www-form-urlencoded"), // }); @@ -72,7 +72,7 @@ impl Request { let mut cookies = vec![]; for cookie in self.clone().cookies() { - let cookie = libcurl::core::RequestCookie { + let cookie = http::RequestCookie { name: cookie.clone().name.value, value: cookie.clone().value.value, }; @@ -104,7 +104,7 @@ impl Request { // if self.content_type().is_none() { // if let Some(body) = self.body { // if let Bytes::Json { .. } = body.value { -// headers.push(libcurl::core::Header { +// headers.push(http::core::Header { // name: String::from("Content-Type"), // value: String::from("application/json"), // }); @@ -112,7 +112,7 @@ impl Request { // } // } - Ok(libcurl::core::Request { + Ok(http::Request { method, url, querystring, @@ -136,42 +136,42 @@ impl Request { } impl Method { - fn eval(self) -> libcurl::core::Method { + fn eval(self) -> http::Method { match self { - Method::Get => libcurl::core::Method::Get, - Method::Head => libcurl::core::Method::Head, - Method::Post => libcurl::core::Method::Post, - Method::Put => libcurl::core::Method::Put, - Method::Delete => libcurl::core::Method::Delete, - Method::Connect => libcurl::core::Method::Connect, - Method::Options => libcurl::core::Method::Options, - Method::Trace => libcurl::core::Method::Trace, - Method::Patch => libcurl::core::Method::Patch, + Method::Get => http::Method::Get, + Method::Head => http::Method::Head, + Method::Post => http::Method::Post, + Method::Put => http::Method::Put, + Method::Delete => http::Method::Delete, + Method::Connect => http::Method::Connect, + Method::Options => http::Method::Options, + Method::Trace => http::Method::Trace, + Method::Patch => http::Method::Patch, } } } -pub fn split_url(url: String) -> (String, Vec) { - match url.find('?') { - None => (url, vec![]), - Some(index) => { - let (url, params) = url.split_at(index); - let params: Vec = params[1..].split('&') - .map(|s| { - match s.find('=') { - None => libcurl::core::Param { name: s.to_string(), value: String::from("") }, - Some(index) => { - let (name, value) = s.split_at(index); - libcurl::core::Param { name: name.to_string(), value: value[1..].to_string() } - } - } - }) - .collect(); - - (url.to_string(), params) - } - } -} +//pub fn split_url(url: String) -> (String, Vec) { +// match url.find('?') { +// None => (url, vec![]), +// Some(index) => { +// let (url, params) = url.split_at(index); +// let params: Vec = params[1..].split('&') +// .map(|s| { +// match s.find('=') { +// None => http::Param { name: s.to_string(), value: String::from("") }, +// Some(index) => { +// let (name, value) = s.split_at(index); +// http::Param { name: name.to_string(), value: value[1..].to_string() } +// } +// } +// }) +// .collect(); +// +// (url.to_string(), params) +// } +// } +//} #[cfg(test)] @@ -333,7 +333,7 @@ mod tests { let mut variables = HashMap::new(); variables.insert(String::from("base_url"), Value::String(String::from("http://localhost:8000"))); let http_request = hello_request().eval(&variables, "current_dir".to_string()).unwrap(); - assert_eq!(http_request, libcurl::core::tests::hello_http_request()); + assert_eq!(http_request, http::hello_http_request()); } #[test] @@ -341,6 +341,6 @@ mod tests { let mut variables = HashMap::new(); variables.insert(String::from("param1"), Value::String(String::from("value1"))); let http_request = query_request().eval(&variables, "current_dir".to_string()).unwrap(); - assert_eq!(http_request, libcurl::core::tests::query_http_request()); + assert_eq!(http_request, http::query_http_request()); } } diff --git a/src/runner/response.rs b/src/runner/response.rs index 347582640..6fa474061 100644 --- a/src/runner/response.rs +++ b/src/runner/response.rs @@ -20,25 +20,17 @@ use std::collections::HashMap; use crate::core::common::{Pos, SourceInfo}; use crate::core::common::Value; -use crate::http::libcurl; +use crate::http; use crate::runner::core::RunnerError; use super::core::*; use super::core::Error; use super::super::core::ast::*; -impl libcurl::core::Response { - pub fn get_header(&self, name: String) -> Vec { - self.headers - .iter() - .filter(|&h| h.name.to_lowercase() == name.to_lowercase()) - .map(|h| h.value.clone()) - .collect() - } -} + impl Response { - pub fn eval_asserts(self, variables: &HashMap, http_response: libcurl::core::Response, context_dir: String) -> Vec { + pub fn eval_asserts(self, variables: &HashMap, http_response: http::Response, context_dir: String) -> Vec { let mut asserts = vec![]; let version = self.clone().version; @@ -200,7 +192,7 @@ impl Response { asserts } - pub fn eval_captures(self, http_response: libcurl::core::Response, variables: &HashMap) -> Result, Error> { + pub fn eval_captures(self, http_response: http::Response, variables: &HashMap) -> Result, Error> { let mut captures = vec![]; for capture in self.captures() { let capture_result = capture.eval(variables, http_response.clone())?; @@ -268,7 +260,7 @@ mod tests { let variables = HashMap::new(); let context_dir = "undefined".to_string(); assert_eq!( - user_response().eval_asserts(&variables, libcurl::core::tests::xml_two_users_http_response(), context_dir), + user_response().eval_asserts(&variables, http::xml_two_users_http_response(), context_dir), vec![ AssertResult::Version { actual: String::from("1.0"), @@ -301,7 +293,7 @@ mod tests { pub fn test_eval_captures() { let variables = HashMap::new(); assert_eq!( - user_response().eval_captures(libcurl::core::tests::xml_two_users_http_response(), &variables).unwrap(), + user_response().eval_captures(http::xml_two_users_http_response(), &variables).unwrap(), vec![ CaptureResult { name: "UserCount".to_string(), diff --git a/tests/libcurl.rs b/tests/libcurl.rs index 7ee89b462..f86698c3e 100644 --- a/tests/libcurl.rs +++ b/tests/libcurl.rs @@ -3,9 +3,10 @@ use std::io::prelude::*; use curl::easy::Easy; -use hurl::http::libcurl; -use hurl::http::libcurl::client::ClientOptions; -use hurl::http::libcurl::core::*; + +use hurl::http::*; + + use server::Server; macro_rules! t { @@ -55,7 +56,7 @@ fn get_easy() { } -fn default_client() -> libcurl::client::Client { +fn default_client() -> Client { let options = ClientOptions { follow_location: false, max_redirect: None, @@ -65,7 +66,7 @@ fn default_client() -> libcurl::client::Client { verbose: true, insecure: false, }; - libcurl::client::Client::init(options) + Client::init(options) } fn default_get_request(url: String) -> Request { @@ -121,12 +122,10 @@ fn test_put() { let response = client.execute(&request, 0).unwrap(); assert_eq!(response.status, 200); assert!(response.body.is_empty()); - } #[test] fn test_patch() { - let mut client = default_client(); let request = Request { method: Method::Patch, @@ -146,7 +145,6 @@ fn test_patch() { let response = client.execute(&request, 0).unwrap(); assert_eq!(response.status, 204); assert!(response.body.is_empty()); - } // endregion @@ -176,7 +174,6 @@ fn test_custom_headers() { let response = client.execute(&request, 0).unwrap(); assert_eq!(response.status, 200); assert!(response.body.is_empty()); - } // endregion @@ -205,7 +202,6 @@ fn test_querystring_params() { let response = client.execute(&request, 0).unwrap(); assert_eq!(response.status, 200); assert!(response.body.is_empty()); - } // endregion @@ -241,7 +237,6 @@ fn test_form_params() { let response = client.execute(&request, 0).unwrap(); assert_eq!(response.status, 200); assert_eq!(response.body, b"Hello World!".to_vec()); - } // endregion @@ -266,9 +261,9 @@ fn test_follow_location() { proxy: None, no_proxy: None, verbose: false, - insecure: false + insecure: false, }; - let mut client = libcurl::client::Client::init(options); + let mut client = Client::init(options); let response = client.execute(&request, 0).unwrap(); assert_eq!(response.status, 200); assert_eq!(response.get_header_values("Content-Length".to_string()).get(0).unwrap(), "0"); @@ -295,7 +290,7 @@ fn test_max_redirect() { verbose: false, insecure: false, }; - let mut client = libcurl::client::Client::init(options); + let mut client = Client::init(options); let request = default_get_request("http://localhost:8000/redirect".to_string()); let response = client.execute(&request, 5).unwrap(); assert_eq!(response.status, 200); @@ -303,7 +298,6 @@ fn test_max_redirect() { let error = client.execute(&request, 11).err().unwrap(); assert_eq!(error, HttpError::TooManyRedirect); - } // endregion @@ -312,7 +306,6 @@ fn test_max_redirect() { #[test] fn test_multipart_form_data() { - let mut client = default_client(); let request = Request { method: Method::Post, @@ -321,27 +314,27 @@ fn test_multipart_form_data() { querystring: vec![], form: vec![], multipart: vec![ - MultipartParam::Param(Param{ + MultipartParam::Param(Param { name: "key1".to_string(), - value: "value1".to_string() + value: "value1".to_string(), }), - MultipartParam::FileParam(FileParam{ + MultipartParam::FileParam(FileParam { name: "upload1".to_string(), filename: "hello.txt".to_string(), data: b"Hello World!".to_vec(), - content_type: "text/plain".to_string() + content_type: "text/plain".to_string(), }), - MultipartParam::FileParam(FileParam{ + MultipartParam::FileParam(FileParam { name: "upload2".to_string(), filename: "hello.html".to_string(), data: b"Hello World!".to_vec(), - content_type: "text/html".to_string() + content_type: "text/html".to_string(), }), - MultipartParam::FileParam(FileParam{ + MultipartParam::FileParam(FileParam { name: "upload3".to_string(), filename: "hello.txt".to_string(), data: b"Hello World!".to_vec(), - content_type: "text/html".to_string() + content_type: "text/html".to_string(), }), ], cookies: vec![], @@ -358,7 +351,6 @@ fn test_multipart_form_data() { let response = client.execute(&request, 0).unwrap(); assert_eq!(response.status, 200); assert_eq!(response.body, b"Hello World!".to_vec()); - } // endregion @@ -367,7 +359,6 @@ fn test_multipart_form_data() { #[test] fn test_post_bytes() { - let mut client = default_client(); let request = Request { method: Method::Post, @@ -378,12 +369,11 @@ fn test_post_bytes() { multipart: vec![], cookies: vec![], body: b"Hello World!".to_vec(), - content_type: None + content_type: None, }; let response = client.execute(&request, 0).unwrap(); assert_eq!(response.status, 200); assert!(response.body.is_empty()); - } // endregion @@ -394,7 +384,7 @@ fn test_post_bytes() { fn test_error_could_not_resolve_host() { let mut client = default_client(); let request = default_get_request("http://unknown".to_string()); - let error = client.execute(&request, 0).err().unwrap(); + let error = client.execute(&request, 0).err().unwrap(); assert_eq!(error, HttpError::CouldNotResolveHost); } @@ -403,7 +393,7 @@ fn test_error_could_not_resolve_host() { fn test_error_fail_to_connect() { let mut client = default_client(); let request = default_get_request("http://localhost:9999".to_string()); - let error = client.execute(&request, 0).err().unwrap(); + let error = client.execute(&request, 0).err().unwrap(); assert_eq!(error, HttpError::FailToConnect); @@ -416,11 +406,10 @@ fn test_error_fail_to_connect() { verbose: true, insecure: false, }; - let mut client = libcurl::client::Client::init(options); + let mut client = Client::init(options); let request = default_get_request("http://localhost:8000/hello".to_string()); - let error = client.execute(&request, 0).err().unwrap(); + let error = client.execute(&request, 0).err().unwrap(); assert_eq!(error, HttpError::FailToConnect); - } @@ -435,7 +424,7 @@ fn test_error_could_not_resolve_proxy_name() { verbose: false, insecure: false, }; - let mut client = libcurl::client::Client::init(options); + let mut client = Client::init(options); let request = default_get_request("http://localhost:8000/hello".to_string()); let error = client.execute(&request, 0).err().unwrap(); assert_eq!(error, HttpError::CouldNotResolveProxyName); @@ -486,7 +475,6 @@ fn test_cookie() { // let response = client.execute(&request, 0).unwrap(); // assert_eq!(response.status, 200); // assert!(response.body.is_empty()); - } #[test] @@ -512,7 +500,6 @@ fn test_cookie_storage() { let response = client.execute(&request, 0).unwrap(); assert_eq!(response.status, 200); assert!(response.body.is_empty()); - } @@ -531,12 +518,11 @@ fn test_cookie_file() { verbose: false, insecure: false, }; - let mut client = libcurl::client::Client::init(options); + let mut client = Client::init(options); let request = default_get_request("http://localhost:8000/cookies/assert-that-cookie2-is-valueA".to_string()); let response = client.execute(&request, 0).unwrap(); assert_eq!(response.status, 200); assert!(response.body.is_empty()); - } // endregion @@ -555,7 +541,7 @@ fn test_proxy() { verbose: false, insecure: false, }; - let mut client = libcurl::client::Client::init(options); + let mut client = Client::init(options); let request = default_get_request("http://localhost:8000/hello".to_string()); let response = client.execute(&request, 0).unwrap(); assert_eq!(response.status, 200); diff --git a/tests/runner.rs b/tests/runner.rs index fd6f41496..05e707bb7 100644 --- a/tests/runner.rs +++ b/tests/runner.rs @@ -21,7 +21,7 @@ use hurl::core::ast; use hurl::core::common::{Pos, SourceInfo}; use hurl::runner; use hurl::format; -use hurl::http::libcurl; +use hurl::http; use std::collections::HashMap; use hurl::core::ast::{Template, TemplateElement, EncodedString}; use hurl::runner::core::RunnerOptions; @@ -37,7 +37,7 @@ fn test_hurl_file() { let content = std::fs::read_to_string(filename).expect("Something went wrong reading the file"); let hurl_file = hurl::parser::parse_hurl_file(content.as_str()).unwrap(); let variables = HashMap::new(); - let options = libcurl::client::ClientOptions { + let options = http::ClientOptions { follow_location: false, max_redirect: None, cookie_input_file: None, @@ -46,7 +46,7 @@ fn test_hurl_file() { verbose: false, insecure: false, }; - let mut client = libcurl::client::Client::init(options); + let mut client = http::Client::init(options); let mut lines: Vec<&str> = regex::Regex::new(r"\n|\r\n") .unwrap() .split(&content) @@ -144,7 +144,7 @@ fn hello_request() -> ast::Request { #[test] fn test_hello() { - let options = libcurl::client::ClientOptions { + let options = http::ClientOptions { follow_location: false, max_redirect: None, cookie_input_file: None, @@ -153,7 +153,7 @@ fn test_hello() { verbose: false, insecure: false, }; - let mut client = libcurl::client::Client::init(options); + let mut client = http::Client::init(options); let source_info = SourceInfo { start: Pos { line: 1, column: 1 }, end: Pos { line: 1, column: 1 },