#![cfg_attr(doctest, allow(unused_imports))]
use crate::security::redact_cache_key;
pub fn log_cache_key(_level: &str, message: &str, key: &str) -> String {
let redacted = redact_cache_key(key);
format!("{}: {}", message, redacted)
}
pub fn sanitize_message(message: &str) -> String {
let mut result = String::with_capacity(message.len());
let mut remaining = message;
while let Some(rel_pos) = remaining.find("://") {
let protocol_start = remaining[..rel_pos]
.rfind(|c: char| c.is_whitespace())
.map(|i| i + 1)
.unwrap_or(0);
let protocol = &remaining[protocol_start..rel_pos];
let after_start = rel_pos + 3;
result.push_str(&remaining[..protocol_start]);
if let Some(at_pos) = remaining[after_start..].find('@') {
let abs_at_pos = after_start + at_pos;
let user_part = &remaining[after_start..abs_at_pos];
let after_host = &remaining[abs_at_pos..];
let host_end_rel = after_host
.find(|c: char| c.is_whitespace() || c == '?' || c == '#')
.unwrap_or(after_host.len());
let host_end = abs_at_pos + host_end_rel;
let host_part = &remaining[abs_at_pos..host_end];
let sanitized_user: String = if user_part.contains(':') {
user_part
.chars()
.take_while(|c| *c != ':')
.chain(std::iter::repeat_n('*', 2))
.collect()
} else {
user_part.to_string()
};
result.push_str(protocol);
result.push_str("://");
result.push_str(&sanitized_user);
result.push_str(host_part);
let mut tail_end = host_end;
if remaining[tail_end..].starts_with(['?', '#']) {
let marker = remaining[tail_end..tail_end + 1].to_string();
let query_end = remaining[tail_end..]
.find(|c: char| c.is_whitespace())
.map(|i| tail_end + i)
.unwrap_or(remaining.len());
result.push_str(&marker);
result.push_str("**");
tail_end = query_end;
}
remaining = &remaining[tail_end..];
} else {
result.push_str(protocol);
result.push_str("://");
remaining = &remaining[after_start..];
}
}
result.push_str(remaining);
result
}
#[cfg(test)]
mod tests {
use super::*;
use crate::security::{redact_cache_key, redact_connection_string};
#[test]
fn test_log_connection_string() {
let conn_str = "redis://user:password123@localhost:6379";
let redacted = redact_connection_string(conn_str);
assert!(!redacted.contains("password123"));
assert!(redacted.contains("user:****"));
}
#[test]
fn test_log_cache_key() {
let key = "user_token_abc123";
let redacted = redact_cache_key(key);
assert!(!redacted.contains("token"));
assert!(redacted.starts_with("****"));
}
#[test]
fn test_sanitize_message() {
let msg = "Connection: redis://user:secret123@localhost:6379";
let sanitized = sanitize_message(msg);
assert!(!sanitized.contains("secret123"));
assert!(sanitized.contains("**"));
}
#[test]
fn test_log_cache_key_info_level() {
log_cache_key("info", "Cache hit", "user_token_abc123");
}
#[test]
fn test_log_cache_key_debug_level() {
log_cache_key("debug", "Cache debug", "session_xyz");
}
#[test]
fn test_log_cache_key_warn_level() {
log_cache_key("warn", "Cache warning", "password_123");
}
#[test]
fn test_log_cache_key_error_level() {
log_cache_key("error", "Cache error", "api_key_test");
}
#[test]
fn test_log_cache_key_default_level() {
log_cache_key("trace", "Cache trace", "normal_key");
log_cache_key("unknown_level", "Cache unknown", "another_key");
}
#[test]
fn test_log_cache_key_non_sensitive_key() {
log_cache_key("info", "Cache access", "user_profile_123");
}
#[test]
fn test_log_cache_key_empty_key() {
log_cache_key("info", "Empty key", "");
}
#[test]
fn test_log_cache_key_empty_message() {
log_cache_key("info", "", "some_key");
}
#[test]
fn test_sanitize_message_no_connection_string() {
let msg = "This is a normal message without connection string";
let sanitized = sanitize_message(msg);
assert_eq!(sanitized, msg);
}
#[test]
fn test_sanitize_message_empty() {
let sanitized = sanitize_message("");
assert_eq!(sanitized, "");
}
#[test]
fn test_sanitize_message_no_at_symbol() {
let msg = "redis://localhost:6379";
let sanitized = sanitize_message(msg);
assert_eq!(sanitized, msg);
}
#[test]
fn test_sanitize_message_with_password() {
let msg = "redis://user:password123@host:6379";
let sanitized = sanitize_message(msg);
assert!(!sanitized.contains("password123"));
}
#[test]
fn test_sanitize_message_multiple_protocols() {
let msg = "redis://user:pass1@host1:6379 and redis://user:pass2@host2:6380";
let sanitized = sanitize_message(msg);
assert!(!sanitized.contains("pass1"));
assert!(!sanitized.contains("pass2"));
}
#[test]
fn test_sanitize_message_user_only_unchanged() {
let msg = "redis://user@host:6379";
assert_eq!(sanitize_message(msg), msg);
}
#[test]
fn test_sanitize_message_query_redacted() {
let msg = "GET redis://user:secret123@host:6379/cache?token=abc123 done";
let sanitized = sanitize_message(msg);
assert!(!sanitized.contains("secret123"));
assert!(!sanitized.contains("token=abc123"));
}
}