From 190d0871d58eae8bbb8e404c51d9004664523649 Mon Sep 17 00:00:00 2001 From: Fabrice Reix Date: Tue, 8 Sep 2020 16:21:42 +0200 Subject: [PATCH] Add HTTP API for libcurl --- src/http/libcurl/client.rs | 432 ++++++++++++++++++++++++++++-- src/http/libcurl/core.rs | 126 ++++++++- tests/libcurl.rs | 523 +++++++++++++++++++++++++++++++++++-- 3 files changed, 1040 insertions(+), 41 deletions(-) diff --git a/src/http/libcurl/client.rs b/src/http/libcurl/client.rs index 87e8ad594..050ed2533 100644 --- a/src/http/libcurl/client.rs +++ b/src/http/libcurl/client.rs @@ -16,40 +16,438 @@ * */ -use super::core::{HttpError, Request, Response, Method}; -use curl::easy::Easy; +use std::str; -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct Client {} +use curl::easy; + +use super::core::*; +use std::io::Read; + + +#[derive(Debug)] +pub struct Client { + pub handle: Box, + + /// unfortunately, follow-location feature from libcurl can not be used + /// libcurl returns a single list of headers for the 2 responses + /// hurl needs the return the headers only for the second (last) response) + pub follow_location: bool, + pub redirect_count: usize, + pub max_redirect: Option, +} + +#[derive(Debug, Clone)] +pub struct ClientOptions { + pub follow_location: bool, + pub max_redirect: Option, + pub cookie_file: Option, + pub cookie_jar: Option, + pub proxy: Option, + pub verbose: bool, +} impl Client { - pub fn execute(&self, request: &Request) -> Result { - let mut handle = Easy::new(); - let mut body = Vec::::new(); - match request.method { - Method::Get => handle.get(true).unwrap(), - Method::Post => handle.post(true).unwrap(), - Method::Put => handle.put(true).unwrap(), - _ => { todo!()} + /// + /// Init HTTP hurl client + /// + pub fn init(options: ClientOptions) -> Client { + let mut h = easy::Easy::new(); + + // Activate cookie storage + // with or without persistence (empty string) + h.cookie_file(options.cookie_file.unwrap_or_else(|| "".to_string()).as_str()).unwrap(); + + + if let Some(cookie_jar) = options.cookie_jar { + h.cookie_jar(cookie_jar.as_str()).unwrap(); } - handle.url(request.url.as_str()).unwrap(); + if let Some(proxy) = options.proxy { + h.proxy(proxy.as_str()).unwrap(); + } + + h.verbose(options.verbose).unwrap(); + + Client { + handle: Box::new(h), + follow_location: options.follow_location, + max_redirect: options.max_redirect, + redirect_count: 0, + } + } + + + /// + /// Execute an http request + /// + pub fn execute(&mut self, request: &Request, redirect_count: usize) -> Result { + self.set_url(&request.url, &request.querystring); + self.set_method(&request.method); + + self.set_cookies(&request.cookies); + 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.headers, data.is_empty()); + + 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); + for line in lines { + eprintln!("> {}", line); + } + } + easy::InfoType::HeaderIn => { + eprint!("< {}", str::from_utf8(data).unwrap()); + } + _ => {} + } + ).unwrap(); + + let mut lines = vec![]; + let mut body = Vec::::new(); { - let mut transfer = handle.transfer(); + let mut transfer = self.handle.transfer(); + if !data.is_empty() { + transfer.read_function(|buf| { + Ok(data.read(buf).unwrap_or(0)) + }).unwrap(); + } + + transfer.header_function(|h| { + match str::from_utf8(h) { + Ok(s) => lines.push(s.to_string()), + Err(e) => println!("Error decoding header {:?}", e), + } + true + }).unwrap(); + transfer.write_function(|data| { body.extend(data); Ok(data.len()) }).unwrap(); - transfer.perform().unwrap(); + + if let Err(e) = transfer.perform() { + match e.code() { + 5 => return Err(HttpError::CouldNotResolveProxyName), + 6 => return Err(HttpError::CouldNotResolveHost), + 7 => return Err(HttpError::FailToConnect), + _ => panic!("{:#?}", e), + } + } } - let status= handle.response_code().unwrap(); + let status = self.handle.response_code().unwrap(); + let headers = self.parse_response_headers(&mut lines); + + if let Some(url) = self.get_follow_location(headers.clone()) { + let request = Request { + method: Method::Get, + url, + headers: vec![], + querystring: vec![], + form: vec![], + multipart: vec![], + cookies: vec![], + body: vec![], + }; + + let redirect_count = redirect_count + 1; + if let Some(max_redirect) = self.max_redirect { + if redirect_count > max_redirect { + return Err(HttpError::TooManyRedirect); + } + } + + return self.execute(&request, redirect_count); + } + self.redirect_count = redirect_count; Ok(Response { status, - body + headers, + body, }) } + + /// + /// set url + /// + fn set_url(&mut self, url: &str, params: &[Param]) { + let url = if params.is_empty() { + url.to_string() + } else { + let url = if url.ends_with('?') { + url.to_string() + } else { + format!("{}?", url) + }; + let s = self.encode_params(params); + format!("{}{}", url, s) + }; + self.handle.url(url.as_str()).unwrap(); + } + + + /// + /// set method + /// + fn set_method(&mut self, method: &Method) { + match method { + Method::Get => self.handle.custom_request("GET").unwrap(), + Method::Post => self.handle.custom_request("POST").unwrap(), + Method::Put => self.handle.custom_request("PUT").unwrap(), + Method::Head => self.handle.custom_request("HEAD").unwrap(), + Method::Delete => self.handle.custom_request("DELETE").unwrap(), + Method::Connect => self.handle.custom_request("CONNECT").unwrap(), + Method::Options => self.handle.custom_request("OPTIONS").unwrap(), + Method::Trace => self.handle.custom_request("TRACE").unwrap(), + Method::Patch => self.handle.custom_request("PATCH").unwrap(), + } + } + + /// + /// set request headers + /// + fn set_headers(&mut self, headers: &[Header], default_content_type: bool) { + let mut list = easy::List::new(); + + for header in headers.to_owned() { + list.append(format!("{}: {}", header.name, header.value).as_str()).unwrap(); + } + + if get_header_values(headers.to_vec(), "Content-Type".to_string()).is_empty() && !default_content_type { + list.append("Content-Type:").unwrap(); + } + + list.append(format!("User-Agent: hurl/{}", clap::crate_version!()).as_str()).unwrap(); + + self.handle.http_headers(list).unwrap(); + } + + /// + /// set request cookies + /// + fn set_cookies(&mut self, cookies: &[RequestCookie]) { + for cookie in cookies { + self.handle.cookie(cookie.to_string().as_str()).unwrap(); + } + } + + + /// + /// set form + /// + fn set_form(&mut self, params: &[Param]) { + if !params.is_empty() { + let s = self.encode_params(params); + self.handle.post_fields_copy(s.as_str().as_bytes()).unwrap(); + //self.handle.write_function(sink); + } + } + + + /// + /// set form + /// + fn set_multipart(&mut self, params: &[MultipartParam]) { + if !params.is_empty() { + let mut form = easy::Form::new(); + 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() + } + } + } + self.handle.httppost(form).unwrap(); + } + } + + /// + /// set body + /// + fn set_body(&mut self, data: &[u8]) { + if !data.is_empty() { + self.handle.post(true).unwrap(); + self.handle.post_field_size(data.len() as u64).unwrap(); + } + } + + /// + /// encode parameters + /// + fn encode_params(&mut self, params: &[Param]) -> String { + params + .iter() + .map(|p| { + let value = self.handle.url_encode(p.value.as_bytes()); + format!("{}={}", p.name, value) + }) + .collect::>() + .join("&") + } + + + /// + /// parse headers from libcurl responses + /// + fn parse_response_headers(&mut self, lines: &mut Vec) -> Vec
{ + let mut headers: Vec
= vec![]; + lines.remove(0); // remove the status line + 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); + } + } + headers + } + + + /// + /// retrieve an optional location to follow + /// You need: + /// 1. the option follow_location set to true + /// 2. a 3xx response code + /// 3. a header Location + /// + fn get_follow_location(&mut self, headers: Vec
) -> Option { + if !self.follow_location { + return None; + } + + let response_code = self.handle.response_code().unwrap(); + if response_code < 300 || response_code >= 400 { + return None; + } + + let location = match get_header_values(headers, "Location".to_string()).get(0) { + None => return None, + Some(value) => value.clone(), + }; + + if location.is_empty() { + None + } else { + Some(location) + } + } + + + /// + /// get cookie storage + /// + pub fn get_cookie_storage(&mut self) -> Vec { + let list = self.handle.cookies().unwrap(); + let mut cookies = vec![]; + for cookie in list.iter() { + let line = str::from_utf8(cookie).unwrap().to_string(); + let fields: Vec<&str> = line.split('\t').collect(); + + let domain = fields.get(0).unwrap().to_string(); + let include_subdomain = fields.get(1).unwrap().to_string(); + let path = fields.get(2).unwrap().to_string(); + let https = fields.get(3).unwrap().to_string(); + 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 + } } + + +impl Header { + /// + /// Parse an http header line received from the server + /// It does not panic. Just return none if it can not be parsed + /// + pub fn parse(line: String) -> Option
{ + match line.find(':') { + Some(index) => { + let (name, value) = line.split_at(index); + Some(Header { + name: name.to_string().trim().to_string(), + value: value[1..].to_string().trim().to_string(), + }) + } + None => None, + } + } +} + + + +/// +/// Split an array of bytes into http lines (\r\n separator) +/// +fn split_lines(data: &[u8]) -> Vec { + let mut lines = vec![]; + let mut start = 0; + let mut i = 0; + while i < (data.len() - 1) { + if data[i] == 13 && data[i + 1] == 10 { + if let Ok(s) = str::from_utf8(&data[start..i]) { + lines.push(s.to_string()); + } + start = i + 2; + i += 2; + } else { + i += 1; + } + } + lines +} + + +#[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!(Header::parse("Foo".to_string()).is_none()); + } + + #[test] + fn test_split_lines_header() { + let data = b"GET /hello HTTP/1.1\r\nHost: localhost:8000\r\n\r\n"; + let lines = split_lines(data); + assert_eq!(lines.len(), 3); + assert_eq!(lines.get(0).unwrap().as_str(), "GET /hello HTTP/1.1"); + 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/libcurl/core.rs b/src/http/libcurl/core.rs index 33c086f87..9e6deab3d 100644 --- a/src/http/libcurl/core.rs +++ b/src/http/libcurl/core.rs @@ -16,6 +16,28 @@ * */ +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, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Response { + pub status: u32, + pub headers: Vec
, + pub body: Vec, +} + + #[derive(Clone, Debug, PartialEq, Eq)] pub enum Method { Get, @@ -29,22 +51,112 @@ pub enum Method { Patch, } +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Header { + pub name: String, + pub value: String, +} #[derive(Clone, Debug, PartialEq, Eq)] -pub struct Request { - pub method: Method, - pub url: String, +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 Response { - pub status: u32, - pub body: Vec, +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 RequestCookie { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}={}", self.name, self.value) + } +} + + #[derive(Clone, Debug, PartialEq, Eq)] pub enum HttpError { - UNDEFINED + CouldNotResolveProxyName, + CouldNotResolveHost, + FailToConnect, + TooManyRedirect } + +impl Response { + + /// + /// return a list of headers values for the given header name + /// + pub fn get_header_values(&self, expected_name: String) -> Vec { + self.headers + .iter() + .filter_map(|Header{ name, value}| if name.clone() == expected_name { Some(value.to_string())} else { None }) + .collect() + } + +} + + +/// +/// 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)] +mod tests { + + use super::*; + + #[test] + fn get_header_values() { + let response = Response { + 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()); + + } + +} \ No newline at end of file diff --git a/tests/libcurl.rs b/tests/libcurl.rs index c665ea61b..c2f11f455 100644 --- a/tests/libcurl.rs +++ b/tests/libcurl.rs @@ -1,13 +1,48 @@ +use std::fs::File; +use std::io::prelude::*; + +use curl::easy::Easy; + +use hurl::http::libcurl; +use hurl::http::libcurl::client::ClientOptions; +use hurl::http::libcurl::core::*; +use server::Server; + +macro_rules! t { + ($e:expr) => { + match $e { + Ok(e) => e, + Err(e) => panic!("{} failed with {:?}", stringify!($e), e), + } + }; +} + +pub mod server; + +pub fn new_header(name: &str, value: &str) -> Header { + Header { + name: name.to_string(), + value: value.to_string(), + } +} #[test] -fn test_easy() { - use curl::easy::Easy; - let url ="http://localhost:8000/hello"; +fn get_easy() { + let s = Server::new(); + s.receive( + "\ + GET /hello HTTP/1.1\r\n\ + Host: 127.0.0.1:$PORT\r\n\ + Accept: */*\r\n\ + \r\n", + ); + s.send("HTTP/1.1 200 OK\r\n\r\nHello World!"); let mut data = Vec::new(); let mut handle = Easy::new(); - handle.url(url).unwrap(); + + handle.url(&s.url("/hello")).unwrap(); { let mut transfer = handle.transfer(); transfer.write_function(|new_data| { @@ -17,25 +52,479 @@ fn test_easy() { transfer.perform().unwrap(); } assert_eq!(data, b"Hello World!"); +} + + +fn default_client() -> libcurl::client::Client { + let options = ClientOptions { + follow_location: false, + max_redirect: None, + cookie_file: None, + cookie_jar: None, + proxy: None, + verbose: false, + }; + libcurl::client::Client::init(options) +} + +fn default_get_request(url: String) -> Request { + Request { + method: Method::Get, + url, + headers: vec![], + querystring: vec![], + form: vec![], + multipart: vec![], + cookies: vec![], + body: vec![] + } +} + + +// region basic + +#[test] +fn test_hello() { + let mut client = default_client(); + let request = default_get_request("http://localhost:8000/hello".to_string()); + let response = client.execute(&request, 0).unwrap(); + assert_eq!(response.status, 200); + assert_eq!(response.body, b"Hello World!".to_vec()); + + assert_eq!(response.headers.len(), 4); + assert!(response.headers.contains(&Header { name: "Content-Length".to_string(), value: "12".to_string() })); + assert!(response.headers.contains(&Header { name: "Content-Type".to_string(), value: "text/html; charset=utf-8".to_string() })); + assert_eq!(response.get_header_values("Date".to_string()).len(), 1); + +} + +// endregion + +// region http method + +#[test] +fn test_put() { + let mut client = default_client(); + let request = Request { + method: Method::Put, + url: "http://localhost:8000/put".to_string(), + headers: vec![], + querystring: vec![], + form: vec![], + multipart: vec![], + cookies: vec![], + body: vec![] + }; + let response = client.execute(&request, 0).unwrap(); + assert_eq!(response.status, 200); + assert!(response.body.is_empty()); } #[test] -fn test_hello() { - use hurl::http::libcurl; - use hurl::http::libcurl::core::*; +fn test_patch() { - let client = libcurl::client::Client {}; + let mut client = default_client(); let request = Request { - method: Method::Get, - url: "http://localhost:8000/hello".to_string() + method: Method::Patch, + url: "http://localhost:8000/patch/file.txt".to_string(), + headers: vec![ + Header { name: "Host".to_string(), value: "www.example.com".to_string() }, + Header { name: "Content-Type".to_string(), value: "application/example".to_string() }, + Header { name: "If-Match".to_string(), value: "\"e0023aa4e\"".to_string() }, + ], + querystring: vec![], + form: vec![], + multipart: vec![], + cookies: vec![], + body: vec![] }; - assert_eq!( - client.execute(&request), - Ok(Response { - status: 200, - body: b"Hello World!".to_vec(), - }) - ); + let response = client.execute(&request, 0).unwrap(); + assert_eq!(response.status, 204); + assert!(response.body.is_empty()); + } +// endregion + +// region headers + +#[test] +fn test_custom_headers() { + let mut client = default_client(); + let request = Request { + method: Method::Get, + url: "http://localhost:8000/custom-headers".to_string(), + headers: vec![ + new_header("Fruit", "Raspberry"), + new_header("Fruit", "Apple"), + new_header("Fruit", "Banana"), + new_header("Fruit", "Grape"), + new_header("Color", "Green"), + ], + querystring: vec![], + form: vec![], + multipart: vec![], + cookies: vec![], + body: vec![] + }; + let response = client.execute(&request, 0).unwrap(); + assert_eq!(response.status, 200); + assert!(response.body.is_empty()); + +} + +// endregion + +// region querystrings + +#[test] +fn test_querystring_params() { + let mut client = default_client(); + let request = Request { + method: Method::Get, + url: "http://localhost:8000/querystring-params".to_string(), + headers: vec![], + querystring: vec![ + Param { name: "param1".to_string(), value: "value1".to_string() }, + Param { name: "param2".to_string(), value: "".to_string() }, + Param { name: "param3".to_string(), value: "a=b".to_string() }, + Param { name: "param4".to_string(), value: "1,2,3".to_string() } + ], + form: vec![], + multipart: vec![], + cookies: vec![], + body: vec![] + }; + let response = client.execute(&request, 0).unwrap(); + assert_eq!(response.status, 200); + assert!(response.body.is_empty()); + +} + +// endregion + +// region form params + +#[test] +fn test_form_params() { + let mut client = default_client(); + let request = Request { + method: Method::Post, + url: "http://localhost:8000/form-params".to_string(), + headers: vec![], + querystring: vec![], + form: vec![ + Param { name: "param1".to_string(), value: "value1".to_string() }, + Param { name: "param2".to_string(), value: "".to_string() }, + Param { name: "param3".to_string(), value: "a=b".to_string() }, + Param { name: "param4".to_string(), value: "a%3db".to_string() } + ], + multipart: vec![], + cookies: vec![], + body: vec![] + }; + let response = client.execute(&request, 0).unwrap(); + assert_eq!(response.status, 200); + assert!(response.body.is_empty()); + + + // make sure you can reuse client for other request + let request = default_get_request("http://localhost:8000/hello".to_string()); + let response = client.execute(&request, 0).unwrap(); + assert_eq!(response.status, 200); + assert_eq!(response.body, b"Hello World!".to_vec()); + +} + +// endregion + +// region redirect + +#[test] +fn test_follow_location() { + let request = default_get_request("http://localhost:8000/redirect".to_string()); + + let mut client = default_client(); + let response = client.execute(&request, 0).unwrap(); + assert_eq!(response.status, 302); + assert_eq!(response.get_header_values("Location".to_string()).get(0).unwrap(), + "http://localhost:8000/redirected"); + assert_eq!(client.redirect_count, 0); + + let options = ClientOptions { + follow_location: true, + max_redirect: None, + cookie_file: None, + cookie_jar: None, + proxy: None, + verbose: false, + }; + let mut client = libcurl::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"); + assert_eq!(client.redirect_count, 1); + + + // make sure that the redirect count is reset to 0 + let request = default_get_request("http://localhost:8000/hello".to_string()); + let response = client.execute(&request, 0).unwrap(); + assert_eq!(response.status, 200); + assert_eq!(response.body, b"Hello World!".to_vec()); + assert_eq!(client.redirect_count, 0); +} + + +#[test] +fn test_max_redirect() { + let options = ClientOptions { + follow_location: true, + max_redirect: Some(10), + cookie_file: None, + cookie_jar: None, + proxy: None, + verbose: false, + }; + let mut client = libcurl::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); + assert_eq!(client.redirect_count, 6); + + let error = client.execute(&request, 11).err().unwrap(); + assert_eq!(error, HttpError::TooManyRedirect); + +} + +// endregion + +// region multipart + +#[test] +fn test_multipart_form_data() { + + let mut client = default_client(); + let request = Request { + method: Method::Post, + url: "http://localhost:8000/multipart-form-data".to_string(), + headers: vec![], + querystring: vec![], + form: vec![], + multipart: vec![ + MultipartParam::Param(Param{ + name: "key1".to_string(), + value: "value1".to_string() + }), + MultipartParam::FileParam(FileParam{ + name: "upload1".to_string(), + filename: "hello.txt".to_string(), + data: b"Hello World!".to_vec(), + content_type: "text/plain".to_string() + }), + MultipartParam::FileParam(FileParam{ + name: "upload2".to_string(), + filename: "hello.html".to_string(), + data: b"Hello World!".to_vec(), + content_type: "text/html".to_string() + }), + MultipartParam::FileParam(FileParam{ + name: "upload3".to_string(), + filename: "hello.txt".to_string(), + data: b"Hello World!".to_vec(), + content_type: "text/html".to_string() + }), + ], + cookies: vec![], + body: vec![] + + }; + let response = client.execute(&request, 0).unwrap(); + assert_eq!(response.status, 200); + assert!(response.body.is_empty()); + +} + +// endregion + +// region http body + +#[test] +fn test_post_bytes() { + + let mut client = default_client(); + let request = Request { + method: Method::Post, + url: "http://localhost:8000/post-base64".to_string(), + headers: vec![], + querystring: vec![], + form: vec![], + multipart: vec![], + cookies: vec![], + body: b"Hello World!".to_vec(), + }; + let response = client.execute(&request, 0).unwrap(); + assert_eq!(response.status, 200); + assert!(response.body.is_empty()); + +} + +// endregion + +// region error + +#[test] +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(); + + assert_eq!(error, HttpError::CouldNotResolveHost); +} + +#[test] +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(); + assert_eq!(error, HttpError::FailToConnect); + + + let options = ClientOptions { + follow_location: false, + max_redirect: None, + cookie_file: None, + cookie_jar: None, + proxy: Some("localhost:9999".to_string()), + verbose: true, + }; + let mut client = libcurl::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::FailToConnect); + +} + + +#[test] +fn test_error_could_not_resolve_proxy_name() { + let options = ClientOptions { + follow_location: false, + max_redirect: None, + cookie_file: None, + cookie_jar: None, + proxy: Some("unknown".to_string()), + verbose: false, + }; + let mut client = libcurl::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); +} + +// endregion + +// region cookie + +#[test] +fn test_cookie() { + let mut client = default_client(); + let request = Request { + method: Method::Get, + url: "http://localhost:8000/cookies/set-request-cookie1-valueA".to_string(), + headers: vec![], + querystring: vec![], + form: vec![], + multipart: vec![], + cookies: vec![ + RequestCookie { name: "cookie1".to_string(), value: "valueA".to_string() } + ], + body: vec![] + }; + let response = client.execute(&request, 0).unwrap(); + assert_eq!(response.status, 200); + assert!(response.body.is_empty()); + + + // For the time-being setting a cookie on a request + // update the cookie store as well + // The same cookie does not need to be set explicitly on further requests + let request = default_get_request("http://localhost:8000/cookies/set-request-cookie1-valueA".to_string()); + let response = client.execute(&request, 0).unwrap(); + assert_eq!(response.status, 200); + assert!(response.body.is_empty()); + +} + +#[test] +fn test_cookie_storage() { + let mut client = default_client(); + let request = default_get_request("http://localhost:8000/cookies/set-session-cookie2-valueA".to_string()); + let response = client.execute(&request, 0).unwrap(); + assert_eq!(response.status, 200); + assert!(response.body.is_empty()); + + let cookie_store = client.get_cookie_storage(); + assert_eq!(cookie_store.get(0).unwrap().clone(), Cookie { + domain: "localhost".to_string(), + include_subdomain: "FALSE".to_string(), + path: "/".to_string(), + https: "FALSE".to_string(), + expires: "0".to_string(), + name: "cookie2".to_string(), + value: "valueA".to_string(), + }); + 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()); + +} + + +#[test] +fn test_cookie_file() { + let temp_file = "/tmp/cookies"; + let mut file = File::create(temp_file).expect("can not create temp file!"); + file.write_all(b"localhost\tFALSE\t/\tFALSE\t0\tcookie2\tvalueA\n").unwrap(); + + let options = ClientOptions { + follow_location: false, + max_redirect: None, + cookie_file: Some(temp_file.to_string()), + cookie_jar: None, + proxy: None, + verbose: false, + }; + let mut client = libcurl::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 + +// region proxy + +#[test] +fn test_proxy() { + // mitmproxy listening on port 8080 + let options = ClientOptions { + follow_location: false, + max_redirect: None, + cookie_file: None, + cookie_jar: None, + proxy: Some("localhost:8080".to_string()), + verbose: false, + }; + let mut client = libcurl::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); + assert_eq!(response.body, b"Hello World!".to_vec()); +} + +// endregion +