mirror of
https://github.com/Orange-OpenSource/hurl.git
synced 2024-12-24 11:32:00 +03:00
This PR adds support for configuring the client cert and private key for establishing a mutual TLS connection to a server.
Example: ``` GET https://localhost:8443/ [Options] cacert: out/HurlCA.crt cert: out/client.crt key: out/client.key HTTP 200 ``` outputs: ``` Request can be run with the following curl command: * curl 'https://localhost:8443/' --cacert out/HurlCA.crt --cert out/client.crt --key out/client.key ```
This commit is contained in:
parent
e2fd4405c3
commit
04ebc958c6
@ -34,6 +34,8 @@ use crate::runner::Value;
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct CliOptions {
|
||||
pub cacert_file: Option<String>,
|
||||
pub client_cert_file: Option<String>,
|
||||
pub client_key_file: Option<String>,
|
||||
pub color: bool,
|
||||
pub compressed: bool,
|
||||
pub connect_timeout: Duration,
|
||||
@ -105,6 +107,21 @@ pub fn app(version: &str) -> Command {
|
||||
.help("CA certificate to verify peer against (PEM format)")
|
||||
.num_args(1)
|
||||
)
|
||||
.arg(
|
||||
clap::Arg::new("client_cert_file")
|
||||
.long("cert")
|
||||
.value_name("FILE")
|
||||
.help("Client certificate file and password")
|
||||
.num_args(1)
|
||||
.short('E')
|
||||
)
|
||||
.arg(
|
||||
clap::Arg::new("client_key_file")
|
||||
.long("key")
|
||||
.value_name("FILE")
|
||||
.help("Private key file name")
|
||||
.num_args(1)
|
||||
)
|
||||
.arg(
|
||||
clap::Arg::new("color")
|
||||
.long("color")
|
||||
@ -378,6 +395,28 @@ pub fn parse_options(matches: &ArgMatches) -> Result<CliOptions, CliError> {
|
||||
}
|
||||
}
|
||||
};
|
||||
let client_cert_file = match get::<String>(matches, "client_cert_file") {
|
||||
None => None,
|
||||
Some(filename) => {
|
||||
if !Path::new(&filename).is_file() {
|
||||
let message = format!("File {} does not exist", filename);
|
||||
return Err(CliError { message });
|
||||
} else {
|
||||
Some(filename)
|
||||
}
|
||||
}
|
||||
};
|
||||
let client_key_file = match get::<String>(matches, "client_key_file") {
|
||||
None => None,
|
||||
Some(filename) => {
|
||||
if !Path::new(&filename).is_file() {
|
||||
let message = format!("File {} does not exist", filename);
|
||||
return Err(CliError { message });
|
||||
} else {
|
||||
Some(filename)
|
||||
}
|
||||
}
|
||||
};
|
||||
let color = output_color(matches);
|
||||
let compressed = has_flag(matches, "compressed");
|
||||
let connect_timeout = get::<u64>(matches, "connect_timeout").unwrap();
|
||||
@ -450,6 +489,8 @@ pub fn parse_options(matches: &ArgMatches) -> Result<CliOptions, CliError> {
|
||||
|
||||
Ok(CliOptions {
|
||||
cacert_file,
|
||||
client_cert_file,
|
||||
client_key_file,
|
||||
color,
|
||||
compressed,
|
||||
connect_timeout,
|
||||
|
@ -144,6 +144,16 @@ impl Client {
|
||||
self.handle.ssl_cert_type("PEM").unwrap();
|
||||
}
|
||||
|
||||
if let Some(client_cert_file) = options.client_cert_file.clone() {
|
||||
self.handle.ssl_cert(client_cert_file).unwrap();
|
||||
self.handle.ssl_cert_type("PEM").unwrap();
|
||||
}
|
||||
|
||||
if let Some(client_key_file) = options.client_key_file.clone() {
|
||||
self.handle.ssl_key(client_key_file).unwrap();
|
||||
self.handle.ssl_cert_type("PEM").unwrap();
|
||||
}
|
||||
|
||||
if let Some(proxy) = options.proxy.clone() {
|
||||
self.handle.proxy(proxy.as_str()).unwrap();
|
||||
}
|
||||
|
@ -20,6 +20,8 @@ use std::time::Duration;
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ClientOptions {
|
||||
pub cacert_file: Option<String>,
|
||||
pub client_cert_file: Option<String>,
|
||||
pub client_key_file: Option<String>,
|
||||
pub follow_location: bool,
|
||||
pub max_redirect: Option<usize>,
|
||||
pub cookie_input_file: Option<String>,
|
||||
@ -45,6 +47,8 @@ impl Default for ClientOptions {
|
||||
fn default() -> Self {
|
||||
ClientOptions {
|
||||
cacert_file: None,
|
||||
client_cert_file: None,
|
||||
client_key_file: None,
|
||||
follow_location: false,
|
||||
max_redirect: Some(50),
|
||||
cookie_input_file: None,
|
||||
@ -71,6 +75,16 @@ impl ClientOptions {
|
||||
arguments.push(cacert_file.clone());
|
||||
}
|
||||
|
||||
if let Some(ref client_cert_file) = self.client_cert_file {
|
||||
arguments.push("--cert".to_string());
|
||||
arguments.push(client_cert_file.clone());
|
||||
}
|
||||
|
||||
if let Some(ref client_key_file) = self.client_key_file {
|
||||
arguments.push("--key".to_string());
|
||||
arguments.push(client_key_file.clone());
|
||||
}
|
||||
|
||||
if self.compressed {
|
||||
arguments.push("--compressed".to_string());
|
||||
}
|
||||
@ -130,6 +144,8 @@ mod tests {
|
||||
assert_eq!(
|
||||
ClientOptions {
|
||||
cacert_file: None,
|
||||
client_cert_file: None,
|
||||
client_key_file: None,
|
||||
follow_location: true,
|
||||
max_redirect: Some(10),
|
||||
cookie_input_file: Some("cookie_file".to_string()),
|
||||
|
@ -108,6 +108,7 @@ fn execute(
|
||||
logger.debug(format!(" fail fast: {}", cli_options.fail_fast).as_str());
|
||||
logger.debug(format!(" follow redirect: {}", cli_options.follow_location).as_str());
|
||||
logger.debug(format!(" insecure: {}", cli_options.insecure).as_str());
|
||||
|
||||
if let Some(n) = cli_options.max_redirect {
|
||||
logger.debug(format!(" max redirect: {}", n).as_str());
|
||||
}
|
||||
|
@ -217,6 +217,8 @@ impl From<&RunnerOptions> for ClientOptions {
|
||||
fn from(runner_options: &RunnerOptions) -> Self {
|
||||
ClientOptions {
|
||||
cacert_file: runner_options.cacert_file.clone(),
|
||||
client_cert_file: runner_options.client_cert_file.clone(),
|
||||
client_key_file: runner_options.client_key_file.clone(),
|
||||
follow_location: runner_options.follow_location,
|
||||
max_redirect: runner_options.max_redirect,
|
||||
cookie_input_file: runner_options.cookie_input_file.clone(),
|
||||
@ -300,6 +302,14 @@ pub fn get_entry_options(
|
||||
runner_options.cacert_file = Some(option.filename.value.clone());
|
||||
logger.debug(format!("cacert: {}", option.filename.value).as_str());
|
||||
}
|
||||
EntryOption::ClientCert(option) => {
|
||||
runner_options.client_cert_file = Some(option.filename.value.clone());
|
||||
logger.debug(format!("cert: {}", option.filename.value).as_str());
|
||||
}
|
||||
EntryOption::ClientKey(option) => {
|
||||
runner_options.client_key_file = Some(option.filename.value.clone());
|
||||
logger.debug(format!("key: {}", option.filename.value).as_str());
|
||||
}
|
||||
EntryOption::Compressed(option) => {
|
||||
runner_options.compressed = option.value;
|
||||
logger.debug(format!("compressed: {}", option.value).as_str());
|
||||
|
@ -26,6 +26,8 @@ use std::time::Duration;
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct RunnerOptions {
|
||||
pub cacert_file: Option<String>,
|
||||
pub client_cert_file: Option<String>,
|
||||
pub client_key_file: Option<String>,
|
||||
pub compressed: bool,
|
||||
pub connect_timeout: Duration,
|
||||
pub context_dir: ContextDir,
|
||||
@ -54,6 +56,8 @@ impl Default for RunnerOptions {
|
||||
fn default() -> Self {
|
||||
RunnerOptions {
|
||||
cacert_file: None,
|
||||
client_cert_file: None,
|
||||
client_key_file: None,
|
||||
compressed: false,
|
||||
connect_timeout: Duration::from_secs(300),
|
||||
context_dir: Default::default(),
|
||||
@ -83,6 +87,8 @@ impl Default for RunnerOptions {
|
||||
impl RunnerOptions {
|
||||
pub fn from(filename: &str, current_dir: &Path, cli_options: &CliOptions) -> Self {
|
||||
let cacert_file = cli_options.cacert_file.clone();
|
||||
let client_cert_file = cli_options.client_cert_file.clone();
|
||||
let client_key_file = cli_options.client_key_file.clone();
|
||||
let follow_location = cli_options.follow_location;
|
||||
let verbosity = match (cli_options.verbose, cli_options.very_verbose) {
|
||||
(true, true) => Some(Verbosity::VeryVerbose),
|
||||
@ -130,6 +136,8 @@ impl RunnerOptions {
|
||||
let very_verbose = cli_options.very_verbose;
|
||||
RunnerOptions {
|
||||
cacert_file,
|
||||
client_cert_file,
|
||||
client_key_file,
|
||||
compressed,
|
||||
connect_timeout,
|
||||
context_dir,
|
||||
|
@ -694,6 +694,8 @@ pub struct Variable {
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum EntryOption {
|
||||
CaCertificate(CaCertificateOption),
|
||||
ClientCert(ClientCertOption),
|
||||
ClientKey(ClientKeyOption),
|
||||
Compressed(CompressedOption),
|
||||
Insecure(InsecureOption),
|
||||
FollowLocation(FollowLocationOption),
|
||||
@ -736,6 +738,26 @@ pub struct CaCertificateOption {
|
||||
pub line_terminator0: LineTerminator,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct ClientCertOption {
|
||||
pub line_terminators: Vec<LineTerminator>,
|
||||
pub space0: Whitespace,
|
||||
pub space1: Whitespace,
|
||||
pub space2: Whitespace,
|
||||
pub filename: Filename,
|
||||
pub line_terminator0: LineTerminator,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct ClientKeyOption {
|
||||
pub line_terminators: Vec<LineTerminator>,
|
||||
pub space0: Whitespace,
|
||||
pub space1: Whitespace,
|
||||
pub space2: Whitespace,
|
||||
pub filename: Filename,
|
||||
pub line_terminator0: LineTerminator,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct RetryOption {
|
||||
pub line_terminators: Vec<LineTerminator>,
|
||||
|
@ -234,6 +234,8 @@ impl Htmlable for EntryOption {
|
||||
fn to_html(&self) -> String {
|
||||
match self {
|
||||
EntryOption::CaCertificate(option) => option.to_html(),
|
||||
EntryOption::ClientCert(option) => option.to_html(),
|
||||
EntryOption::ClientKey(option) => option.to_html(),
|
||||
EntryOption::Compressed(option) => option.to_html(),
|
||||
EntryOption::Insecure(option) => option.to_html(),
|
||||
EntryOption::FollowLocation(option) => option.to_html(),
|
||||
@ -299,6 +301,40 @@ impl Htmlable for CaCertificateOption {
|
||||
}
|
||||
}
|
||||
|
||||
impl Htmlable for ClientCertOption {
|
||||
fn to_html(&self) -> String {
|
||||
let mut buffer = String::from("");
|
||||
add_line_terminators(&mut buffer, self.line_terminators.clone());
|
||||
buffer.push_str("<span class=\"line\">");
|
||||
buffer.push_str(self.space0.to_html().as_str());
|
||||
buffer.push_str("<span class=\"string\">cert</span>");
|
||||
buffer.push_str(self.space1.to_html().as_str());
|
||||
buffer.push_str("<span>:</span>");
|
||||
buffer.push_str(self.space2.to_html().as_str());
|
||||
buffer.push_str(self.filename.to_html().as_str());
|
||||
buffer.push_str("</span>");
|
||||
buffer.push_str(self.line_terminator0.to_html().as_str());
|
||||
buffer
|
||||
}
|
||||
}
|
||||
|
||||
impl Htmlable for ClientKeyOption {
|
||||
fn to_html(&self) -> String {
|
||||
let mut buffer = String::from("");
|
||||
add_line_terminators(&mut buffer, self.line_terminators.clone());
|
||||
buffer.push_str("<span class=\"line\">");
|
||||
buffer.push_str(self.space0.to_html().as_str());
|
||||
buffer.push_str("<span class=\"string\">key</span>");
|
||||
buffer.push_str(self.space1.to_html().as_str());
|
||||
buffer.push_str("<span>:</span>");
|
||||
buffer.push_str(self.space2.to_html().as_str());
|
||||
buffer.push_str(self.filename.to_html().as_str());
|
||||
buffer.push_str("</span>");
|
||||
buffer.push_str(self.line_terminator0.to_html().as_str());
|
||||
buffer
|
||||
}
|
||||
}
|
||||
|
||||
impl Htmlable for FollowLocationOption {
|
||||
fn to_html(&self) -> String {
|
||||
let mut buffer = String::from("");
|
||||
|
@ -349,6 +349,8 @@ fn option(reader: &mut Reader) -> ParseResult<'static, EntryOption> {
|
||||
choice(
|
||||
&[
|
||||
option_cacert,
|
||||
option_cert,
|
||||
option_key,
|
||||
option_compressed,
|
||||
option_insecure,
|
||||
option_follow_location,
|
||||
@ -386,6 +388,50 @@ fn option_cacert(reader: &mut Reader) -> ParseResult<'static, EntryOption> {
|
||||
Ok(EntryOption::CaCertificate(option))
|
||||
}
|
||||
|
||||
fn option_cert(reader: &mut Reader) -> ParseResult<'static, EntryOption> {
|
||||
let line_terminators = optional_line_terminators(reader)?;
|
||||
let space0 = zero_or_more_spaces(reader)?;
|
||||
try_literal("cert", reader)?;
|
||||
let space1 = zero_or_more_spaces(reader)?;
|
||||
try_literal(":", reader)?;
|
||||
let space2 = zero_or_more_spaces(reader)?;
|
||||
let f = filename::parse(reader)?;
|
||||
let line_terminator0 = line_terminator(reader)?;
|
||||
|
||||
let option = ClientCertOption {
|
||||
line_terminators,
|
||||
space0,
|
||||
space1,
|
||||
space2,
|
||||
filename: f,
|
||||
line_terminator0,
|
||||
};
|
||||
|
||||
Ok(EntryOption::ClientCert(option))
|
||||
}
|
||||
|
||||
fn option_key(reader: &mut Reader) -> ParseResult<'static, EntryOption> {
|
||||
let line_terminators = optional_line_terminators(reader)?;
|
||||
let space0 = zero_or_more_spaces(reader)?;
|
||||
try_literal("key", reader)?;
|
||||
let space1 = zero_or_more_spaces(reader)?;
|
||||
try_literal(":", reader)?;
|
||||
let space2 = zero_or_more_spaces(reader)?;
|
||||
let f = filename::parse(reader)?;
|
||||
let line_terminator0 = line_terminator(reader)?;
|
||||
|
||||
let option = ClientKeyOption {
|
||||
line_terminators,
|
||||
space0,
|
||||
space1,
|
||||
space2,
|
||||
filename: f,
|
||||
line_terminator0,
|
||||
};
|
||||
|
||||
Ok(EntryOption::ClientKey(option))
|
||||
}
|
||||
|
||||
fn option_compressed(reader: &mut Reader) -> ParseResult<'static, EntryOption> {
|
||||
let line_terminators = optional_line_terminators(reader)?;
|
||||
let space0 = zero_or_more_spaces(reader)?;
|
||||
|
@ -842,6 +842,8 @@ impl Tokenizable for EntryOption {
|
||||
fn tokenize(&self) -> Vec<Token> {
|
||||
match self {
|
||||
EntryOption::CaCertificate(option) => option.tokenize(),
|
||||
EntryOption::ClientCert(option) => option.tokenize(),
|
||||
EntryOption::ClientKey(option) => option.tokenize(),
|
||||
EntryOption::Compressed(option) => option.tokenize(),
|
||||
EntryOption::Insecure(option) => option.tokenize(),
|
||||
EntryOption::FollowLocation(option) => option.tokenize(),
|
||||
@ -877,6 +879,48 @@ impl Tokenizable for CaCertificateOption {
|
||||
}
|
||||
}
|
||||
|
||||
impl Tokenizable for ClientCertOption {
|
||||
fn tokenize(&self) -> Vec<Token> {
|
||||
let mut tokens: Vec<Token> = vec![];
|
||||
tokens.append(
|
||||
&mut self
|
||||
.line_terminators
|
||||
.iter()
|
||||
.flat_map(|e| e.tokenize())
|
||||
.collect(),
|
||||
);
|
||||
tokens.append(&mut self.space0.tokenize());
|
||||
tokens.push(Token::String("cert".to_string()));
|
||||
tokens.append(&mut self.space1.tokenize());
|
||||
tokens.push(Token::Colon(String::from(":")));
|
||||
tokens.append(&mut self.space2.tokenize());
|
||||
tokens.append(&mut self.filename.tokenize());
|
||||
tokens.append(&mut self.line_terminator0.tokenize());
|
||||
tokens
|
||||
}
|
||||
}
|
||||
|
||||
impl Tokenizable for ClientKeyOption {
|
||||
fn tokenize(&self) -> Vec<Token> {
|
||||
let mut tokens: Vec<Token> = vec![];
|
||||
tokens.append(
|
||||
&mut self
|
||||
.line_terminators
|
||||
.iter()
|
||||
.flat_map(|e| e.tokenize())
|
||||
.collect(),
|
||||
);
|
||||
tokens.append(&mut self.space0.tokenize());
|
||||
tokens.push(Token::String("key".to_string()));
|
||||
tokens.append(&mut self.space1.tokenize());
|
||||
tokens.push(Token::Colon(String::from(":")));
|
||||
tokens.append(&mut self.space2.tokenize());
|
||||
tokens.append(&mut self.filename.tokenize());
|
||||
tokens.append(&mut self.line_terminator0.tokenize());
|
||||
tokens
|
||||
}
|
||||
}
|
||||
|
||||
impl Tokenizable for CompressedOption {
|
||||
fn tokenize(&self) -> Vec<Token> {
|
||||
let mut tokens: Vec<Token> = vec![];
|
||||
|
Loading…
Reference in New Issue
Block a user