rama_net/tls/
keylog.rs

1//! Keylog facility used by any tls implementation
2//! supported by rama, and which can be used for your owns as well.
3//!
4//! Center to this module is the `KeyLogger` which is a wrapper around
5//! a FS file
6
7use parking_lot::RwLock;
8use rama_core::error::{ErrorContext, OpaqueError};
9use std::{
10    collections::{HashMap, hash_map::Entry},
11    fs::OpenOptions,
12    io::Write,
13    path::{Component, Path, PathBuf},
14    sync::OnceLock,
15};
16
17/// Get a key log file handle for the given path
18/// only one file handle will be opened per unique path String.
19///
20/// # To be unique or ditto
21///
22/// Paths are case-sensitive by default for rama, as utf-8 compatible.
23/// Normalize yourself prior to passing a path to this function if you're concerned.
24pub fn new_key_log_file_handle(path: String) -> Result<KeyLogFileHandle, OpaqueError> {
25    let path: PathBuf = path
26        .parse()
27        .with_context(|| format!("parse path str as Path: {path}"))?;
28
29    if let Some(parent) = path.parent() {
30        std::fs::create_dir_all(parent)
31            .with_context(|| format!("create parent dir(s) at {parent:?} for key log file"))?;
32    }
33
34    let path = normalize_path(&path);
35
36    let mapping = GLOBAL_KEY_LOG_FILE_MAPPING.get_or_init(Default::default);
37    if let Some(handle) = mapping.read().get(&path).cloned() {
38        return Ok(handle);
39    }
40    let mut mut_mapping = mapping.write();
41    match mut_mapping.entry(path.clone()) {
42        Entry::Occupied(entry) => Ok(entry.get().clone()),
43        Entry::Vacant(entry) => {
44            let handle = try_init_key_log_file_handle(path)?;
45            entry.insert(handle.clone());
46            Ok(handle)
47        }
48    }
49}
50
51// copied from
52// <https://github.com/rust-lang/cargo/blob/fede83ccf973457de319ba6fa0e36ead454d2e20/src/cargo/util/paths.rs#L61>
53pub fn normalize_path(path: &Path) -> PathBuf {
54    let mut components = path.components().peekable();
55    let mut ret = if let Some(c @ Component::Prefix(..)) = components.peek().cloned() {
56        components.next();
57        PathBuf::from(c.as_os_str())
58    } else {
59        PathBuf::new()
60    };
61
62    for component in components {
63        match component {
64            Component::Prefix(..) => unreachable!(),
65            Component::RootDir => {
66                ret.push(component.as_os_str());
67            }
68            Component::CurDir => {}
69            Component::ParentDir => {
70                ret.pop();
71            }
72            Component::Normal(c) => {
73                ret.push(c);
74            }
75        }
76    }
77    ret
78}
79
80fn try_init_key_log_file_handle(path: PathBuf) -> Result<KeyLogFileHandle, OpaqueError> {
81    tracing::trace!(
82        file = ?path,
83        "KeyLogFileHandle: try to create a new handle",
84    );
85
86    if let Some(parent) = path.parent() {
87        std::fs::create_dir_all(parent).context("create parent dir(s) of key log file")?;
88    }
89
90    let mut file = OpenOptions::new()
91        .append(true)
92        .create(true)
93        .open(&path)
94        .with_context(|| format!("create key log file {path:?}"))?;
95
96    let (tx, rx) = flume::unbounded::<String>();
97
98    let path_name = path.clone();
99    std::thread::spawn(move || {
100        tracing::trace!(
101            file = ?path_name,
102            "KeyLogFileHandle[rx]: receiver task up and running",
103        );
104        while let Ok(line) = rx.recv() {
105            if let Err(err) = file.write_all(line.as_bytes()) {
106                tracing::error!(
107                    file = ?path_name,
108                    error = %err,
109                    "KeyLogFileHandle[rx]: failed to write file",
110                );
111            }
112        }
113    });
114
115    Ok(KeyLogFileHandle { path, sender: tx })
116}
117
118static GLOBAL_KEY_LOG_FILE_MAPPING: OnceLock<RwLock<HashMap<PathBuf, KeyLogFileHandle>>> =
119    OnceLock::new();
120
121#[derive(Debug, Clone)]
122/// Handle to a (tls) keylog file.
123///
124/// See [`new_key_log_file_handle`] for more info,
125/// as that is the one creating it.
126pub struct KeyLogFileHandle {
127    path: PathBuf,
128    sender: flume::Sender<String>,
129}
130
131impl KeyLogFileHandle {
132    /// Write a line to the keylogger.
133    pub fn write_log_line(&self, line: String) {
134        if let Err(err) = self.sender.send(line) {
135            tracing::error!(
136                file = ?self.path,
137                error = %err,
138                "KeyLogFileHandle[tx]: failed to send log line for writing",
139            );
140        }
141    }
142}