#![forbid(unsafe_code)]
#![deny(missing_docs)]
pub mod attempts;
pub mod credential;
pub mod documents;
pub mod tls;
use dynamic_config::{Error, Fetched};
pub fn guarded<F>(on_change: &mut F, document: Fetched, described: &str) -> Result<(), Error>
where
F: FnMut(Fetched) -> Result<(), Error>,
{
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| on_change(document))).unwrap_or_else(
|_| {
Err(Error::remote(format!(
"{described}: the watch callback panicked; the watch is stopped"
)))
},
)
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum LoneAuthority {
Secret,
Username,
}
#[must_use]
pub fn redacted(url: &str, lone: LoneAuthority) -> String {
let Some((scheme, rest)) = url.split_once("://") else {
return url.to_owned();
};
let Some((authority, tail)) = rest.rsplit_once('@') else {
return url.to_owned();
};
match authority.split_once(':') {
Some((user, _)) => format!("{scheme}://{user}:***@{tail}"),
None => match lone {
LoneAuthority::Secret => format!("{scheme}://***@{tail}"),
LoneAuthority::Username => format!("{scheme}://{authority}:***@{tail}"),
},
}
}
#[must_use]
pub fn redacted_list(urls: &str, lone: LoneAuthority) -> String {
urls.split(',')
.map(|url| redacted(url, lone))
.collect::<Vec<_>>()
.join(",")
}
#[cfg(test)]
mod tests {
use dynamic_config::Format;
use super::*;
#[test]
fn a_panicking_callback_ends_the_watch_rather_than_the_thread() {
let mut on_change = |_: Fetched| -> Result<(), Error> { panic!("the caller's bug") };
let error = guarded(
&mut on_change,
Fetched::new("{}".to_owned(), Format::Json),
"store the-key",
)
.expect_err("a panic becomes an error");
assert!(error.to_string().contains("store the-key"), "{error}");
assert!(error.to_string().contains("panicked"), "{error}");
}
#[test]
fn a_callbacks_own_error_is_passed_through_unchanged() {
let mut on_change =
|_: Fetched| -> Result<(), Error> { Err(Error::remote("bad document")) };
let error = guarded(
&mut on_change,
Fetched::new("{}".to_owned(), Format::Json),
"store the-key",
)
.expect_err("the callback failed");
assert_eq!(error.to_string(), "bad document");
}
#[test]
fn a_password_never_reaches_an_error_message() {
for lone in [LoneAuthority::Secret, LoneAuthority::Username] {
assert_eq!(
redacted("redis://app:hunter2@redis.internal:6379", lone),
"redis://app:***@redis.internal:6379"
);
assert_eq!(
redacted("redis://app:p@ss@w@rd@redis.internal:6379", lone),
"redis://app:***@redis.internal:6379"
);
assert_eq!(
redacted("redis://redis.internal:6379", lone),
"redis://redis.internal:6379"
);
assert_eq!(redacted("not a url", lone), "not a url");
}
}
#[test]
fn a_lone_authority_is_read_the_way_its_store_reads_it() {
assert_eq!(
redacted(
"nats://hunter2-token@nats.internal:4222",
LoneAuthority::Secret
),
"nats://***@nats.internal:4222"
);
assert_eq!(
redacted("redis://app@redis.internal:6379", LoneAuthority::Username),
"redis://app:***@redis.internal:6379"
);
}
#[test]
fn every_server_in_a_list_is_redacted() {
assert_eq!(
redacted_list(
"nats://hunter2@a:4222,nats://hunter2@b:4222",
LoneAuthority::Secret
),
"nats://***@a:4222,nats://***@b:4222"
);
}
}