use exfiltrate_internal::build_info::{BuildInfo, PROTOCOL_VERSION, PeerRole};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
#[non_exhaustive]
pub enum BindFailure {
#[default]
Warn,
Panic,
Silent,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub struct Batteries {
pub build_info: bool,
pub uptime: bool,
pub threads: bool,
pub memory: bool,
pub panics: bool,
pub env: bool,
}
impl Batteries {
pub const fn standard() -> Batteries {
Batteries {
build_info: true,
uptime: true,
threads: true,
memory: true,
panics: true,
env: false,
}
}
pub const fn none() -> Batteries {
Batteries {
build_info: false,
uptime: false,
threads: false,
memory: false,
panics: false,
env: false,
}
}
pub const fn all() -> Batteries {
Batteries {
env: true,
..Batteries::standard()
}
}
}
impl Default for Batteries {
fn default() -> Self {
Batteries::standard()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
pub struct AppInfo {
pub name: String,
pub version: String,
}
#[macro_export]
macro_rules! app_info {
() => {
$crate::AppInfo {
name: env!("CARGO_PKG_NAME").to_string(),
version: env!("CARGO_PKG_VERSION").to_string(),
}
};
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub struct Config {
pub addr: Option<String>,
pub on_bind_failure: BindFailure,
pub app: Option<AppInfo>,
pub batteries: Batteries,
pub log_capacity: usize,
pub panic_capacity: usize,
pub event_queue_capacity: usize,
pub env_redact_patterns: Vec<String>,
#[cfg(not(target_arch = "wasm32"))]
pub token: Option<String>,
#[cfg(not(target_arch = "wasm32"))]
pub announce_token: bool,
pub instance_registry: bool,
}
impl Default for Config {
fn default() -> Self {
Config {
addr: None,
on_bind_failure: BindFailure::default(),
app: None,
batteries: Batteries::default(),
log_capacity: 10_000,
panic_capacity: 64,
event_queue_capacity: 256,
env_redact_patterns: default_redact_patterns(),
#[cfg(not(target_arch = "wasm32"))]
token: None,
#[cfg(not(target_arch = "wasm32"))]
announce_token: false,
instance_registry: true,
}
}
}
pub fn default_redact_patterns() -> Vec<String> {
[
"secret",
"token",
"password",
"passwd",
"key",
"credential",
"auth",
"session",
"cookie",
"private",
]
.into_iter()
.map(str::to_string)
.collect()
}
impl Config {
pub fn with_addr(mut self, addr: impl Into<String>) -> Config {
self.addr = Some(addr.into());
self
}
#[cfg(not(target_arch = "wasm32"))]
pub fn with_token(mut self, token: impl Into<String>) -> Config {
self.token = Some(token.into());
self
}
#[cfg(not(target_arch = "wasm32"))]
pub fn with_generated_token(mut self) -> Config {
match exfiltrate_internal::auth::generate_token() {
Ok(token) => {
self.token = Some(token);
self.announce_token = true;
}
Err(error) => {
crate::diagnostic(&format!(
"exfiltrate: could not generate a token ({error}); \
the server will refuse any non-loopback address."
));
}
}
self
}
pub fn with_app(mut self, app: AppInfo) -> Config {
self.app = Some(app);
self
}
pub fn with_batteries(mut self, batteries: Batteries) -> Config {
self.batteries = batteries;
self
}
pub fn with_bind_failure(mut self, on_bind_failure: BindFailure) -> Config {
self.on_bind_failure = on_bind_failure;
self
}
pub fn should_redact(&self, name: &str) -> bool {
let name = name.to_ascii_lowercase();
self.env_redact_patterns
.iter()
.any(|pattern| name.contains(&pattern.to_ascii_lowercase()))
}
pub fn build_info(&self) -> BuildInfo {
let (app_name, app_version) = match &self.app {
Some(app) => (app.name.clone(), app.version.clone()),
None => (executable_name(), String::new()),
};
BuildInfo {
protocol_version: PROTOCOL_VERSION,
role: PeerRole::Server,
exfiltrate_version: env!("CARGO_PKG_VERSION").to_string(),
app_name,
app_version,
target_triple: env!("EXFILTRATE_TARGET").to_string(),
profile: env!("EXFILTRATE_PROFILE").to_string(),
features: enabled_features(),
git_sha: non_empty(env!("EXFILTRATE_GIT_SHA")),
build_timestamp: non_empty(env!("EXFILTRATE_BUILD_TIMESTAMP")),
}
}
}
fn non_empty(value: &str) -> Option<String> {
if value.is_empty() {
None
} else {
Some(value.to_string())
}
}
pub fn enabled_features() -> Vec<String> {
Vec::new()
}
fn executable_name() -> String {
#[cfg(not(target_arch = "wasm32"))]
{
std::env::current_exe()
.ok()
.and_then(|path| {
path.file_stem()
.map(|stem| stem.to_string_lossy().into_owned())
})
.unwrap_or_default()
}
#[cfg(target_arch = "wasm32")]
{
String::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_default_config_keeps_env_out_of_the_battery() {
let config = Config::default();
assert!(config.batteries.build_info);
assert!(!config.batteries.env, "env must be opt-in");
}
#[test]
fn redaction_matches_on_a_case_insensitive_substring() {
let config = Config::default();
assert!(config.should_redact("AWS_SECRET_ACCESS_KEY"));
assert!(config.should_redact("github_token"));
assert!(config.should_redact("MY_Private_Thing"));
assert!(!config.should_redact("HOME"));
assert!(!config.should_redact("PATH"));
}
#[test]
fn build_info_reports_this_crate_and_this_target() {
let info = Config::default().build_info();
assert_eq!(info.exfiltrate_version, env!("CARGO_PKG_VERSION"));
assert!(!info.target_triple.is_empty());
assert!(!info.profile.is_empty());
assert_eq!(info.role, PeerRole::Server);
}
#[test]
fn an_explicit_app_identity_beats_the_executable_name() {
let config = Config::default().with_app(AppInfo {
name: "demo".to_string(),
version: "1.2.3".to_string(),
});
let info = config.build_info();
assert_eq!(info.app_name, "demo");
assert_eq!(info.app_version, "1.2.3");
}
#[test]
fn batteries_presets_differ_only_where_documented() {
assert!(Batteries::all().env);
assert!(!Batteries::standard().env);
assert_eq!(
Batteries::none(),
Batteries {
build_info: false,
uptime: false,
threads: false,
memory: false,
panics: false,
env: false,
}
);
}
}