From c95ac59cd61910745832f21e3409f15d3f65e1fd Mon Sep 17 00:00:00 2001 From: Greg Shuflin Date: Thu, 20 May 2021 20:44:54 -0700 Subject: [PATCH 1/3] Add support for client and peer certificates This commit adds two additional command-line flags for specifying SSL certificates to be used by drill when making HTTP requests. `--cert` is used to specify a client certificate file (following the `curl` option with the same name). Again similarly to curl, `--cacert` is used to specify a PEM format certificate that should be used to verify the remote peer. Co-authored-by: bittrance@gmail.com --- Cargo.lock | 2 +- Cargo.toml | 6 ++--- src/actions/request.rs | 48 ++++++++++++++++++++++++++++++++++++++- src/benchmark.rs | 4 ++-- src/config.rs | 25 +++++++++++++++++++- src/expandable/include.rs | 1 + src/main.rs | 6 ++++- 7 files changed, 83 insertions(+), 9 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 75e190e..7dd5875 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -295,7 +295,7 @@ dependencies = [ "lazy_static", "linked-hash-map", "num_cpus", - "openssl-sys", + "openssl", "rand 0.7.3", "regex", "reqwest", diff --git a/Cargo.toml b/Cargo.toml index e411b9d..a851429 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,7 +19,7 @@ yaml-rust = "0.4.3" url = "2.1.1" linked-hash-map = "0.5.3" tokio = { version = "0.2.20", features = ["rt-core", "rt-threaded", "time", "net", "io-driver"] } -reqwest = { version = "0.10.4", features = ["cookies", "trust-dns"] } +reqwest = { version = "0.10.4", features = ["cookies", "trust-dns", "native-tls"] } async-trait = "0.1.30" futures = "0.3.5" lazy_static = "1.4.0" @@ -29,9 +29,9 @@ hdrhistogram = "7.4.0" # Add openssl-sys as a direct dependency so it can be cross compiled to # x86_64-unknown-linux-musl using the "vendored" feature below -openssl-sys = "0.9.66" +openssl = "0.10.35" [features] # Force openssl-sys to statically link in the openssl library. Necessary when # cross compiling to x86_64-unknown-linux-musl. -vendored = ["openssl-sys/vendored"] +vendored = ["openssl/vendored"] diff --git a/src/actions/request.rs b/src/actions/request.rs index 8151fda..793f5cf 100644 --- a/src/actions/request.rs +++ b/src/actions/request.rs @@ -3,6 +3,7 @@ use std::time::{Duration, Instant}; use async_trait::async_trait; use colored::Colorize; +use openssl; use reqwest::{ header::{self, HeaderMap, HeaderName, HeaderValue}, ClientBuilder, Method, Response, @@ -152,7 +153,27 @@ impl Request { // Resolve the body let (client, request) = { let mut pool2 = pool.lock().unwrap(); - let client = pool2.entry(domain).or_insert_with(|| ClientBuilder::default().danger_accept_invalid_certs(config.no_check_certificate).build().unwrap()); + let client = pool2.entry(domain).or_insert_with(|| { + let mut builder = ClientBuilder::default().danger_accept_invalid_certs(config.no_check_certificate); + if let Some(ref pem) = config.maybe_cert { + let identity = make_identity(pem); + builder = builder.identity(identity); + } + + if let Some(ref cacert) = config.maybe_cacert { + let cacert = match reqwest::Certificate::from_pem(&cacert) { + Ok(cert) => cert, + Err(e) => { + eprintln!("Reqwest certificate error: {}", e); + std::process::exit(-1); + } + }; + + builder = builder.add_root_certificate(cacert); + } + + builder.build().unwrap() + }); let request = if let Some(body) = self.body.as_ref() { interpolated_body = uninterpolator.get_or_insert(interpolator::Interpolator::new(context)).resolve(body, !config.relaxed_interpolations); @@ -321,6 +342,31 @@ impl Runnable for Request { } } +fn make_identity(pem: &Vec) -> reqwest::Identity { + fn make_der(pem: &Vec) -> Result, openssl::error::ErrorStack> { + let key = openssl::pkey::PKey::private_key_from_pem(&pem)?; + let crt = openssl::x509::X509::from_pem(&pem)?; + + let pkcs12_builder = openssl::pkcs12::Pkcs12::builder(); + let pkcs12 = pkcs12_builder.build("", "client crt", &key, &crt)?; + pkcs12.to_der() + } + + match make_der(&pem) { + Ok(der) => match reqwest::Identity::from_pkcs12_der(&der, "") { + Ok(identity) => identity, + Err(e) => { + eprintln!("Reqwest ssl error: {}", e); + std::process::exit(-1) + } + }, + Err(e) => { + eprintln!("Openssl error: {}", e); + std::process::exit(-1); + } + } +} + fn log_request(request: &reqwest::Request) { let mut message = String::new(); write!(message, "{}", ">>>".bold().green()).unwrap(); diff --git a/src/benchmark.rs b/src/benchmark.rs index 94f6fee..0ae0e2f 100644 --- a/src/benchmark.rs +++ b/src/benchmark.rs @@ -53,8 +53,8 @@ fn join(l: Vec, sep: &str) -> String { ) } -pub fn execute(benchmark_path: &str, report_path_option: Option<&str>, relaxed_interpolations: bool, no_check_certificate: bool, quiet: bool, nanosec: bool, timeout: Option<&str>, verbose: bool) -> BenchmarkResult { - let config = Arc::new(Config::new(benchmark_path, relaxed_interpolations, no_check_certificate, quiet, nanosec, timeout.map_or(10, |t| t.parse().unwrap_or(10)), verbose)); +pub fn execute(benchmark_path: &str, report_path_option: Option<&str>, relaxed_interpolations: bool, maybe_cert_path: Option<&str>, maybe_cacert_path: Option<&str>, no_check_certificate: bool, quiet: bool, nanosec: bool, timeout: Option<&str>, verbose: bool) -> BenchmarkResult { + let config = Arc::new(Config::new(benchmark_path, relaxed_interpolations, maybe_cert_path, maybe_cacert_path, no_check_certificate, quiet, nanosec, timeout.map_or(10, |t| t.parse().unwrap_or(10)), verbose)); if report_path_option.is_some() { println!("{}: {}. Ignoring {} and {} properties...", "Report mode".yellow(), "on".purple(), "concurrency".yellow(), "iterations".yellow()); diff --git a/src/config.rs b/src/config.rs index 958548a..40428c5 100644 --- a/src/config.rs +++ b/src/config.rs @@ -3,6 +3,7 @@ use yaml_rust::{Yaml, YamlLoader}; use crate::benchmark::Context; use crate::interpolator; use crate::reader; +use std::io::Read; const NITERATIONS: i64 = 1; const NRAMPUP: i64 = 0; @@ -13,6 +14,8 @@ pub struct Config { pub iterations: i64, pub relaxed_interpolations: bool, pub no_check_certificate: bool, + pub maybe_cert: Option>, + pub maybe_cacert: Option>, pub rampup: i64, pub quiet: bool, pub nanosec: bool, @@ -21,7 +24,7 @@ pub struct Config { } impl Config { - pub fn new(path: &str, relaxed_interpolations: bool, no_check_certificate: bool, quiet: bool, nanosec: bool, timeout: u64, verbose: bool) -> Config { + pub fn new(path: &str, relaxed_interpolations: bool, maybe_cert_path: Option<&str>, maybe_cacert_path: Option<&str>, no_check_certificate: bool, quiet: bool, nanosec: bool, timeout: u64, verbose: bool) -> Config { let config_file = reader::read_file(path); let config_docs = YamlLoader::load_from_str(config_file.as_str()).unwrap(); @@ -39,12 +42,32 @@ impl Config { panic!("The concurrency can not be higher than the number of iterations") } + let maybe_cert = maybe_cert_path.map(|path| { + let mut pem = Vec::new(); + if let Err(e) = std::fs::File::open(path).and_then(|mut path| path.read_to_end(&mut pem)) { + eprintln!("Error opening --cert file {}: {}", path, e); + std::process::exit(-1); + } + pem + }); + + let maybe_cacert = maybe_cacert_path.map(|path| { + let mut cert = Vec::new(); + if let Err(e) = std::fs::File::open(path).and_then(|mut path| path.read_to_end(&mut cert)) { + eprintln!("Error opening --cacert file {}: {}", path, e); + std::process::exit(-1); + } + cert + }); + Config { base, concurrency, iterations, relaxed_interpolations, no_check_certificate, + maybe_cert, + maybe_cacert, rampup, quiet, nanosec, diff --git a/src/expandable/include.rs b/src/expandable/include.rs index e2f5232..528d069 100644 --- a/src/expandable/include.rs +++ b/src/expandable/include.rs @@ -77,6 +77,7 @@ pub fn expand_from_filepath(parent_path: &str, mut benchmark: &mut Benchmark, ac } } +#[cfg(test)] mod tests { use super::*; diff --git a/src/main.rs b/src/main.rs index ec0b687..5e2c889 100644 --- a/src/main.rs +++ b/src/main.rs @@ -23,6 +23,8 @@ fn main() { let stats_option = matches.is_present("stats"); let compare_path_option = matches.value_of("compare"); let threshold_option = matches.value_of("threshold"); + let cert = matches.value_of("cert"); + let cacert = matches.value_of("cacert"); let no_check_certificate = matches.is_present("no-check-certificate"); let relaxed_interpolations = matches.is_present("relaxed-interpolations"); let quiet = matches.is_present("quiet"); @@ -32,7 +34,7 @@ fn main() { #[cfg(windows)] let _ = control::set_virtual_terminal(true); - let benchmark_result = benchmark::execute(benchmark_file, report_path_option, relaxed_interpolations, no_check_certificate, quiet, nanosec, timeout, verbose); + let benchmark_result = benchmark::execute(benchmark_file, report_path_option, relaxed_interpolations, cert, cacert, no_check_certificate, quiet, nanosec, timeout, verbose); let list_reports = benchmark_result.reports; let duration = benchmark_result.duration; @@ -52,6 +54,8 @@ fn app_args<'a>() -> clap::ArgMatches<'a> { .arg(Arg::with_name("compare").short("c").long("compare").help("Sets a compare file").takes_value(true).conflicts_with("report")) .arg(Arg::with_name("threshold").short("t").long("threshold").help("Sets a threshold value in ms amongst the compared file").takes_value(true).conflicts_with("report")) .arg(Arg::with_name("relaxed-interpolations").long("relaxed-interpolations").help("Do not panic if an interpolation is not present. (Not recommended)").takes_value(false)) + .arg(Arg::with_name("cert").help("Use the specified client certificate (analogous to curl --cert)").long("cert").required(false).takes_value(true)) + .arg(Arg::with_name("cacert").help("Use the specified certificate to verify the peer (analogous to curl --cacert)").long("cacert").required(false).takes_value(true)) .arg(Arg::with_name("no-check-certificate").long("no-check-certificate").help("Disables SSL certification check. (Not recommended)").takes_value(false)) .arg(Arg::with_name("quiet").short("q").long("quiet").help("Disables output").takes_value(false)) .arg(Arg::with_name("timeout").short("o").long("timeout").help("Set timeout in seconds for all requests").takes_value(true)) From 1d0795b88313da379b114d84ab124734fc0508da Mon Sep 17 00:00:00 2001 From: Anders Qvist Date: Wed, 9 Mar 2022 22:36:44 +0100 Subject: [PATCH 2/3] Extract pem-to-der converter to top-level function. --- src/actions/request.rs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/actions/request.rs b/src/actions/request.rs index 793f5cf..a87dcfb 100644 --- a/src/actions/request.rs +++ b/src/actions/request.rs @@ -342,17 +342,17 @@ impl Runnable for Request { } } -fn make_identity(pem: &Vec) -> reqwest::Identity { - fn make_der(pem: &Vec) -> Result, openssl::error::ErrorStack> { - let key = openssl::pkey::PKey::private_key_from_pem(&pem)?; - let crt = openssl::x509::X509::from_pem(&pem)?; +fn try_pem_to_der(pem: &Vec) -> Result, openssl::error::ErrorStack> { + let key = openssl::pkey::PKey::private_key_from_pem(&pem)?; + let crt = openssl::x509::X509::from_pem(&pem)?; - let pkcs12_builder = openssl::pkcs12::Pkcs12::builder(); - let pkcs12 = pkcs12_builder.build("", "client crt", &key, &crt)?; - pkcs12.to_der() - } + let pkcs12_builder = openssl::pkcs12::Pkcs12::builder(); + let pkcs12 = pkcs12_builder.build("", "client crt", &key, &crt)?; + pkcs12.to_der() +} - match make_der(&pem) { +fn make_identity(pem: &Vec) -> reqwest::Identity { + match try_pem_to_der(pem) { Ok(der) => match reqwest::Identity::from_pkcs12_der(&der, "") { Ok(identity) => identity, Err(e) => { From 3191ac7556216ba76aabfe3bb4dd005928346416 Mon Sep 17 00:00:00 2001 From: Anders Qvist Date: Wed, 9 Mar 2022 22:40:21 +0100 Subject: [PATCH 3/3] Add cert options to README. --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 1ad3226..20b3889 100644 --- a/README.md +++ b/README.md @@ -253,6 +253,8 @@ FLAGS: OPTIONS: -b, --benchmark Sets the benchmark file + --cacert Use the specified certificate to verify the peer (analogous to curl --cacert) + --cert Use the specified client certificate (analogous to curl --cert) -c, --compare Sets a compare file -r, --report Sets a report file -t, --threshold Sets a threshold value in ms amongst the compared file