Merge pull request #21 from Orange-OpenSource/feature/using_libcurl

Using libcurl under the hood
This commit is contained in:
Fabrice Reix 2020-09-17 21:40:28 +02:00 committed by GitHub
commit beb89df90d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
41 changed files with 1791 additions and 5849 deletions

955
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -1,2 +1,2 @@
<!DOCTYPE html> <!DOCTYPE html>
<html><head><title>Hurl Report</title><link rel="stylesheet" type="text/css" href="report.css"></head><body><h2>Hurl Report</h2><div class="date">Wed, 26 Aug 2020 11:50:20 +0200</div><table><thead><tr><td>filename</td><td>duration</td></tr></thead><tbody><tr><td class="success">tests/assert_base64.hurl</td><td>0.019s</td></tr><tr><td class="success">tests/assert_header.hurl</td><td>0.023s</td></tr><tr><td class="success">tests/assert_json.hurl</td><td>0.073s</td></tr><tr><td class="success">tests/assert_match.hurl</td><td>0.049s</td></tr><tr><td class="success">tests/assert_regex.hurl</td><td>0.025s</td></tr><tr><td class="success">tests/assert_xpath.hurl</td><td>0.023s</td></tr><tr><td class="success">tests/bytes.hurl</td><td>0.02s</td></tr><tr><td class="success">tests/capture_and_assert.hurl</td><td>0.022s</td></tr><tr><td class="success">tests/captures.hurl</td><td>0.078s</td></tr><tr><td class="success">tests/cookies.hurl</td><td>0.256s</td></tr><tr><td class="success">tests/delete.hurl</td><td>0.033s</td></tr><tr><td class="success">tests/empty.hurl</td><td>0s</td></tr><tr><td class="success">tests/encoding.hurl</td><td>0.059s</td></tr><tr><td class="failure">tests/error_assert_base64.hurl</td><td>0.052s</td></tr><tr><td class="failure">tests/error_assert_file.hurl</td><td>0.025s</td></tr><tr><td class="failure">tests/error_assert_header_not_found.hurl</td><td>0.028s</td></tr><tr><td class="failure">tests/error_assert_header_value.hurl</td><td>0.025s</td></tr><tr><td class="failure">tests/error_assert_http_version.hurl</td><td>0.025s</td></tr><tr><td class="failure">tests/error_assert_invalid_predicate_type.hurl</td><td>0.02s</td></tr><tr><td class="failure">tests/error_assert_match_utf8.hurl</td><td>0.022s</td></tr><tr><td class="failure">tests/error_assert_query_invalid_regex.hurl</td><td>0.018s</td></tr><tr><td class="failure">tests/error_assert_query_invalid_xpath.hurl</td><td>0.021s</td></tr><tr><td class="failure">tests/error_assert_status.hurl</td><td>0.019s</td></tr><tr><td class="failure">tests/error_assert_template_variable_not_found.hurl</td><td>0.027s</td></tr><tr><td class="failure">tests/error_assert_value_error.hurl</td><td>0.03s</td></tr><tr><td class="failure">tests/error_assert_variable.hurl</td><td>0.025s</td></tr><tr><td class="failure">tests/error_file_read_access.hurl</td><td>0s</td></tr><tr><td class="failure">tests/error_invalid_jsonpath.hurl</td><td>0.043s</td></tr><tr><td class="failure">tests/error_invalid_url.hurl</td><td>0s</td></tr><tr><td class="failure">tests/error_invalid_xml.hurl</td><td>0.036s</td></tr><tr><td class="failure">tests/error_multipart_form_data.hurl</td><td>0s</td></tr><tr><td class="failure">tests/error_predicate.hurl</td><td>0.06s</td></tr><tr><td class="failure">tests/error_query_header_not_found.hurl</td><td>0.033s</td></tr><tr><td class="failure">tests/error_query_invalid_json.hurl</td><td>0.022s</td></tr><tr><td class="failure">tests/error_query_invalid_utf8.hurl</td><td>0.024s</td></tr><tr><td class="failure">tests/error_template_variable_not_found.hurl</td><td>0s</td></tr><tr><td class="failure">tests/error_template_variable_not_renderable.hurl</td><td>0.021s</td></tr><tr><td class="success">tests/form_params.hurl</td><td>0.045s</td></tr><tr><td class="success">tests/headers.hurl</td><td>0.14s</td></tr><tr><td class="success">tests/hello.hurl</td><td>0.046s</td></tr><tr><td class="success">tests/multipart_form_data.hurl</td><td>0.024s</td></tr><tr><td class="success">tests/no_entry.hurl</td><td>0s</td></tr><tr><td class="success">tests/output.hurl</td><td>0.052s</td></tr><tr><td class="success">tests/patch.hurl</td><td>0.023s</td></tr><tr><td class="success">tests/post_base64.hurl</td><td>0.027s</td></tr><tr><td class="success">tests/post_file.hurl</td><td>0.02s</td></tr><tr><td class="success">tests/post_json.hurl</td><td>0.294s</td></tr><tr><td class="success">tests/post_multilines.hurl</td><td>0.078s</td></tr><tr><td class="success">tests/post_xml.hurl</td><td>0.068s</td></tr><tr><td class="success">tests/predicates-string.hurl</td><td>0.045s</td></tr><tr><td class="success">tests/put.hurl</td><td>0.024s</td></tr><tr><td class="success">tests/querystring_params.hurl</td><td>0.12s</td></tr><tr><td class="success">tests/redirect.hurl</td><td>0.062s</td></tr><tr><td class="success">tests/utf8.hurl</td><td>0.035s</td></tr></tbody></table></body></html> <html><head><title>Hurl Report</title><link rel="stylesheet" type="text/css" href="report.css"></head><body><h2>Hurl Report</h2><div class="date">Thu, 17 Sep 2020 15:58:48 +0200</div><table><thead><tr><td>filename</td><td>duration</td></tr></thead><tbody><tr><td class="success">tests/assert_base64.hurl</td><td>0.002s</td></tr><tr><td class="success">tests/assert_header.hurl</td><td>0.002s</td></tr><tr><td class="success">tests/assert_json.hurl</td><td>0.008s</td></tr><tr><td class="success">tests/assert_match.hurl</td><td>0.016s</td></tr><tr><td class="success">tests/assert_regex.hurl</td><td>0.004s</td></tr><tr><td class="success">tests/assert_xpath.hurl</td><td>0.002s</td></tr><tr><td class="success">tests/bytes.hurl</td><td>0.001s</td></tr><tr><td class="success">tests/capture_and_assert.hurl</td><td>0.002s</td></tr><tr><td class="success">tests/captures.hurl</td><td>0.008s</td></tr><tr><td class="success">tests/cookies.hurl</td><td>0.017s</td></tr><tr><td class="success">tests/delete.hurl</td><td>0.001s</td></tr><tr><td class="success">tests/empty.hurl</td><td>0s</td></tr><tr><td class="success">tests/encoding.hurl</td><td>0.004s</td></tr><tr><td class="failure">tests/error_assert_base64.hurl</td><td>0.002s</td></tr><tr><td class="failure">tests/error_assert_file.hurl</td><td>0.002s</td></tr><tr><td class="failure">tests/error_assert_header_not_found.hurl</td><td>0.002s</td></tr><tr><td class="failure">tests/error_assert_header_value.hurl</td><td>0.002s</td></tr><tr><td class="failure">tests/error_assert_http_version.hurl</td><td>0.002s</td></tr><tr><td class="failure">tests/error_assert_invalid_predicate_type.hurl</td><td>0.003s</td></tr><tr><td class="failure">tests/error_assert_match_utf8.hurl</td><td>0.002s</td></tr><tr><td class="failure">tests/error_assert_query_invalid_regex.hurl</td><td>0.002s</td></tr><tr><td class="failure">tests/error_assert_query_invalid_xpath.hurl</td><td>0.002s</td></tr><tr><td class="failure">tests/error_assert_status.hurl</td><td>0.002s</td></tr><tr><td class="failure">tests/error_assert_template_variable_not_found.hurl</td><td>0.001s</td></tr><tr><td class="failure">tests/error_assert_value_error.hurl</td><td>0.005s</td></tr><tr><td class="failure">tests/error_assert_variable.hurl</td><td>0.004s</td></tr><tr><td class="failure">tests/error_file_read_access.hurl</td><td>0s</td></tr><tr><td class="failure">tests/error_invalid_jsonpath.hurl</td><td>0.002s</td></tr><tr><td class="failure">tests/error_invalid_url.hurl</td><td>0.074s</td></tr><tr><td class="failure">tests/error_invalid_xml.hurl</td><td>0.002s</td></tr><tr><td class="failure">tests/error_multipart_form_data.hurl</td><td>0s</td></tr><tr><td class="failure">tests/error_predicate.hurl</td><td>0.013s</td></tr><tr><td class="failure">tests/error_query_header_not_found.hurl</td><td>0.002s</td></tr><tr><td class="failure">tests/error_query_invalid_json.hurl</td><td>0.002s</td></tr><tr><td class="failure">tests/error_query_invalid_utf8.hurl</td><td>0.002s</td></tr><tr><td class="failure">tests/error_template_variable_not_found.hurl</td><td>0s</td></tr><tr><td class="failure">tests/error_template_variable_not_renderable.hurl</td><td>0.002s</td></tr><tr><td class="failure">tests/follow_redirect.hurl</td><td>0.002s</td></tr><tr><td class="success">tests/form_params.hurl</td><td>0.004s</td></tr><tr><td class="success">tests/headers.hurl</td><td>0.011s</td></tr><tr><td class="success">tests/hello.hurl</td><td>0.003s</td></tr><tr><td class="success">tests/multipart_form_data.hurl</td><td>0.002s</td></tr><tr><td class="success">tests/no_entry.hurl</td><td>0s</td></tr><tr><td class="success">tests/output.hurl</td><td>0.003s</td></tr><tr><td class="success">tests/patch.hurl</td><td>0.002s</td></tr><tr><td class="success">tests/post_base64.hurl</td><td>0.002s</td></tr><tr><td class="success">tests/post_file.hurl</td><td>0.002s</td></tr><tr><td class="success">tests/post_json.hurl</td><td>0.016s</td></tr><tr><td class="success">tests/post_multilines.hurl</td><td>0.005s</td></tr><tr><td class="success">tests/post_xml.hurl</td><td>0.004s</td></tr><tr><td class="success">tests/predicates-string.hurl</td><td>0.004s</td></tr><tr><td class="success">tests/put.hurl</td><td>0.001s</td></tr><tr><td class="success">tests/querystring_params.hurl</td><td>0.008s</td></tr><tr><td class="success">tests/redirect.hurl</td><td>0.003s</td></tr><tr><td class="success">tests/utf8.hurl</td><td>0.002s</td></tr></tbody></table></body></html>

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,7 @@
error: Invalid Utf8 error: Invalid Decoding
--> tests/error_assert_match_utf8.hurl:4:1 --> tests/error_assert_match_utf8.hurl:4:1
| |
4 | body matches ".*" 4 | body matches ".*"
| ^^^^ The http response is not a valid utf8 string | ^^^^ The body can not be decoded with charset 'utf-8'
| |

View File

@ -1,7 +1,7 @@
error: Invalid url error: Http Connection
--> tests/error_invalid_url.hurl:1:5 --> tests/error_invalid_url.hurl:1:5
| |
1 | GET unknown 1 | GET unknown
| ^^^^^^^ Invalid url <unknown> | ^^^^^^^ can not connect to unknown
| |

View File

@ -2,6 +2,6 @@ error: File ReadAccess
--> tests/error_multipart_form_data.hurl:4:15 --> tests/error_multipart_form_data.hurl:4:15
| |
4 | upload1: file,unknown; 4 | upload1: file,unknown;
| ^^^^^^^ File unknown can not be read | ^^^^^^^ File tests/unknown can not be read
| |

View File

@ -1,7 +1,7 @@
error: Invalid Utf8 error: Invalid Decoding
--> tests/error_query_invalid_utf8.hurl:4:1 --> tests/error_query_invalid_utf8.hurl:4:1
| |
4 | jsonpath "$.errors" countEquals 2 4 | jsonpath "$.errors" countEquals 2
| ^^^^^^^^^^^^^^^^^^^ The http response is not a valid utf8 string | ^^^^^^^^^^^^^^^^^^^ The body can not be decoded with charset 'utf-8'
| |

View File

@ -3,15 +3,13 @@ HTTP/1.0 200
GET http://localhost:8000/default-headers GET http://localhost:8000/default-headers
User-Agent: hurl/1.0 User-Agent: hurl/1.0
Host: localhost # comment Host: localhost:8000 # comment
Content-Length: 0
HTTP/1.0 200 HTTP/1.0 200
GET http://localhost:8000/default-headers GET http://localhost:8000/default-headers
User-Agent: hurl/1.0 User-Agent: hurl/1.0
Host: localhost # comment Host: localhost:8000 # comment
Content-Length: 0
HTTP/1.0 200 HTTP/1.0 200
GET http://localhost:8000/custom-headers GET http://localhost:8000/custom-headers

View File

@ -6,8 +6,8 @@ from tests import app
def default_headers(): def default_headers():
print('> host:' + request.headers['Host'] + "'") print('> host:' + request.headers['Host'] + "'")
assert 'hurl' in request.headers['User-Agent'] assert 'hurl' in request.headers['User-Agent']
assert request.headers['Host'] == 'localhost' assert request.headers['Host'] == 'localhost:8000'
assert int(request.headers['Content-Length']) == 0 assert 'Content-Length' not in request.headers
return '' return ''

View File

@ -1,6 +1,10 @@
from tests import app from tests import app
from flask import request
@app.route("/hello") @app.route("/hello")
def hello(): def hello():
assert 'Content-Type' not in request.headers
assert 'Content-Length' not in request.headers
assert len(request.data) == 0
return 'Hello World!' return 'Hello World!'

View File

