diff --git a/.taplo.toml b/.taplo.toml new file mode 100644 index 00000000..604d7419 --- /dev/null +++ b/.taplo.toml @@ -0,0 +1,3 @@ +[formatting] +array_auto_collapse = false +array_auto_expand = false diff --git a/Cargo.lock b/Cargo.lock index 77638fc6..1b925d92 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -756,6 +756,18 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "flume" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" +dependencies = [ + "futures-core", + "futures-sink", + "nanorand", + "spin", +] + [[package]] name = "fnv" version = "1.0.7" @@ -933,8 +945,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi", + "wasm-bindgen", ] [[package]] @@ -1475,6 +1489,15 @@ dependencies = [ "uuid", ] +[[package]] +name = "nanorand" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3" +dependencies = [ + "getrandom", +] + [[package]] name = "nom" version = "7.1.3" @@ -1663,18 +1686,18 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "pin-project" -version = "1.1.6" +version = "1.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf123a161dde1e524adf36f90bc5d8d3462824a9c43553ad07a8183161189ec" +checksum = "be57f64e946e500c8ee36ef6331845d40a93055567ec57e8fae13efd33759b95" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.6" +version = "1.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4502d8515ca9f32f1fb543d987f63d95a14934883db45bdb48060b6b69257f8" +checksum = "3c0f5fad0874fc7abcd4d750e76917eaebbecaa2c20bde22e1dbeeba8beb758c" dependencies = [ "proc-macro2", "quote", @@ -1683,9 +1706,9 @@ dependencies = [ [[package]] name = "pin-project-lite" -version = "0.2.14" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" +checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff" [[package]] name = "pin-utils" @@ -1716,9 +1739,9 @@ dependencies = [ [[package]] name = "prettyplease" -version = "0.2.24" +version = "0.2.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "910d41a655dac3b764f1ade94821093d3610248694320cd072303a8eedcf221d" +checksum = "64d1ec885c64d0457d564db4ec299b2dae3f9c02808b8ad9c3a089c591b18033" dependencies = [ "proc-macro2", "syn", @@ -2070,6 +2093,7 @@ name = "rama-tls" version = "0.2.0-alpha.4" dependencies = [ "boring", + "flume", "moka", "parking_lot", "pin-project-lite", @@ -2504,6 +2528,9 @@ name = "spin" version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] [[package]] name = "strsim" @@ -2519,9 +2546,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.82" +version = "2.0.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83540f837a8afc019423a8edb95b52a8effe46957ee402287f4292fae35be021" +checksum = "5023162dfcd14ef8f32034d8bcd4cc5ddc61ef7a247c024a33e24e1f24d21b56" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index fbee31d3..99f9e476 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -69,12 +69,14 @@ opentelemetry = { version = "0.26.0", default-features = false, features = [ "trace", ] } nom = "7.1.3" -opentelemetry-otlp = { version = "0.26.0", features = [ "tokio" ] } +opentelemetry-otlp = { version = "0.26.0", features = ["tokio"] } opentelemetry_sdk = { version = "0.26.0", default-features = false, features = [ "trace", "rt-tokio", ] } -opentelemetry-semantic-conventions = { version = "0.26", features = [ "semconv_experimental" ] } +opentelemetry-semantic-conventions = { version = "0.26", features = [ + "semconv_experimental", +] } quickcheck = "1.0" quote = "1.0" rcgen = "0.13.0" @@ -122,6 +124,7 @@ hickory-resolver = { version = "0.24.1", default-features = false, features = [ "tokio-runtime", ] } arc-swap = "1.7.1" +flume = "0.11.1" [workspace.lints.rust] unreachable_pub = "deny" diff --git a/rama-net/src/tls/mod.rs b/rama-net/src/tls/mod.rs index 4094ce79..1e723051 100644 --- a/rama-net/src/tls/mod.rs +++ b/rama-net/src/tls/mod.rs @@ -1,11 +1,6 @@ //! rama common tls types //! -use std::{ - borrow::Cow, - path::{Path, PathBuf}, -}; - use rama_utils::str::NonEmptyString; mod enums; @@ -66,18 +61,25 @@ pub enum KeyLogIntent { /// You can choose to disable the key logging explicitly Disabled, /// Request a keys to be logged to the given file path. - File(std::path::PathBuf), + File(String), } impl KeyLogIntent { /// get the file path if intended - pub fn file_path(&self) -> Option> { + pub fn file_path(&self) -> Option { + match self { + KeyLogIntent::Disabled => None, + KeyLogIntent::Environment => std::env::var("SSLKEYLOGFILE").ok().clone(), + KeyLogIntent::File(keylog_filename) => Some(keylog_filename.clone()), + } + } + + /// consume itself into the file path if intended + pub fn into_file_path(self) -> Option { match self { KeyLogIntent::Disabled => None, - KeyLogIntent::Environment => std::env::var("SSLKEYLOGFILE") - .ok() - .map(|s| Cow::Owned(PathBuf::from(s))), - KeyLogIntent::File(keylog_filename) => Some(Cow::Borrowed(keylog_filename.as_path())), + KeyLogIntent::Environment => std::env::var("SSLKEYLOGFILE").ok().clone(), + KeyLogIntent::File(keylog_filename) => Some(keylog_filename), } } } diff --git a/rama-tls/Cargo.toml b/rama-tls/Cargo.toml index 97e650cf..b524a63b 100644 --- a/rama-tls/Cargo.toml +++ b/rama-tls/Cargo.toml @@ -21,7 +21,8 @@ rustls-ring = ["rustls", "tokio-rustls/ring", "rustls/ring", "rama-net/rustls-ri [dependencies] boring = { workspace = true, optional = true } -moka = { workspace = true, features = [ "sync" ], optional = true } +flume = { workspace = true, features = ["async"] } +moka = { workspace = true, features = ["sync"], optional = true } parking_lot = { workspace = true } pin-project-lite = { workspace = true } rama-core = { version = "0.2.0-alpha.4", path = "../rama-core" } diff --git a/rama-tls/src/boring/client/connector_data.rs b/rama-tls/src/boring/client/connector_data.rs index 907e4716..e44871f4 100644 --- a/rama-tls/src/boring/client/connector_data.rs +++ b/rama-tls/src/boring/client/connector_data.rs @@ -20,6 +20,8 @@ use rama_net::{address::Host, tls::client::ServerVerifyMode}; use std::{fmt, sync::Arc}; use tracing::trace; +use crate::keylog::new_key_log_file_handle; + #[derive(Debug, Clone)] /// Internal data used as configuration/input for the [`super::HttpsConnector`]. /// @@ -74,20 +76,11 @@ impl TlsConnectorData { .clone() .unwrap_or_default() .file_path() - .as_deref() { - // open file in append mode and write keylog to it with callback - trace!(path = ?keylog_filename, "boring connector: open keylog file for debug purposes"); - let file = std::fs::OpenOptions::new() - .append(true) - .create(true) - .open(keylog_filename) - .context("build (boring) ssl connector: set keylog: open file")?; + let handle = new_key_log_file_handle(keylog_filename)?; cfg_builder.set_keylog_callback(move |_, line| { - use std::io::Write; let line = format!("{}\n", line); - let mut file = &file; - let _ = file.write_all(line.as_bytes()); + handle.write_log_line(line); }); } diff --git a/rama-tls/src/boring/server/service.rs b/rama-tls/src/boring/server/service.rs index a46576e2..bfd12508 100644 --- a/rama-tls/src/boring/server/service.rs +++ b/rama-tls/src/boring/server/service.rs @@ -4,8 +4,8 @@ use crate::{ boring::ssl::{AlpnError, SslAcceptor, SslMethod, SslRef}, tokio_boring::SslStream, }, - types::client::ClientHello, - types::SecureTransport, + keylog::new_key_log_file_handle, + types::{client::ClientHello, SecureTransport}, }; use parking_lot::Mutex; use rama_core::{ @@ -184,19 +184,10 @@ where } if let Some(keylog_filename) = tls_config.keylog_intent.file_path() { - trace!(path = ?keylog_filename, "boring acceptor service: open keylog file for debug purposes"); - // TODO: do not open a file each time, just use 1 global one - // open file in append mode and write keylog to it with callback - let file = std::fs::OpenOptions::new() - .append(true) - .create(true) - .open(keylog_filename) - .context("build boring ssl acceptor: set keylog: open file")?; + let handle = new_key_log_file_handle(keylog_filename)?; acceptor_builder.set_keylog_callback(move |_, line| { - use std::io::Write; let line = format!("{}\n", line); - let mut file = &file; - let _ = file.write_all(line.as_bytes()); + handle.write_log_line(line); }); } diff --git a/rama-tls/src/keylog.rs b/rama-tls/src/keylog.rs new file mode 100644 index 00000000..238112e7 --- /dev/null +++ b/rama-tls/src/keylog.rs @@ -0,0 +1,97 @@ +//! Keylog facility used by any tls implementation +//! supported by rama, and which can be used for your owns as well. +//! +//! Center to thsi module is the `KeyLogger` which is a wrapper around +//! a FS file + +use parking_lot::RwLock; +use rama_core::error::{ErrorContext, OpaqueError}; +use std::{ + collections::{hash_map::Entry, HashMap}, + fs::OpenOptions, + io::Write, + sync::OnceLock, +}; + +/// Get a key log file handle for the given path +/// only one file handle will be opened per unique path String. +/// +/// # To be unique or ditto +/// +/// Paths are case-sensitive by default for rama, as utf-8 compatible. +/// Normalize yourself prior to passing a path to this function if you're concerned. +pub fn new_key_log_file_handle(path: String) -> Result { + let mapping = GLOBAL_KEY_LOG_FILE_MAPPING.get_or_init(Default::default); + if let Some(handle) = mapping.read().get(&path).cloned() { + return Ok(handle); + } + let mut mut_mapping = mapping.write(); + match mut_mapping.entry(path.clone()) { + Entry::Occupied(entry) => Ok(entry.get().clone()), + Entry::Vacant(entry) => { + let handle = try_init_key_log_file_handle(path)?; + entry.insert(handle.clone()); + Ok(handle) + } + } +} + +fn try_init_key_log_file_handle(path: String) -> Result { + tracing::trace!( + file = ?path, + "KeyLogFileHandle: try to create a new handle", + ); + + let mut file = OpenOptions::new() + .append(true) + .create(true) + .open(&path) + .with_context(|| format!("create key log file {path:?}"))?; + + let (tx, rx) = flume::unbounded::(); + + let path_name = path.clone(); + std::thread::spawn(move || { + tracing::trace!( + file = ?path_name, + "KeyLogFileHandle[rx]: receiver task up and running", + ); + while let Ok(line) = rx.recv() { + if let Err(err) = file.write_all(line.as_bytes()) { + tracing::error!( + file = path_name, + error = %err, + "KeyLogFileHandle[rx]: failed to write file", + ); + } + } + }); + + Ok(KeyLogFileHandle { path, sender: tx }) +} + +static GLOBAL_KEY_LOG_FILE_MAPPING: OnceLock>> = + OnceLock::new(); + +#[derive(Debug, Clone)] +/// Handle to a (tls) keylog file. +/// +/// See [`new_key_log_file_handle`] for more info, +/// as that is the one creating it. +pub struct KeyLogFileHandle { + path: String, + sender: flume::Sender, +} + +impl KeyLogFileHandle { + /// Write a line to the keylogger. + pub fn write_log_line(&self, line: String) { + if let Err(err) = self.sender.send(line) { + tracing::error!( + file = %self.path, + error = %err, + "KeyLogFileHandle[tx]: failed to send log line for writing", + ); + } + } +} diff --git a/rama-tls/src/lib.rs b/rama-tls/src/lib.rs index 3cad9174..a10b07d9 100644 --- a/rama-tls/src/lib.rs +++ b/rama-tls/src/lib.rs @@ -29,6 +29,8 @@ pub use rustls as std; #[cfg(feature = "boring")] pub use boring as std; +pub mod keylog; + pub mod types { //! common tls types #[doc(inline)] diff --git a/rama-tls/src/rustls/client/connector_data.rs b/rama-tls/src/rustls/client/connector_data.rs index de4b126d..826ca85d 100644 --- a/rama-tls/src/rustls/client/connector_data.rs +++ b/rama-tls/src/rustls/client/connector_data.rs @@ -28,7 +28,7 @@ pub struct TlsConnectorData { struct ClientConfigInput { protocol_versions: Option>, client_auth: Option<(Vec>, PrivateKeyDer<'static>)>, - key_logger: Option>, + key_logger: Option, alpn_protos: Option>>, cert_verifier: Option>, } @@ -114,7 +114,8 @@ impl TlsConnectorData { }; if let Some(key_logger) = self.client_config_input.key_logger.clone() { - client_config.key_log = key_logger; + let key_log = KeyLogFile::new(key_logger)?; + client_config.key_log = Arc::new(key_log); } if let Some(alpn_protos) = self.client_config_input.alpn_protos.clone() { @@ -273,15 +274,6 @@ impl TryFrom for TlsConnectorData { } }; - // set key logger if one is requested - let key_logger = match value.key_logger.clone().unwrap_or_default().file_path() { - Some(path) => { - let key_logger = KeyLogFile::new(path).context("rustls/TlsConnectorData")?; - Some(Arc::new(key_logger)) - } - None => None, - }; - let mut alpn_protos = None; let mut server_name = None; @@ -305,7 +297,11 @@ impl TryFrom for TlsConnectorData { client_config_input: Arc::new(ClientConfigInput { protocol_versions, client_auth, - key_logger, + key_logger: value + .key_logger + .clone() + .unwrap_or_default() + .into_file_path(), alpn_protos, cert_verifier, }), diff --git a/rama-tls/src/rustls/key_log.rs b/rama-tls/src/rustls/key_log.rs index e3ceb12b..d3c141b7 100644 --- a/rama-tls/src/rustls/key_log.rs +++ b/rama-tls/src/rustls/key_log.rs @@ -1,97 +1,53 @@ -use crate::rustls::dep::rustls::KeyLog; -use rama_core::error::{ErrorContext, OpaqueError}; -use std::fmt::{Debug, Formatter}; -use std::fs::{File, OpenOptions}; -use std::io; -use std::io::Write; -use std::path::Path; -use std::sync::Mutex; -use tracing::{trace, warn}; +use std::fmt; -// Internal mutable state for KeyLogFile -struct KeyLogFileInner { - file: File, - buf: Vec, -} +use crate::keylog::{new_key_log_file_handle, KeyLogFileHandle}; +use crate::rustls::dep::rustls::KeyLog; +use rama_core::error::OpaqueError; -impl KeyLogFileInner { - fn new(path: impl AsRef) -> Result { - let path_pref = path.as_ref(); - let file = OpenOptions::new() - .append(true) - .create(true) - .open(path_pref) - .with_context(|| format!("create key log file {path_pref:?}"))?; - Ok(Self { - file, - buf: Vec::new(), - }) - } +#[derive(Debug, Clone)] +/// [`KeyLog`] implementation that opens a file for the given path. +pub(super) struct KeyLogFile(KeyLogFileHandle); - fn try_write(&mut self, label: &str, client_random: &[u8], secret: &[u8]) -> io::Result<()> { - self.buf.truncate(0); - write!(self.buf, "{} ", label)?; - for b in client_random.iter() { - write!(self.buf, "{:02x}", b)?; - } - write!(self.buf, " ")?; - for b in secret.iter() { - write!(self.buf, "{:02x}", b)?; - } - writeln!(self.buf)?; - self.file.write_all(&self.buf) +impl KeyLogFile { + /// Makes a new [`KeyLogFile`]. + pub(super) fn new(path: String) -> Result { + let handle = new_key_log_file_handle(path)?; + Ok(KeyLogFile(handle)) } } -impl Debug for KeyLogFileInner { - fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { - f.debug_struct("KeyLogFileInner") - // Note: we omit self.buf deliberately as it may contain key data. - .field("file", &self.file) - .finish() +impl KeyLog for KeyLogFile { + #[inline] + fn log(&self, label: &str, client_random: &[u8], secret: &[u8]) { + let line = format!( + "{} {:02x} {:02x}\n", + label, + PlainHex { + slice: client_random + }, + PlainHex { slice: secret }, + ); + self.0.write_log_line(line); } } -/// [`KeyLog`] implementation that opens a file whose name is -/// given by the `SSLKEYLOGFILE` environment variable, and writes -/// keys into it. -/// -/// If `SSLKEYLOGFILE` is not set, this does nothing. -/// -/// If such a file cannot be opened, or cannot be written then -/// this does nothing but logs errors at warning-level. -pub(super) struct KeyLogFile(Mutex); - -impl KeyLogFile { - /// Makes a new `KeyLogFile`. - pub(super) fn new(path: impl AsRef) -> Result { - let path = path.as_ref(); - trace!(?path, "rustls: open keylog file for debug purposes"); - Ok(Self(Mutex::new(KeyLogFileInner::new(path)?))) - } +struct PlainHex<'a, T: 'a> { + slice: &'a [T], } -impl KeyLog for KeyLogFile { - fn log(&self, label: &str, client_random: &[u8], secret: &[u8]) { - match self - .0 - .lock() - .unwrap() - .try_write(label, client_random, secret) - { - Ok(()) => {} - Err(e) => { - warn!("error writing to key log file: {}", e); - } - } +impl<'a, T: fmt::LowerHex> fmt::LowerHex for PlainHex<'a, T> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + fmt_inner_hex(self.slice, f, fmt::LowerHex::fmt) } } -impl Debug for KeyLogFile { - fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { - match self.0.try_lock() { - Ok(key_log_file) => write!(f, "{:?}", key_log_file), - Err(_) => write!(f, "KeyLogFile {{ }}"), - } +fn fmt_inner_hex fmt::Result>( + slice: &[T], + f: &mut fmt::Formatter, + fmt_fn: F, +) -> fmt::Result { + for val in slice.iter() { + fmt_fn(val, f)?; } + Ok(()) } diff --git a/tests/integration/examples/example_tests/utils/mod.rs b/tests/integration/examples/example_tests/utils/mod.rs index 1a309712..788ed326 100644 --- a/tests/integration/examples/example_tests/utils/mod.rs +++ b/tests/integration/examples/example_tests/utils/mod.rs @@ -90,6 +90,7 @@ where .run() .unwrap() .command() + .env("SSLKEYLOGFILE", "./target/test_ssl_key_log.txt") .spawn() .unwrap();