mirror of
https://github.com/Orange-OpenSource/hurl.git
synced 2024-12-28 05:23:56 +03:00
Merge pull request #21 from Orange-OpenSource/feature/using_libcurl
Using libcurl under the hood
This commit is contained in:
commit
beb89df90d
955
Cargo.lock
generated
955
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -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"
|
||||||
|
@ -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
@ -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'
|
||||||
|
|
|
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
|
|
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
|
|
|
||||||
|
|
||||||
|
@ -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'
|
||||||
|
|
|
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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 ''
|
||||||
|
|
||||||
|
|
||||||
|
@ -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!'
|
||||||
|
|
||||||
|
136
src/bin/hurl.rs
136
src/bin/hurl.rs
@ -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');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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()));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|
@ -1,216 +0,0 @@
|
|||||||
/*
|
|
||||||
* hurl (https://hurl.dev)
|
|
||||||
* Copyright (C) 2020 Orange
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
use std::path::Path;
|
|
||||||
|
|
||||||
use super::core::*;
|
|
||||||
use super::request::*;
|
|
||||||
use super::response::*;
|
|
||||||
|
|
||||||
pub struct Client {
|
|
||||||
_inner_client: reqwest::Client,
|
|
||||||
options: ClientOptions,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
||||||
pub struct ClientOptions {
|
|
||||||
pub noproxy_hosts: Vec<String>,
|
|
||||||
pub insecure: bool,
|
|
||||||
pub redirect: Redirect,
|
|
||||||
pub http_proxy: Option<String>,
|
|
||||||
pub https_proxy: Option<String>,
|
|
||||||
pub all_proxy: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
||||||
pub enum Redirect {
|
|
||||||
None,
|
|
||||||
Limited(usize),
|
|
||||||
Unlimited,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
||||||
pub struct HttpError {
|
|
||||||
pub url: String,
|
|
||||||
pub message: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
||||||
pub struct Proxy {
|
|
||||||
pub protocol: Option<ProxyProtocol>,
|
|
||||||
pub host: String,
|
|
||||||
pub port: Option<u16>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
||||||
pub enum ProxyProtocol {
|
|
||||||
Http,
|
|
||||||
Https,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
pub fn get_redirect_policy(redirect: Redirect) -> reqwest::RedirectPolicy {
|
|
||||||
match redirect {
|
|
||||||
Redirect::None => reqwest::RedirectPolicy::none(),
|
|
||||||
Redirect::Limited(max) => reqwest::RedirectPolicy::limited(max),
|
|
||||||
Redirect::Unlimited {} => reqwest::RedirectPolicy::custom(|attempt| { attempt.follow() }),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Client {
|
|
||||||
pub fn init(options: ClientOptions) -> Client {
|
|
||||||
let client_builder = reqwest::Client::builder()
|
|
||||||
.redirect(get_redirect_policy(options.redirect.clone()))
|
|
||||||
.use_sys_proxy()
|
|
||||||
.danger_accept_invalid_hostnames(options.insecure)
|
|
||||||
.danger_accept_invalid_certs(options.insecure)
|
|
||||||
.cookie_store(false);
|
|
||||||
Client {
|
|
||||||
_inner_client: client_builder.build().unwrap(),
|
|
||||||
options,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn execute(&self, request: &Request) -> Result<Response, HttpError> {
|
|
||||||
let mut headers = reqwest::header::HeaderMap::new();
|
|
||||||
for header in request.clone().headers() {
|
|
||||||
headers.append(
|
|
||||||
reqwest::header::HeaderName::from_lowercase(
|
|
||||||
header.name.to_lowercase().as_str().as_bytes(),
|
|
||||||
)
|
|
||||||
.unwrap(),
|
|
||||||
reqwest::header::HeaderValue::from_str(header.value.as_str()).unwrap(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
let client_builder = reqwest::Client::builder()
|
|
||||||
.redirect(get_redirect_policy(self.options.redirect.clone()))
|
|
||||||
.danger_accept_invalid_hostnames(self.options.insecure)
|
|
||||||
.danger_accept_invalid_certs(self.options.insecure)
|
|
||||||
.cookie_store(false);
|
|
||||||
|
|
||||||
|
|
||||||
let client_builder = if let Some(url) = self.options.http_proxy.clone() {
|
|
||||||
let proxy = reqwest::Proxy::http(url.as_str()).unwrap();
|
|
||||||
client_builder.proxy(proxy)
|
|
||||||
} else {
|
|
||||||
client_builder
|
|
||||||
};
|
|
||||||
let client_builder = if let Some(url) = self.options.https_proxy.clone() {
|
|
||||||
let proxy = reqwest::Proxy::https(url.as_str()).unwrap();
|
|
||||||
client_builder.proxy(proxy)
|
|
||||||
} else {
|
|
||||||
client_builder
|
|
||||||
};
|
|
||||||
let client_builder = if let Some(url) = self.options.all_proxy.clone() {
|
|
||||||
let proxy = reqwest::Proxy::all(url.as_str()).unwrap();
|
|
||||||
client_builder.proxy(proxy)
|
|
||||||
} else {
|
|
||||||
client_builder
|
|
||||||
};
|
|
||||||
|
|
||||||
let client_builder = if self.options.noproxy_hosts.contains(&request.url.host.clone()) {
|
|
||||||
client_builder.no_proxy()
|
|
||||||
} else {
|
|
||||||
client_builder
|
|
||||||
};
|
|
||||||
|
|
||||||
let client = client_builder.build().unwrap();
|
|
||||||
|
|
||||||
|
|
||||||
let req = if request.multipart.is_empty() {
|
|
||||||
client
|
|
||||||
.request(
|
|
||||||
request.clone().method.to_reqwest(),
|
|
||||||
reqwest::Url::parse(request.clone().url().as_str()).unwrap(),
|
|
||||||
)
|
|
||||||
.headers(headers)
|
|
||||||
.body(request.clone().body)
|
|
||||||
.build()
|
|
||||||
.unwrap()
|
|
||||||
} else {
|
|
||||||
let mut form = reqwest::multipart::Form::new();
|
|
||||||
for param in request.multipart.clone() {
|
|
||||||
match param {
|
|
||||||
MultipartParam::TextParam { name, value } => {
|
|
||||||
form = form.text(name, value)
|
|
||||||
}
|
|
||||||
MultipartParam::FileParam { name, filename, content_type } => {
|
|
||||||
if let Some(content_type) = content_type {
|
|
||||||
let path = Path::new(filename.as_str());
|
|
||||||
let part = reqwest::multipart::Part::file(path).unwrap()
|
|
||||||
.mime_str(content_type.as_str())
|
|
||||||
.unwrap();
|
|
||||||
form = form.part(name, part);
|
|
||||||
} else {
|
|
||||||
form = form.file(name, filename).unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
client
|
|
||||||
.request(
|
|
||||||
request.clone().method.to_reqwest(),
|
|
||||||
reqwest::Url::parse(request.clone().url().as_str()).unwrap(),
|
|
||||||
)
|
|
||||||
.headers(headers)
|
|
||||||
.multipart(form)
|
|
||||||
.build()
|
|
||||||
.unwrap()
|
|
||||||
};
|
|
||||||
|
|
||||||
match client.execute(req) {
|
|
||||||
Ok(mut resp) => {
|
|
||||||
let mut headers = vec![];
|
|
||||||
//eprintln!(">>> response headers {:?}", resp.headers().clone());
|
|
||||||
for (name, value) in resp.headers() {
|
|
||||||
headers.push(Header {
|
|
||||||
name: name.as_str().to_string(),
|
|
||||||
value: value.to_str().unwrap().to_string(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
let version = match resp.version() {
|
|
||||||
reqwest::Version::HTTP_10 => Version::Http10,
|
|
||||||
reqwest::Version::HTTP_11 => Version::Http11,
|
|
||||||
reqwest::Version::HTTP_2 => Version::Http2,
|
|
||||||
v => panic!("Version {:?} not supported!", v),
|
|
||||||
};
|
|
||||||
let mut buf: Vec<u8> = vec![];
|
|
||||||
resp.copy_to(&mut buf).unwrap(); // TODO Test error case
|
|
||||||
resp.content_length(); // dirty hack to prevent error "connection closed before message completed"?
|
|
||||||
|
|
||||||
Ok(Response {
|
|
||||||
version,
|
|
||||||
status: resp.status().as_u16(),
|
|
||||||
headers,
|
|
||||||
body: buf,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
Err(HttpError {
|
|
||||||
message: format!("{:?}", e.to_string()),
|
|
||||||
url: request.clone().url(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,472 +0,0 @@
|
|||||||
/*
|
|
||||||
* hurl (https://hurl.dev)
|
|
||||||
* Copyright (C) 2020 Orange
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
use core::fmt;
|
|
||||||
|
|
||||||
use chrono::NaiveDateTime;
|
|
||||||
use cookie::Cookie as ExternalCookie;
|
|
||||||
|
|
||||||
use super::core::*;
|
|
||||||
|
|
||||||
//use std::collections::HashMap;
|
|
||||||
|
|
||||||
// cookies
|
|
||||||
// keep cookies same name different domains
|
|
||||||
// send the most specific?? send the 2 of them?
|
|
||||||
// more flexible to keep list of cookies internally
|
|
||||||
|
|
||||||
pub type Domain = String;
|
|
||||||
pub type Name = String;
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
||||||
pub struct ResponseCookie {
|
|
||||||
pub name: String,
|
|
||||||
pub value: String,
|
|
||||||
pub max_age: Option<i64>,
|
|
||||||
pub domain: Option<String>,
|
|
||||||
pub path: Option<String>,
|
|
||||||
pub secure: Option<bool>,
|
|
||||||
pub http_only: Option<bool>,
|
|
||||||
pub expires: Option<String>,
|
|
||||||
pub same_site: Option<String>,
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct ParseCookieError {}
|
|
||||||
|
|
||||||
impl std::str::FromStr for ResponseCookie {
|
|
||||||
type Err = ParseCookieError;
|
|
||||||
|
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
||||||
let c = ExternalCookie::parse(s).unwrap();
|
|
||||||
let name = c.name().to_string();
|
|
||||||
let value = c.value().to_string();
|
|
||||||
let max_age = match c.max_age() {
|
|
||||||
None => None,
|
|
||||||
Some(d) => Some(d.num_seconds())
|
|
||||||
};
|
|
||||||
let domain = match c.domain() {
|
|
||||||
None => None,
|
|
||||||
Some(v) => Some(v.to_string())
|
|
||||||
};
|
|
||||||
let path = match c.path() {
|
|
||||||
None => None,
|
|
||||||
Some(v) => Some(v.to_string())
|
|
||||||
};
|
|
||||||
let secure = match c.secure() {
|
|
||||||
None => None,
|
|
||||||
Some(value) => Some(value)
|
|
||||||
};
|
|
||||||
let http_only = match c.http_only() {
|
|
||||||
None => None,
|
|
||||||
Some(value) => Some(value)
|
|
||||||
};
|
|
||||||
let expires = match c.expires() {
|
|
||||||
None => None,
|
|
||||||
Some(time) => Some(time.rfc822().to_string())
|
|
||||||
};
|
|
||||||
let same_site = match c.same_site() {
|
|
||||||
None => None,
|
|
||||||
Some(s) => Some(s.to_string())
|
|
||||||
};
|
|
||||||
Ok(ResponseCookie { name, value, max_age, domain, path, secure, expires, http_only, same_site })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Display for ResponseCookie {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
||||||
let max_age = match self.clone().max_age {
|
|
||||||
None => String::from(""),
|
|
||||||
Some(v) => format!("; Max-Age:{}", v)
|
|
||||||
};
|
|
||||||
let domain = match self.clone().domain {
|
|
||||||
None => String::from(""),
|
|
||||||
Some(v) => format!("; Domain:{}", v)
|
|
||||||
};
|
|
||||||
let path = match self.clone().path {
|
|
||||||
None => String::from(""),
|
|
||||||
Some(v) => format!("; Path:{}", v)
|
|
||||||
};
|
|
||||||
write!(f, "{}={}{}{}{}",
|
|
||||||
self.name,
|
|
||||||
self.value,
|
|
||||||
max_age,
|
|
||||||
domain,
|
|
||||||
path)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Display for Cookie {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
||||||
write!(f, "{}={}; domain={}; path={}",
|
|
||||||
self.name,
|
|
||||||
self.value,
|
|
||||||
self.domain,
|
|
||||||
self.path,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ResponseCookie {
|
|
||||||
|
|
||||||
|
|
||||||
// pub fn to_header(&self) -> Header {
|
|
||||||
// return Header {
|
|
||||||
// name: String::from("Cookie"),
|
|
||||||
// value: format!("{}={}", self.name, self.value),
|
|
||||||
// };
|
|
||||||
// //format!("Cookie: {}", self.to_string());
|
|
||||||
// }
|
|
||||||
|
|
||||||
|
|
||||||
pub fn encode_cookie(header_name: String, header_value: String) -> Header {
|
|
||||||
let name = String::from("Cookie");
|
|
||||||
let value = format!("{}={};", header_name, header_value);
|
|
||||||
Header { name, value }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
|
||||||
pub struct CookieJar {
|
|
||||||
inner: Vec<Cookie>
|
|
||||||
}
|
|
||||||
|
|
||||||
impl CookieJar {
|
|
||||||
pub fn init(cookies: Vec<Cookie>) -> CookieJar {
|
|
||||||
CookieJar { inner: cookies }
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn cookies(self) -> Vec<Cookie> {
|
|
||||||
self.inner
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
pub fn get_cookies(self, domain: String, path: String) -> Vec<ResponseCookie> {
|
|
||||||
self.inner
|
|
||||||
.iter()
|
|
||||||
.filter(|c| c.is_usable(domain.clone(), path.clone()))
|
|
||||||
.map(|c| ResponseCookie {
|
|
||||||
name: c.clone().name,
|
|
||||||
value: c.clone().value,
|
|
||||||
max_age: None,
|
|
||||||
domain: Some(c.domain.clone()),
|
|
||||||
path: Some(c.path.clone()),
|
|
||||||
secure: Some(c.secure),
|
|
||||||
http_only: None,
|
|
||||||
expires: None,
|
|
||||||
same_site: None,
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn update_cookies(&mut self, default_domain: String, _default_path: String, cookie: ResponseCookie) {
|
|
||||||
match cookie.max_age {
|
|
||||||
Some(0) => {
|
|
||||||
//eprintln!("delete cookie {:?}", cookie);
|
|
||||||
self.inner.retain(|c| c.name != cookie.name);
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
|
|
||||||
// replace value if same name+domain
|
|
||||||
let domain = match cookie.clone().domain {
|
|
||||||
None => default_domain,
|
|
||||||
Some(d) => d,
|
|
||||||
};
|
|
||||||
let path = match cookie.clone().path {
|
|
||||||
None => String::from("/"), // do not use default path for the time-beingdefault_path,
|
|
||||||
Some(p) => p,
|
|
||||||
};
|
|
||||||
|
|
||||||
// find existing cookie
|
|
||||||
for c in self.inner.iter_mut() {
|
|
||||||
if c.name == cookie.name && c.domain == domain {
|
|
||||||
c.value = cookie.value;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let secure = if let Some(v) = cookie.secure { v } else { false };
|
|
||||||
|
|
||||||
// push new cookie
|
|
||||||
self.inner.push(Cookie {
|
|
||||||
name: cookie.clone().name,
|
|
||||||
value: cookie.clone().value,
|
|
||||||
domain,
|
|
||||||
path,
|
|
||||||
subdomains: cookie.domain.is_some(),
|
|
||||||
secure,
|
|
||||||
expires: None,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
||||||
pub struct Cookie {
|
|
||||||
pub name: String,
|
|
||||||
pub value: String,
|
|
||||||
pub domain: String,
|
|
||||||
pub path: String,
|
|
||||||
pub subdomains: bool,
|
|
||||||
pub secure: bool,
|
|
||||||
pub expires: Option<NaiveDateTime>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Cookie {
|
|
||||||
fn is_usable(&self, domain: String, path: String) -> bool {
|
|
||||||
|
|
||||||
// domain
|
|
||||||
if !is_subdomain(self.clone().domain, domain.clone()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if !self.subdomains && domain != self.clone().domain {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// path
|
|
||||||
if !is_subpath(self.clone().path, path) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_subdomain(domain: String, subdomain: String) -> bool {
|
|
||||||
if domain.as_str() == "" {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut domain_segments: Vec<&str> = domain.split('.').collect();
|
|
||||||
if domain_segments.get(0).unwrap() == &"" {
|
|
||||||
domain_segments.remove(0);
|
|
||||||
}
|
|
||||||
domain_segments.reverse();
|
|
||||||
|
|
||||||
let mut subdomain_segments: Vec<&str> = subdomain.split('.').collect();
|
|
||||||
if subdomain_segments.get(0).unwrap() == &"" {
|
|
||||||
subdomain_segments.remove(0);
|
|
||||||
}
|
|
||||||
subdomain_segments.reverse();
|
|
||||||
if domain_segments.len() > subdomain_segments.len() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
for i in 0..domain_segments.len() {
|
|
||||||
if domain_segments.get(i).unwrap() != subdomain_segments.get(i).unwrap() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_subpath(path: String, subpath: String) -> bool {
|
|
||||||
if path.as_str() == "" {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut path_segments: Vec<&str> = path.split('/').collect();
|
|
||||||
if path_segments.get(0).unwrap() == &"" {
|
|
||||||
path_segments.remove(0);
|
|
||||||
}
|
|
||||||
path_segments.reverse();
|
|
||||||
if path_segments.get(0).unwrap() == &"" {
|
|
||||||
path_segments.remove(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut subpath_segments: Vec<&str> = subpath.split('/').collect();
|
|
||||||
if subpath_segments.get(0).unwrap() == &"" {
|
|
||||||
subpath_segments.remove(0);
|
|
||||||
}
|
|
||||||
subpath_segments.reverse();
|
|
||||||
if path_segments.len() > subpath_segments.len() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
for i in 0..path_segments.len() {
|
|
||||||
if path_segments.get(i).unwrap() != subpath_segments.get(i).unwrap() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Cookie {
|
|
||||||
pub fn to_netscape(&self) -> String {
|
|
||||||
let domain_name = self.domain.to_string();
|
|
||||||
let include_domains = if self.subdomains { "TRUE" } else { "FALSE" }.to_string();
|
|
||||||
let path = self.path.clone();
|
|
||||||
let https_only = if self.secure { "TRUE" } else { "FALSE" }.to_string();
|
|
||||||
let expires = if let Some(expires) = self.expires {
|
|
||||||
expires.timestamp().to_string()
|
|
||||||
} else {
|
|
||||||
"0".to_string()
|
|
||||||
};
|
|
||||||
let name = self.name.clone();
|
|
||||||
let value = self.value.clone();
|
|
||||||
format!("{}\t{}\t{}\t{}\t{}\t{}\t{}",
|
|
||||||
domain_name,
|
|
||||||
include_domains,
|
|
||||||
path,
|
|
||||||
https_only,
|
|
||||||
expires,
|
|
||||||
name,
|
|
||||||
value
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn from_netscape(s: &str) -> Option<Cookie> {
|
|
||||||
let tokens = s.split('\t').collect::<Vec<&str>>();
|
|
||||||
if tokens.len() != 7 { return None; }
|
|
||||||
|
|
||||||
let domain = (*tokens.get(0).unwrap()).to_string();
|
|
||||||
let subdomains = (*tokens.get(1).unwrap()).to_string().as_str() == "TRUE";
|
|
||||||
let path = (*tokens.get(2).unwrap()).to_string();
|
|
||||||
let secure = (*tokens.get(3).unwrap()).to_string().as_str() == "TRUE";
|
|
||||||
let expires = None;
|
|
||||||
let name = (*tokens.get(5).unwrap()).to_string();
|
|
||||||
let value = (*tokens.get(6).unwrap()).to_string();
|
|
||||||
|
|
||||||
Some(Cookie { name, value, domain, path, subdomains, secure, expires })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
fn cookie_lsid() -> Cookie {
|
|
||||||
Cookie {
|
|
||||||
name: String::from("LSID"),
|
|
||||||
value: String::from("DQAAAK…Eaem_vYg"),
|
|
||||||
domain: String::from("docs.foo.com"),
|
|
||||||
path: String::from("/accounts"),
|
|
||||||
subdomains: false,
|
|
||||||
secure: false,
|
|
||||||
expires: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn cookie_hsid() -> Cookie {
|
|
||||||
Cookie {
|
|
||||||
name: String::from("HSID"),
|
|
||||||
value: String::from("AYQEVn…DKrdst"),
|
|
||||||
domain: String::from(".foo.com"),
|
|
||||||
path: String::from("/"),
|
|
||||||
subdomains: true,
|
|
||||||
secure: false,
|
|
||||||
expires: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn cookie_ssid() -> Cookie {
|
|
||||||
Cookie {
|
|
||||||
name: String::from("SSID"),
|
|
||||||
value: String::from("Ap4P…GTEq"),
|
|
||||||
domain: String::from("foo.com"),
|
|
||||||
path: String::from("/"),
|
|
||||||
subdomains: true,
|
|
||||||
secure: false,
|
|
||||||
expires: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn sample_cookiejar() -> CookieJar {
|
|
||||||
CookieJar {
|
|
||||||
inner: vec![
|
|
||||||
cookie_lsid(),
|
|
||||||
cookie_hsid(),
|
|
||||||
cookie_ssid(),
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_is_usable() {
|
|
||||||
let domain = String::from("example.org");
|
|
||||||
let path = String::from("/");
|
|
||||||
assert_eq!(cookie_lsid().is_usable(domain.clone(), path.clone()), false);
|
|
||||||
assert_eq!(cookie_hsid().is_usable(domain.clone(), path.clone()), false);
|
|
||||||
assert_eq!(cookie_ssid().is_usable(domain, path.clone()), false);
|
|
||||||
|
|
||||||
let domain = String::from("foo.com");
|
|
||||||
let path = String::from("/");
|
|
||||||
assert_eq!(cookie_lsid().is_usable(domain.clone(), path.clone()), false);
|
|
||||||
assert_eq!(cookie_hsid().is_usable(domain.clone(), path.clone()), true);
|
|
||||||
assert_eq!(cookie_ssid().is_usable(domain, path.clone()), true);
|
|
||||||
|
|
||||||
let domain = String::from("foo.com");
|
|
||||||
let path = String::from("/accounts");
|
|
||||||
assert_eq!(cookie_lsid().is_usable(domain.clone(), path.clone()), false);
|
|
||||||
assert_eq!(cookie_hsid().is_usable(domain.clone(), path.clone()), true);
|
|
||||||
assert_eq!(cookie_ssid().is_usable(domain, path), true);
|
|
||||||
|
|
||||||
let domain = String::from("docs.foo.com");
|
|
||||||
let path = String::from("/accounts");
|
|
||||||
assert_eq!(cookie_lsid().is_usable(domain.clone(), path.clone()), true);
|
|
||||||
assert_eq!(cookie_hsid().is_usable(domain.clone(), path.clone()), true);
|
|
||||||
assert_eq!(cookie_ssid().is_usable(domain, path), true);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_get_cookies() {
|
|
||||||
let domain = String::from("docs.foo.com");
|
|
||||||
let path = String::from("/accounts");
|
|
||||||
assert_eq!(sample_cookiejar().get_cookies(domain, path).len(), 3);
|
|
||||||
|
|
||||||
let domain = String::from("toto.docs.foo.com");
|
|
||||||
let path = String::from("/accounts");
|
|
||||||
assert_eq!(sample_cookiejar().get_cookies(domain, path).len(), 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_is_subdomain() {
|
|
||||||
assert_eq!(is_subdomain(String::from("foo.example.org"), String::from("example.org")), false);
|
|
||||||
assert_eq!(is_subdomain(String::from("example.org"), String::from("toto.org")), false);
|
|
||||||
|
|
||||||
assert_eq!(is_subdomain(String::from("example.org"), String::from("example.org")), true);
|
|
||||||
assert_eq!(is_subdomain(String::from("example.org"), String::from("foo.example.org")), true);
|
|
||||||
assert_eq!(is_subdomain(String::from(".example.org"), String::from("foo.example.org")), true);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_is_subpath() {
|
|
||||||
assert_eq!(is_subpath(String::from("/toto"), String::from("/toto")), true);
|
|
||||||
assert_eq!(is_subpath(String::from("/"), String::from("/toto")), true);
|
|
||||||
assert_eq!(is_subpath(String::from("/to"), String::from("/toto")), false);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_from_netscape() {
|
|
||||||
assert_eq!(Cookie::from_netscape("localhost\tFALSE\t/\tFALSE\t0\tcookie2\tvalueA").unwrap(),
|
|
||||||
Cookie {
|
|
||||||
name: "cookie2".to_string(),
|
|
||||||
value: "valueA".to_string(),
|
|
||||||
domain: "localhost".to_string(),
|
|
||||||
path: "/".to_string(),
|
|
||||||
subdomains: false,
|
|
||||||
secure: false,
|
|
||||||
expires: None,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
128
src/http/core.rs
128
src/http/core.rs
@ -1,128 +0,0 @@
|
|||||||
/*
|
|
||||||
* hurl (https://hurl.dev)
|
|
||||||
* Copyright (C) 2020 Orange
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
extern crate reqwest;
|
|
||||||
|
|
||||||
use std::fmt;
|
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
pub enum Encoding {
|
|
||||||
Utf8,
|
|
||||||
Latin1,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Display for Encoding {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
||||||
write!(f, "{}", match self {
|
|
||||||
Encoding::Utf8 => "utf8",
|
|
||||||
Encoding::Latin1 => "iso8859-1"
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
|
||||||
pub struct Url {
|
|
||||||
pub scheme: String,
|
|
||||||
pub host: String,
|
|
||||||
pub port: Option<u16>,
|
|
||||||
pub path: String,
|
|
||||||
pub query_string: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
|
||||||
pub struct Header {
|
|
||||||
pub name: String,
|
|
||||||
pub value: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_header_value(headers: Vec<Header>, name: &str) -> Option<String> {
|
|
||||||
for header in headers {
|
|
||||||
if header.name.as_str() == name {
|
|
||||||
return Some(header.value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
|
||||||
pub struct Param {
|
|
||||||
pub name: String,
|
|
||||||
pub value: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn encode_form_params(params: Vec<Param>) -> Vec<u8> {
|
|
||||||
params
|
|
||||||
.iter()
|
|
||||||
//.map(|p| format!("{}={}", p.name, utf8_percent_encode(p.value.as_str(), FRAGMENT)))
|
|
||||||
.map(|p| format!("{}={}", p.name, url_encode(p.value.clone())))
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
.join("&")
|
|
||||||
.into_bytes()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
fn url_encode(s: String) -> String {
|
|
||||||
const MAX_CHAR_VAL: u32 = std::char::MAX as u32;
|
|
||||||
let mut buff = [0; 4];
|
|
||||||
s.chars()
|
|
||||||
.map(|ch| {
|
|
||||||
match ch as u32 {
|
|
||||||
0..=47 | 58..=64 | 91..=96 | 123..=MAX_CHAR_VAL => {
|
|
||||||
ch.encode_utf8(&mut buff);
|
|
||||||
buff[0..ch.len_utf8()].iter().map(|&byte| format!("%{:x}", byte)).collect::<String>()
|
|
||||||
}
|
|
||||||
_ => ch.to_string(),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect::<String>()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_encode_form_params() {
|
|
||||||
assert_eq!(
|
|
||||||
encode_form_params(vec![
|
|
||||||
Param {
|
|
||||||
name: String::from("param1"),
|
|
||||||
value: String::from("value1"),
|
|
||||||
},
|
|
||||||
Param {
|
|
||||||
name: String::from("param2"),
|
|
||||||
value: String::from(""),
|
|
||||||
}
|
|
||||||
]),
|
|
||||||
vec![
|
|
||||||
112, 97, 114, 97, 109, 49, 61, 118, 97, 108, 117, 101, 49, 38, 112, 97, 114, 97, 109,
|
|
||||||
50, 61
|
|
||||||
]
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
std::str::from_utf8(&encode_form_params(vec![
|
|
||||||
Param { name: String::from("param1"), value: String::from("value1") },
|
|
||||||
Param { name: String::from("param2"), value: String::from("") },
|
|
||||||
Param { name: String::from("param3"), value: String::from("a=b") },
|
|
||||||
Param { name: String::from("param4"), value: String::from("a%3db") },
|
|
||||||
])).unwrap(),
|
|
||||||
"param1=value1¶m2=¶m3=a%3db¶m4=a%253db"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,119 +0,0 @@
|
|||||||
/*
|
|
||||||
* hurl (https://hurl.dev)
|
|
||||||
* Copyright (C) 2020 Orange
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
use serde::ser::{Serializer, SerializeStruct};
|
|
||||||
use serde::Serialize;
|
|
||||||
|
|
||||||
use super::cookie::*;
|
|
||||||
use super::request::*;
|
|
||||||
use super::response::*;
|
|
||||||
|
|
||||||
impl Serialize for Request {
|
|
||||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
|
||||||
where
|
|
||||||
S: Serializer,
|
|
||||||
{
|
|
||||||
// 3 is the number of fields in the struct.
|
|
||||||
let mut state = serializer.serialize_struct("??", 3)?;
|
|
||||||
state.serialize_field("method", &self.clone().method.to_text())?;
|
|
||||||
state.serialize_field("url", &self.clone().url())?;
|
|
||||||
state.serialize_field("queryString", &self.clone().querystring)?;
|
|
||||||
state.serialize_field("headers", &self.clone().headers())?;
|
|
||||||
state.serialize_field("cookies", &self.clone().cookies)?;
|
|
||||||
|
|
||||||
if let Some(params) = self.clone().form_params() {
|
|
||||||
state.serialize_field("format_params", ¶ms)?;
|
|
||||||
}
|
|
||||||
state.serialize_field("body", &base64::encode(&self.body))?;
|
|
||||||
|
|
||||||
state.end()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Serialize for Response {
|
|
||||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
|
||||||
where
|
|
||||||
S: Serializer,
|
|
||||||
{
|
|
||||||
// 3 is the number of fields in the struct.
|
|
||||||
let mut state = serializer.serialize_struct("??", 3)?;
|
|
||||||
state.serialize_field("httpVersion", &self.clone().version)?;
|
|
||||||
state.serialize_field("status", &self.clone().status)?;
|
|
||||||
state.serialize_field("cookies", &self.clone().cookies())?;
|
|
||||||
state.serialize_field("headers", &self.clone().headers)?;
|
|
||||||
|
|
||||||
// WIP - Serialize response body only for json for the timebeing
|
|
||||||
let content_type = self.get_header("content_type", true);
|
|
||||||
if let Some(value) = content_type.first() {
|
|
||||||
if value.as_str() == "application/json; charset=UTF-8" {
|
|
||||||
let s = String::from_utf8(self.body.clone()).expect("Found invalid UTF-8");
|
|
||||||
let result: Result<serde_json::Value, serde_json::Error> = serde_json::from_str(s.as_str());
|
|
||||||
if let Ok(v) = result {
|
|
||||||
state.serialize_field("json", &v)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
state.end()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Serialize for ResponseCookie {
|
|
||||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
|
||||||
where
|
|
||||||
S: Serializer,
|
|
||||||
{
|
|
||||||
// 3 is the number of fields in the struct.
|
|
||||||
let mut state = serializer.serialize_struct("??", 3)?;
|
|
||||||
state.serialize_field("name", &self.clone().name)?;
|
|
||||||
state.serialize_field("value", &self.clone().value)?;
|
|
||||||
if let Some(value) = self.clone().domain {
|
|
||||||
state.serialize_field("domain", &value)?;
|
|
||||||
}
|
|
||||||
state.end()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Serialize for Cookie {
|
|
||||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
|
||||||
where
|
|
||||||
S: Serializer,
|
|
||||||
{
|
|
||||||
let mut state = serializer.serialize_struct("InternalCookie", 3)?;
|
|
||||||
state.serialize_field("name", &self.clone().name)?;
|
|
||||||
state.serialize_field("value", &self.clone().value)?;
|
|
||||||
state.serialize_field("domain", &self.clone().domain)?;
|
|
||||||
state.serialize_field("path", &self.clone().path)?;
|
|
||||||
state.serialize_field("include_subdomain", &self.clone().subdomains)?;
|
|
||||||
state.end()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
impl Serialize for Version {
|
|
||||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
|
||||||
where
|
|
||||||
S: Serializer,
|
|
||||||
{
|
|
||||||
match self {
|
|
||||||
Version::Http10 => serializer.serialize_str("HTTP/1.0"),
|
|
||||||
Version::Http11 => serializer.serialize_str("HTTP/1.1"),
|
|
||||||
Version::Http2 => serializer.serialize_str("HTTP/2"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,391 +0,0 @@
|
|||||||
/*
|
|
||||||
* hurl (https://hurl.dev)
|
|
||||||
* Copyright (C) 2020 Orange
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
extern crate url as external_url;
|
|
||||||
|
|
||||||
use chrono::DateTime;
|
|
||||||
|
|
||||||
use super::cookie::*;
|
|
||||||
use super::core::*;
|
|
||||||
use super::request::*;
|
|
||||||
use super::response::*;
|
|
||||||
|
|
||||||
type ParseError = String;
|
|
||||||
|
|
||||||
pub fn parse_request(value: serde_json::Value) -> Result<Request, ParseError> {
|
|
||||||
if let serde_json::Value::Object(map) = value {
|
|
||||||
let method = match map.get("method") {
|
|
||||||
Some(serde_json::Value::String(s)) => parse_method(s.clone())?,
|
|
||||||
_ => return Err("expecting a string for the method".to_string()),
|
|
||||||
};
|
|
||||||
let url = match map.get("url") {
|
|
||||||
Some(serde_json::Value::String(s)) => parse_url(s.clone())?,
|
|
||||||
_ => return Err("expecting a string for the url".to_string()),
|
|
||||||
};
|
|
||||||
|
|
||||||
let headers = match map.get("headers") {
|
|
||||||
Some(serde_json::Value::Array(values)) => {
|
|
||||||
let mut headers = vec![];
|
|
||||||
for value in values {
|
|
||||||
let header = parse_header(value.clone())?;
|
|
||||||
headers.push(header);
|
|
||||||
}
|
|
||||||
headers
|
|
||||||
}
|
|
||||||
_ => vec![],
|
|
||||||
};
|
|
||||||
|
|
||||||
let cookies = match map.get("cookies") {
|
|
||||||
Some(serde_json::Value::Array(values)) => {
|
|
||||||
let mut headers = vec![];
|
|
||||||
for value in values {
|
|
||||||
let header = parse_response_cookie(value.clone())?;
|
|
||||||
headers.push(header);
|
|
||||||
}
|
|
||||||
headers
|
|
||||||
}
|
|
||||||
_ => vec![],
|
|
||||||
};
|
|
||||||
|
|
||||||
let multipart = vec![];
|
|
||||||
|
|
||||||
Ok(Request {
|
|
||||||
method,
|
|
||||||
url,
|
|
||||||
querystring: vec![],
|
|
||||||
headers,
|
|
||||||
cookies,
|
|
||||||
body: vec![],
|
|
||||||
multipart,
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
Err("expecting an object for the request".to_string())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn parse_response(value: serde_json::Value) -> Result<Response, ParseError> {
|
|
||||||
if let serde_json::Value::Object(map) = value {
|
|
||||||
let status = match map.get("status") {
|
|
||||||
Some(serde_json::Value::Number(x)) => if let Some(x) = x.as_u64() {
|
|
||||||
x as u16
|
|
||||||
} else {
|
|
||||||
return Err("expecting a integer for the status".to_string());
|
|
||||||
},
|
|
||||||
_ => return Err("expecting a number for the status".to_string()),
|
|
||||||
};
|
|
||||||
|
|
||||||
let version = match map.get("httpVersion") {
|
|
||||||
Some(serde_json::Value::String(s)) => parse_version(s.clone())?,
|
|
||||||
_ => return Err("expecting a string for the version".to_string()),
|
|
||||||
};
|
|
||||||
|
|
||||||
let headers = match map.get("headers") {
|
|
||||||
Some(serde_json::Value::Array(values)) => {
|
|
||||||
let mut headers = vec![];
|
|
||||||
for value in values {
|
|
||||||
let header = parse_header(value.clone())?;
|
|
||||||
headers.push(header);
|
|
||||||
}
|
|
||||||
headers
|
|
||||||
}
|
|
||||||
_ => vec![],
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(Response {
|
|
||||||
version,
|
|
||||||
status,
|
|
||||||
headers,
|
|
||||||
body: vec![],
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
Err("expecting an object for the response".to_string())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_method(s: String) -> Result<Method, ParseError> {
|
|
||||||
match s.as_str() {
|
|
||||||
"GET" => Ok(Method::Get),
|
|
||||||
"HEAD" => Ok(Method::Head),
|
|
||||||
"POST" => Ok(Method::Post),
|
|
||||||
"PUT" => Ok(Method::Put),
|
|
||||||
"DELETE" => Ok(Method::Delete),
|
|
||||||
"CONNECT" => Ok(Method::Connect),
|
|
||||||
"OPTIONS" => Ok(Method::Options),
|
|
||||||
"TRACE" => Ok(Method::Trace),
|
|
||||||
"PATCH" => Ok(Method::Patch),
|
|
||||||
_ => Err(format!("Invalid method <{}>", s))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_url(s: String) -> Result<Url, ParseError> {
|
|
||||||
match external_url::Url::parse(s.as_str()) {
|
|
||||||
Err(_) => Err(format!("Invalid url <{}>", s)),
|
|
||||||
Ok(u) => Ok(Url {
|
|
||||||
scheme: u.scheme().to_string(),
|
|
||||||
host: u.host_str().unwrap().to_string(),
|
|
||||||
port: u.port(),
|
|
||||||
path: u.path().to_string(),
|
|
||||||
query_string: if let Some(s) = u.query() { s.to_string() } else { "".to_string() },
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_header(value: serde_json::Value) -> Result<Header, ParseError> {
|
|
||||||
if let serde_json::Value::Object(map) = value {
|
|
||||||
let name = match map.get("name") {
|
|
||||||
Some(serde_json::Value::String(s)) => s.to_string(),
|
|
||||||
_ => return Err("expecting a string for the header name".to_string()),
|
|
||||||
};
|
|
||||||
let value = match map.get("value") {
|
|
||||||
Some(serde_json::Value::String(s)) => s.to_string(),
|
|
||||||
_ => return Err("expecting a string for the header value".to_string()),
|
|
||||||
};
|
|
||||||
Ok(Header { name, value })
|
|
||||||
} else {
|
|
||||||
Err("Expecting object for one header".to_string())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn parse_response_cookie(value: serde_json::Value) -> Result<ResponseCookie, ParseError> {
|
|
||||||
if let serde_json::Value::Object(map) = value {
|
|
||||||
let name = match map.get("name") {
|
|
||||||
Some(serde_json::Value::String(s)) => s.to_string(),
|
|
||||||
_ => return Err("expecting a string for the cookie name".to_string()),
|
|
||||||
};
|
|
||||||
let value = match map.get("value") {
|
|
||||||
Some(serde_json::Value::String(s)) => s.to_string(),
|
|
||||||
_ => return Err("expecting a string for the cookie value".to_string()),
|
|
||||||
};
|
|
||||||
let domain = match map.get("domain") {
|
|
||||||
None => None,
|
|
||||||
Some(serde_json::Value::String(s)) => Some(s.to_string()),
|
|
||||||
_ => return Err("expecting a string for the cookie domain".to_string()),
|
|
||||||
};
|
|
||||||
let path = match map.get("path") {
|
|
||||||
None => None,
|
|
||||||
Some(serde_json::Value::String(s)) => Some(s.to_string()),
|
|
||||||
_ => return Err("expecting a string for the cookie path".to_string()),
|
|
||||||
};
|
|
||||||
let expires = match map.get("expires") {
|
|
||||||
None => None,
|
|
||||||
Some(serde_json::Value::String(s)) => Some(s.to_string()),
|
|
||||||
_ => return Err("expecting a string for the cookie expires".to_string()),
|
|
||||||
};
|
|
||||||
let secure = match map.get("secure") {
|
|
||||||
None => None,
|
|
||||||
Some(serde_json::Value::Bool(value)) => Some(*value),
|
|
||||||
_ => return Err("expecting a bool for the cookie secure flag".to_string()),
|
|
||||||
};
|
|
||||||
let http_only = match map.get("http_only") {
|
|
||||||
None => None,
|
|
||||||
Some(serde_json::Value::Bool(value)) => Some(*value),
|
|
||||||
_ => return Err("expecting a bool for the cookie http_only flag".to_string()),
|
|
||||||
};
|
|
||||||
let same_site = match map.get("same_site") {
|
|
||||||
None => None,
|
|
||||||
Some(serde_json::Value::String(s)) => Some(s.to_string()),
|
|
||||||
_ => return Err("expecting a string for the cookie same_site".to_string()),
|
|
||||||
};
|
|
||||||
Ok(ResponseCookie { name, value, max_age: None, domain, path, secure, http_only, expires, same_site })
|
|
||||||
} else {
|
|
||||||
Err("Expecting object for one cookie".to_string())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn parse_cookie(value: serde_json::Value) -> Result<Cookie, ParseError> {
|
|
||||||
if let serde_json::Value::Object(map) = value {
|
|
||||||
let name = match map.get("name") {
|
|
||||||
Some(serde_json::Value::String(s)) => s.to_string(),
|
|
||||||
_ => return Err("expecting a string for the cookie name".to_string()),
|
|
||||||
};
|
|
||||||
let value = match map.get("value") {
|
|
||||||
Some(serde_json::Value::String(s)) => s.to_string(),
|
|
||||||
_ => return Err("expecting a string for the cookie value".to_string()),
|
|
||||||
};
|
|
||||||
let domain = match map.get("domain") {
|
|
||||||
Some(serde_json::Value::String(s)) => s.to_string(),
|
|
||||||
_ => return Err("expecting a string for the cookie domain".to_string()),
|
|
||||||
};
|
|
||||||
let path = match map.get("path") {
|
|
||||||
Some(serde_json::Value::String(s)) => s.to_string(),
|
|
||||||
_ => return Err("expecting a string for the cookie path".to_string()),
|
|
||||||
};
|
|
||||||
let subdomains = match map.get("include_subdomain") {
|
|
||||||
Some(serde_json::Value::Bool(v)) => *v,
|
|
||||||
_ => return Err("expecting a bool for the include_subdomain".to_string()),
|
|
||||||
};
|
|
||||||
let secure = match map.get("secure") {
|
|
||||||
None => false,
|
|
||||||
Some(serde_json::Value::Bool(value)) => *value,
|
|
||||||
_ => return Err("expecting a bool for the secure flag".to_string()),
|
|
||||||
};
|
|
||||||
let expires = match map.get("expired") {
|
|
||||||
None => None,
|
|
||||||
Some(serde_json::Value::String(v)) => {
|
|
||||||
match DateTime::parse_from_rfc3339(v.as_str()) {
|
|
||||||
Ok(v) => Some(v.naive_utc()),
|
|
||||||
Err(_) => return Err("expecting a String (date) for the expired fieldate can be parsed".to_string()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => return Err("expecting a String (date) for the expired field".to_string()),
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(Cookie { name, value, domain, path, subdomains, secure, expires })
|
|
||||||
} else {
|
|
||||||
Err("Expecting object for one cookie".to_string())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_version(s: String) -> Result<Version, ParseError> {
|
|
||||||
match s.as_str() {
|
|
||||||
"HTTP/1.0" => Ok(Version::Http10),
|
|
||||||
"HTTP/1.1" => Ok(Version::Http11),
|
|
||||||
"HTTP/2" => Ok(Version::Http2),
|
|
||||||
_ => Err("Expecting version HTTP/1.0, HTTP/1.2 or HTTP/2".to_string())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use super::super::request::tests::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_parse_request() {
|
|
||||||
let v: serde_json::Value = serde_json::from_str(r#"{
|
|
||||||
"method": "GET",
|
|
||||||
"url": "http://localhost:8000/hello",
|
|
||||||
"headers": []
|
|
||||||
}"#).unwrap();
|
|
||||||
assert_eq!(parse_request(v).unwrap(), hello_http_request());
|
|
||||||
|
|
||||||
let v: serde_json::Value = serde_json::from_str(r#"{
|
|
||||||
"method": "GET",
|
|
||||||
"url": "http://localhost:8000/querystring-params?param1=value1¶m2=a%20b",
|
|
||||||
"headers": []
|
|
||||||
}"#).unwrap();
|
|
||||||
assert_eq!(parse_request(v).unwrap(), Request {
|
|
||||||
method: Method::Get,
|
|
||||||
url: Url {
|
|
||||||
scheme: "http".to_string(),
|
|
||||||
host: "localhost".to_string(),
|
|
||||||
port: Some(8000),
|
|
||||||
path: "/querystring-params".to_string(),
|
|
||||||
query_string: "param1=value1¶m2=a%20b".to_string(),
|
|
||||||
},
|
|
||||||
querystring: vec![],
|
|
||||||
headers: vec![],
|
|
||||||
cookies: vec![],
|
|
||||||
body: vec![],
|
|
||||||
multipart: vec![],
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
let v: serde_json::Value = serde_json::from_str(r#"{
|
|
||||||
"method": "GET",
|
|
||||||
"url": "http://localhost/custom",
|
|
||||||
"headers": [
|
|
||||||
{"name": "User-Agent", "value": "iPhone"},
|
|
||||||
{"name": "Foo", "value": "Bar"}
|
|
||||||
],
|
|
||||||
"cookies": [
|
|
||||||
{"name": "theme", "value": "light"},
|
|
||||||
{"name": "sessionToken", "value": "abc123"}
|
|
||||||
]
|
|
||||||
}"#).unwrap();
|
|
||||||
assert_eq!(parse_request(v).unwrap(), custom_http_request());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_parse_response() {
|
|
||||||
let v: serde_json::Value = serde_json::from_str(r#"{
|
|
||||||
"status": 200,
|
|
||||||
"httpVersion": "HTTP/1.0",
|
|
||||||
"headers": [
|
|
||||||
{"name": "Content-Type", "value": "text/html; charset=utf-8" },
|
|
||||||
{"name": "Content-Length", "value": "12" }
|
|
||||||
|
|
||||||
]
|
|
||||||
}"#).unwrap();
|
|
||||||
assert_eq!(parse_response(v).unwrap(), Response {
|
|
||||||
version: Version::Http10,
|
|
||||||
status: 200,
|
|
||||||
headers: vec![
|
|
||||||
Header { name: String::from("Content-Type"), value: String::from("text/html; charset=utf-8") },
|
|
||||||
Header { name: String::from("Content-Length"), value: String::from("12") },
|
|
||||||
],
|
|
||||||
body: vec![],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_parse_method() {
|
|
||||||
assert_eq!(parse_method("GET".to_string()).unwrap(), Method::Get);
|
|
||||||
|
|
||||||
let error = parse_method("x".to_string()).err().unwrap();
|
|
||||||
assert_eq!(error, "Invalid method <x>");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_parse_url() {
|
|
||||||
assert_eq!(
|
|
||||||
parse_url("http://localhost:8000/query?param1=value1".to_string()).unwrap(),
|
|
||||||
Url {
|
|
||||||
scheme: "http".to_string(),
|
|
||||||
host: "localhost".to_string(),
|
|
||||||
port: Some(8000),
|
|
||||||
path: "/query".to_string(),
|
|
||||||
query_string: "param1=value1".to_string(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_parse_header() {
|
|
||||||
let v: serde_json::Value = serde_json::from_str(r#"{
|
|
||||||
"name": "name1",
|
|
||||||
"value": "value1"
|
|
||||||
}"#).unwrap();
|
|
||||||
assert_eq!(parse_header(v).unwrap(), Header { name: "name1".to_string(), value: "value1".to_string() });
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_parse_response_cookie() {
|
|
||||||
let v: serde_json::Value = serde_json::from_str(r#"{
|
|
||||||
"name": "name1",
|
|
||||||
"value": "value1"
|
|
||||||
}"#).unwrap();
|
|
||||||
assert_eq!(parse_response_cookie(v).unwrap(),
|
|
||||||
ResponseCookie {
|
|
||||||
name: "name1".to_string(),
|
|
||||||
value: "value1".to_string(),
|
|
||||||
max_age: None,
|
|
||||||
domain: None,
|
|
||||||
path: None,
|
|
||||||
secure: None,
|
|
||||||
http_only: None,
|
|
||||||
expires: None,
|
|
||||||
same_site: None,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_parse_version() {
|
|
||||||
assert_eq!(parse_version("HTTP/1.0".to_string()).unwrap(), Version::Http10);
|
|
||||||
}
|
|
||||||
}
|
|
@ -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::*;
|
||||||
|
@ -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¶m2
|
||||||
|
pub fn query_http_request() -> Request {
|
||||||
|
Request {
|
||||||
|
method: Method::Get,
|
||||||
|
url: "http://localhost:8000/querystring-params".to_string(),
|
||||||
|
querystring: vec![
|
||||||
|
Param { name: String::from("param1"), value: String::from("value1") },
|
||||||
|
Param { name: String::from("param2"), value: String::from("a b") },
|
||||||
|
],
|
||||||
|
headers: vec![],
|
||||||
|
cookies: vec![],
|
||||||
|
body: vec![],
|
||||||
|
multipart: vec![],
|
||||||
|
form: vec![],
|
||||||
|
content_type: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -226,7 +312,112 @@ pub mod tests {
|
|||||||
cookies: vec![],
|
cookies: vec![],
|
||||||
body: "param1=value1¶m2=¶m3=a%3db¶m4=a%253db".to_string().into_bytes(),
|
body: "param1=value1¶m2=¶m3=a%3db¶m4=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],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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;
|
@ -1,413 +0,0 @@
|
|||||||
/*
|
|
||||||
* hurl (https://hurl.dev)
|
|
||||||
* Copyright (C) 2020 Orange
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
use std::fmt;
|
|
||||||
|
|
||||||
use percent_encoding::{AsciiSet, CONTROLS, percent_decode, utf8_percent_encode};
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
use super::cookie::*;
|
|
||||||
use super::core::*;
|
|
||||||
|
|
||||||
const FRAGMENT: &AsciiSet = &CONTROLS
|
|
||||||
.add(b' ')
|
|
||||||
.add(b'"')
|
|
||||||
.add(b':')
|
|
||||||
.add(b'/')
|
|
||||||
.add(b'<')
|
|
||||||
.add(b'>')
|
|
||||||
.add(b'+')
|
|
||||||
.add(b'=')
|
|
||||||
.add(b'?')
|
|
||||||
.add(b'%')
|
|
||||||
.add(b'`');
|
|
||||||
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
||||||
pub struct Request {
|
|
||||||
pub method: Method,
|
|
||||||
pub url: Url,
|
|
||||||
pub querystring: Vec<Param>,
|
|
||||||
pub headers: Vec<Header>,
|
|
||||||
pub cookies: Vec<ResponseCookie>,
|
|
||||||
pub body: Vec<u8>,
|
|
||||||
pub multipart: Vec<MultipartParam>,
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
||||||
pub enum MultipartParam {
|
|
||||||
TextParam { name: String, value: String },
|
|
||||||
FileParam { name: String, filename: String, content_type: Option<String> },
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
fn has_header(headers: &[Header], name: String) -> bool {
|
|
||||||
for header in headers {
|
|
||||||
if header.name.as_str() == name {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
pub enum RequestEncoding {
|
|
||||||
Utf8,
|
|
||||||
Latin1,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Display for RequestEncoding {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
||||||
write!(f, "{}", match self {
|
|
||||||
RequestEncoding::Utf8 => "utf8",
|
|
||||||
RequestEncoding::Latin1 => "iso8859-1"
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Request {
|
|
||||||
pub fn host(self) -> String {
|
|
||||||
self.url.host
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn url(self) -> String {
|
|
||||||
let port = match self.url.port {
|
|
||||||
None => String::from(""),
|
|
||||||
Some(p) => format!(":{}", p)
|
|
||||||
};
|
|
||||||
let querystring = self.url.query_string.clone();
|
|
||||||
|
|
||||||
// add params
|
|
||||||
let querystring = if self.querystring.is_empty() {
|
|
||||||
querystring
|
|
||||||
} else {
|
|
||||||
let mut buf = querystring.clone();
|
|
||||||
if !querystring.is_empty() {
|
|
||||||
buf.push('&');
|
|
||||||
}
|
|
||||||
for param in self.querystring {
|
|
||||||
if !buf.is_empty() {
|
|
||||||
buf.push('&');
|
|
||||||
}
|
|
||||||
let encoded = utf8_percent_encode(param.value.as_str(), FRAGMENT).to_string();
|
|
||||||
buf.push_str(format!("{}={}", param.name, encoded).as_str());
|
|
||||||
}
|
|
||||||
buf
|
|
||||||
};
|
|
||||||
let querystring = if querystring.is_empty() {
|
|
||||||
"".to_string()
|
|
||||||
} else {
|
|
||||||
format!("?{}", querystring)
|
|
||||||
};
|
|
||||||
return format!("{}://{}{}{}{}",
|
|
||||||
self.url.scheme,
|
|
||||||
self.url.host,
|
|
||||||
port,
|
|
||||||
self.url.path,
|
|
||||||
querystring
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn headers(self) -> Vec<Header> {
|
|
||||||
let mut headers: Vec<Header> = self.headers.clone();
|
|
||||||
let user_agent = format!("hurl/{}", clap::crate_version!());
|
|
||||||
let default_headers = vec![
|
|
||||||
(String::from("User-Agent"), user_agent),
|
|
||||||
(String::from("Host"), self.url.clone().host)
|
|
||||||
];
|
|
||||||
|
|
||||||
for (name, value) in default_headers {
|
|
||||||
if !has_header(&self.headers, name.clone()) {
|
|
||||||
headers.push(Header { name, value });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !self.cookies.is_empty() {
|
|
||||||
headers.push(Header {
|
|
||||||
name: String::from("Cookie"),
|
|
||||||
value: self.cookies
|
|
||||||
.iter()
|
|
||||||
.map(|c| format!("{}={}", c.name, c.value))
|
|
||||||
.collect::<Vec<String>>()
|
|
||||||
.join("; "),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
headers
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn content_type(self) -> Option<String> {
|
|
||||||
for Header { name, value } in self.headers {
|
|
||||||
if name.as_str() == "Content-Type" {
|
|
||||||
return Some(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn add_session_cookies(&mut self, cookies: Vec<ResponseCookie>) {
|
|
||||||
//eprintln!("add session cookies {:?}", cookies);
|
|
||||||
|
|
||||||
for cookie in cookies {
|
|
||||||
|
|
||||||
// TBC: both request and session cookies should have a domain => should not be an Option
|
|
||||||
let session_domain = cookie.clone().domain.unwrap();
|
|
||||||
match self.clone().get_cookie(cookie.clone().name) {
|
|
||||||
Some(ResponseCookie { domain: Some(domain), .. }) => {
|
|
||||||
if session_domain != domain {
|
|
||||||
self.cookies.push(cookie.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
self.cookies.push(cookie.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
pub fn get_cookie(self, name: String) -> Option<ResponseCookie> {
|
|
||||||
for cookie in self.cookies {
|
|
||||||
if cookie.name == name {
|
|
||||||
return Some(cookie);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
pub fn form_params(self) -> Option<Vec<Param>> {
|
|
||||||
if self.clone().content_type() != Some(String::from("application/x-www-form-urlencoded")) {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
let decoded = percent_decode(&self.body);
|
|
||||||
let params = match decoded.decode_utf8() {
|
|
||||||
Ok(v) => {
|
|
||||||
let params: Vec<&str> = v.split('&').collect();
|
|
||||||
params.iter().map(|s| Param::parse(s)).collect()
|
|
||||||
}
|
|
||||||
_ => vec![]
|
|
||||||
};
|
|
||||||
Some(params)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
pub fn encoding(&self) -> Encoding {
|
|
||||||
if let Some(v) = self.get_header("content-type", true) {
|
|
||||||
if v.contains("charset=ISO-8859-1") {
|
|
||||||
return Encoding::Latin1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Encoding::Utf8
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_header(&self, name: &str, case_sensitive: bool) -> Option<String> {
|
|
||||||
for header in self.headers.clone() {
|
|
||||||
if header.name == name
|
|
||||||
|| !case_sensitive && header.name.to_lowercase() == name.to_lowercase()
|
|
||||||
{
|
|
||||||
return Some(header.value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Param {
|
|
||||||
fn parse(s: &str) -> Param {
|
|
||||||
match s.find('=') {
|
|
||||||
None => Param { name: s.to_string(), value: String::from("") },
|
|
||||||
Some(i) => {
|
|
||||||
let (name, value) = s.split_at(i);
|
|
||||||
Param { name: name.to_string(), value: value[1..].to_string() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
|
||||||
pub enum Method {
|
|
||||||
Get,
|
|
||||||
Head,
|
|
||||||
Post,
|
|
||||||
Put,
|
|
||||||
Delete,
|
|
||||||
Connect,
|
|
||||||
Options,
|
|
||||||
Trace,
|
|
||||||
Patch,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Method {
|
|
||||||
pub fn to_reqwest(&self) -> reqwest::Method {
|
|
||||||
match self {
|
|
||||||
Method::Get => reqwest::Method::GET,
|
|
||||||
Method::Head => reqwest::Method::HEAD,
|
|
||||||
Method::Post => reqwest::Method::POST,
|
|
||||||
Method::Put => reqwest::Method::PUT,
|
|
||||||
Method::Delete => reqwest::Method::DELETE,
|
|
||||||
Method::Connect => reqwest::Method::CONNECT,
|
|
||||||
Method::Options => reqwest::Method::OPTIONS,
|
|
||||||
Method::Trace => reqwest::Method::TRACE,
|
|
||||||
Method::Patch => reqwest::Method::PATCH,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
pub mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
pub fn hello_http_request() -> Request {
|
|
||||||
Request {
|
|
||||||
method: Method::Get,
|
|
||||||
url: Url {
|
|
||||||
scheme: "http".to_string(),
|
|
||||||
host: "localhost".to_string(),
|
|
||||||
port: Some(8000),
|
|
||||||
path: "/hello".to_string(),
|
|
||||||
query_string: "".to_string(),
|
|
||||||
},
|
|
||||||
querystring: vec![],
|
|
||||||
headers: vec![],
|
|
||||||
cookies: vec![],
|
|
||||||
body: vec![],
|
|
||||||
multipart: vec![],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GET http://localhost:8000/querystring-params?param1=value1¶m2
|
|
||||||
pub fn query_http_request() -> Request {
|
|
||||||
Request {
|
|
||||||
method: Method::Get,
|
|
||||||
url: Url {
|
|
||||||
scheme: "http".to_string(),
|
|
||||||
host: "localhost".to_string(),
|
|
||||||
port: Some(8000),
|
|
||||||
path: "/querystring-params".to_string(),
|
|
||||||
query_string: "".to_string(),
|
|
||||||
},
|
|
||||||
querystring: vec![
|
|
||||||
Param { name: String::from("param1"), value: String::from("value1") },
|
|
||||||
Param { name: String::from("param2"), value: String::from("a b") },
|
|
||||||
],
|
|
||||||
headers: vec![],
|
|
||||||
cookies: vec![],
|
|
||||||
body: vec![],
|
|
||||||
multipart: vec![],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
pub fn custom_http_request() -> Request {
|
|
||||||
Request {
|
|
||||||
method: Method::Get,
|
|
||||||
url: Url {
|
|
||||||
scheme: "http".to_string(),
|
|
||||||
host: "localhost".to_string(),
|
|
||||||
port: None,
|
|
||||||
path: "/custom".to_string(),
|
|
||||||
query_string: "".to_string(),
|
|
||||||
},
|
|
||||||
querystring: vec![],
|
|
||||||
headers: vec![
|
|
||||||
Header { name: String::from("User-Agent"), value: String::from("iPhone") },
|
|
||||||
Header { name: String::from("Foo"), value: String::from("Bar") },
|
|
||||||
],
|
|
||||||
cookies: vec![
|
|
||||||
ResponseCookie {
|
|
||||||
name: String::from("theme"),
|
|
||||||
value: String::from("light"),
|
|
||||||
max_age: None,
|
|
||||||
domain: None,
|
|
||||||
path: None,
|
|
||||||
secure: None,
|
|
||||||
http_only: None,
|
|
||||||
expires: None,
|
|
||||||
same_site: None,
|
|
||||||
},
|
|
||||||
ResponseCookie {
|
|
||||||
name: String::from("sessionToken"),
|
|
||||||
value: String::from("abc123"),
|
|
||||||
max_age: None,
|
|
||||||
domain: None,
|
|
||||||
path: None,
|
|
||||||
secure: None,
|
|
||||||
http_only: None,
|
|
||||||
expires: None,
|
|
||||||
same_site: None,
|
|
||||||
}
|
|
||||||
],
|
|
||||||
body: vec![],
|
|
||||||
multipart: vec![],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
pub fn form_http_request() -> Request {
|
|
||||||
Request {
|
|
||||||
method: Method::Post,
|
|
||||||
url: Url {
|
|
||||||
scheme: "http".to_string(),
|
|
||||||
host: "localhost".to_string(),
|
|
||||||
port: None,
|
|
||||||
path: "/form-params".to_string(),
|
|
||||||
query_string: "".to_string(),
|
|
||||||
},
|
|
||||||
querystring: vec![],
|
|
||||||
headers: vec![
|
|
||||||
Header { name: String::from("Content-Type"), value: String::from("application/x-www-form-urlencoded") },
|
|
||||||
],
|
|
||||||
cookies: vec![],
|
|
||||||
body: "param1=value1¶m2=¶m3=a%3db¶m4=a%253db".to_string().into_bytes(),
|
|
||||||
multipart: vec![],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
pub fn test_headers() {
|
|
||||||
assert_eq!(hello_http_request().headers(), vec![
|
|
||||||
Header { name: String::from("User-Agent"), value: format!("hurl/{}", clap::crate_version!()) },
|
|
||||||
Header { name: String::from("Host"), value: String::from("localhost") }
|
|
||||||
]);
|
|
||||||
|
|
||||||
assert_eq!(custom_http_request().headers(), vec![
|
|
||||||
Header { name: String::from("User-Agent"), value: String::from("iPhone") },
|
|
||||||
Header { name: String::from("Foo"), value: String::from("Bar") },
|
|
||||||
Header { name: String::from("Host"), value: String::from("localhost") },
|
|
||||||
Header { name: String::from("Cookie"), value: String::from("theme=light; sessionToken=abc123") },
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
pub fn test_url() {
|
|
||||||
assert_eq!(hello_http_request().url(), String::from("http://localhost:8000/hello"));
|
|
||||||
assert_eq!(query_http_request().url(), String::from("http://localhost:8000/querystring-params?param1=value1¶m2=a%20b"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
pub fn test_form_params() {
|
|
||||||
assert_eq!(hello_http_request().form_params(), None);
|
|
||||||
assert_eq!(form_http_request().form_params().unwrap(), vec![
|
|
||||||
Param { name: String::from("param1"), value: String::from("value1") },
|
|
||||||
Param { name: String::from("param2"), value: String::from("") },
|
|
||||||
Param { name: String::from("param3"), value: String::from("a=b") },
|
|
||||||
Param { name: String::from("param4"), value: String::from("a%3db") },
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,268 +0,0 @@
|
|||||||
/*
|
|
||||||
* hurl (https://hurl.dev)
|
|
||||||
* Copyright (C) 2020 Orange
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
use std::fmt;
|
|
||||||
|
|
||||||
use super::cookie::*;
|
|
||||||
use super::core::*;
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
||||||
pub struct Response {
|
|
||||||
pub version: Version,
|
|
||||||
pub status: u16,
|
|
||||||
pub headers: Vec<Header>,
|
|
||||||
pub body: Vec<u8>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
||||||
pub enum Version {
|
|
||||||
Http10,
|
|
||||||
Http11,
|
|
||||||
Http2,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Display for Version {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
||||||
let s = match self {
|
|
||||||
Version::Http10 => "1.0",
|
|
||||||
Version::Http11 => "1.1",
|
|
||||||
Version::Http2 => "2",
|
|
||||||
};
|
|
||||||
write!(f, "{}", s)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Response {
|
|
||||||
pub fn get_header(&self, name: &str, case_sensitive: bool) -> Vec<String> {
|
|
||||||
let mut values = vec![];
|
|
||||||
for header in self.headers.clone() {
|
|
||||||
if header.name == name
|
|
||||||
|| !case_sensitive && header.name.to_lowercase() == name.to_lowercase()
|
|
||||||
{
|
|
||||||
values.push(header.value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
values
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_cookie(&self, name: &str) -> Option<ResponseCookie> {
|
|
||||||
for cookie in self.cookies() {
|
|
||||||
if cookie.name.as_str() == name
|
|
||||||
{
|
|
||||||
return Some(cookie);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn cookies(&self) -> Vec<ResponseCookie> {
|
|
||||||
let mut cookies = vec![];
|
|
||||||
for Header { name, value } in self.clone().headers {
|
|
||||||
if name.to_lowercase() == "set-cookie" {
|
|
||||||
let c = cookie::Cookie::parse(value.as_str()).unwrap();
|
|
||||||
// eprintln!(">>> parse set-cookie header");
|
|
||||||
// eprintln!(">>> c = {:?}", c);
|
|
||||||
//
|
|
||||||
// let fields = value.split(";").collect::<Vec<&str>>();
|
|
||||||
// let name_value = fields.get(0).unwrap().split("=").collect::<Vec<&str>>();
|
|
||||||
// let name = name_value.get(0).unwrap().to_string();
|
|
||||||
// let value = name_value.get(1).unwrap().to_string();
|
|
||||||
let name = c.name().to_string();
|
|
||||||
let value = c.value().to_string();
|
|
||||||
let max_age = match c.max_age() {
|
|
||||||
None => None,
|
|
||||||
Some(d) => Some(d.num_seconds())
|
|
||||||
};
|
|
||||||
let expires = match c.expires() {
|
|
||||||
None => None,
|
|
||||||
Some(time) => Some(time.rfc822().to_string())
|
|
||||||
};
|
|
||||||
let domain = match c.domain() {
|
|
||||||
None => None,
|
|
||||||
Some(v) => Some(v.to_string())
|
|
||||||
};
|
|
||||||
let path = match c.path() {
|
|
||||||
None => None,
|
|
||||||
Some(v) => Some(v.to_string())
|
|
||||||
};
|
|
||||||
let secure = if let Some(value) = c.secure() {
|
|
||||||
Some(value)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
let http_only = if let Some(value) = c.http_only() {
|
|
||||||
Some(value)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
let same_site = match c.same_site() {
|
|
||||||
None => None,
|
|
||||||
Some(v) => Some(v.to_string())
|
|
||||||
};
|
|
||||||
cookies.push(ResponseCookie { name, value, max_age, expires, domain, path, secure, http_only, same_site });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
cookies
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Response {
|
|
||||||
pub fn content_type(&self) -> Option<String> {
|
|
||||||
let values = self.get_header("content-type", true);
|
|
||||||
if let Some(value) = values.first() {
|
|
||||||
Some(value.clone())
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn encoding(&self) -> Encoding {
|
|
||||||
if let Some(value) = self.content_type() {
|
|
||||||
if value.contains("charset=ISO-8859-1") {
|
|
||||||
return Encoding::Latin1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Encoding::Utf8
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn has_utf8_body(&self) -> bool {
|
|
||||||
if let Some(value) = self.content_type() {
|
|
||||||
value.contains("charset=utf-8")
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
pub fn is_html(&self) -> bool {
|
|
||||||
if let Some(value) = self.content_type() {
|
|
||||||
value.contains("html")
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
pub mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
pub fn hello_http_response() -> Response {
|
|
||||||
Response {
|
|
||||||
version: Version::Http10,
|
|
||||||
status: 200,
|
|
||||||
headers: vec![
|
|
||||||
Header { name: String::from("Content-Type"), value: String::from("text/html; charset=utf-8") },
|
|
||||||
Header { name: String::from("Content-Length"), value: String::from("12") },
|
|
||||||
],
|
|
||||||
body: String::into_bytes(String::from("Hello World!")),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn html_http_response() -> Response {
|
|
||||||
Response {
|
|
||||||
version: Version::Http10,
|
|
||||||
status: 200,
|
|
||||||
headers: vec![
|
|
||||||
Header { name: String::from("Content-Type"), value: String::from("text/html; charset=utf-8") },
|
|
||||||
],
|
|
||||||
body: String::into_bytes(String::from("<html><head><meta charset=\"UTF-8\"></head><body><br></body></html>")),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn xml_invalid_response() -> Response {
|
|
||||||
Response {
|
|
||||||
version: Version::Http10,
|
|
||||||
status: 200,
|
|
||||||
headers: vec![
|
|
||||||
Header { name: String::from("Content-Type"), value: String::from("text/html; charset=utf-8") },
|
|
||||||
Header { name: String::from("Content-Length"), value: String::from("12") },
|
|
||||||
],
|
|
||||||
body: String::into_bytes(r#"
|
|
||||||
xxx
|
|
||||||
"#.to_string()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn xml_two_users_http_response() -> Response {
|
|
||||||
Response {
|
|
||||||
version: Version::Http10,
|
|
||||||
status: 200,
|
|
||||||
headers: vec![
|
|
||||||
Header { name: String::from("Content-Type"), value: String::from("text/html; charset=utf-8") },
|
|
||||||
Header { name: String::from("Content-Length"), value: String::from("12") },
|
|
||||||
],
|
|
||||||
body: String::into_bytes(r#"
|
|
||||||
<?xml version="1.0"?>
|
|
||||||
<users>
|
|
||||||
<user id="1">Bob</user>
|
|
||||||
<user id="2">Bill</user>
|
|
||||||
</users>
|
|
||||||
"#.to_string()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn xml_three_users_http_response() -> Response {
|
|
||||||
Response {
|
|
||||||
version: Version::Http10,
|
|
||||||
status: 200,
|
|
||||||
headers: vec![
|
|
||||||
Header { name: String::from("Content-Type"), value: String::from("text/html; charset=utf-8") },
|
|
||||||
Header { name: String::from("Content-Length"), value: String::from("12") },
|
|
||||||
],
|
|
||||||
body: String::into_bytes(r#"
|
|
||||||
<?xml version="1.0"?>
|
|
||||||
<users>
|
|
||||||
<user id="1">Bob</user>
|
|
||||||
<user id="2">Bill</user>
|
|
||||||
<user id="3">Bruce</user>
|
|
||||||
</users>
|
|
||||||
"#.to_string()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn json_http_response() -> Response {
|
|
||||||
Response {
|
|
||||||
version: Version::Http10,
|
|
||||||
status: 0,
|
|
||||||
headers: vec![],
|
|
||||||
body: String::into_bytes(r#"
|
|
||||||
{
|
|
||||||
"success":false,
|
|
||||||
"errors": [
|
|
||||||
{ "id": "error1"},
|
|
||||||
{"id": "error2"}
|
|
||||||
],
|
|
||||||
"duration": 1.5
|
|
||||||
}
|
|
||||||
"#.to_string()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn bytes_http_response() -> Response {
|
|
||||||
Response {
|
|
||||||
version: Version::Http10,
|
|
||||||
status: 200,
|
|
||||||
headers: vec![
|
|
||||||
Header { name: String::from("Content-Type"), value: String::from("application/octet-stream") },
|
|
||||||
Header { name: String::from("Content-Length"), value: String::from("1") },
|
|
||||||
],
|
|
||||||
body: vec![255],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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),
|
||||||
|
@ -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),
|
||||||
|
@ -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>() {
|
||||||
|
@ -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(),
|
||||||
|
@ -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("");
|
||||||
|
}
|
@ -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,
|
||||||
|
@ -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,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
@ -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
223
src/runner/multipart.rs
Normal 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());
|
||||||
|
}
|
||||||
|
}
|
@ -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());
|
||||||
}
|
}
|
||||||
|
@ -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"));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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(),
|
||||||
|
@ -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!");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
298
tests/http.rs
298
tests/http.rs
@ -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¶m2¶m3=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¶m2='
|
|
||||||
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¶m2=¶m3=a%3db¶m4=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);
|
|
||||||
*/
|
|
||||||
}
|
|
@ -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());
|
||||||
|
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user