@ -15,7 +15,7 @@
* limitations under the License. * limitations under the License.
* *
*/ */
//use hurl::parser::error::ParseError;
use std::collections::HashMap; use std::collections::HashMap;
use std::env; use std::env;
use std::fs; use std::fs;
@ -30,11 +30,11 @@ use clap::{AppSettings, ArgMatches};
use hurl::cli; use hurl::cli;
use hurl::core::common::FormatError; use hurl::core::common::FormatError;
use hurl::html; use hurl::html;
use hurl::http; use hurl::http::libcurl;
use hurl::parser; use hurl::parser;
use hurl::runner; use hurl::runner;
use hurl::runner::core::*; use hurl::runner::core::*;
use hurl::runner::{log_deserialize}; use hurl::runner::log_deserialize;
use hurl::format; use hurl::format;
@ -46,11 +46,11 @@ pub struct CLIOptions {
pub insecure: bool, pub insecure: bool,
pub variables: HashMap<String, String>, pub variables: HashMap<String, String>,
pub to_entry: Option<usize>, pub to_entry: Option<usize>,
pub redirect: http::client::Redirect, pub follow_location: bool,
pub http_proxy: Option<String>, pub max_redirect: Option<usize>,
pub https_proxy: Option<String>, pub proxy: Option<String>,
pub all_proxy: Option<String>, pub no_proxy: Option<String>,
pub noproxy_hosts: Vec<String>, pub cookie_input_file: Option<String>,
} }
@ -58,12 +58,9 @@ fn execute(filename: &str,
contents: String, contents: String,
current_dir: &Path, current_dir: &Path,
file_root: Option<String>, file_root: Option<String>,
cookies: Vec<http::cookie::Cookie>,
cli_options: CLIOptions, cli_options: CLIOptions,
logger: format::logger::Logger, logger: format::logger::Logger,
) -> HurlResult { ) -> HurlResult {
let mut cookiejar = http::cookie::CookieJar::init(cookies);
match parser::parse_hurl_file(contents.as_str()) { match parser::parse_hurl_file(contents.as_str()) {
Err(e) => { Err(e) => {
let error = hurl::format::error::Error { let error = hurl::format::error::Error {
@ -79,26 +76,23 @@ fn execute(filename: &str,
std::process::exit(2); std::process::exit(2);
} }
Ok(hurl_file) => { Ok(hurl_file) => {
logger.verbose(format!("Fail fast: {}", cli_options.fail_fast).as_str()); logger.verbose(format!("fail fast: {}", cli_options.fail_fast).as_str());
logger.verbose(format!("variables: {:?}", cli_options.variables).as_str()); logger.verbose(format!("insecure: {}", cli_options.insecure).as_str());
if let Some(proxy) = cli_options.http_proxy.clone() { logger.verbose(format!("follow redirect: {}", cli_options.follow_location).as_str());
logger.verbose(format!("http_proxy: {}", proxy).as_str()); if let Some(n) = cli_options.max_redirect {
logger.verbose(format!("max redirect: {}", n).as_str());
} }
if let Some(proxy) = cli_options.https_proxy.clone() { if let Some(proxy) = cli_options.proxy.clone() {
logger.verbose(format!("https_proxy: {}", proxy).as_str()); logger.verbose(format!("proxy: {}", proxy).as_str());
} }
if let Some(proxy) = cli_options.all_proxy.clone() {
logger.verbose(format!("all_proxy: {}", proxy).as_str()); if !cli_options.variables.is_empty() {
} logger.verbose("variables:");
if !cli_options.noproxy_hosts.is_empty() { for (name, value) in cli_options.variables.clone() {
logger.verbose(format!("noproxy: {}", cli_options.noproxy_hosts.join(", ")).as_str()); logger.verbose(format!(" {}={}", name, value).as_str());
}
} }
match cli_options.redirect {
http::client::Redirect::None {} => {}
http::client::Redirect::Limited(n) => logger.verbose(format!("follow redirect (max: {})", n).as_str()),
http::client::Redirect::Unlimited {} => logger.verbose("follow redirect"),
};
if let Some(to_entry) = cli_options.to_entry { if let Some(to_entry) = cli_options.to_entry {
if to_entry < hurl_file.entries.len() { if to_entry < hurl_file.entries.len() {
@ -108,16 +102,24 @@ fn execute(filename: &str,
} }
} }
let noproxy_hosts = cli_options.noproxy_hosts.clone(); let follow_location = cli_options.follow_location;
let redirect = cli_options.redirect.clone(); let verbose = cli_options.verbose;
let client = http::client::Client::init(http::client::ClientOptions { let insecure = cli_options.insecure;
noproxy_hosts, let max_redirect = cli_options.max_redirect;
insecure: cli_options.insecure, let proxy = cli_options.proxy;
redirect, let no_proxy = cli_options.no_proxy;
http_proxy: cli_options.http_proxy.clone(), let cookie_input_file = cli_options.cookie_input_file;
https_proxy: cli_options.https_proxy.clone(), let options = libcurl::client::ClientOptions {
all_proxy: cli_options.all_proxy.clone(), follow_location,
}); max_redirect,
cookie_input_file,
proxy,
no_proxy,
verbose,
insecure,
};
let mut client = libcurl::client::Client::init(options);
let context_dir = match file_root { let context_dir = match file_root {
None => { None => {
@ -140,9 +142,8 @@ fn execute(filename: &str,
to_entry: cli_options.to_entry, to_entry: cli_options.to_entry,
}; };
runner::file::run(hurl_file, runner::file::run(hurl_file,
client, &mut client,
filename.to_string(), filename.to_string(),
&mut cookiejar,
context_dir, context_dir,
options, options,
logger, logger,
@ -163,19 +164,6 @@ fn output_color(matches: ArgMatches) -> bool {
} }
fn noproxy_host(matches: ArgMatches) -> Vec<String> {
match matches.value_of("noproxy") {
Some(value) => {
value.split(',').map(|e| e.trim().to_string()).collect()
}
_ => if let Ok(v) = std::env::var("no_proxy") {
v.split(',').map(|e| e.trim().to_string()).collect()
} else {
vec![]
}
}
}
fn to_entry(matches: ArgMatches, logger: format::logger::Logger) -> Option<usize> { fn to_entry(matches: ArgMatches, logger: format::logger::Logger) -> Option<usize> {
match matches.value_of("to_entry") { match matches.value_of("to_entry") {
Some(value) => { Some(value) => {
@ -352,7 +340,7 @@ fn app() -> clap::App<'static, 'static> {
) )
.arg( .arg(
clap::Arg::with_name("redirect") clap::Arg::with_name("follow_location")
.short("L") .short("L")
.long("location") .long("location")
.help("Follow redirects"), .help("Follow redirects"),
@ -361,7 +349,6 @@ fn app() -> clap::App<'static, 'static> {
clap::Arg::with_name("max_redirects") clap::Arg::with_name("max_redirects")
.long("max-redirs") .long("max-redirs")
.value_name("NUM") .value_name("NUM")
.default_value("50")
.allow_hyphen_values(true) .allow_hyphen_values(true)
.help("Maximum number of redirects allowed"), .help("Maximum number of redirects allowed"),
) )
@ -431,12 +418,22 @@ fn parse_options(matches: ArgMatches, logger: format::logger::Logger) -> Result<
let fail_fast = !matches.is_present("fail_at_end"); let fail_fast = !matches.is_present("fail_at_end");
let variables = variables(matches.clone(), logger.clone()); let variables = variables(matches.clone(), logger.clone());
let to_entry = to_entry(matches.clone(), logger); let to_entry = to_entry(matches.clone(), logger);
let redirect = cli::options::redirect(matches.is_present("redirect"), matches.value_of("max_redirects").unwrap_or_default())?; let proxy = matches.value_of("proxy").map(|x| x.to_string());
let http_proxy = cli::options::proxy(matches.value_of("proxy"), env::var("http_proxy").ok())?; let no_proxy = matches.value_of("proxy").map(|x| x.to_string());
let https_proxy = cli::options::proxy(matches.value_of("proxy"), env::var("https_proxy").ok())?;
let all_proxy = cli::options::proxy(matches.value_of("proxy"), env::var("all_proxy").ok())?;
let noproxy_hosts = noproxy_host(matches.clone());
let insecure = matches.is_present("insecure"); let insecure = matches.is_present("insecure");
let follow_location = matches.is_present("follow_location");
let cookie_input_file = matches.value_of("cookie_input_file").map(|x| x.to_string());
let max_redirect = match matches.value_of("max_redirects") {
None => Some(50),
Some("-1") => None,
Some(s) => {
match s.parse::<usize>() {
Ok(x) => Some(x),
Err(_) => return Err(cli::Error{ message: "max_redirs option can not be parsed".to_string() })
}
}
};
Ok(CLIOptions { Ok(CLIOptions {
verbose, verbose,
color, color,
@ -444,11 +441,11 @@ fn parse_options(matches: ArgMatches, logger: format::logger::Logger) -> Result<
insecure, insecure,
variables, variables,
to_entry, to_entry,
redirect, follow_location,
http_proxy, max_redirect,
https_proxy, proxy,
all_proxy, no_proxy,
noproxy_hosts, cookie_input_file,
}) })
} }
@ -485,12 +482,6 @@ fn main() -> Result<(), cli::Error> {
color: output_color(matches.clone()), color: output_color(matches.clone()),
}; };
let cookies = match matches.value_of("cookies_input_file") {
Some(filename) => unwrap_or_exit(cli::options::cookies(filename), logger.clone()),
None => vec![],
};
let (mut hurl_results, json_file) = json_file(matches.clone(), logger.clone()); let (mut hurl_results, json_file) = json_file(matches.clone(), logger.clone());
let html_report = html_report(matches.clone(), logger.clone()); let html_report = html_report(matches.clone(), logger.clone());
@ -536,7 +527,6 @@ fn main() -> Result<(), cli::Error> {
contents, contents,
current_dir, current_dir,
file_root.clone(), file_root.clone(),
cookies.clone(),
cli_options.clone(), cli_options.clone(),
logger.clone(), logger.clone(),
); );
@ -661,7 +651,7 @@ fn write_cookies_file(file_path: PathBuf, hurl_results: Vec<HurlResult>, logger:
} }
Some(result) => { Some(result) => {
for cookie in result.cookies.clone() { for cookie in result.cookies.clone() {
s.push_str(cookie.to_netscape().as_str()); s.push_str(cookie.to_string().as_str());
s.push('\n'); s.push('\n');
} }
} }

View File

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

View File

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

View File

@ -1,216 +0,0 @@
/*
* hurl (https://hurl.dev)
* Copyright (C) 2020 Orange
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
use std::path::Path;
use super::core::*;
use super::request::*;
use super::response::*;
pub struct Client {
_inner_client: reqwest::Client,
options: ClientOptions,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ClientOptions {
pub noproxy_hosts: Vec<String>,
pub insecure: bool,
pub redirect: Redirect,
pub http_proxy: Option<String>,
pub https_proxy: Option<String>,
pub all_proxy: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Redirect {
None,
Limited(usize),
Unlimited,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct HttpError {
pub url: String,
pub message: String,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Proxy {
pub protocol: Option<ProxyProtocol>,
pub host: String,
pub port: Option<u16>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum ProxyProtocol {
Http,
Https,
}
pub fn get_redirect_policy(redirect: Redirect) -> reqwest::RedirectPolicy {
match redirect {
Redirect::None => reqwest::RedirectPolicy::none(),
Redirect::Limited(max) => reqwest::RedirectPolicy::limited(max),
Redirect::Unlimited {} => reqwest::RedirectPolicy::custom(|attempt| { attempt.follow() }),
}
}
impl Client {
pub fn init(options: ClientOptions) -> Client {
let client_builder = reqwest::Client::builder()
.redirect(get_redirect_policy(options.redirect.clone()))
.use_sys_proxy()
.danger_accept_invalid_hostnames(options.insecure)
.danger_accept_invalid_certs(options.insecure)
.cookie_store(false);
Client {
_inner_client: client_builder.build().unwrap(),
options,
}
}
pub fn execute(&self, request: &Request) -> Result<Response, HttpError> {
let mut headers = reqwest::header::HeaderMap::new();
for header in request.clone().headers() {
headers.append(
reqwest::header::HeaderName::from_lowercase(
header.name.to_lowercase().as_str().as_bytes(),
)
.unwrap(),
reqwest::header::HeaderValue::from_str(header.value.as_str()).unwrap(),
);
}
let client_builder = reqwest::Client::builder()
.redirect(get_redirect_policy(self.options.redirect.clone()))
.danger_accept_invalid_hostnames(self.options.insecure)
.danger_accept_invalid_certs(self.options.insecure)
.cookie_store(false);
let client_builder = if let Some(url) = self.options.http_proxy.clone() {
let proxy = reqwest::Proxy::http(url.as_str()).unwrap();
client_builder.proxy(proxy)
} else {
client_builder
};
let client_builder = if let Some(url) = self.options.https_proxy.clone() {
let proxy = reqwest::Proxy::https(url.as_str()).unwrap();
client_builder.proxy(proxy)
} else {
client_builder
};
let client_builder = if let Some(url) = self.options.all_proxy.clone() {
let proxy = reqwest::Proxy::all(url.as_str()).unwrap();
client_builder.proxy(proxy)
} else {
client_builder
};
let client_builder = if self.options.noproxy_hosts.contains(&request.url.host.clone()) {
client_builder.no_proxy()
} else {
client_builder
};
let client = client_builder.build().unwrap();
let req = if request.multipart.is_empty() {
client
.request(
request.clone().method.to_reqwest(),
reqwest::Url::parse(request.clone().url().as_str()).unwrap(),
)
.headers(headers)
.body(request.clone().body)
.build()
.unwrap()
} else {
let mut form = reqwest::multipart::Form::new();
for param in request.multipart.clone() {
match param {
MultipartParam::TextParam { name, value } => {
form = form.text(name, value)
}
MultipartParam::FileParam { name, filename, content_type } => {
if let Some(content_type) = content_type {
let path = Path::new(filename.as_str());
let part = reqwest::multipart::Part::file(path).unwrap()
.mime_str(content_type.as_str())
.unwrap();
form = form.part(name, part);
} else {
form = form.file(name, filename).unwrap();
}
}
}
}
client
.request(
request.clone().method.to_reqwest(),
reqwest::Url::parse(request.clone().url().as_str()).unwrap(),
)
.headers(headers)
.multipart(form)
.build()
.unwrap()
};
match client.execute(req) {
Ok(mut resp) => {
let mut headers = vec![];
//eprintln!(">>> response headers {:?}", resp.headers().clone());
for (name, value) in resp.headers() {
headers.push(Header {
name: name.as_str().to_string(),
value: value.to_str().unwrap().to_string(),
})
}
let version = match resp.version() {
reqwest::Version::HTTP_10 => Version::Http10,
reqwest::Version::HTTP_11 => Version::Http11,
reqwest::Version::HTTP_2 => Version::Http2,
v => panic!("Version {:?} not supported!", v),
};
let mut buf: Vec<u8> = vec![];
resp.copy_to(&mut buf).unwrap(); // TODO Test error case
resp.content_length(); // dirty hack to prevent error "connection closed before message completed"?
Ok(Response {
version,
status: resp.status().as_u16(),
headers,
body: buf,
})
}
Err(e) => {
Err(HttpError {
message: format!("{:?}", e.to_string()),
url: request.clone().url(),
})
}
}
}
}

View File

@ -1,472 +0,0 @@
/*
* hurl (https://hurl.dev)
* Copyright (C) 2020 Orange
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
use core::fmt;
use chrono::NaiveDateTime;
use cookie::Cookie as ExternalCookie;
use super::core::*;
//use std::collections::HashMap;
// cookies
// keep cookies same name different domains
// send the most specific?? send the 2 of them?
// more flexible to keep list of cookies internally
pub type Domain = String;
pub type Name = String;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ResponseCookie {
pub name: String,
pub value: String,
pub max_age: Option<i64>,
pub domain: Option<String>,
pub path: Option<String>,
pub secure: Option<bool>,
pub http_only: Option<bool>,
pub expires: Option<String>,
pub same_site: Option<String>,
}
pub struct ParseCookieError {}
impl std::str::FromStr for ResponseCookie {
type Err = ParseCookieError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let c = ExternalCookie::parse(s).unwrap();
let name = c.name().to_string();
let value = c.value().to_string();
let max_age = match c.max_age() {
None => None,
Some(d) => Some(d.num_seconds())
};
let domain = match c.domain() {
None => None,
Some(v) => Some(v.to_string())
};
let path = match c.path() {
None => None,
Some(v) => Some(v.to_string())
};
let secure = match c.secure() {
None => None,
Some(value) => Some(value)
};
let http_only = match c.http_only() {
None => None,
Some(value) => Some(value)
};
let expires = match c.expires() {
None => None,
Some(time) => Some(time.rfc822().to_string())
};
let same_site = match c.same_site() {
None => None,
Some(s) => Some(s.to_string())
};
Ok(ResponseCookie { name, value, max_age, domain, path, secure, expires, http_only, same_site })
}
}
impl fmt::Display for ResponseCookie {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let max_age = match self.clone().max_age {
None => String::from(""),
Some(v) => format!("; Max-Age:{}", v)
};
let domain = match self.clone().domain {
None => String::from(""),
Some(v) => format!("; Domain:{}", v)
};
let path = match self.clone().path {
None => String::from(""),
Some(v) => format!("; Path:{}", v)
};
write!(f, "{}={}{}{}{}",
self.name,
self.value,
max_age,
domain,
path)
}
}
impl fmt::Display for Cookie {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}={}; domain={}; path={}",
self.name,
self.value,
self.domain,
self.path,
)
}
}
impl ResponseCookie {
// pub fn to_header(&self) -> Header {
// return Header {
// name: String::from("Cookie"),
// value: format!("{}={}", self.name, self.value),
// };
// //format!("Cookie: {}", self.to_string());
// }
pub fn encode_cookie(header_name: String, header_value: String) -> Header {
let name = String::from("Cookie");
let value = format!("{}={};", header_name, header_value);
Header { name, value }
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct CookieJar {
inner: Vec<Cookie>
}
impl CookieJar {
pub fn init(cookies: Vec<Cookie>) -> CookieJar {
CookieJar { inner: cookies }
}
pub fn cookies(self) -> Vec<Cookie> {
self.inner
}
pub fn get_cookies(self, domain: String, path: String) -> Vec<ResponseCookie> {
self.inner
.iter()
.filter(|c| c.is_usable(domain.clone(), path.clone()))
.map(|c| ResponseCookie {
name: c.clone().name,
value: c.clone().value,
max_age: None,
domain: Some(c.domain.clone()),
path: Some(c.path.clone()),
secure: Some(c.secure),
http_only: None,
expires: None,
same_site: None,
})
.collect()
}
pub fn update_cookies(&mut self, default_domain: String, _default_path: String, cookie: ResponseCookie) {
match cookie.max_age {
Some(0) => {
//eprintln!("delete cookie {:?}", cookie);
self.inner.retain(|c| c.name != cookie.name);
}
_ => {
// replace value if same name+domain
let domain = match cookie.clone().domain {
None => default_domain,
Some(d) => d,
};
let path = match cookie.clone().path {
None => String::from("/"), // do not use default path for the time-beingdefault_path,
Some(p) => p,
};
// find existing cookie
for c in self.inner.iter_mut() {
if c.name == cookie.name && c.domain == domain {
c.value = cookie.value;
return;
}
}
let secure = if let Some(v) = cookie.secure { v } else { false };
// push new cookie
self.inner.push(Cookie {
name: cookie.clone().name,
value: cookie.clone().value,
domain,
path,
subdomains: cookie.domain.is_some(),
secure,
expires: None,
});
}
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Cookie {
pub name: String,
pub value: String,
pub domain: String,
pub path: String,
pub subdomains: bool,
pub secure: bool,
pub expires: Option<NaiveDateTime>,
}
impl Cookie {
fn is_usable(&self, domain: String, path: String) -> bool {
// domain
if !is_subdomain(self.clone().domain, domain.clone()) {
return false;
}
if !self.subdomains && domain != self.clone().domain {
return false;
}
// path
if !is_subpath(self.clone().path, path) {
return false;
}
true
}
}
fn is_subdomain(domain: String, subdomain: String) -> bool {
if domain.as_str() == "" {
return false;
}
let mut domain_segments: Vec<&str> = domain.split('.').collect();
if domain_segments.get(0).unwrap() == &"" {
domain_segments.remove(0);
}
domain_segments.reverse();
let mut subdomain_segments: Vec<&str> = subdomain.split('.').collect();
if subdomain_segments.get(0).unwrap() == &"" {
subdomain_segments.remove(0);
}
subdomain_segments.reverse();
if domain_segments.len() > subdomain_segments.len() {
return false;
}
for i in 0..domain_segments.len() {
if domain_segments.get(i).unwrap() != subdomain_segments.get(i).unwrap() {
return false;
}
}
true
}
fn is_subpath(path: String, subpath: String) -> bool {
if path.as_str() == "" {
return false;
}
let mut path_segments: Vec<&str> = path.split('/').collect();
if path_segments.get(0).unwrap() == &"" {
path_segments.remove(0);
}
path_segments.reverse();
if path_segments.get(0).unwrap() == &"" {
path_segments.remove(0);
}
let mut subpath_segments: Vec<&str> = subpath.split('/').collect();
if subpath_segments.get(0).unwrap() == &"" {
subpath_segments.remove(0);
}
subpath_segments.reverse();
if path_segments.len() > subpath_segments.len() {
return false;
}
for i in 0..path_segments.len() {
if path_segments.get(i).unwrap() != subpath_segments.get(i).unwrap() {
return false;
}
}
true
}
impl Cookie {
pub fn to_netscape(&self) -> String {
let domain_name = self.domain.to_string();
let include_domains = if self.subdomains { "TRUE" } else { "FALSE" }.to_string();
let path = self.path.clone();
let https_only = if self.secure { "TRUE" } else { "FALSE" }.to_string();
let expires = if let Some(expires) = self.expires {
expires.timestamp().to_string()
} else {
"0".to_string()
};
let name = self.name.clone();
let value = self.value.clone();
format!("{}\t{}\t{}\t{}\t{}\t{}\t{}",
domain_name,
include_domains,
path,
https_only,
expires,
name,
value
)
}
pub fn from_netscape(s: &str) -> Option<Cookie> {
let tokens = s.split('\t').collect::<Vec<&str>>();
if tokens.len() != 7 { return None; }
let domain = (*tokens.get(0).unwrap()).to_string();
let subdomains = (*tokens.get(1).unwrap()).to_string().as_str() == "TRUE";
let path = (*tokens.get(2).unwrap()).to_string();
let secure = (*tokens.get(3).unwrap()).to_string().as_str() == "TRUE";
let expires = None;
let name = (*tokens.get(5).unwrap()).to_string();
let value = (*tokens.get(6).unwrap()).to_string();
Some(Cookie { name, value, domain, path, subdomains, secure, expires })
}
}
#[cfg(test)]
mod tests {
use super::*;
fn cookie_lsid() -> Cookie {
Cookie {
name: String::from("LSID"),
value: String::from("DQAAAK…Eaem_vYg"),
domain: String::from("docs.foo.com"),
path: String::from("/accounts"),
subdomains: false,
secure: false,
expires: None,
}
}
fn cookie_hsid() -> Cookie {
Cookie {
name: String::from("HSID"),
value: String::from("AYQEVn…DKrdst"),
domain: String::from(".foo.com"),
path: String::from("/"),
subdomains: true,
secure: false,
expires: None,
}
}
fn cookie_ssid() -> Cookie {
Cookie {
name: String::from("SSID"),
value: String::from("Ap4P…GTEq"),
domain: String::from("foo.com"),
path: String::from("/"),
subdomains: true,
secure: false,
expires: None,
}
}
fn sample_cookiejar() -> CookieJar {
CookieJar {
inner: vec![
cookie_lsid(),
cookie_hsid(),
cookie_ssid(),
]
}
}
#[test]
fn test_is_usable() {
let domain = String::from("example.org");
let path = String::from("/");
assert_eq!(cookie_lsid().is_usable(domain.clone(), path.clone()), false);
assert_eq!(cookie_hsid().is_usable(domain.clone(), path.clone()), false);
assert_eq!(cookie_ssid().is_usable(domain, path.clone()), false);
let domain = String::from("foo.com");
let path = String::from("/");
assert_eq!(cookie_lsid().is_usable(domain.clone(), path.clone()), false);
assert_eq!(cookie_hsid().is_usable(domain.clone(), path.clone()), true);
assert_eq!(cookie_ssid().is_usable(domain, path.clone()), true);
let domain = String::from("foo.com");
let path = String::from("/accounts");
assert_eq!(cookie_lsid().is_usable(domain.clone(), path.clone()), false);
assert_eq!(cookie_hsid().is_usable(domain.clone(), path.clone()), true);
assert_eq!(cookie_ssid().is_usable(domain, path), true);
let domain = String::from("docs.foo.com");
let path = String::from("/accounts");
assert_eq!(cookie_lsid().is_usable(domain.clone(), path.clone()), true);
assert_eq!(cookie_hsid().is_usable(domain.clone(), path.clone()), true);
assert_eq!(cookie_ssid().is_usable(domain, path), true);
}
#[test]
fn test_get_cookies() {
let domain = String::from("docs.foo.com");
let path = String::from("/accounts");
assert_eq!(sample_cookiejar().get_cookies(domain, path).len(), 3);
let domain = String::from("toto.docs.foo.com");
let path = String::from("/accounts");
assert_eq!(sample_cookiejar().get_cookies(domain, path).len(), 2);
}
#[test]
fn test_is_subdomain() {
assert_eq!(is_subdomain(String::from("foo.example.org"), String::from("example.org")), false);
assert_eq!(is_subdomain(String::from("example.org"), String::from("toto.org")), false);
assert_eq!(is_subdomain(String::from("example.org"), String::from("example.org")), true);
assert_eq!(is_subdomain(String::from("example.org"), String::from("foo.example.org")), true);
assert_eq!(is_subdomain(String::from(".example.org"), String::from("foo.example.org")), true);
}
#[test]
fn test_is_subpath() {
assert_eq!(is_subpath(String::from("/toto"), String::from("/toto")), true);
assert_eq!(is_subpath(String::from("/"), String::from("/toto")), true);
assert_eq!(is_subpath(String::from("/to"), String::from("/toto")), false);
}
#[test]
fn test_from_netscape() {
assert_eq!(Cookie::from_netscape("localhost\tFALSE\t/\tFALSE\t0\tcookie2\tvalueA").unwrap(),
Cookie {
name: "cookie2".to_string(),
value: "valueA".to_string(),
domain: "localhost".to_string(),
path: "/".to_string(),
subdomains: false,
secure: false,
expires: None,
}
);
}
}

View File

@ -1,128 +0,0 @@
/*
* hurl (https://hurl.dev)
* Copyright (C) 2020 Orange
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
extern crate reqwest;
use std::fmt;
use serde::{Deserialize, Serialize};
pub enum Encoding {
Utf8,
Latin1,
}
impl fmt::Display for Encoding {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", match self {
Encoding::Utf8 => "utf8",
Encoding::Latin1 => "iso8859-1"
})
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Url {
pub scheme: String,
pub host: String,
pub port: Option<u16>,
pub path: String,
pub query_string: String,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Header {
pub name: String,
pub value: String,
}
pub fn get_header_value(headers: Vec<Header>, name: &str) -> Option<String> {
for header in headers {
if header.name.as_str() == name {
return Some(header.value);
}
}
None
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Param {
pub name: String,
pub value: String,
}
pub fn encode_form_params(params: Vec<Param>) -> Vec<u8> {
params
.iter()
//.map(|p| format!("{}={}", p.name, utf8_percent_encode(p.value.as_str(), FRAGMENT)))
.map(|p| format!("{}={}", p.name, url_encode(p.value.clone())))
.collect::<Vec<_>>()
.join("&")
.into_bytes()
}
fn url_encode(s: String) -> String {
const MAX_CHAR_VAL: u32 = std::char::MAX as u32;
let mut buff = [0; 4];
s.chars()
.map(|ch| {
match ch as u32 {
0..=47 | 58..=64 | 91..=96 | 123..=MAX_CHAR_VAL => {
ch.encode_utf8(&mut buff);
buff[0..ch.len_utf8()].iter().map(|&byte| format!("%{:x}", byte)).collect::<String>()
}
_ => ch.to_string(),
}
})
.collect::<String>()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_encode_form_params() {
assert_eq!(
encode_form_params(vec![
Param {
name: String::from("param1"),
value: String::from("value1"),
},
Param {
name: String::from("param2"),
value: String::from(""),
}
]),
vec![
112, 97, 114, 97, 109, 49, 61, 118, 97, 108, 117, 101, 49, 38, 112, 97, 114, 97, 109,
50, 61
]
);
assert_eq!(
std::str::from_utf8(&encode_form_params(vec![
Param { name: String::from("param1"), value: String::from("value1") },
Param { name: String::from("param2"), value: String::from("") },
Param { name: String::from("param3"), value: String::from("a=b") },
Param { name: String::from("param4"), value: String::from("a%3db") },
])).unwrap(),
"param1=value1&param2=&param3=a%3db&param4=a%253db"
);
}
}

View File

@ -1,119 +0,0 @@
/*
* hurl (https://hurl.dev)
* Copyright (C) 2020 Orange
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
use serde::ser::{Serializer, SerializeStruct};
use serde::Serialize;
use super::cookie::*;
use super::request::*;
use super::response::*;
impl Serialize for Request {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
// 3 is the number of fields in the struct.
let mut state = serializer.serialize_struct("??", 3)?;
state.serialize_field("method", &self.clone().method.to_text())?;
state.serialize_field("url", &self.clone().url())?;
state.serialize_field("queryString", &self.clone().querystring)?;
state.serialize_field("headers", &self.clone().headers())?;
state.serialize_field("cookies", &self.clone().cookies)?;
if let Some(params) = self.clone().form_params() {
state.serialize_field("format_params", &params)?;
}
state.serialize_field("body", &base64::encode(&self.body))?;
state.end()
}
}
impl Serialize for Response {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
// 3 is the number of fields in the struct.
let mut state = serializer.serialize_struct("??", 3)?;
state.serialize_field("httpVersion", &self.clone().version)?;
state.serialize_field("status", &self.clone().status)?;
state.serialize_field("cookies", &self.clone().cookies())?;
state.serialize_field("headers", &self.clone().headers)?;
// WIP - Serialize response body only for json for the timebeing
let content_type = self.get_header("content_type", true);
if let Some(value) = content_type.first() {
if value.as_str() == "application/json; charset=UTF-8" {
let s = String::from_utf8(self.body.clone()).expect("Found invalid UTF-8");
let result: Result<serde_json::Value, serde_json::Error> = serde_json::from_str(s.as_str());
if let Ok(v) = result {
state.serialize_field("json", &v)?;
}
}
}
state.end()
}
}
impl Serialize for ResponseCookie {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
// 3 is the number of fields in the struct.
let mut state = serializer.serialize_struct("??", 3)?;
state.serialize_field("name", &self.clone().name)?;
state.serialize_field("value", &self.clone().value)?;
if let Some(value) = self.clone().domain {
state.serialize_field("domain", &value)?;
}
state.end()
}
}
impl Serialize for Cookie {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut state = serializer.serialize_struct("InternalCookie", 3)?;
state.serialize_field("name", &self.clone().name)?;
state.serialize_field("value", &self.clone().value)?;
state.serialize_field("domain", &self.clone().domain)?;
state.serialize_field("path", &self.clone().path)?;
state.serialize_field("include_subdomain", &self.clone().subdomains)?;
state.end()
}
}
impl Serialize for Version {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
match self {
Version::Http10 => serializer.serialize_str("HTTP/1.0"),
Version::Http11 => serializer.serialize_str("HTTP/1.1"),
Version::Http2 => serializer.serialize_str("HTTP/2"),
}
}
}

View File

@ -1,391 +0,0 @@
/*
* hurl (https://hurl.dev)
* Copyright (C) 2020 Orange
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
extern crate url as external_url;
use chrono::DateTime;
use super::cookie::*;
use super::core::*;
use super::request::*;
use super::response::*;
type ParseError = String;
pub fn parse_request(value: serde_json::Value) -> Result<Request, ParseError> {
if let serde_json::Value::Object(map) = value {
let method = match map.get("method") {
Some(serde_json::Value::String(s)) => parse_method(s.clone())?,
_ => return Err("expecting a string for the method".to_string()),
};
let url = match map.get("url") {
Some(serde_json::Value::String(s)) => parse_url(s.clone())?,
_ => return Err("expecting a string for the url".to_string()),
};
let headers = match map.get("headers") {
Some(serde_json::Value::Array(values)) => {
let mut headers = vec![];
for value in values {
let header = parse_header(value.clone())?;
headers.push(header);
}
headers
}
_ => vec![],
};
let cookies = match map.get("cookies") {
Some(serde_json::Value::Array(values)) => {
let mut headers = vec![];
for value in values {
let header = parse_response_cookie(value.clone())?;
headers.push(header);
}
headers
}
_ => vec![],
};
let multipart = vec![];
Ok(Request {
method,
url,
querystring: vec![],
headers,
cookies,
body: vec![],
multipart,
})
} else {
Err("expecting an object for the request".to_string())
}
}
pub fn parse_response(value: serde_json::Value) -> Result<Response, ParseError> {
if let serde_json::Value::Object(map) = value {
let status = match map.get("status") {
Some(serde_json::Value::Number(x)) => if let Some(x) = x.as_u64() {
x as u16
} else {
return Err("expecting a integer for the status".to_string());
},
_ => return Err("expecting a number for the status".to_string()),
};
let version = match map.get("httpVersion") {
Some(serde_json::Value::String(s)) => parse_version(s.clone())?,
_ => return Err("expecting a string for the version".to_string()),
};
let headers = match map.get("headers") {
Some(serde_json::Value::Array(values)) => {
let mut headers = vec![];
for value in values {
let header = parse_header(value.clone())?;
headers.push(header);
}
headers
}
_ => vec![],
};
Ok(Response {
version,
status,
headers,
body: vec![],
})
} else {
Err("expecting an object for the response".to_string())
}
}
fn parse_method(s: String) -> Result<Method, ParseError> {
match s.as_str() {
"GET" => Ok(Method::Get),
"HEAD" => Ok(Method::Head),
"POST" => Ok(Method::Post),
"PUT" => Ok(Method::Put),
"DELETE" => Ok(Method::Delete),
"CONNECT" => Ok(Method::Connect),
"OPTIONS" => Ok(Method::Options),
"TRACE" => Ok(Method::Trace),
"PATCH" => Ok(Method::Patch),
_ => Err(format!("Invalid method <{}>", s))
}
}
fn parse_url(s: String) -> Result<Url, ParseError> {
match external_url::Url::parse(s.as_str()) {
Err(_) => Err(format!("Invalid url <{}>", s)),
Ok(u) => Ok(Url {
scheme: u.scheme().to_string(),
host: u.host_str().unwrap().to_string(),
port: u.port(),
path: u.path().to_string(),
query_string: if let Some(s) = u.query() { s.to_string() } else { "".to_string() },
})
}
}
fn parse_header(value: serde_json::Value) -> Result<Header, ParseError> {
if let serde_json::Value::Object(map) = value {
let name = match map.get("name") {
Some(serde_json::Value::String(s)) => s.to_string(),
_ => return Err("expecting a string for the header name".to_string()),
};
let value = match map.get("value") {
Some(serde_json::Value::String(s)) => s.to_string(),
_ => return Err("expecting a string for the header value".to_string()),
};
Ok(Header { name, value })
} else {
Err("Expecting object for one header".to_string())
}
}
pub fn parse_response_cookie(value: serde_json::Value) -> Result<ResponseCookie, ParseError> {
if let serde_json::Value::Object(map) = value {
let name = match map.get("name") {
Some(serde_json::Value::String(s)) => s.to_string(),
_ => return Err("expecting a string for the cookie name".to_string()),
};
let value = match map.get("value") {
Some(serde_json::Value::String(s)) => s.to_string(),
_ => return Err("expecting a string for the cookie value".to_string()),
};
let domain = match map.get("domain") {
None => None,
Some(serde_json::Value::String(s)) => Some(s.to_string()),
_ => return Err("expecting a string for the cookie domain".to_string()),
};
let path = match map.get("path") {
None => None,
Some(serde_json::Value::String(s)) => Some(s.to_string()),
_ => return Err("expecting a string for the cookie path".to_string()),
};
let expires = match map.get("expires") {
None => None,
Some(serde_json::Value::String(s)) => Some(s.to_string()),
_ => return Err("expecting a string for the cookie expires".to_string()),
};
let secure = match map.get("secure") {
None => None,
Some(serde_json::Value::Bool(value)) => Some(*value),
_ => return Err("expecting a bool for the cookie secure flag".to_string()),
};
let http_only = match map.get("http_only") {
None => None,
Some(serde_json::Value::Bool(value)) => Some(*value),
_ => return Err("expecting a bool for the cookie http_only flag".to_string()),
};
let same_site = match map.get("same_site") {
None => None,
Some(serde_json::Value::String(s)) => Some(s.to_string()),
_ => return Err("expecting a string for the cookie same_site".to_string()),
};
Ok(ResponseCookie { name, value, max_age: None, domain, path, secure, http_only, expires, same_site })
} else {
Err("Expecting object for one cookie".to_string())
}
}
pub fn parse_cookie(value: serde_json::Value) -> Result<Cookie, ParseError> {
if let serde_json::Value::Object(map) = value {
let name = match map.get("name") {
Some(serde_json::Value::String(s)) => s.to_string(),
_ => return Err("expecting a string for the cookie name".to_string()),
};
let value = match map.get("value") {
Some(serde_json::Value::String(s)) => s.to_string(),
_ => return Err("expecting a string for the cookie value".to_string()),
};
let domain = match map.get("domain") {
Some(serde_json::Value::String(s)) => s.to_string(),
_ => return Err("expecting a string for the cookie domain".to_string()),
};
let path = match map.get("path") {
Some(serde_json::Value::String(s)) => s.to_string(),
_ => return Err("expecting a string for the cookie path".to_string()),
};
let subdomains = match map.get("include_subdomain") {
Some(serde_json::Value::Bool(v)) => *v,
_ => return Err("expecting a bool for the include_subdomain".to_string()),
};
let secure = match map.get("secure") {
None => false,
Some(serde_json::Value::Bool(value)) => *value,
_ => return Err("expecting a bool for the secure flag".to_string()),
};
let expires = match map.get("expired") {
None => None,
Some(serde_json::Value::String(v)) => {
match DateTime::parse_from_rfc3339(v.as_str()) {
Ok(v) => Some(v.naive_utc()),
Err(_) => return Err("expecting a String (date) for the expired fieldate can be parsed".to_string()),
}
}
_ => return Err("expecting a String (date) for the expired field".to_string()),
};
Ok(Cookie { name, value, domain, path, subdomains, secure, expires })
} else {
Err("Expecting object for one cookie".to_string())
}
}
fn parse_version(s: String) -> Result<Version, ParseError> {
match s.as_str() {
"HTTP/1.0" => Ok(Version::Http10),
"HTTP/1.1" => Ok(Version::Http11),
"HTTP/2" => Ok(Version::Http2),
_ => Err("Expecting version HTTP/1.0, HTTP/1.2 or HTTP/2".to_string())
}
}
#[cfg(test)]
mod tests {
use super::*;
use super::super::request::tests::*;
#[test]
fn test_parse_request() {
let v: serde_json::Value = serde_json::from_str(r#"{
"method": "GET",
"url": "http://localhost:8000/hello",
"headers": []
}"#).unwrap();
assert_eq!(parse_request(v).unwrap(), hello_http_request());
let v: serde_json::Value = serde_json::from_str(r#"{
"method": "GET",
"url": "http://localhost:8000/querystring-params?param1=value1&param2=a%20b",
"headers": []
}"#).unwrap();
assert_eq!(parse_request(v).unwrap(), Request {
method: Method::Get,
url: Url {
scheme: "http".to_string(),
host: "localhost".to_string(),
port: Some(8000),
path: "/querystring-params".to_string(),
query_string: "param1=value1&param2=a%20b".to_string(),
},
querystring: vec![],
headers: vec![],
cookies: vec![],
body: vec![],
multipart: vec![],
});
let v: serde_json::Value = serde_json::from_str(r#"{
"method": "GET",
"url": "http://localhost/custom",
"headers": [
{"name": "User-Agent", "value": "iPhone"},
{"name": "Foo", "value": "Bar"}
],
"cookies": [
{"name": "theme", "value": "light"},
{"name": "sessionToken", "value": "abc123"}
]
}"#).unwrap();
assert_eq!(parse_request(v).unwrap(), custom_http_request());
}
#[test]
fn test_parse_response() {
let v: serde_json::Value = serde_json::from_str(r#"{
"status": 200,
"httpVersion": "HTTP/1.0",
"headers": [
{"name": "Content-Type", "value": "text/html; charset=utf-8" },
{"name": "Content-Length", "value": "12" }
]
}"#).unwrap();
assert_eq!(parse_response(v).unwrap(), Response {
version: Version::Http10,
status: 200,
headers: vec![
Header { name: String::from("Content-Type"), value: String::from("text/html; charset=utf-8") },
Header { name: String::from("Content-Length"), value: String::from("12") },
],
body: vec![],
});
}
#[test]
fn test_parse_method() {
assert_eq!(parse_method("GET".to_string()).unwrap(), Method::Get);
let error = parse_method("x".to_string()).err().unwrap();
assert_eq!(error, "Invalid method <x>");
}
#[test]
fn test_parse_url() {
assert_eq!(
parse_url("http://localhost:8000/query?param1=value1".to_string()).unwrap(),
Url {
scheme: "http".to_string(),
host: "localhost".to_string(),
port: Some(8000),
path: "/query".to_string(),
query_string: "param1=value1".to_string(),
});
}
#[test]
fn test_parse_header() {
let v: serde_json::Value = serde_json::from_str(r#"{
"name": "name1",
"value": "value1"
}"#).unwrap();
assert_eq!(parse_header(v).unwrap(), Header { name: "name1".to_string(), value: "value1".to_string() });
}
#[test]
fn test_parse_response_cookie() {
let v: serde_json::Value = serde_json::from_str(r#"{
"name": "name1",
"value": "value1"
}"#).unwrap();
assert_eq!(parse_response_cookie(v).unwrap(),
ResponseCookie {
name: "name1".to_string(),
value: "value1".to_string(),
max_age: None,
domain: None,
path: None,
secure: None,
http_only: None,
expires: None,
same_site: None,
}
);
}
#[test]
fn test_parse_version() {
assert_eq!(parse_version("HTTP/1.0".to_string()).unwrap(), Version::Http10);
}
}

View File

@ -22,6 +22,8 @@ use curl::easy;
use super::core::*; use super::core::*;
use std::io::Read; use std::io::Read;
use encoding::{Encoding, DecoderTrap};
use encoding::all::ISO_8859_1;
#[derive(Debug)] #[derive(Debug)]
@ -34,20 +36,23 @@ pub struct Client {
pub follow_location: bool, pub follow_location: bool,
pub redirect_count: usize, pub redirect_count: usize,
pub max_redirect: Option<usize>, pub max_redirect: Option<usize>,
pub verbose: bool,
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct ClientOptions { pub struct ClientOptions {
pub follow_location: bool, pub follow_location: bool,
pub max_redirect: Option<usize>, pub max_redirect: Option<usize>,
pub cookie_file: Option<String>, pub cookie_input_file: Option<String>,
pub cookie_jar: Option<String>,
pub proxy: Option<String>, pub proxy: Option<String>,
pub no_proxy: Option<String>,
pub verbose: bool, pub verbose: bool,
pub insecure: bool,
} }
impl Client { impl Client {
/// ///
/// Init HTTP hurl client /// Init HTTP hurl client
/// ///
@ -56,27 +61,34 @@ impl Client {
// Activate cookie storage // Activate cookie storage
// with or without persistence (empty string) // with or without persistence (empty string)
h.cookie_file(options.cookie_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(cookie_jar) = options.cookie_jar {
h.cookie_jar(cookie_jar.as_str()).unwrap();
}
if let Some(proxy) = options.proxy { if let Some(proxy) = options.proxy {
h.proxy(proxy.as_str()).unwrap(); h.proxy(proxy.as_str()).unwrap();
} }
if let Some(s) = options.no_proxy {
h.noproxy(s.as_str()).unwrap();
}
h.verbose(options.verbose).unwrap(); h.verbose(options.verbose).unwrap();
h.ssl_verify_host(options.insecure).unwrap();
h.ssl_verify_peer(options.insecure).unwrap();
Client { Client {
handle: Box::new(h), handle: Box::new(h),
follow_location: options.follow_location, follow_location: options.follow_location,
max_redirect: options.max_redirect, max_redirect: options.max_redirect,
redirect_count: 0, redirect_count: 0,
verbose: options.verbose,
} }
} }
///
/// reset HTTP hurl client
///
pub fn reset(&mut self) {
self.handle.reset();
self.handle.verbose(self.verbose).unwrap();
}
/// ///
/// Execute an http request /// Execute an http request
@ -93,7 +105,8 @@ impl Client {
let b = request.body.clone(); let b = request.body.clone();
let mut data: &[u8] = b.as_ref(); let mut data: &[u8] = b.as_ref();
self.set_body(data); self.set_body(data);
self.set_headers(&request.headers, data.is_empty()); self.set_headers(&request);
self.handle.debug_function(|info_type, data| self.handle.debug_function(|info_type, data|
match info_type { match info_type {
@ -106,7 +119,9 @@ impl Client {
} }
} }
easy::InfoType::HeaderIn => { easy::InfoType::HeaderIn => {
eprint!("< {}", str::from_utf8(data).unwrap()); if let Some(s) = decode_header(data) {
eprint!("< {}", s);
}
} }
_ => {} _ => {}
} }
@ -123,9 +138,8 @@ impl Client {
} }
transfer.header_function(|h| { transfer.header_function(|h| {
match str::from_utf8(h) { if let Some(s) = decode_header(h) {
Ok(s) => lines.push(s.to_string()), lines.push(s)
Err(e) => println!("Error decoding header {:?}", e),
} }
true true
}).unwrap(); }).unwrap();
@ -140,6 +154,7 @@ impl Client {
5 => return Err(HttpError::CouldNotResolveProxyName), 5 => return Err(HttpError::CouldNotResolveProxyName),
6 => return Err(HttpError::CouldNotResolveHost), 6 => return Err(HttpError::CouldNotResolveHost),
7 => return Err(HttpError::FailToConnect), 7 => return Err(HttpError::FailToConnect),
60 => return Err(HttpError::SSLCertificate),
_ => panic!("{:#?}", e), _ => panic!("{:#?}", e),
} }
} }
@ -161,6 +176,7 @@ impl Client {
multipart: vec![], multipart: vec![],
cookies: vec![], cookies: vec![],
body: vec![], body: vec![],
content_type: None,
}; };
let redirect_count = redirect_count + 1; let redirect_count = redirect_count + 1;
@ -173,6 +189,7 @@ impl Client {
return self.execute(&request, redirect_count); return self.execute(&request, redirect_count);
} }
self.redirect_count = redirect_count; self.redirect_count = redirect_count;
self.reset();
Ok(Response { Ok(Response {
version, version,
@ -191,6 +208,8 @@ impl Client {
} else { } else {
let url = if url.ends_with('?') { let url = if url.ends_with('?') {
url.to_string() url.to_string()
} else if url.contains('?') {
format!("{}&", url)
} else { } else {
format!("{}?", url) format!("{}?", url)
}; };
@ -218,32 +237,44 @@ impl Client {
} }
} }
/// ///
/// set request headers /// set request headers
/// ///
fn set_headers(&mut self, headers: &[Header], default_content_type: bool) { fn set_headers(&mut self, request: &Request) {
let mut list = easy::List::new(); let mut list = easy::List::new();
for header in headers.to_owned() { 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(headers.to_vec(), "Content-Type".to_string()).is_empty() && !default_content_type { if get_header_values(request.headers.clone(), "Content-Type".to_string()).is_empty() {
list.append("Content-Type:").unwrap(); if let Some(s) = request.content_type.clone() {
list.append(format!("Content-Type: {}", s).as_str()).unwrap();
} else {
list.append("Content-Type:").unwrap(); // remove header Content-Type
}
} }
list.append(format!("User-Agent: hurl/{}", clap::crate_version!()).as_str()).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();
}
self.handle.http_headers(list).unwrap(); self.handle.http_headers(list).unwrap();
} }
/// ///
/// set request cookies /// set request cookies
/// ///
fn set_cookies(&mut self, cookies: &[RequestCookie]) { fn set_cookies(&mut self, _cookies: &[RequestCookie]) {
for cookie in cookies { //for cookie in cookies {
self.handle.cookie(cookie.to_string().as_str()).unwrap(); //self.handle.cookie(cookie.to_string().as_str()).unwrap(); // added in the store beforehand
} //}
} }
@ -393,6 +424,17 @@ impl Client {
} }
cookies cookies
} }
///
/// Add cookie to Cookiejar
/// TODO check domain/path,...
pub fn add_cookie(&mut self, cookie: Cookie) {
if self.verbose {
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();
}
} }
@ -438,6 +480,25 @@ fn split_lines(data: &[u8]) -> Vec<String> {
} }
///
/// Decode optionally header value as text with utf8 or iso-8859-1 encoding
///
pub fn decode_header(data: &[u8]) -> Option<String> {
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
}
}
}
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;

View File

@ -28,8 +28,28 @@ pub struct Request {
pub multipart: Vec<MultipartParam>, pub multipart: Vec<MultipartParam>,
pub cookies: Vec<RequestCookie>, pub cookies: Vec<RequestCookie>,
pub body: Vec<u8>, pub body: Vec<u8>,
pub content_type: Option<String>,
} }
//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<String> {
// 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)] #[derive(Clone, Debug, PartialEq, Eq)]
pub struct Response { pub struct Response {
pub version: Version, pub version: Version,
@ -76,24 +96,56 @@ pub enum Version {
Http2, 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)] #[derive(Clone, Debug, PartialEq, Eq)]
pub struct Header { pub struct Header {
pub name: String, pub name: String,
pub value: 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)] #[derive(Clone, Debug, PartialEq, Eq)]
pub struct Param { pub struct Param {
pub name: String, pub name: String,
pub value: 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)] #[derive(Clone, Debug, PartialEq, Eq)]
pub enum MultipartParam { pub enum MultipartParam {
Param(Param), Param(Param),
FileParam(FileParam), 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)] #[derive(Clone, Debug, PartialEq, Eq)]
pub struct FileParam { pub struct FileParam {
pub name: String, pub name: String,
@ -102,6 +154,12 @@ pub struct FileParam {
pub content_type: String, 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)] #[derive(Clone, Debug, PartialEq, Eq)]
pub struct RequestCookie { pub struct RequestCookie {
@ -120,6 +178,12 @@ pub struct Cookie {
pub value: 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 { impl fmt::Display for RequestCookie {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
@ -135,6 +199,7 @@ pub enum HttpError {
FailToConnect, FailToConnect,
TooManyRedirect, TooManyRedirect,
CouldNotParseResponse, CouldNotParseResponse,
SSLCertificate,
} }
@ -162,6 +227,7 @@ pub fn get_header_values(headers: Vec<Header>, expected_name: String) -> Vec<Str
pub mod tests { pub mod tests {
use super::*; use super::*;
#[test] #[test]
fn get_header_values() { fn get_header_values() {
let response = Response { let response = Response {
@ -186,6 +252,7 @@ pub mod tests {
body: vec![], body: vec![],
multipart: vec![], multipart: vec![],
form: vec![], form: vec![],
content_type: None,
} }
} }
@ -211,6 +278,25 @@ pub mod tests {
body: vec![], body: vec![],
multipart: vec![], multipart: vec![],
form: vec![], form: vec![],
content_type: None,
}
}
// GET http://localhost:8000/querystring-params?param1=value1&param2
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,
} }
} }
@ -226,7 +312,112 @@ pub mod tests {
cookies: vec![], cookies: vec![],
body: "param1=value1&param2=&param3=a%3db&param4=a%253db".to_string().into_bytes(), body: "param1=value1&param2=&param3=a%3db&param4=a%253db".to_string().into_bytes(),
multipart: vec![], multipart: vec![],
form: 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("<html><head><meta charset=\"UTF-8\"></head><body><br></body></html>")),
}
}
pub fn xml_invalid_response() -> Response {
Response {
version: Version::Http10,
status: 200,
headers: vec![
Header { name: String::from("Content-Type"), value: String::from("text/html; charset=utf-8") },
Header { name: String::from("Content-Length"), value: String::from("12") },
],
body: String::into_bytes(r#"
xxx
"#.to_string()),
}
}
pub fn xml_two_users_http_response() -> Response {
Response {
version: Version::Http10,
status: 200,
headers: vec![
Header { name: String::from("Content-Type"), value: String::from("text/html; charset=utf-8") },
Header { name: String::from("Content-Length"), value: String::from("12") },
],
body: String::into_bytes(r#"
<?xml version="1.0"?>
<users>
<user id="1">Bob</user>
<user id="2">Bill</user>
</users>
"#.to_string()),
}
}
pub fn xml_three_users_http_response() -> Response {
Response {
version: Version::Http10,
status: 200,
headers: vec![
Header { name: String::from("Content-Type"), value: String::from("text/html; charset=utf-8") },
Header { name: String::from("Content-Length"), value: String::from("12") },
],
body: String::into_bytes(r#"
<?xml version="1.0"?>
<users>
<user id="1">Bob</user>
<user id="2">Bill</user>
<user id="3">Bruce</user>
</users>
"#.to_string()),
}
}
pub fn json_http_response() -> Response {
Response {
version: Version::Http10,
status: 0,
headers: vec![],
body: String::into_bytes(r#"
{
"success":false,
"errors": [
{ "id": "error1"},
{"id": "error2"}
],
"duration": 1.5
}
"#.to_string()),
}
}
pub fn bytes_http_response() -> Response {
Response {
version: Version::Http10,
status: 200,
headers: vec![
Header { name: String::from("Content-Type"), value: String::from("application/octet-stream") },
Header { name: String::from("Content-Length"), value: String::from("1") },
],
body: vec![255],
} }
} }
} }

View File

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

View File

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

View File

@ -1,268 +0,0 @@
/*
* hurl (https://hurl.dev)
* Copyright (C) 2020 Orange
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
use std::fmt;
use super::cookie::*;
use super::core::*;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Response {
pub version: Version,
pub status: u16,
pub headers: Vec<Header>,
pub body: Vec<u8>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Version {
Http10,
Http11,
Http2,
}
impl fmt::Display for Version {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let s = match self {
Version::Http10 => "1.0",
Version::Http11 => "1.1",
Version::Http2 => "2",
};
write!(f, "{}", s)
}
}
impl Response {
pub fn get_header(&self, name: &str, case_sensitive: bool) -> Vec<String> {
let mut values = vec![];
for header in self.headers.clone() {
if header.name == name
|| !case_sensitive && header.name.to_lowercase() == name.to_lowercase()
{
values.push(header.value);
}
}
values
}
pub fn get_cookie(&self, name: &str) -> Option<ResponseCookie> {
for cookie in self.cookies() {
if cookie.name.as_str() == name
{
return Some(cookie);
}
}
None
}
pub fn cookies(&self) -> Vec<ResponseCookie> {
let mut cookies = vec![];
for Header { name, value } in self.clone().headers {
if name.to_lowercase() == "set-cookie" {
let c = cookie::Cookie::parse(value.as_str()).unwrap();
// eprintln!(">>> parse set-cookie header");
// eprintln!(">>> c = {:?}", c);
//
// let fields = value.split(";").collect::<Vec<&str>>();
// let name_value = fields.get(0).unwrap().split("=").collect::<Vec<&str>>();
// let name = name_value.get(0).unwrap().to_string();
// let value = name_value.get(1).unwrap().to_string();
let name = c.name().to_string();
let value = c.value().to_string();
let max_age = match c.max_age() {
None => None,
Some(d) => Some(d.num_seconds())
};
let expires = match c.expires() {
None => None,
Some(time) => Some(time.rfc822().to_string())
};
let domain = match c.domain() {
None => None,
Some(v) => Some(v.to_string())
};
let path = match c.path() {
None => None,
Some(v) => Some(v.to_string())
};
let secure = if let Some(value) = c.secure() {
Some(value)
} else {
None
};
let http_only = if let Some(value) = c.http_only() {
Some(value)
} else {
None
};
let same_site = match c.same_site() {
None => None,
Some(v) => Some(v.to_string())
};
cookies.push(ResponseCookie { name, value, max_age, expires, domain, path, secure, http_only, same_site });
}
}
cookies
}
}
impl Response {
pub fn content_type(&self) -> Option<String> {
let values = self.get_header("content-type", true);
if let Some(value) = values.first() {
Some(value.clone())
} else {
None
}
}
pub fn encoding(&self) -> Encoding {
if let Some(value) = self.content_type() {
if value.contains("charset=ISO-8859-1") {
return Encoding::Latin1;
}
}
Encoding::Utf8
}
pub fn has_utf8_body(&self) -> bool {
if let Some(value) = self.content_type() {
value.contains("charset=utf-8")
} else {
false
}
}
pub fn is_html(&self) -> bool {
if let Some(value) = self.content_type() {
value.contains("html")
} else {
false
}
}
}
#[cfg(test)]
pub mod tests {
use super::*;
pub fn hello_http_response() -> Response {
Response {
version: Version::Http10,
status: 200,
headers: vec![
Header { name: String::from("Content-Type"), value: String::from("text/html; charset=utf-8") },
Header { name: String::from("Content-Length"), value: String::from("12") },
],
body: String::into_bytes(String::from("Hello World!")),
}
}
pub fn html_http_response() -> Response {
Response {
version: Version::Http10,
status: 200,
headers: vec![
Header { name: String::from("Content-Type"), value: String::from("text/html; charset=utf-8") },
],
body: String::into_bytes(String::from("<html><head><meta charset=\"UTF-8\"></head><body><br></body></html>")),
}
}
pub fn xml_invalid_response() -> Response {
Response {
version: Version::Http10,
status: 200,
headers: vec![
Header { name: String::from("Content-Type"), value: String::from("text/html; charset=utf-8") },
Header { name: String::from("Content-Length"), value: String::from("12") },
],
body: String::into_bytes(r#"
xxx
"#.to_string()),
}
}
pub fn xml_two_users_http_response() -> Response {
Response {
version: Version::Http10,
status: 200,
headers: vec![
Header { name: String::from("Content-Type"), value: String::from("text/html; charset=utf-8") },
Header { name: String::from("Content-Length"), value: String::from("12") },
],
body: String::into_bytes(r#"
<?xml version="1.0"?>
<users>
<user id="1">Bob</user>
<user id="2">Bill</user>
</users>
"#.to_string()),
}
}
pub fn xml_three_users_http_response() -> Response {
Response {
version: Version::Http10,
status: 200,
headers: vec![
Header { name: String::from("Content-Type"), value: String::from("text/html; charset=utf-8") },
Header { name: String::from("Content-Length"), value: String::from("12") },
],
body: String::into_bytes(r#"
<?xml version="1.0"?>
<users>
<user id="1">Bob</user>
<user id="2">Bill</user>
<user id="3">Bruce</user>
</users>
"#.to_string()),
}
}
pub fn json_http_response() -> Response {
Response {
version: Version::Http10,
status: 0,
headers: vec![],
body: String::into_bytes(r#"
{
"success":false,
"errors": [
{ "id": "error1"},
{"id": "error2"}
],
"duration": 1.5
}
"#.to_string()),
}
}
pub fn bytes_http_response() -> Response {
Response {
version: Version::Http10,
status: 200,
headers: vec![
Header { name: String::from("Content-Type"), value: String::from("application/octet-stream") },
Header { name: String::from("Content-Length"), value: String::from("1") },
],
body: vec![255],
}
}
}

View File

@ -19,7 +19,7 @@
use std::collections::HashMap; use std::collections::HashMap;
use crate::core::common::Value; use crate::core::common::Value;
use crate::http; use crate::http::libcurl;
use super::core::{Error, RunnerError}; use super::core::{Error, RunnerError};
use super::core::*; use super::core::*;
@ -107,7 +107,7 @@ impl AssertResult {
} }
impl Assert { impl Assert {
pub fn eval(self, http_response: http::response::Response, variables: &HashMap<String, Value>) -> AssertResult { pub fn eval(self, http_response: libcurl::core::Response, variables: &HashMap<String, Value>) -> AssertResult {
let actual = self.query.eval(variables, http_response); let actual = self.query.eval(variables, http_response);
let source_info = self.predicate.clone().predicate_func.source_info; let source_info = self.predicate.clone().predicate_func.source_info;
let predicate_result = match actual.clone() { let predicate_result = match actual.clone() {
@ -162,7 +162,7 @@ pub mod tests {
fn test_eval() { fn test_eval() {
let variables = HashMap::new(); let variables = HashMap::new();
assert_eq!( assert_eq!(
assert_count_user().eval(http::response::tests::xml_three_users_http_response(), &variables), assert_count_user().eval(libcurl::core::tests::xml_three_users_http_response(), &variables),
AssertResult::Explicit { AssertResult::Explicit {
actual: Ok(Some(Value::Nodeset(3))), actual: Ok(Some(Value::Nodeset(3))),
source_info: SourceInfo::init(1, 14, 1, 27), source_info: SourceInfo::init(1, 14, 1, 27),

View File

@ -20,14 +20,15 @@ use std::collections::HashMap;
use regex::Regex; use regex::Regex;
use crate::core::common::Value; use crate::core::common::Value;
use crate::http; use crate::http::libcurl;
use super::core::{CaptureResult, Error}; use super::core::{CaptureResult, Error};
use super::core::RunnerError; use super::core::RunnerError;
use super::super::core::ast::*; use super::super::core::ast::*;
impl Capture { impl Capture {
pub fn eval(self, variables: &HashMap<String, Value>, http_response: http::response::Response) -> Result<CaptureResult, Error> {
pub fn eval(self, variables: &HashMap<String, Value>, http_response: libcurl::core::Response) -> Result<CaptureResult, Error> {
let name = self.name.value; let name = self.name.value;
let value = self.query.clone().eval(variables, http_response)?; let value = self.query.clone().eval(variables, http_response)?;
let value = match value { let value = match value {
@ -183,7 +184,7 @@ pub mod tests {
}, },
}; };
let error = capture.eval(&variables, http::response::tests::xml_three_users_http_response()).err().unwrap(); let error = capture.eval(&variables, libcurl::core::tests::xml_three_users_http_response()).err().unwrap();
assert_eq!(error.source_info.start, Pos { line: 1, column: 7 }); assert_eq!(error.source_info.start, Pos { line: 1, column: 7 });
assert_eq!(error.inner, RunnerError::QueryInvalidXpathEval) assert_eq!(error.inner, RunnerError::QueryInvalidXpathEval)
} }
@ -238,13 +239,13 @@ pub mod tests {
#[test] #[test]
fn test_capture() { fn test_capture() {
let variables = HashMap::new(); let variables = HashMap::new();
assert_eq!(user_count_capture().eval(&variables, http::response::tests::xml_three_users_http_response()).unwrap(), assert_eq!(user_count_capture().eval(&variables, libcurl::core::tests::xml_three_users_http_response()).unwrap(),
CaptureResult { CaptureResult {
name: "UserCount".to_string(), name: "UserCount".to_string(),
value: Value::from_f64(3.0), value: Value::from_f64(3.0),
}); });
assert_eq!(duration_capture().eval(&variables, http::response::tests::json_http_response()).unwrap(), assert_eq!(duration_capture().eval(&variables, libcurl::core::tests::json_http_response()).unwrap(),
CaptureResult { CaptureResult {
name: "duration".to_string(), name: "duration".to_string(),
value: Value::from_f64(1.5), value: Value::from_f64(1.5),

View File

@ -99,7 +99,6 @@ impl ResponseCookie {
/// ///
pub fn max_age(&self) -> Option<i64> { pub fn max_age(&self) -> Option<i64> {
for attr in self.attributes.clone() { for attr in self.attributes.clone() {
eprintln!("{:#?}", attr);
if attr.name.as_str() == "Max-Age" { if attr.name.as_str() == "Max-Age" {
if let Some(v) = attr.value { if let Some(v) = attr.value {
if let Ok(v2) = v.as_str().parse::<i64>() { if let Ok(v2) = v.as_str().parse::<i64>() {

View File

@ -17,11 +17,9 @@
*/ */
use std::collections::HashMap; use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use crate::core::common::{FormatError, SourceInfo, Value}; use crate::core::common::{FormatError, SourceInfo, Value};
use crate::http; use crate::http::libcurl;
use crate::http::cookie::Cookie;
#[derive(Clone, Debug, PartialEq, Eq)] #[derive(Clone, Debug, PartialEq, Eq)]
pub struct RunnerOptions { pub struct RunnerOptions {
@ -31,15 +29,13 @@ pub struct RunnerOptions {
} }
//region resultlet noproxy_hosts = noproxy_host(matches.clone());
#[derive(Clone, Debug, PartialEq, Eq)] #[derive(Clone, Debug, PartialEq, Eq)]
pub struct HurlResult { pub struct HurlResult {
pub filename: String, pub filename: String,
pub entries: Vec<EntryResult>, pub entries: Vec<EntryResult>,
pub time_in_ms: u128, pub time_in_ms: u128,
pub success: bool, pub success: bool,
pub cookies: Vec<Cookie>, pub cookies: Vec<libcurl::core::Cookie>,
} }
impl HurlResult { impl HurlResult {
@ -54,8 +50,8 @@ impl HurlResult {
#[derive(Clone, Debug, PartialEq, Eq)] #[derive(Clone, Debug, PartialEq, Eq)]
pub struct EntryResult { pub struct EntryResult {
pub request: Option<http::request::Request>, pub request: Option<libcurl::core::Request>,
pub response: Option<http::response::Response>, pub response: Option<libcurl::core::Response>,
//pub captures: Vec<(String, Value)>, //pub captures: Vec<(String, Value)>,
pub captures: Vec<CaptureResult>, pub captures: Vec<CaptureResult>,
pub asserts: Vec<AssertResult>, pub asserts: Vec<AssertResult>,
@ -85,25 +81,20 @@ pub type PredicateResult = Result<(), Error>;
// region error // region error
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[derive(Clone, Debug, PartialEq, Eq)]
pub struct Error { pub struct Error {
pub source_info: SourceInfo, pub source_info: SourceInfo,
pub inner: RunnerError, pub inner: RunnerError,
pub assert: bool, pub assert: bool,
} }
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[derive(Clone, Debug, PartialEq, Eq)]
pub enum RunnerError { pub enum RunnerError {
TemplateVariableNotDefined { name: String }, TemplateVariableNotDefined { name: String },
VariableNotDefined { name: String }, VariableNotDefined { name: String },
InvalidURL(String), InvalidURL(String),
HttpConnection { url: String, message: String }, HttpConnection { url: String, message: String },
FileReadAccess { value: String }, FileReadAccess { value: String },
// Capture
//CaptureNonScalarUnsupported,
//??CaptureError {},
InvalidUtf8,
InvalidDecoding { charset: String }, InvalidDecoding { charset: String },
InvalidCharset { charset: String }, InvalidCharset { charset: String },
@ -157,7 +148,6 @@ impl FormatError for Error {
RunnerError::AssertVersion { .. } => "Assert Http Version".to_string(), RunnerError::AssertVersion { .. } => "Assert Http Version".to_string(),
RunnerError::AssertStatus { .. } => "Assert Status".to_string(), RunnerError::AssertStatus { .. } => "Assert Status".to_string(),
RunnerError::QueryInvalidJson { .. } => "Invalid Json".to_string(), RunnerError::QueryInvalidJson { .. } => "Invalid Json".to_string(),
RunnerError::InvalidUtf8 { .. } => "Invalid Utf8".to_string(),
RunnerError::QueryInvalidJsonpathExpression { .. } => "Invalid jsonpath".to_string(), RunnerError::QueryInvalidJsonpathExpression { .. } => "Invalid jsonpath".to_string(),
RunnerError::PredicateType { .. } => "Assert - Inconsistent predicate type".to_string(), RunnerError::PredicateType { .. } => "Assert - Inconsistent predicate type".to_string(),
RunnerError::SubqueryInvalidInput { .. } => "Subquery error".to_string(), RunnerError::SubqueryInvalidInput { .. } => "Subquery error".to_string(),
@ -174,7 +164,7 @@ impl FormatError for Error {
match &self.inner { match &self.inner {
RunnerError::InvalidURL(url) => format!("Invalid url <{}>", url), RunnerError::InvalidURL(url) => format!("Invalid url <{}>", url),
RunnerError::TemplateVariableNotDefined { name } => format!("You must set the variable {}", name), RunnerError::TemplateVariableNotDefined { name } => format!("You must set the variable {}", name),
RunnerError::HttpConnection { url, message } => format!("can not connect to {} ({})", url, message), RunnerError::HttpConnection { url, .. } => format!("can not connect to {}", url),
RunnerError::AssertVersion { actual, .. } => format!("actual value is <{}>", actual), RunnerError::AssertVersion { actual, .. } => format!("actual value is <{}>", actual),
RunnerError::AssertStatus { actual, .. } => format!("actual value is <{}>", actual), RunnerError::AssertStatus { actual, .. } => format!("actual value is <{}>", actual),
RunnerError::PredicateValue(value) => format!("actual value is <{}>", value.to_string()), RunnerError::PredicateValue(value) => format!("actual value is <{}>", value.to_string()),
@ -187,7 +177,6 @@ impl FormatError for Error {
RunnerError::AssertHeaderValueError { actual } => format!("actual value is <{}>", actual), RunnerError::AssertHeaderValueError { actual } => format!("actual value is <{}>", actual),
RunnerError::AssertBodyValueError { actual, .. } => format!("actual value is <{}>", actual), RunnerError::AssertBodyValueError { actual, .. } => format!("actual value is <{}>", actual),
RunnerError::QueryInvalidJson { .. } => "The http response is not a valid json".to_string(), RunnerError::QueryInvalidJson { .. } => "The http response is not a valid json".to_string(),
RunnerError::InvalidUtf8 { .. } => "The http response is not a valid utf8 string".to_string(),
RunnerError::QueryInvalidJsonpathExpression { value } => format!("the jsonpath expression '{}' is not valid", value), RunnerError::QueryInvalidJsonpathExpression { value } => format!("the jsonpath expression '{}' is not valid", value),
RunnerError::PredicateType { .. } => "predicate type inconsistent with value return by query".to_string(), RunnerError::PredicateType { .. } => "predicate type inconsistent with value return by query".to_string(),
RunnerError::SubqueryInvalidInput => "Type from query result and subquery do not match".to_string(), RunnerError::SubqueryInvalidInput => "Type from query result and subquery do not match".to_string(),

View File

@ -18,21 +18,18 @@
use std::collections::HashMap; use std::collections::HashMap;
use std::time::Instant; use std::time::Instant;
use encoding::{DecoderTrap, Encoding};
use encoding::all::ISO_8859_1;
use crate::core::ast::*; use crate::core::ast::*;
use crate::core::common::SourceInfo; use crate::core::common::SourceInfo;
use crate::core::common::Value; use crate::core::common::Value;
use crate::http; use crate::http::libcurl;
use crate::http::cookie::CookieJar;
use super::core::*; use super::core::*;
use super::core::{Error, RunnerError}; use super::core::{Error, RunnerError};
use crate::format::logger::Logger; use crate::format::logger::Logger;
/// Run an entry with the hurl http client /// Run an entry with the hurl http client
/// ///
/// # Examples /// # Examples
@ -42,26 +39,23 @@ use crate::format::logger::Logger;
/// use hurl::runner; /// use hurl::runner;
/// ///
/// // Create an http client /// // Create an http client
/// let client = http::client::Client::init(http::client::ClientOptions { //// let client = http::client::Client::init(http::client::ClientOptions {
/// noproxy_hosts: vec![], //// noproxy_hosts: vec![],
/// insecure: false, //// insecure: false,
/// redirect: http::client::Redirect::None, //// redirect: http::client::Redirect::None,
/// http_proxy: None, //// http_proxy: None,
/// https_proxy: None, //// https_proxy: None,
/// all_proxy: None //// all_proxy: None
/// }); //// });
/// ``` /// ```
pub fn run(entry: Entry, http_client: &http::client::Client, pub fn run(entry: Entry,
http_client: &mut libcurl::client::Client,
entry_index: usize, entry_index: usize,
variables: &mut HashMap<String, Value>, variables: &mut HashMap<String, Value>,
cookiejar: &mut CookieJar,
context_dir: String, context_dir: String,
logger: &Logger, logger: &Logger,
) -> EntryResult { ) -> EntryResult {
let http_request = match entry.clone().request.eval(variables, context_dir.clone()) {
//let mut entry_log_builder = EntryLogBuilder::init();
let mut http_request = match entry.clone().request.eval(variables, context_dir.clone()) {
Ok(r) => r, Ok(r) => r,
Err(error) => { Err(error) => {
return EntryResult { return EntryResult {
@ -75,27 +69,44 @@ pub fn run(entry: Entry, http_client: &http::client::Client,
}; };
} }
}; };
let cookies = cookiejar.clone().get_cookies(http_request.clone().host(), String::from("/"));
http_request.add_session_cookies(cookies);
logger.verbose("------------------------------------------------------------------------------"); logger.verbose("------------------------------------------------------------------------------");
logger.verbose(format!("executing entry {}", entry_index + 1).as_str()); logger.verbose(format!("executing entry {}", entry_index + 1).as_str());
for line in http_request.verbose_output() {
logger.send(line); // Temporary - add cookie from request to the cookie store
} // should be set explicitly
if let Some(params) = http_request.clone().form_params() { // url should be valid at the point
logger.verbose("Form Params"); // do not use cookie from request
for param in params { use url::Url;
logger.verbose(format!(" {}={}", param.name, param.value).as_str()); if let Ok(url) = Url::parse(http_request.url.as_str()) {
for c in http_request.cookies.clone() {
let cookie = libcurl::core::Cookie {
domain: url.host_str().unwrap().to_string(),
include_subdomain: "FALSE".to_string(),
path: "/".to_string(),
https: "FALSE".to_string(),
expires: "0".to_string(),
name: c.name,
value: c.value,
};
http_client.add_cookie(cookie);
} }
}; }
logger.verbose("");
logger.verbose("Cookie store:");
for cookie in http_client.get_cookie_storage() {
logger.verbose(cookie.to_string().as_str());
}
logger.verbose("");
log_request(logger, &http_request);
let start = Instant::now(); let start = Instant::now();
let http_response = match http_client.execute(&http_request) { let http_response = match http_client.execute(&http_request, 0) {
Ok(response) => response, Ok(response) => response,
Err(e) => { Err(_) => {
return EntryResult { return EntryResult {
request: Some(http_request), request: Some(http_request.clone()),
response: None, response: None,
captures: vec![], captures: vec![],
asserts: vec![], asserts: vec![],
@ -106,8 +117,8 @@ pub fn run(entry: Entry, http_client: &http::client::Client,
end: entry.clone().request.url.source_info.end, end: entry.clone().request.url.source_info.end,
}, },
inner: RunnerError::HttpConnection { inner: RunnerError::HttpConnection {
message: e.message, message: "".to_string(),
url: e.url, url: http_request.url,
}, },
assert: false, assert: false,
}], }],
@ -115,15 +126,10 @@ pub fn run(entry: Entry, http_client: &http::client::Client,
}; };
} }
}; };
let time_in_ms = start.elapsed().as_millis(); let time_in_ms = start.elapsed().as_millis();
for line in http_response.verbose_output() { logger.verbose(format!("Response Time: {}ms", time_in_ms).as_str());
logger.receive(line);
}
//entry_log_builder.response(http_response.clone(), verbose);
//hurl_log.entries.push(log_builder.build());
let captures = match entry.response.clone() { let captures = match entry.response.clone() {
None => vec![], None => vec![],
Some(response) => match response.eval_captures(http_response.clone(), variables) { Some(response) => match response.eval_captures(http_response.clone(), variables) {
@ -146,57 +152,25 @@ pub fn run(entry: Entry, http_client: &http::client::Client,
variables.insert(capture_result.name, capture_result.value); variables.insert(capture_result.name, capture_result.value);
} }
let asserts = match entry.response { let asserts = match entry.response {
None => vec![], None => vec![],
Some(response) => response.eval_asserts(variables, http_response.clone(), context_dir) Some(response) => response.eval_asserts(variables, http_response.clone(), context_dir)
}; };
let errors = asserts let errors = asserts
.iter() .iter()
.filter_map(|assert| assert.clone().error()) .filter_map(|assert| assert.clone().error())
.map(|Error { source_info, inner, .. }| Error { source_info, inner, assert: true }) .map(|Error { source_info, inner, .. }| Error { source_info, inner, assert: true })
.collect(); .collect();
// update cookies
// for the domain
let domain = http_request.clone().host();
//let mut cookies = cookie_store.get_cookies(host);
// TEMPORARY also update store from request cookie
// TODO - DO BE REMOVED - add explicit directive in hurl file to interract with cookiejar
for cookie in http_request.clone().cookies {
cookiejar.update_cookies(
domain.clone(),
http_request.clone().url.path,
cookie,
);
}
for cookie in http_response.cookies() {
cookiejar.update_cookies(
domain.clone(),
http_request.clone().url.path,
cookie,
);
}
if !captures.is_empty() { if !captures.is_empty() {
logger.verbose("Captures"); logger.verbose("Captures");
for capture in captures.clone() { for capture in captures.clone() {
logger.verbose(format!("{}: {:?}", capture.name, capture.value).as_str()); logger.verbose(format!("{}: {}", capture.name, capture.value).as_str());
}
}
if !cookiejar.clone().cookies().is_empty() {
logger.verbose("CookieJar");
for cookie in cookiejar.clone().cookies() {
logger.verbose(cookie.to_string().as_str());
} }
} }
logger.verbose("");
EntryResult { EntryResult {
request: Some(http_request), request: Some(http_request),
response: Some(http_response), response: Some(http_response),
@ -208,76 +182,39 @@ pub fn run(entry: Entry, http_client: &http::client::Client,
} }
pub fn log_request(logger: &Logger, request: &libcurl::core::Request) {
logger.verbose("Request");
// cookies logger.verbose(format!("{} {}", request.method, request.url).as_str());
// for all domains for header in request.headers.clone() {
logger.verbose(header.to_string().as_str());
// but only pass cookies for one domain for the request
fn decode_bytes(bytes: Vec<u8>, encoding: http::core::Encoding) -> Result<String, RunnerError> {
match encoding {
http::core::Encoding::Utf8 {} => match String::from_utf8(bytes) {
Ok(s) => Ok(s),
Err(_) => Err(RunnerError::InvalidDecoding { charset: encoding.to_string() }),
},
http::core::Encoding::Latin1 {} => match ISO_8859_1.decode(&bytes, DecoderTrap::Strict) {
Ok(s) => Ok(s),
Err(_) => Err(RunnerError::InvalidDecoding { charset: encoding.to_string() }),
},
} }
} if !request.querystring.is_empty() {
logger.verbose("[QueryStringParams]");
for param in request.querystring.clone() {
impl http::request::Request { logger.verbose(param.to_string().as_str());
pub fn verbose_output(&self) -> Vec<String> {
let mut lines = vec![];
lines.push(format!("{} {}", self.clone().method.to_text(), self.clone().url()));
for header in self.clone().headers() {
lines.push(header.to_string());
} }
lines.push("".to_string());
if !self.body.is_empty() {
let body = match decode_bytes(self.body.clone(), self.encoding()) {
Ok(s) => s,
Err(_) => format!("{:x?}", self.body)
};
let body_lines: Vec<&str> = regex::Regex::new(r"\n|\r\n")
.unwrap()
.split(&body)
.collect();
for line in body_lines {
lines.push(line.to_string());
}
}
lines
} }
} if !request.form.is_empty() {
logger.verbose("[FormParams]");
impl http::response::Response { for param in request.form.clone() {
pub fn verbose_output(&self) -> Vec<String> { logger.verbose(param.to_string().as_str());
let mut lines = vec![];
lines.push(format!("HTTP/{} {}", self.version.to_text(), self.status));
for header in self.clone().headers {
lines.push(header.to_string());
} }
lines.push("".to_string());
if !self.body.is_empty() {
//let body = body_text(self.clone().body, get_header_value(self.clone().headers, "content-type"));
//let body = substring(body.as_str(), 0, limit_body);
let body = match decode_bytes(self.body.clone(), self.encoding()) {
Ok(s) => s,
Err(_) => format!("{:x?}", self.body)
};
let body_lines: Vec<&str> = regex::Regex::new(r"\n|\r\n")
.unwrap()
.split(&body)
.collect();
for line in body_lines {
lines.push(line.to_string());
}
}
lines
} }
} if !request.multipart.is_empty() {
logger.verbose("[MultipartFormData]");
for param in request.multipart.clone() {
logger.verbose(param.to_string().as_str());
}
}
if !request.cookies.is_empty() {
logger.verbose("[Cookies]");
for cookie in request.cookies.clone() {
logger.verbose(cookie.to_string().as_str());
}
}
if let Some(s) = request.content_type.clone() {
logger.verbose("");
logger.verbose(format!("implicit content-type={}", s).as_str());
}
logger.verbose("");
}

View File

@ -20,7 +20,8 @@ use std::time::Instant;
use crate::core::ast::*; use crate::core::ast::*;
use crate::core::common::Value; use crate::core::common::Value;
use crate::http; use crate::http::libcurl;
use super::core::*; use super::core::*;
use super::super::format; use super::super::format;
@ -34,7 +35,7 @@ use crate::core::common::FormatError;
/// # Example /// # Example
/// ///
/// ``` /// ```
/// use hurl::http; /// use hurl::http::libcurl;
/// use hurl::runner; /// use hurl::runner;
/// use hurl::format; /// use hurl::format;
/// ///
@ -47,15 +48,16 @@ use crate::core::common::FormatError;
/// let hurl_file = hurl::parser::parse_hurl_file(s).unwrap(); /// let hurl_file = hurl::parser::parse_hurl_file(s).unwrap();
/// ///
/// // Create an http client /// // Create an http client
/// let mut cookie_store = http::cookie::CookieJar::init(vec![]); /// let options = libcurl::client::ClientOptions {
/// let client = http::client::Client::init(http::client::ClientOptions { /// follow_location: false,
/// noproxy_hosts: vec![], /// max_redirect: None,
/// cookie_input_file: None,
/// proxy: None,
/// no_proxy: None,
/// verbose: false,
/// insecure: false, /// insecure: false,
/// redirect: http::client::Redirect::None, /// };
/// http_proxy: None, /// let mut client = libcurl::client::Client::init(options);
/// https_proxy: None,
/// all_proxy: None
/// });
/// ///
/// // Define runner options /// // Define runner options
/// let variables = std::collections::HashMap::new(); /// let variables = std::collections::HashMap::new();
@ -79,9 +81,8 @@ use crate::core::common::FormatError;
/// let context_dir = "current_dir".to_string(); /// let context_dir = "current_dir".to_string();
/// let hurl_results = runner::file::run( /// let hurl_results = runner::file::run(
/// hurl_file, /// hurl_file,
/// client, /// &mut client,
/// filename, /// filename,
/// &mut cookie_store,
/// context_dir, /// context_dir,
/// options, /// options,
/// logger /// logger
@ -91,9 +92,8 @@ use crate::core::common::FormatError;
/// ``` /// ```
pub fn run( pub fn run(
hurl_file: HurlFile, hurl_file: HurlFile,
http_client: http::client::Client, http_client: &mut libcurl::client::Client,
filename: String, filename: String,
cookiejar: &mut http::cookie::CookieJar,
context_dir: String, context_dir: String,
options: RunnerOptions, options: RunnerOptions,
logger: format::logger::Logger, logger: format::logger::Logger,
@ -113,7 +113,7 @@ pub fn run(
let start = Instant::now(); let start = Instant::now();
for (entry_index, entry) in hurl_file.entries.iter().take(n).cloned().enumerate().collect::<Vec<(usize, Entry)>>() { for (entry_index, entry) in hurl_file.entries.iter().take(n).cloned().enumerate().collect::<Vec<(usize, Entry)>>() {
let entry_result = entry::run(entry, &http_client, entry_index, &mut variables, cookiejar, context_dir.clone(), &logger); let entry_result = entry::run(entry, http_client, entry_index, &mut variables, context_dir.clone(), &logger);
entries.push(entry_result.clone()); entries.push(entry_result.clone());
for e in entry_result.errors.clone() { for e in entry_result.errors.clone() {
let error = format::error::Error { let error = format::error::Error {
@ -135,7 +135,7 @@ pub fn run(
let time_in_ms = start.elapsed().as_millis(); let time_in_ms = start.elapsed().as_millis();
let success = entries.iter().flat_map(|e| e.errors.clone()).next().is_none(); let success = entries.iter().flat_map(|e| e.errors.clone()).next().is_none();
let cookies = cookiejar.clone().cookies(); let cookies = http_client.get_cookie_storage();
HurlResult { HurlResult {
filename, filename,
entries, entries,

View File

@ -17,7 +17,6 @@
*/ */
use crate::http;
use crate::http::libcurl::core::*; use crate::http::libcurl::core::*;
use super::cookie::*; use super::cookie::*;
@ -77,14 +76,14 @@ fn parse_entry_result(value: serde_json::Value) -> Result<EntryResult, String> {
let request = match value.get("request") { let request = match value.get("request") {
None => None, None => None,
Some(v) => { Some(v) => {
let r = http::import::parse_request(v.clone())?; let r = parse_request(v.clone())?;
Some(r) Some(r)
} }
}; };
let response = match value.get("response") { let response = match value.get("response") {
None => None, None => None,
Some(v) => { Some(v) => {
let r = http::import::parse_response(v.clone())?; let r = parse_response(v.clone())?;
Some(r) Some(r)
} }
}; };
@ -162,6 +161,7 @@ pub fn parse_request(value: serde_json::Value) -> Result<Request, ParseError> {
// TODO // TODO
let multipart = vec![]; let multipart = vec![];
let body = vec![]; let body = vec![];
let content_type = None;
Ok(Request { Ok(Request {
method, method,
@ -172,6 +172,7 @@ pub fn parse_request(value: serde_json::Value) -> Result<Request, ParseError> {
body, body,
multipart, multipart,
form, form,
content_type,
}) })
} else { } else {
Err("expecting an object for the request".to_string()) Err("expecting an object for the request".to_string())
@ -376,6 +377,7 @@ mod tests {
body: vec![], body: vec![],
form: vec![], form: vec![],
multipart: vec![], multipart: vec![],
content_type: None,
}); });

View File

@ -64,8 +64,8 @@ impl Serialize for AssertResult {
S: Serializer, S: Serializer,
{ {
let mut state = serializer.serialize_struct("??", 3)?; let mut state = serializer.serialize_struct("??", 3)?;
if let AssertResult::Version { source_info, actual, expected } = self { if let AssertResult::Version { actual, expected,.. } = self {
state.serialize_field("source_info", source_info)?; //state.serialize_field("source_info", source_info)?;
state.serialize_field("actual", actual)?; state.serialize_field("actual", actual)?;
state.serialize_field("expected", expected)?; state.serialize_field("expected", expected)?;
}; };
@ -123,16 +123,6 @@ impl Serialize for Response {
state.serialize_field("headers", &self.clone().headers)?; state.serialize_field("headers", &self.clone().headers)?;
// TODO Serialize body // TODO Serialize body
// let content_type = self.get_header("content_type", true);
// if let Some(value) = content_type.first() {
// if value.as_str() == "application/json; charset=UTF-8" {
// let s = String::from_utf8(self.body.clone()).expect("Found invalid UTF-8");
// let result: Result<serde_json::Value, serde_json::Error> = serde_json::from_str(s.as_str());
// if let Ok(v) = result {
// state.serialize_field("json", &v)?;
// }
// }
// }
state.end() state.end()
} }
} }
@ -173,22 +163,6 @@ impl Serialize for RequestCookie {
} }
} }
//impl Serialize for Cookie {
// fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
// where
// S: Serializer,
// {
// let mut state = serializer.serialize_struct("InternalCookie", 3)?;
// state.serialize_field("name", &self.clone().name)?;
// state.serialize_field("value", &self.clone().value)?;
// state.serialize_field("domain", &self.clone().domain)?;
// state.serialize_field("path", &self.clone().path)?;
// state.serialize_field("include_subdomain", &self.clone().subdomains)?;
// state.end()
// }
//}
impl Serialize for Version { impl Serialize for Version {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where where
@ -235,3 +209,20 @@ impl Serialize for ResponseCookie {
} }
} }
impl Serialize for Cookie {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut state = serializer.serialize_struct("Cookie", 3)?;
state.serialize_field("domain", &self.clone().domain)?;
state.serialize_field("include_subdomain", &self.clone().include_subdomain)?;
state.serialize_field("path", &self.clone().path)?;
state.serialize_field("https", &self.clone().https)?;
state.serialize_field("expires", &self.clone().expires)?;
state.serialize_field("name", &self.clone().name)?;
state.serialize_field("value", &self.clone().value)?;
state.end()
}
}

View File

@ -34,12 +34,12 @@ mod http_response;
mod json; mod json;
pub mod log_serialize; pub mod log_serialize;
pub mod log_deserialize; pub mod log_deserialize;
mod multipart;
mod predicate; mod predicate;
mod query; mod query;
pub mod request; pub mod request;
mod response; mod response;
mod template; mod template;
mod text;
mod xpath; mod xpath;
mod expr; mod expr;

223
src/runner/multipart.rs Normal file
View File

@ -0,0 +1,223 @@
/*
* hurl (https://hurl.dev)
* Copyright (C) 2020 Orange
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
extern crate libxml;
use std::collections::HashMap;
use std::ffi::OsStr;
use std::fs::File;
#[allow(unused)]
use std::io::prelude::*;
use std::io::Read;
use std::path::Path;
use crate::core::ast::*;
use crate::core::common::Value;
use crate::http::libcurl;
use super::core::{Error, RunnerError};
impl MultipartParam {
pub fn eval(self,
variables: &HashMap<String, Value>,
context_dir: String,
) -> Result<libcurl::core::MultipartParam, Error> {
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 }))
}
MultipartParam::FileParam(param) => {
let file_param = param.eval(context_dir)?;
Ok(libcurl::core::MultipartParam::FileParam(file_param))
}
}
}
}
impl FileParam {
pub fn eval(self, context_dir: String) -> Result<libcurl::core::FileParam, Error> {
let name = self.key.value;
let filename = self.value.filename.clone();
let path = Path::new(filename.value.as_str());
let absolute_filename = if path.is_absolute() {
filename.value.clone()
} else {
Path::new(context_dir.as_str()).join(filename.value.clone()).to_str().unwrap().to_string()
};
let data = match File::open(absolute_filename.clone()) {
Ok(mut f) => {
let mut bytes = Vec::new();
match f.read_to_end(&mut bytes) {
Ok(_) => bytes,
Err(_) => return Err(Error {
source_info: filename.source_info,
inner: RunnerError::FileReadAccess { value: absolute_filename },
assert: false,
})
}
}
Err(_) => return Err(Error {
source_info: filename.source_info,
inner: RunnerError::FileReadAccess { value: absolute_filename },
assert: false,
})
};
if !Path::new(&absolute_filename).exists() {
return Err(Error {
source_info: filename.source_info,
inner: RunnerError::FileReadAccess { value: filename.value.clone() },
assert: false,
});
}
let content_type = self.value.content_type();
Ok(libcurl::core::FileParam {
name,
filename: filename.value,
data,
content_type,
})
}
}
impl FileValue {
pub fn content_type(&self) -> String {
match self.content_type.clone() {
None => match Path::new(self.filename.value.as_str()).extension().and_then(OsStr::to_str) {
Some("gif") => "image/gif".to_string(),
Some("jpg") => "image/jpeg".to_string(),
Some("jpeg") => "image/jpeg".to_string(),
Some("png") => "image/png".to_string(),
Some("svg") => "image/svg+xml".to_string(),
Some("txt") => "text/plain".to_string(),
Some("htm") => "text/html".to_string(),
Some("html") => "text/html".to_string(),
Some("pdf") => "application/pdf".to_string(),
Some("xml") => "application/xml".to_string(),
_ => "application/octet-stream".to_string(),
}
Some(content_type) => content_type
}
}
}
#[cfg(test)]
mod tests {
use crate::core::common::SourceInfo;
use super::*;
pub fn whitespace() -> Whitespace {
Whitespace {
value: String::from(" "),
source_info: SourceInfo::init(0, 0, 0, 0),
}
}
#[test]
pub fn test_eval_file_param() {
let line_terminator = LineTerminator {
space0: whitespace(),
comment: None,
newline: whitespace(),
};
assert_eq!(FileParam {
line_terminators: vec![],
space0: whitespace(),
key: EncodedString {
value: "upload1".to_string(),
encoded: "upload1".to_string(),
quotes: false,
source_info: SourceInfo::init(0, 0, 0, 0),
},
space1: whitespace(),
space2: whitespace(),
value: FileValue {
space0: whitespace(),
filename: Filename { value: "hello.txt".to_string(), source_info: SourceInfo::init(0, 0, 0, 0) },
space1: whitespace(),
space2: whitespace(),
content_type: None,
},
line_terminator0: line_terminator,
}.eval("integration/tests".to_string()).unwrap(),
libcurl::core::FileParam {
name: "upload1".to_string(),
filename: "hello.txt".to_string(),
data: b"Hello World!".to_vec(),
content_type: "text/plain".to_string(),
});
}
#[test]
pub fn test_file_value_content_type() {
assert_eq!(FileValue {
space0: whitespace(),
filename: Filename {
value: "hello.txt".to_string(),
source_info: SourceInfo::init(0, 0, 0, 0),
},
space1: whitespace(),
space2: whitespace(),
content_type: None,
}.content_type(), "text/plain".to_string());
assert_eq!(FileValue {
space0: whitespace(),
filename: Filename {
value: "hello.html".to_string(),
source_info: SourceInfo::init(0, 0, 0, 0),
},
space1: whitespace(),
space2: whitespace(),
content_type: None,
}.content_type(), "text/html".to_string());
assert_eq!(FileValue {
space0: whitespace(),
filename: Filename {
value: "hello.txt".to_string(),
source_info: SourceInfo::init(0, 0, 0, 0),
},
space1: whitespace(),
space2: whitespace(),
content_type: Some("text/html".to_string()),
}.content_type(), "text/html".to_string());
assert_eq!(FileValue {
space0: whitespace(),
filename: Filename {
value: "hello".to_string(),
source_info: SourceInfo::init(0, 0, 0, 0),
},
space1: whitespace(),
space2: whitespace(),
content_type: None,
}.content_type(), "application/octet-stream".to_string());
}
}

View File

@ -15,108 +15,51 @@
* limitations under the License. * limitations under the License.
* *
*/ */
use std::collections::HashMap; use std::collections::HashMap;
use encoding::{DecoderTrap, Encoding};
use encoding::all::ISO_8859_1;
use regex::Regex; use regex::Regex;
#[cfg(test)]
use crate::core::common::{Pos, SourceInfo};
use crate::core::common::Value; use crate::core::common::Value;
//use crate::core::jsonpath; use crate::http::libcurl;
use crate::http;
use crate::http::cookie::ResponseCookie;
use crate::jsonpath; use crate::jsonpath;
use super::cookie;
use super::core::{Error, RunnerError}; use super::core::{Error, RunnerError};
//use super::http;
use super::super::core::ast::*; use super::super::core::ast::*;
use super::xpath; use super::xpath;
// QueryResult
// success => just the value is kept
// error => thrown within asserts / logged within Captures
// => hurl report => do you need more information than just the value?
// each assert returned by an entry
// contains a queryLog which contains a query result
// json
// 1.0 and 1 are different?
// 1.01 and 1.010 different on ast => but same on value
// value not found
// Option<Value>
// source info for the input?
// value not found/ does not exist => add error
// depends on the query type
// jsonpath will return an empty list? no match to be clarified
// header return really nothing
// check that header does not exist?
// only for assert specific not found error to work with predicate exist
// => does not make sense for capture
// implicit assert
// status qnd body never fails!
// semantic not in the type system
// in the test methods availibility
// error source_info for invalid response content
// source info => all the query!! not just the expression! => should come before invalid expression
// error: Invalid XML response
// --> test.hurl:10:2
// 10 | xpath //person countEquals 10
// | ^^^^^^^^^^^^^^
// error: Invalid XML response
// --> test.hurl:10:7
// 10 | xpath //person countEquals 10
// | ^^^^^^^^
// also use for implicit assert => query header => distinguihed between the 2 header quqry (implicit vs explicit)
// error: Header not Found
// --> test.hurl:10:2
// 10 | Custom: XXX
// | ^^^^^^
// an enum is always a value?
pub type QueryResult = Result<Option<Value>, Error>; pub type QueryResult = Result<Option<Value>, Error>;
impl http::response::Response {
// utf8 except if content-type include another charset impl libcurl::core::Response {
pub fn text(&self) -> Result<String, RunnerError> { pub fn is_html(&self) -> bool {
if let Some(v) = self.content_type() { for header in self.headers.clone() {
if v.contains("charset=ISO-8859-1") { if header.name.to_lowercase() == "content-type" {
match ISO_8859_1.decode(&self.body, DecoderTrap::Strict) { return header.value.contains("html");
Ok(s) => return Ok(s),
Err(_) => return Err(RunnerError::InvalidDecoding { charset: "iso8859-1".to_string() }),
}
} }
} }
match String::from_utf8(self.body.clone()) { false
Ok(s) => Ok(s), }
Err(_) => Err(RunnerError::InvalidUtf8 {}),
pub fn get_cookie(&self, name: String) -> Option<cookie::ResponseCookie> {
for cookie in self.cookies() {
if cookie.name == name {
return Some(cookie);
}
} }
None
} }
} }
impl Query { impl Query {
pub fn eval(self, variables: &HashMap<String, Value>, http_response: http::response::Response) -> QueryResult { pub fn eval(self, variables: &HashMap<String, Value>, http_response: libcurl::core::Response) -> QueryResult {
match self.value { match self.value {
QueryValue::Status {} => Ok(Some(Value::Integer(i64::from(http_response.status)))), QueryValue::Status {} => Ok(Some(Value::Integer(i64::from(http_response.status)))),
QueryValue::Header { name, .. } => { QueryValue::Header { name, .. } => {
let header_name = name.eval(variables)?; let header_name = name.eval(variables)?;
let values = http_response.get_header(header_name.as_str(), false); let values = http_response.get_header(header_name);
if values.is_empty() { if values.is_empty() {
Ok(None) Ok(None)
} else if values.len() == 1 { } else if values.len() == 1 {
@ -129,7 +72,7 @@ impl Query {
} }
QueryValue::Cookie { expr: CookiePath { name, attribute }, .. } => { QueryValue::Cookie { expr: CookiePath { name, attribute }, .. } => {
let cookie_name = name.eval(variables)?; let cookie_name = name.eval(variables)?;
match http_response.get_cookie(cookie_name.as_str()) { match http_response.get_cookie(cookie_name) {
None => Ok(None), None => Ok(None),
Some(cookie) => { Some(cookie) => {
let attribute_name = if let Some(attribute) = attribute { let attribute_name = if let Some(attribute) = attribute {
@ -263,16 +206,16 @@ impl Query {
} }
impl CookieAttributeName { impl CookieAttributeName {
pub fn eval(self, cookie: ResponseCookie) -> Option<Value> { pub fn eval(self, cookie: cookie::ResponseCookie) -> Option<Value> {
match self { match self {
CookieAttributeName::Value(_) => Some(Value::String(cookie.value)), CookieAttributeName::Value(_) => Some(Value::String(cookie.value)),
CookieAttributeName::Expires(_) => cookie.expires.map(Value::String), CookieAttributeName::Expires(_) => cookie.expires().map(Value::String),
CookieAttributeName::MaxAge(_) => cookie.max_age.map(Value::Integer), CookieAttributeName::MaxAge(_) => cookie.max_age().map(Value::Integer),
CookieAttributeName::Domain(_) => cookie.domain.map(Value::String), CookieAttributeName::Domain(_) => cookie.domain().map(Value::String),
CookieAttributeName::Path(_) => cookie.path.map(Value::String), CookieAttributeName::Path(_) => cookie.path().map(Value::String),
CookieAttributeName::Secure(_) => cookie.secure.map(Value::Bool), CookieAttributeName::Secure(_) => if cookie.has_secure() { Some(Value::Bool(true)) } else { None },
CookieAttributeName::HttpOnly(_) => cookie.http_only.map(Value::Bool), CookieAttributeName::HttpOnly(_) => if cookie.has_httponly() { Some(Value::Bool(true)) } else { None },
CookieAttributeName::SameSite(_) => cookie.same_site.map(Value::String), CookieAttributeName::SameSite(_) => cookie.samesite().map(Value::String),
} }
} }
} }
@ -280,7 +223,7 @@ impl CookieAttributeName {
#[cfg(test)] #[cfg(test)]
pub mod tests { pub mod tests {
use crate::http::cookie::ResponseCookie; use crate::core::common::{Pos, SourceInfo};
use super::*; use super::*;
@ -354,9 +297,9 @@ pub mod tests {
} }
} }
pub fn json_http_response() -> http::response::Response { pub fn json_http_response() -> libcurl::core::Response {
http::response::Response { libcurl::core::Response {
version: http::response::Version::Http10, version: libcurl::core::Version::Http10,
status: 0, status: 0,
headers: vec![], headers: vec![],
body: String::into_bytes(r#" body: String::into_bytes(r#"
@ -493,7 +436,7 @@ pub mod tests {
fn test_query_status() { fn test_query_status() {
let variables = HashMap::new(); let variables = HashMap::new();
assert_eq!( assert_eq!(
Query { source_info: SourceInfo::init(0, 0, 0, 0), value: QueryValue::Status {} }.eval(&variables, http::response::tests::hello_http_response()).unwrap().unwrap(), Query { source_info: SourceInfo::init(0, 0, 0, 0), value: QueryValue::Status {} }.eval(&variables, libcurl::core::tests::hello_http_response()).unwrap().unwrap(),
Value::Integer(200) Value::Integer(200)
); );
} }
@ -521,7 +464,7 @@ pub mod tests {
// let error = query_header.eval(http::hello_http_response()).err().unwrap(); // 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.source_info.start, Pos { line: 1, column: 8 });
// assert_eq!(error.inner, RunnerError::QueryHeaderNotFound); // assert_eq!(error.inner, RunnerError::QueryHeaderNotFound);
assert_eq!(query_header.eval(&variables, http::response::tests::hello_http_response()).unwrap(), None); assert_eq!(query_header.eval(&variables, libcurl::core::tests::hello_http_response()).unwrap(), None);
} }
#[test] #[test]
@ -545,7 +488,7 @@ pub mod tests {
}, },
}; };
assert_eq!( assert_eq!(
query_header.eval(&variables, http::response::tests::hello_http_response()).unwrap().unwrap(), query_header.eval(&variables, libcurl::core::tests::hello_http_response()).unwrap().unwrap(),
Value::String(String::from("text/html; charset=utf-8")) Value::String(String::from("text/html; charset=utf-8"))
); );
} }
@ -557,11 +500,11 @@ pub mod tests {
value: String::from(""), value: String::from(""),
source_info: SourceInfo::init(0, 0, 0, 0), source_info: SourceInfo::init(0, 0, 0, 0),
}; };
let response = http::response::Response { let response = libcurl::core::Response {
version: http::response::Version::Http10, version: libcurl::core::Version::Http10,
status: 0, status: 0,
headers: vec![ headers: vec![
http::core::Header { libcurl::core::Header {
name: "Set-Cookie".to_string(), name: "Set-Cookie".to_string(),
value: "LSID=DQAAAKEaem_vYg; Path=/accounts; Expires=Wed, 13 Jan 2021 22:23:01 GMT; Secure; HttpOnly".to_string(), value: "LSID=DQAAAKEaem_vYg; Path=/accounts; Expires=Wed, 13 Jan 2021 22:23:01 GMT; Secure; HttpOnly".to_string(),
} }
@ -667,18 +610,27 @@ pub mod tests {
#[test] #[test]
fn test_eval_cookie_attribute_name() { fn test_eval_cookie_attribute_name() {
let cookie = cookie::ResponseCookie {
// "LSID=DQAAAKEaem_vYg; Path=/accounts; Expires=Wed, 13 Jan 2021 22:23:01 GMT; Secure; HttpOnly"
let cookie = ResponseCookie {
name: "LSID".to_string(), name: "LSID".to_string(),
value: "DQAAAKEaem_vYg".to_string(), value: "DQAAAKEaem_vYg".to_string(),
domain: None, attributes: vec![
path: Some("/accounts".to_string()), cookie::CookieAttribute {
max_age: None, name: "Path".to_string(),
expires: Some("Wed, 13 Jan 2021 22:23:01 GMT".to_string()), value: Some("/accounts".to_string()),
secure: Some(true), },
http_only: Some(true), cookie::CookieAttribute {
same_site: None, name: "Expires".to_string(),
value: Some("Wed, 13 Jan 2021 22:23:01 GMT".to_string()),
},
cookie::CookieAttribute {
name: "Secure".to_string(),
value: None,
},
cookie::CookieAttribute {
name: "HttpOnly".to_string(),
value: None,
}
],
}; };
assert_eq!(CookieAttributeName::Value("_".to_string()).eval(cookie.clone()).unwrap(), Value::String("DQAAAKEaem_vYg".to_string())); assert_eq!(CookieAttributeName::Value("_".to_string()).eval(cookie.clone()).unwrap(), Value::String("DQAAAKEaem_vYg".to_string()));
assert_eq!(CookieAttributeName::Domain("_".to_string()).eval(cookie.clone()), None); assert_eq!(CookieAttributeName::Domain("_".to_string()).eval(cookie.clone()), None);
@ -687,7 +639,7 @@ pub mod tests {
assert_eq!(CookieAttributeName::Expires("_".to_string()).eval(cookie.clone()).unwrap(), Value::String("Wed, 13 Jan 2021 22:23:01 GMT".to_string())); assert_eq!(CookieAttributeName::Expires("_".to_string()).eval(cookie.clone()).unwrap(), Value::String("Wed, 13 Jan 2021 22:23:01 GMT".to_string()));
assert_eq!(CookieAttributeName::Secure("_".to_string()).eval(cookie.clone()).unwrap(), Value::Bool(true)); assert_eq!(CookieAttributeName::Secure("_".to_string()).eval(cookie.clone()).unwrap(), Value::Bool(true));
assert_eq!(CookieAttributeName::HttpOnly("_".to_string()).eval(cookie.clone()).unwrap(), Value::Bool(true)); assert_eq!(CookieAttributeName::HttpOnly("_".to_string()).eval(cookie.clone()).unwrap(), Value::Bool(true));
assert_eq!(CookieAttributeName::SameSite("_".to_string()).eval(cookie.clone()), None); assert_eq!(CookieAttributeName::SameSite("_".to_string()).eval(cookie), None);
} }
#[test] #[test]
@ -697,29 +649,29 @@ pub mod tests {
Query { Query {
source_info: SourceInfo::init(0, 0, 0, 0), source_info: SourceInfo::init(0, 0, 0, 0),
value: QueryValue::Body {}, value: QueryValue::Body {},
}.eval(&variables, http::response::tests::hello_http_response()).unwrap().unwrap(), }.eval(&variables, libcurl::core::tests::hello_http_response()).unwrap().unwrap(),
Value::String(String::from("Hello World!")) Value::String(String::from("Hello World!"))
); );
let error = Query { let error = Query {
source_info: SourceInfo::init(1, 1, 1, 2), source_info: SourceInfo::init(1, 1, 1, 2),
value: QueryValue::Body {}, value: QueryValue::Body {},
}.eval(&variables, http::response::tests::bytes_http_response()).err().unwrap(); }.eval(&variables, libcurl::core::tests::bytes_http_response()).err().unwrap();
assert_eq!(error.source_info, SourceInfo::init(1, 1, 1, 2)); assert_eq!(error.source_info, SourceInfo::init(1, 1, 1, 2));
assert_eq!(error.inner, RunnerError::InvalidUtf8 {}); assert_eq!(error.inner, RunnerError::InvalidDecoding { charset: "utf-8".to_string()});
} }
#[test] #[test]
fn test_query_invalid_utf8() { fn test_query_invalid_utf8() {
let variables = HashMap::new(); let variables = HashMap::new();
let http_response = http::response::Response { let http_response = libcurl::core::Response {
version: http::response::Version::Http10, version: libcurl::core::Version::Http10,
status: 0, status: 0,
headers: vec![], headers: vec![],
body: vec![200], body: vec![200],
}; };
let error = xpath_users().eval(&variables, http_response).err().unwrap(); let error = xpath_users().eval(&variables, http_response).err().unwrap();
assert_eq!(error.source_info.start, Pos { line: 1, column: 1 }); assert_eq!(error.source_info.start, Pos { line: 1, column: 1 });
assert_eq!(error.inner, RunnerError::InvalidUtf8); assert_eq!(error.inner, RunnerError::InvalidDecoding { charset: "utf-8".to_string() });
} }
#[test] #[test]
@ -745,7 +697,7 @@ pub mod tests {
}, },
}, },
}; };
let error = query.eval(&variables, http::response::tests::xml_two_users_http_response()).err().unwrap(); let error = query.eval(&variables, libcurl::core::tests::xml_two_users_http_response()).err().unwrap();
assert_eq!(error.inner, RunnerError::QueryInvalidXpathEval); assert_eq!(error.inner, RunnerError::QueryInvalidXpathEval);
assert_eq!(error.source_info.start, Pos { line: 1, column: 7 }); assert_eq!(error.source_info.start, Pos { line: 1, column: 7 });
} }
@ -754,8 +706,8 @@ pub mod tests {
fn test_query_xpath() { fn test_query_xpath() {
let variables = HashMap::new(); let variables = HashMap::new();
assert_eq!(xpath_users().eval(&variables, http::response::tests::xml_two_users_http_response()).unwrap().unwrap(), Value::Nodeset(2)); 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, http::response::tests::xml_two_users_http_response()).unwrap().unwrap(), Value::Float(2, 0)); assert_eq!(xpath_count_user_query().eval(&variables, libcurl::core::tests::xml_two_users_http_response()).unwrap().unwrap(), Value::Float(2, 0));
} }
#[cfg(test)] #[cfg(test)]
@ -786,7 +738,7 @@ pub mod tests {
#[test] #[test]
fn test_query_xpath_with_html() { fn test_query_xpath_with_html() {
let variables = HashMap::new(); let variables = HashMap::new();
assert_eq!(xpath_html_charset().eval(&variables, http::response::tests::html_http_response()).unwrap().unwrap(), Value::String(String::from("UTF-8"))); assert_eq!(xpath_html_charset().eval(&variables, libcurl::core::tests::html_http_response()).unwrap().unwrap(), Value::String(String::from("UTF-8")));
} }
#[test] #[test]
@ -823,8 +775,8 @@ pub mod tests {
#[test] #[test]
fn test_query_invalid_json() { fn test_query_invalid_json() {
let variables = HashMap::new(); let variables = HashMap::new();
let http_response = http::response::Response { let http_response = libcurl::core::Response {
version: http::response::Version::Http10, version: libcurl::core::Version::Http10,
status: 0, status: 0,
headers: vec![], headers: vec![],
body: String::into_bytes(String::from("xxx")), body: String::into_bytes(String::from("xxx")),
@ -837,8 +789,8 @@ pub mod tests {
#[test] #[test]
fn test_query_json_not_found() { fn test_query_json_not_found() {
let variables = HashMap::new(); let variables = HashMap::new();
let http_response = http::response::Response { let http_response = libcurl::core::Response {
version: http::response::Version::Http10, version: libcurl::core::Version::Http10,
status: 0, status: 0,
headers: vec![], headers: vec![],
body: String::into_bytes(String::from("{}")), body: String::into_bytes(String::from("{}")),
@ -867,11 +819,11 @@ pub mod tests {
fn test_query_regex() { fn test_query_regex() {
let variables = HashMap::new(); let variables = HashMap::new();
assert_eq!( assert_eq!(
regex_name().eval(&variables, http::response::tests::hello_http_response()).unwrap().unwrap(), regex_name().eval(&variables, libcurl::core::tests::hello_http_response()).unwrap().unwrap(),
Value::String("World".to_string()) Value::String("World".to_string())
); );
let error = regex_invalid().eval(&variables, http::response::tests::hello_http_response()).err().unwrap(); let error = regex_invalid().eval(&variables, libcurl::core::tests::hello_http_response()).err().unwrap();
assert_eq!(error.source_info, SourceInfo::init(1, 7, 1, 10)); assert_eq!(error.source_info, SourceInfo::init(1, 7, 1, 10));
assert_eq!(error.inner, RunnerError::InvalidRegex()); assert_eq!(error.inner, RunnerError::InvalidRegex());
} }

View File

@ -20,158 +20,99 @@ extern crate serde_json;
extern crate url as external_url; extern crate url as external_url;
use std::collections::HashMap; use std::collections::HashMap;
use std::path::Path; #[allow(unused)]
use std::io::prelude::*;
use crate::core::ast::*; use crate::core::ast::*;
use crate::core::common::Value; use crate::core::common::Value;
use crate::http; use crate::http::libcurl;
use super::core::{Error, RunnerError}; use super::core::Error;
impl Request { impl Request {
pub fn eval(self, pub fn eval(self,
variables: &HashMap<String, Value>, variables: &HashMap<String, Value>,
context_dir: String, context_dir: String,
) -> Result<http::request::Request, Error> { ) -> Result<libcurl::core::Request, Error> {
let method = self.method.clone().eval(); let method = self.method.clone().eval();
let url = self.clone().url.eval(&variables)?;
let mut querystring: Vec<http::core::Param> = vec![];
// query string from url let url = self.clone().url.eval(&variables)?;
// parse url string
let (url, params) = match external_url::Url::parse(url.as_str()) {
Err(_) => {
return Err(Error {
source_info: self.clone().url.source_info,
inner: RunnerError::InvalidURL(url),
assert: false,
});
}
Ok(u) => {
let url = http::core::Url {
scheme: u.scheme().to_string(),
host: u.host_str().unwrap().to_string(),
port: u.port(),
path: u.path().to_string(),
query_string: if let Some(s) = u.query() { s.to_string() } else { "".to_string() },
};
let params: Vec<http::core::Param> = vec![];
(url, params)
}
};
for param in params {
querystring.push(param);
}
for param in self.clone().querystring_params() {
let name = param.key.value;
let value = param.value.eval(variables)?;
querystring.push(http::core::Param { name, value });
}
// headers // headers
let mut headers: Vec<http::core::Header> = vec![]; let mut headers: Vec<libcurl::core::Header> = vec![];
for header in self.clone().headers { for header in self.clone().headers {
let name = header.key.value; let name = header.key.value;
let value = header.value.eval(variables)?; let value = header.value.eval(variables)?;
headers.push(http::core::Header { headers.push(libcurl::core::Header {
name, name,
value, value,
}); });
} }
let mut querystring: Vec<libcurl::core::Param> = vec![];
// add cookies for param in self.clone().querystring_params() {
//let host = url.host.as_str(); let name = param.key.value;
let mut cookies = vec![]; let value = param.value.eval(variables)?;
// TODO cookie from header querystring.push(libcurl::core::Param { name, value });
for cookie in self.clone().cookies() {
let cookie = http::cookie::ResponseCookie {
name: cookie.clone().name.value,
value: cookie.clone().value.value,
max_age: None,
domain: Some(url.clone().host),
path: None,
secure: None,
http_only: None,
expires: None,
same_site: None,
};
//headers.push(cookie.to_header());
cookies.push(cookie);
} }
let mut form: Vec<libcurl::core::Param> = 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 });
}
// if !self.clone().form_params().is_empty() {
// headers.push(libcurl::core::Header {
// name: String::from("Content-Type"),
// value: String::from("application/x-www-form-urlencoded"),
// });
// }
if !self.clone().form_params().is_empty() { let mut cookies = vec![];
headers.push(http::core::Header { for cookie in self.clone().cookies() {
name: String::from("Content-Type"), let cookie = libcurl::core::RequestCookie {
value: String::from("application/x-www-form-urlencoded"), name: cookie.clone().name.value,
}); value: cookie.clone().value.value,
};
cookies.push(cookie);
} }
let bytes = match self.clone().body { let bytes = match self.clone().body {
Some(body) => body.eval(variables, context_dir.clone())?, Some(body) => body.eval(variables, context_dir.clone())?,
None => { None => vec![]
if !self.clone().form_params().is_empty() {
let mut params = vec![];
for param in self.clone().form_params() {
let name = param.key.value;
let value = param.value.eval(variables)?;
params.push(http::core::Param {
name,
value,
});
}
http::core::encode_form_params(params)
} else {
vec![]
}
}
}; };
let mut multipart = vec![]; let mut multipart = vec![];
for multipart_param in self.clone().multipart_form_data() { for multipart_param in self.clone().multipart_form_data() {
match multipart_param { let param = multipart_param.eval(variables, context_dir.clone())?;
MultipartParam::FileParam(FileParam { key, value: FileValue { filename, content_type, .. }, .. }) => { multipart.push(param);
let name = key.value;
let path = Path::new(filename.value.as_str());
let absolute_filename = if path.is_absolute() {
filename.clone().value
} else {
Path::new(context_dir.as_str()).join(filename.value.clone()).to_str().unwrap().to_string()
};
if !Path::new(&absolute_filename).exists() {
return Err(Error {
source_info: filename.source_info,
inner: RunnerError::FileReadAccess { value: filename.value.clone() },
assert: false,
});
}
multipart.push(http::request::MultipartParam::FileParam { name, filename: absolute_filename, content_type });
}
MultipartParam::Param(KeyValue { key, value, .. }) => {
let name = key.value;
let value = value.eval(variables)?;
multipart.push(http::request::MultipartParam::TextParam { name, value });
}
}
} }
let content_type = if !form.is_empty() {
Some("application/x-www-form-urlencoded".to_string())
} else if !multipart.is_empty() {
Some("multipart/form-data".to_string())
} else if let Some(Body { value:Bytes::Json {..}, ..}) = self.body {
Some("application/json".to_string())
} else {
None
};
// add implicit content type // add implicit content type
if self.content_type().is_none() { // if self.content_type().is_none() {
if let Some(body) = self.body { // if let Some(body) = self.body {
if let Bytes::Json { .. } = body.value { // if let Bytes::Json { .. } = body.value {
headers.push(http::core::Header { // headers.push(libcurl::core::Header {
name: String::from("Content-Type"), // name: String::from("Content-Type"),
value: String::from("application/json"), // value: String::from("application/json"),
}); // });
} // }
} // }
} // }
Ok(http::request::Request { Ok(libcurl::core::Request {
method, method,
url, url,
querystring, querystring,
@ -179,6 +120,8 @@ impl Request {
cookies, cookies,
body: bytes, body: bytes,
multipart, multipart,
form,
content_type
}) })
} }
@ -193,33 +136,33 @@ impl Request {
} }
impl Method { impl Method {
fn eval(self) -> http::request::Method { fn eval(self) -> libcurl::core::Method {
match self { match self {
Method::Get => http::request::Method::Get, Method::Get => libcurl::core::Method::Get,
Method::Head => http::request::Method::Head, Method::Head => libcurl::core::Method::Head,
Method::Post => http::request::Method::Post, Method::Post => libcurl::core::Method::Post,
Method::Put => http::request::Method::Put, Method::Put => libcurl::core::Method::Put,
Method::Delete => http::request::Method::Delete, Method::Delete => libcurl::core::Method::Delete,
Method::Connect => http::request::Method::Connect, Method::Connect => libcurl::core::Method::Connect,
Method::Options => http::request::Method::Options, Method::Options => libcurl::core::Method::Options,
Method::Trace => http::request::Method::Trace, Method::Trace => libcurl::core::Method::Trace,
Method::Patch => http::request::Method::Patch, Method::Patch => libcurl::core::Method::Patch,
} }
} }
} }
pub fn split_url(url: String) -> (String, Vec<http::core::Param>) { pub fn split_url(url: String) -> (String, Vec<libcurl::core::Param>) {
match url.find('?') { match url.find('?') {
None => (url, vec![]), None => (url, vec![]),
Some(index) => { Some(index) => {
let (url, params) = url.split_at(index); let (url, params) = url.split_at(index);
let params: Vec<http::core::Param> = params[1..].split('&') let params: Vec<libcurl::core::Param> = params[1..].split('&')
.map(|s| { .map(|s| {
match s.find('=') { match s.find('=') {
None => http::core::Param { name: s.to_string(), value: String::from("") }, None => libcurl::core::Param { name: s.to_string(), value: String::from("") },
Some(index) => { Some(index) => {
let (name, value) = s.split_at(index); let (name, value) = s.split_at(index);
http::core::Param { name: name.to_string(), value: value[1..].to_string() } libcurl::core::Param { name: name.to_string(), value: value[1..].to_string() }
} }
} }
}) })
@ -230,56 +173,41 @@ pub fn split_url(url: String) -> (String, Vec<http::core::Param>) {
} }
} }
pub fn eval_url(s: String) -> Result<http::core::Url, RunnerError> {
match url::Url::parse(s.as_str()) {
Err(_) => Err(RunnerError::InvalidURL(s)),
Ok(u) => Ok(http::core::Url {
scheme: u.scheme().to_string(),
host: u.host_str().unwrap().to_string(),
port: u.port(),
path: u.path().to_string(),
query_string: match u.query() {
None => "".to_string(),
Some(s) => s.to_string()
},
})
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::core::common::SourceInfo; use crate::core::common::SourceInfo;
use super::*; use super::*;
use super::super::core::RunnerError;
pub fn hello_request() -> Request { pub fn whitespace() -> Whitespace {
Whitespace {
// GET {{base_url}}/hello
let whitespace = Whitespace {
value: String::from(" "), value: String::from(" "),
source_info: SourceInfo::init(0, 0, 0, 0), source_info: SourceInfo::init(0, 0, 0, 0),
}; }
}
pub fn hello_request() -> Request {
let line_terminator = LineTerminator { let line_terminator = LineTerminator {
space0: whitespace.clone(), space0: whitespace(),
comment: None, comment: None,
newline: whitespace.clone(), newline: whitespace(),
}; };
Request { Request {
line_terminators: vec![], line_terminators: vec![],
space0: whitespace.clone(), space0: whitespace(),
method: Method::Get, method: Method::Get,
space1: whitespace.clone(), space1: whitespace(),
url: Template { url: Template {
elements: vec![ elements: vec![
TemplateElement::Expression(Expr { TemplateElement::Expression(Expr {
space0: whitespace.clone(), space0: whitespace(),
variable: Variable { variable: Variable {
name: String::from("base_url"), name: String::from("base_url"),
source_info: SourceInfo::init(1, 7, 1, 15), source_info: SourceInfo::init(1, 7, 1, 15),
}, },
space1: whitespace.clone(), space1: whitespace(),
}), }),
TemplateElement::String { TemplateElement::String {
value: String::from("/hello"), value: String::from("/hello"),
@ -298,41 +226,33 @@ mod tests {
} }
pub fn simple_key_value(key: EncodedString, value: Template) -> KeyValue { pub fn simple_key_value(key: EncodedString, value: Template) -> KeyValue {
let whitespace = Whitespace {
value: String::from(" "),
source_info: SourceInfo::init(0, 0, 0, 0),
};
let line_terminator = LineTerminator { let line_terminator = LineTerminator {
space0: whitespace.clone(), space0: whitespace(),
comment: None, comment: None,
newline: whitespace.clone(), newline: whitespace(),
}; };
KeyValue { KeyValue {
line_terminators: vec![], line_terminators: vec![],
space0: whitespace.clone(), space0: whitespace(),
key, key,
space1: whitespace.clone(), space1: whitespace(),
space2: whitespace.clone(), space2: whitespace(),
value, value,
line_terminator0: line_terminator.clone(), line_terminator0: line_terminator,
} }
} }
pub fn query_request() -> Request { pub fn query_request() -> Request {
let whitespace = Whitespace {
value: String::from(" "),
source_info: SourceInfo::init(0, 0, 0, 0),
};
let line_terminator = LineTerminator { let line_terminator = LineTerminator {
space0: whitespace.clone(), space0: whitespace(),
comment: None, comment: None,
newline: whitespace.clone(), newline: whitespace(),
}; };
Request { Request {
line_terminators: vec![], line_terminators: vec![],
space0: whitespace.clone(), space0: whitespace(),
method: Method::Get, method: Method::Get,
space1: whitespace.clone(), space1: whitespace(),
url: Template { url: Template {
elements: vec![ elements: vec![
TemplateElement::String { TemplateElement::String {
@ -348,8 +268,8 @@ mod tests {
sections: vec![ sections: vec![
Section { Section {
line_terminators: vec![], line_terminators: vec![],
space0: whitespace.clone(), space0: whitespace(),
line_terminator0: line_terminator.clone(), line_terminator0: line_terminator,
value: SectionValue::QueryParams(vec![ value: SectionValue::QueryParams(vec![
simple_key_value( simple_key_value(
EncodedString { EncodedString {
@ -362,12 +282,12 @@ mod tests {
quotes: false, quotes: false,
elements: vec![ elements: vec![
TemplateElement::Expression(Expr { TemplateElement::Expression(Expr {
space0: whitespace.clone(), space0: whitespace(),
variable: Variable { variable: Variable {
name: String::from("param1"), name: String::from("param1"),
source_info: SourceInfo::init(1, 7, 1, 15), source_info: SourceInfo::init(1, 7, 1, 15),
}, },
space1: whitespace.clone(), space1: whitespace(),
}) })
], ],
source_info: SourceInfo::init(0, 0, 0, 0), source_info: SourceInfo::init(0, 0, 0, 0),
@ -392,7 +312,6 @@ mod tests {
}, },
) )
]), ]),
source_info: SourceInfo::init(0, 0, 0, 0), source_info: SourceInfo::init(0, 0, 0, 0),
}, },
], ],
@ -412,40 +331,16 @@ mod tests {
#[test] #[test]
pub fn test_hello_request() { pub fn test_hello_request() {
let mut variables = HashMap::new(); let mut variables = HashMap::new();
// let cookies = HashMap::new();
variables.insert(String::from("base_url"), Value::String(String::from("http://localhost:8000"))); 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(); let http_request = hello_request().eval(&variables, "current_dir".to_string()).unwrap();
assert_eq!(http_request, http::request::tests::hello_http_request()); assert_eq!(http_request, libcurl::core::tests::hello_http_request());
} }
#[test] #[test]
pub fn test_query_request() { pub fn test_query_request() {
let mut variables = HashMap::new(); let mut variables = HashMap::new();
//let cookies = HashMap::new();
variables.insert(String::from("param1"), Value::String(String::from("value1"))); variables.insert(String::from("param1"), Value::String(String::from("value1")));
let http_request = query_request().eval(&variables, "current_dir".to_string()).unwrap(); let http_request = query_request().eval(&variables, "current_dir".to_string()).unwrap();
assert_eq!(http_request, http::request::tests::query_http_request()); assert_eq!(http_request, libcurl::core::tests::query_http_request());
}
#[test]
pub fn test_split_url() {
assert_eq!(
split_url(String::from("http://localhost:8000/hello")),
(String::from("http://localhost:8000/hello"), vec![])
);
assert_eq!(
split_url(String::from("http://localhost:8000/hello?param1=value1")),
(String::from("http://localhost:8000/hello"), vec![http::core::Param { name: String::from("param1"), value: String::from("value1") }])
);
}
#[test]
pub fn test_eval_url() {
assert_eq!(eval_url(String::from("xxx")).err().unwrap(), RunnerError::InvalidURL(String::from("xxx")));
let url = eval_url(String::from("http://localhost:8000/querystring-params?param1=value1")).unwrap();
assert_eq!(url.host, "localhost");
assert_eq!(url.port, Some(8000));
// assert_eq!(url.querystring.unwrap(), String::from("param1=value1"));
} }
} }

View File

@ -17,38 +17,33 @@
*/ */
use std::collections::HashMap; use std::collections::HashMap;
use encoding::{DecoderTrap, Encoding};
use encoding::all::ISO_8859_1;
use crate::core::common::{Pos, SourceInfo}; use crate::core::common::{Pos, SourceInfo};
use crate::core::common::Value; use crate::core::common::Value;
use crate::http; use crate::http::libcurl;
use crate::runner::core::RunnerError; use crate::runner::core::RunnerError;
use super::core::*; use super::core::*;
use super::core::Error; use super::core::Error;
use super::super::core::ast::*; use super::super::core::ast::*;
pub fn decode_bytes(bytes: Vec<u8>, encoding: http::core::Encoding) -> Result<String, RunnerError> { impl libcurl::core::Response {
match encoding { pub fn get_header(&self, name: String) -> Vec<String> {
http::core::Encoding::Utf8 {} => match String::from_utf8(bytes) { self.headers
Ok(s) => Ok(s), .iter()
Err(_) => Err(RunnerError::InvalidDecoding { charset: encoding.to_string() }), .filter(|&h| h.name.to_lowercase() == name.to_lowercase())
}, .map(|h| h.value.clone())
http::core::Encoding::Latin1 {} => match ISO_8859_1.decode(&bytes, DecoderTrap::Strict) { .collect()
Ok(s) => Ok(s),
Err(_) => Err(RunnerError::InvalidDecoding { charset: encoding.to_string() }),
},
} }
} }
impl Response { impl Response {
pub fn eval_asserts(self, variables: &HashMap<String, Value>, http_response: http::response::Response, context_dir: String) -> Vec<AssertResult> { pub fn eval_asserts(self, variables: &HashMap<String, Value>, http_response: libcurl::core::Response, context_dir: String) -> Vec<AssertResult> {
let mut asserts = vec![]; let mut asserts = vec![];
let version = self.clone().version; let version = self.clone().version;
asserts.push(AssertResult::Version { asserts.push(AssertResult::Version {
actual: http_response.version.to_text(), actual: http_response.version.to_string(),
expected: version.value.as_str().to_string(), expected: version.value.as_str().to_string(),
source_info: version.source_info, source_info: version.source_info,
}); });
@ -71,7 +66,7 @@ impl Response {
} }
Ok(expected) => { Ok(expected) => {
let header_name = header.key.value.clone(); let header_name = header.key.value.clone();
let actuals = http_response.get_header(header_name.as_str(), false); let actuals = http_response.get_header(header_name);
if actuals.is_empty() { if actuals.is_empty() {
asserts.push(AssertResult::Header { asserts.push(AssertResult::Header {
actual: Err(Error { actual: Err(Error {
@ -119,7 +114,7 @@ impl Response {
Ok(s) => Ok(Value::String(s)), Ok(s) => Ok(Value::String(s)),
Err(e) => Err(e), Err(e) => Err(e),
}; };
let actual = match decode_bytes(http_response.body.clone(), http_response.encoding()) { let actual = match http_response.text() {
Ok(s) => Ok(Value::String(s)), Ok(s) => Ok(Value::String(s)),
Err(e) => Err(Error { Err(e) => Err(Error {
source_info: SourceInfo { source_info: SourceInfo {
@ -138,7 +133,7 @@ impl Response {
} }
Bytes::Xml { value } => { Bytes::Xml { value } => {
let expected = Ok(Value::String(value)); let expected = Ok(Value::String(value));
let actual = match decode_bytes(http_response.body.clone(), http_response.encoding()) { let actual = match http_response.text() {
Ok(s) => Ok(Value::String(s)), Ok(s) => Ok(Value::String(s)),
Err(e) => Err(Error { Err(e) => Err(Error {
source_info: SourceInfo { source_info: SourceInfo {
@ -160,7 +155,7 @@ impl Response {
Ok(s) => Ok(Value::String(s)), Ok(s) => Ok(Value::String(s)),
Err(e) => Err(e), Err(e) => Err(e),
}; };
let actual = match decode_bytes(http_response.body.clone(), http_response.encoding()) { let actual = match http_response.text() {
Ok(s) => Ok(Value::String(s)), Ok(s) => Ok(Value::String(s)),
Err(e) => Err(Error { Err(e) => Err(Error {
source_info: SourceInfo { source_info: SourceInfo {
@ -205,7 +200,7 @@ impl Response {
asserts asserts
} }
pub fn eval_captures(self, http_response: http::response::Response, variables: &HashMap<String, Value>) -> Result<Vec<CaptureResult>, Error> { pub fn eval_captures(self, http_response: libcurl::core::Response, variables: &HashMap<String, Value>) -> Result<Vec<CaptureResult>, Error> {
let mut captures = vec![]; let mut captures = vec![];
for capture in self.captures() { for capture in self.captures() {
let capture_result = capture.eval(variables, http_response.clone())?; let capture_result = capture.eval(variables, http_response.clone())?;
@ -273,7 +268,7 @@ mod tests {
let variables = HashMap::new(); let variables = HashMap::new();
let context_dir = "undefined".to_string(); let context_dir = "undefined".to_string();
assert_eq!( assert_eq!(
user_response().eval_asserts(&variables, http::response::tests::xml_two_users_http_response(), context_dir), user_response().eval_asserts(&variables, libcurl::core::tests::xml_two_users_http_response(), context_dir),
vec![ vec![
AssertResult::Version { AssertResult::Version {
actual: String::from("1.0"), actual: String::from("1.0"),
@ -306,7 +301,7 @@ mod tests {
pub fn test_eval_captures() { pub fn test_eval_captures() {
let variables = HashMap::new(); let variables = HashMap::new();
assert_eq!( assert_eq!(
user_response().eval_captures(http::response::tests::xml_two_users_http_response(), &variables).unwrap(), user_response().eval_captures(libcurl::core::tests::xml_two_users_http_response(), &variables).unwrap(),
vec![ vec![
CaptureResult { CaptureResult {
name: "UserCount".to_string(), name: "UserCount".to_string(),

View File

@ -1,184 +0,0 @@
/*
* hurl (https://hurl.dev)
* Copyright (C) 2020 Orange
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
use std::fmt;
use crate::http::core::*;
use crate::http::request::*;
use crate::http::response::*;
impl Request {
pub fn to_text(&self) -> String {
let mut s = format!("{} {}\n",
self.clone().method.to_text(),
self.clone().url()
);
for header in self.clone().headers() {
s.push_str(header.to_text().as_str());
}
s.push_str("\n");
let body = match self.clone().form_params() {
None => body_text(self.clone().body, self.clone().content_type()),
Some(params) => {
let mut buf = String::from("[Form Params]");
for param in params {
buf.push_str(format!("\n{}={}", param.name, param.value).as_str())
}
buf
}
};
s.push_str(body.as_str());
s.push_str("\n");
s
}
}
impl Response {
pub fn to_text(&self, limit_body: usize) -> String {
let mut s = format!("HTTP/{} {}\n", self.version.to_text(), self.status);
for header in self.headers.clone() {
s.push_str(header.to_text().as_str());
}
s.push_str("");
// shoudl use number of char, not a number of bytes!!
//let limit_body = 400; // TODO should be explicitly pass as a command-line argument
if !self.body.is_empty() {
let body = body_text(self.clone().body, get_header_value(self.clone().headers, "content-type"));
s.push_str(substring(body.as_str(), 0, limit_body));
}
s
}
}
impl Method {
pub fn to_text(&self) -> String {
match self {
Method::Get => String::from("GET"),
Method::Head => String::from("HEAD"),
Method::Post => String::from("POST"),
Method::Put => String::from("PUT"),
Method::Delete => String::from("DELETE"),
Method::Connect => String::from("CONNECT"),
Method::Options => String::from("OPTIONS"),
Method::Trace => String::from("TRACE"),
Method::Patch => String::from("PATCH"),
}
}
}
impl Version {
pub fn to_text(&self) -> String {
match self {
Version::Http10 => String::from("1.0"),
Version::Http11 => String::from("1.1"),
Version::Http2 => String::from("2"),
}
}
}
impl Header {
fn to_text(&self) -> String {
return format!("{}: {}\n", self.name, self.value);
}
}
impl fmt::Display for Header {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}: {}", self.name, self.value)
}
}
fn body_text(bytes: Vec<u8>, content_type: Option<String>) -> String {
match content_type {
Some(content_type) =>
if is_text(content_type.as_str()) {
match String::from_utf8(bytes.clone()) {
Ok(v) => v,
Err(_) => format!("{:?}", bytes)
}
} else {
format!("{:?}", bytes)
}
_ => {
if bytes.is_empty() {
String::from("")
} else {
format!("{:?}", bytes)
}
}
}
}
fn is_text(content_type: &str) -> bool {
for s in &[
"application/json",
"text/html",
"charset=utf-8",
"application/x-www-form-urlencoded"
] {
if content_type.contains(s) {
return true;
}
}
false
}
fn substring(s: &str, start: usize, len: usize) -> &str {
let mut char_pos = 0;
let mut byte_start = 0;
let mut it = s.chars();
loop {
if char_pos == start { break; }
if let Some(c) = it.next() {
char_pos += 1;
byte_start += c.len_utf8();
} else { break; }
}
char_pos = 0;
let mut byte_end = byte_start;
loop {
if char_pos == len { break; }
if let Some(c) = it.next() {
char_pos += 1;
byte_end += c.len_utf8();
} else { break; }
}
&s[byte_start..byte_end]
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_is_text() {
assert_eq!(is_text("application/json"), true);
assert_eq!(is_text("application/json;charset=utf-8"), true);
}
#[test]
fn test_substring() {
assert_eq!(substring("", 0, 0), "");
assert_eq!(substring("hello world!", 0, 5), "hello");
assert_eq!(substring("hello world!", 0, 15), "hello world!");
}
}

View File

@ -1,298 +0,0 @@
/*
* hurl (https://hurl.dev)
* Copyright (C) 2020 Orange
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
extern crate hurl;
use hurl::http;
fn default_client_options() -> http::client::ClientOptions {
http::client::ClientOptions {
noproxy_hosts: vec![],
insecure: true,
redirect: http::client::Redirect::None,
http_proxy: None,
https_proxy: None,
all_proxy: None,
}
}
#[test]
fn test_hello() {
let client = http::client::Client::init(default_client_options());
let request = http::request::Request {
method: http::request::Method::Get,
url: http::core::Url {
scheme: "http".to_string(),
host: "localhost".to_string(),
port: Some(8000),
path: "/hello".to_string(),
query_string: "".to_string(), // querystring: None
}, //"http://localhost:8000/hello".to_string(),
//querystring_params: vec![],
querystring: vec![],
headers: vec![
http::core::Header {
name: String::from("User-Agent"),
value: String::from("hurl/0.1.1"),
},
http::core::Header {
name: String::from("Host"),
value: String::from("TBD"),
},
],
cookies: vec![],
body: vec![],
multipart: vec![],
};
let result = client.execute(&request);
println!("{:?}", result);
let response = result.unwrap();
assert_eq!(response.status, 200);
assert_eq!(response.body.len(), 12);
assert_eq!(
String::from_utf8(response.body).unwrap(),
"Hello World!".to_string()
);
}
//#[test]
//fn test_text_utf8() {
// let client = Client::init(ClientOptions {});
//
// let request = Request {
// method: Method::Get,
// url: "http://localhost:5000/cafe".to_string(),
// headers: vec![],
// body: vec![],
// };
// let response = client.execute(&request).unwrap();
// assert_eq!(response.status, 200);
// assert_eq!(response.body.len(), 5);
// assert_eq!(
// String::from_utf8(response.body).unwrap(),
// "café".to_string()
// );
//}
#[cfg(test)]
fn hello_request() -> http::request::Request {
http::request::Request {
method: http::request::Method::Get,
url: http::core::Url {
scheme: "http".to_string(),
host: "localhost".to_string(),
port: Some(8000),
path: "/hello".to_string(),
query_string: "".to_string(), // querystring: None
}, //"http://localhost:8000/hello".to_string(),
querystring: vec![],
headers: vec![],
cookies: vec![],
body: vec![],
multipart: vec![],
}
}
#[test]
fn test_multiple_calls() {
let client = http::client::Client::init(default_client_options());
let response = client.execute(&hello_request()).unwrap();
assert_eq!(response.status, 200);
let response = client.execute(&hello_request()).unwrap();
assert_eq!(response.status, 200);
}
#[test]
fn test_response_headers() {
let client = http::client::Client::init(default_client_options());
let response = client.execute(&hello_request()).unwrap();
println!("{:?}", response);
assert_eq!(response.status, 200);
assert_eq!(
response
.get_header("Content-Length", false)
.first()
.unwrap(),
"12"
);
}
#[test]
fn test_send_cookie() {
let client = http::client::Client::init(default_client_options());
let request = http::request::Request {
method: http::request::Method::Get,
url: http::core::Url {
scheme: "http".to_string(),
host: "localhost".to_string(),
port: Some(8000),
path: "/cookies/set-request-cookie1-valueA".to_string(),
query_string: "".to_string(), // querystring: None
}, //"http://localhost:8000/send-cookie".to_string(),
querystring: vec![],
headers: vec![http::core::Header {
name: "Cookie".to_string(),
value: "cookie1=valueA;".to_string(),
}],
cookies: vec![],
body: vec![],
multipart: vec![],
};
let response = client.execute(&request).unwrap();
assert_eq!(response.status, 200);
let _client = http::client::Client::init(default_client_options());
// let _cookie_header = http::cookie::Cookie {
// name: "Cookie1".to_string(),
// value: "valueA;".to_string(),
// max_age: None,
// domain: None,
// path: None,
// }.to_header();
/*
let request = Request {
method: Method::Get,
url: "http://localhost:5000/send-cookie1-value1".to_string(),
headers: vec![cookie_header],
body: vec![],
};
let response = client.execute(&request).unwrap();
assert_eq!(response.status, 200);
*/
}
#[test]
fn test_redirect() {
let client = http::client::Client::init(default_client_options());
let request = http::request::Request {
method: http::request::Method::Get,
url: http::core::Url {
scheme: "http".to_string(),
host: "localhost".to_string(),
port: Some(8000),
path: "/redirect".to_string(),
query_string: "".to_string(), // querystring: None
}, // "http://localhost:8000/redirect".to_string(),
querystring: vec![],
headers: vec![],
cookies: vec![],
body: vec![],
multipart: vec![],
};
let response = client.execute(&request).unwrap();
assert_eq!(response.status, 302);
assert_eq!(
response.get_header("location", true).first().unwrap(),
"http://localhost:8000/redirected"
);
}
#[test]
fn test_querystring_param() {
let client = http::client::Client::init(default_client_options());
let request = http::request::Request {
method: http::request::Method::Get,
url: http::core::Url {
scheme: "http".to_string(),
host: "localhost".to_string(),
port: Some(8000),
path: "/querystring-params".to_string(),
query_string: "".to_string(), // querystring: Some(String::from("param1=value1&param2&param3=a%3db"))
},
querystring: vec![
http::core::Param {
name: String::from("param1"),
value: String::from("value1"),
},
http::core::Param {
name: String::from("param2"),
value: String::from(""),
},
http::core::Param {
name: String::from("param3"),
value: String::from("a=b"),
},
http::core::Param {
name: String::from("param4"),
value: String::from("1,2,3"),
},
],
headers: vec![],
cookies: vec![],
body: vec![],
multipart: vec![],
};
let response = client.execute(&request).unwrap();
assert_eq!(response.status, 200);
}
#[test]
// curl -H 'Host:localhost:5000' -H 'content-type:application/x-www-form-urlencoded' -X POST 'http://localhost:5000/form-params' --data-binary 'param1=value1&param2='
fn test_form_param() {
let client = http::client::Client::init(default_client_options());
let request = http::request::Request {
method: http::request::Method::Post,
url: http::core::Url {
scheme: "http".to_string(),
host: "localhost".to_string(),
port: Some(8000),
path: "/form-params".to_string(),
query_string: "".to_string(),
}, // "http://localhost:8000/form-params".to_string(),
querystring: vec![],
headers: vec![http::core::Header {
name: "Content-Type".to_string(),
value: "application/x-www-form-urlencoded".to_string(),
}],
cookies: vec![],
body: "param1=value1&param2=&param3=a%3db&param4=a%253db"
.to_string()
.into_bytes(),
multipart: vec![],
};
let response = client.execute(&request).unwrap();
assert_eq!(response.status, 200);
/*
let client = Client::init(ClientOptions {}); // TO BE FIXED connection ended before message read => sync wait??
let request = Request {
method: Method::Post,
url: "http://localhost:5000/form-params".to_string(),
headers: vec![Header {
name: "Content-Type".to_string(),
value: "application/x-www-form-urlencoded".to_string(),
}],
body: encode_form_params(vec![
Param {
name: "param1".to_string(),
value: "value1".to_string(),
},
Param {
name: "param2".to_string(),
value: "".to_string(),
},
]),
};
let response = client.execute(&request).unwrap();
assert_eq!(response.status, 200);
*/
}

View File

@ -59,10 +59,11 @@ fn default_client() -> libcurl::client::Client {
let options = ClientOptions { let options = ClientOptions {
follow_location: false, follow_location: false,
max_redirect: None, max_redirect: None,
cookie_file: None, cookie_input_file: None,
cookie_jar: None,
proxy: None, proxy: None,
verbose: false, no_proxy: None,
verbose: true,
insecure: false,
}; };
libcurl::client::Client::init(options) libcurl::client::Client::init(options)
} }
@ -76,7 +77,8 @@ fn default_get_request(url: String) -> Request {
form: vec![], form: vec![],
multipart: vec![], multipart: vec![],
cookies: vec![], cookies: vec![],
body: vec![] body: vec![],
content_type: None,
} }
} }
@ -113,7 +115,8 @@ fn test_put() {
form: vec![], form: vec![],
multipart: vec![], multipart: vec![],
cookies: vec![], cookies: vec![],
body: vec![] body: vec![],
content_type: None,
}; };
let response = client.execute(&request, 0).unwrap(); let response = client.execute(&request, 0).unwrap();
assert_eq!(response.status, 200); assert_eq!(response.status, 200);
@ -137,7 +140,8 @@ fn test_patch() {
form: vec![], form: vec![],
multipart: vec![], multipart: vec![],
cookies: vec![], cookies: vec![],
body: vec![] body: vec![],
content_type: None,
}; };
let response = client.execute(&request, 0).unwrap(); let response = client.execute(&request, 0).unwrap();
assert_eq!(response.status, 204); assert_eq!(response.status, 204);
@ -166,7 +170,8 @@ fn test_custom_headers() {
form: vec![], form: vec![],
multipart: vec![], multipart: vec![],
cookies: vec![], cookies: vec![],
body: vec![] body: vec![],
content_type: None,
}; };
let response = client.execute(&request, 0).unwrap(); let response = client.execute(&request, 0).unwrap();
assert_eq!(response.status, 200); assert_eq!(response.status, 200);
@ -194,7 +199,8 @@ fn test_querystring_params() {
form: vec![], form: vec![],
multipart: vec![], multipart: vec![],
cookies: vec![], cookies: vec![],
body: vec![] body: vec![],
content_type: None,
}; };
let response = client.execute(&request, 0).unwrap(); let response = client.execute(&request, 0).unwrap();
assert_eq!(response.status, 200); assert_eq!(response.status, 200);
@ -222,7 +228,8 @@ fn test_form_params() {
], ],
multipart: vec![], multipart: vec![],
cookies: vec![], cookies: vec![],
body: vec![] body: vec![],
content_type: Some("application/x-www-form-urlencoded".to_string()),
}; };
let response = client.execute(&request, 0).unwrap(); let response = client.execute(&request, 0).unwrap();
assert_eq!(response.status, 200); assert_eq!(response.status, 200);
@ -255,10 +262,11 @@ fn test_follow_location() {
let options = ClientOptions { let options = ClientOptions {
follow_location: true, follow_location: true,
max_redirect: None, max_redirect: None,
cookie_file: None, cookie_input_file: None,
cookie_jar: None,
proxy: None, proxy: None,
no_proxy: None,
verbose: false, verbose: false,
insecure: false
}; };
let mut client = libcurl::client::Client::init(options); let mut client = libcurl::client::Client::init(options);
let response = client.execute(&request, 0).unwrap(); let response = client.execute(&request, 0).unwrap();
@ -281,10 +289,11 @@ fn test_max_redirect() {
let options = ClientOptions { let options = ClientOptions {
follow_location: true, follow_location: true,
max_redirect: Some(10), max_redirect: Some(10),
cookie_file: None, cookie_input_file: None,
cookie_jar: None,
proxy: None, proxy: None,
no_proxy: None,
verbose: false, verbose: false,
insecure: false,
}; };
let mut client = libcurl::client::Client::init(options); let mut client = libcurl::client::Client::init(options);
let request = default_get_request("http://localhost:8000/redirect".to_string()); let request = default_get_request("http://localhost:8000/redirect".to_string());
@ -336,13 +345,20 @@ fn test_multipart_form_data() {
}), }),
], ],
cookies: vec![], cookies: vec![],
body: vec![] body: vec![],
content_type: Some("multipart/form-data".to_string()),
}; };
let response = client.execute(&request, 0).unwrap(); let response = client.execute(&request, 0).unwrap();
assert_eq!(response.status, 200); assert_eq!(response.status, 200);
assert!(response.body.is_empty()); 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 // endregion
@ -362,6 +378,7 @@ fn test_post_bytes() {
multipart: vec![], multipart: vec![],
cookies: vec![], cookies: vec![],
body: b"Hello World!".to_vec(), body: b"Hello World!".to_vec(),
content_type: None
}; };
let response = client.execute(&request, 0).unwrap(); let response = client.execute(&request, 0).unwrap();
assert_eq!(response.status, 200); assert_eq!(response.status, 200);
@ -393,10 +410,11 @@ fn test_error_fail_to_connect() {
let options = ClientOptions { let options = ClientOptions {
follow_location: false, follow_location: false,
max_redirect: None, max_redirect: None,
cookie_file: None, cookie_input_file: None,
cookie_jar: None,
proxy: Some("localhost:9999".to_string()), proxy: Some("localhost:9999".to_string()),
no_proxy: None,
verbose: true, verbose: true,
insecure: false,
}; };
let mut client = libcurl::client::Client::init(options); let mut client = libcurl::client::Client::init(options);
let request = default_get_request("http://localhost:8000/hello".to_string()); let request = default_get_request("http://localhost:8000/hello".to_string());
@ -411,10 +429,11 @@ fn test_error_could_not_resolve_proxy_name() {
let options = ClientOptions { let options = ClientOptions {
follow_location: false, follow_location: false,
max_redirect: None, max_redirect: None,
cookie_file: None, cookie_input_file: None,
cookie_jar: None,
proxy: Some("unknown".to_string()), proxy: Some("unknown".to_string()),
no_proxy: None,
verbose: false, verbose: false,
insecure: false,
}; };
let mut client = libcurl::client::Client::init(options); let mut client = libcurl::client::Client::init(options);
let request = default_get_request("http://localhost:8000/hello".to_string()); let request = default_get_request("http://localhost:8000/hello".to_string());
@ -439,8 +458,22 @@ fn test_cookie() {
cookies: vec![ cookies: vec![
RequestCookie { name: "cookie1".to_string(), value: "valueA".to_string() } RequestCookie { name: "cookie1".to_string(), value: "valueA".to_string() }
], ],
body: vec![] body: vec![],
content_type: None,
}; };
// set cookie from the request cookie (temporary)
let request_cookie = request.cookies.get(0).unwrap();
let cookie = Cookie {
domain: "localhost".to_string(),
include_subdomain: "FALSE".to_string(),
path: "/".to_string(),
https: "FALSE".to_string(),
expires: "0".to_string(),
name: request_cookie.name.clone(),
value: request_cookie.value.clone(),
};
client.add_cookie(cookie);
let response = client.execute(&request, 0).unwrap(); let response = client.execute(&request, 0).unwrap();
assert_eq!(response.status, 200); assert_eq!(response.status, 200);
assert!(response.body.is_empty()); assert!(response.body.is_empty());
@ -449,10 +482,10 @@ fn test_cookie() {
// For the time-being setting a cookie on a request // For the time-being setting a cookie on a request
// update the cookie store as well // update the cookie store as well
// The same cookie does not need to be set explicitly on further requests // 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 request = default_get_request("http://localhost:8000/cookies/set-request-cookie1-valueA".to_string());
let response = client.execute(&request, 0).unwrap(); // let response = client.execute(&request, 0).unwrap();
assert_eq!(response.status, 200); // assert_eq!(response.status, 200);
assert!(response.body.is_empty()); // assert!(response.body.is_empty());
} }
@ -474,6 +507,7 @@ fn test_cookie_storage() {
name: "cookie2".to_string(), name: "cookie2".to_string(),
value: "valueA".to_string(), value: "valueA".to_string(),
}); });
let request = default_get_request("http://localhost:8000/cookies/assert-that-cookie2-is-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(); let response = client.execute(&request, 0).unwrap();
assert_eq!(response.status, 200); assert_eq!(response.status, 200);
@ -491,10 +525,11 @@ fn test_cookie_file() {
let options = ClientOptions { let options = ClientOptions {
follow_location: false, follow_location: false,
max_redirect: None, max_redirect: None,
cookie_file: Some(temp_file.to_string()), cookie_input_file: Some(temp_file.to_string()),
cookie_jar: None,
proxy: None, proxy: None,
no_proxy: None,
verbose: false, verbose: false,
insecure: false,
}; };
let mut client = libcurl::client::Client::init(options); 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 request = default_get_request("http://localhost:8000/cookies/assert-that-cookie2-is-valueA".to_string());
@ -514,10 +549,11 @@ fn test_proxy() {
let options = ClientOptions { let options = ClientOptions {
follow_location: false, follow_location: false,
max_redirect: None, max_redirect: None,
cookie_file: None, cookie_input_file: None,
cookie_jar: None,
proxy: Some("localhost:8080".to_string()), proxy: Some("localhost:8080".to_string()),
no_proxy: None,
verbose: false, verbose: false,
insecure: false,
}; };
let mut client = libcurl::client::Client::init(options); let mut client = libcurl::client::Client::init(options);
let request = default_get_request("http://localhost:8000/hello".to_string()); let request = default_get_request("http://localhost:8000/hello".to_string());

View File

@ -21,7 +21,7 @@ use hurl::core::ast;
use hurl::core::common::{Pos, SourceInfo}; use hurl::core::common::{Pos, SourceInfo};
use hurl::runner; use hurl::runner;
use hurl::format; use hurl::format;
use hurl::http; use hurl::http::libcurl;
use std::collections::HashMap; use std::collections::HashMap;
use hurl::core::ast::{Template, TemplateElement, EncodedString}; use hurl::core::ast::{Template, TemplateElement, EncodedString};
use hurl::runner::core::RunnerOptions; use hurl::runner::core::RunnerOptions;
@ -30,7 +30,6 @@ use hurl::runner::core::RunnerOptions;
// can be used for debugging // can be used for debugging
#[test] #[test]
fn test_hurl_file() { fn test_hurl_file() {
let mut cookie_store = http::cookie::CookieJar::init(vec![]);
//let filename = "integration/tests/post_json.hurl"; //let filename = "integration/tests/post_json.hurl";
//let filename = "integration/tests/error_assert_match_utf8.hurl"; //let filename = "integration/tests/error_assert_match_utf8.hurl";
let filename = "integration/tests/error_template_variable_not_renderable.hurl"; let filename = "integration/tests/error_template_variable_not_renderable.hurl";
@ -38,14 +37,16 @@ fn test_hurl_file() {
let content = std::fs::read_to_string(filename).expect("Something went wrong reading the 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 hurl_file = hurl::parser::parse_hurl_file(content.as_str()).unwrap();
let variables = HashMap::new(); let variables = HashMap::new();
let client = http::client::Client::init(http::client::ClientOptions { let options = libcurl::client::ClientOptions {
noproxy_hosts: vec![], follow_location: false,
max_redirect: None,
cookie_input_file: None,
proxy: None,
no_proxy: None,
verbose: false,
insecure: false, insecure: false,
redirect: http::client::Redirect::None, };
http_proxy: None, let mut client = libcurl::client::Client::init(options);
https_proxy: None,
all_proxy: None
});
let mut lines: Vec<&str> = regex::Regex::new(r"\n|\r\n") let mut lines: Vec<&str> = regex::Regex::new(r"\n|\r\n")
.unwrap() .unwrap()
.split(&content) .split(&content)
@ -68,10 +69,9 @@ fn test_hurl_file() {
let _hurl_log = runner::file::run( let _hurl_log = runner::file::run(
hurl_file, hurl_file,
client, &mut client,
//&mut variables, //&mut variables,
filename.to_string(), filename.to_string(),
&mut cookie_store,
"current_dir".to_string(), "current_dir".to_string(),
options, options,
logger logger
@ -144,15 +144,16 @@ fn hello_request() -> ast::Request {
#[test] #[test]
fn test_hello() { fn test_hello() {
let mut cookie_store = http::cookie::CookieJar::init(vec![]); let options = libcurl::client::ClientOptions {
let client = http::client::Client::init(http::client::ClientOptions { follow_location: false,
noproxy_hosts: vec![], max_redirect: None,
cookie_input_file: None,
proxy: None,
no_proxy: None,
verbose: false,
insecure: false, insecure: false,
redirect: http::client::Redirect::None, };
http_proxy: None, let mut client = libcurl::client::Client::init(options);
https_proxy: None,
all_proxy: None
});
let source_info = SourceInfo { let source_info = SourceInfo {
start: Pos { line: 1, column: 1 }, start: Pos { line: 1, column: 1 },
end: Pos { line: 1, column: 1 }, end: Pos { line: 1, column: 1 },
@ -207,9 +208,8 @@ fn test_hello() {
}; };
let _hurl_log = runner::file::run( let _hurl_log = runner::file::run(
hurl_file, hurl_file,
client, &mut client,
String::from("filename"), String::from("filename"),
&mut cookie_store,
"current_dir".to_string(), "current_dir".to_string(),
options, options,
logger logger