use std::fmt;
use std::net::IpAddr;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
pub const RWS_CONFIG_ACCESS_LOG_ANONYMIZE_IP: &str = "RWS_CONFIG_ACCESS_LOG_ANONYMIZE_IP";
pub const RWS_CONFIG_ACCESS_LOG_IP_HASH_SECRET: &str = "RWS_CONFIG_ACCESS_LOG_IP_HASH_SECRET";
pub const RWS_CONFIG_LOG_REDACT_HEADERS: &str = "RWS_CONFIG_LOG_REDACT_HEADERS";
pub const RWS_CONFIG_LOG_REDACT_QUERY: &str = "RWS_CONFIG_LOG_REDACT_QUERY";
pub const REDACTED: &str = "REDACTED";
pub const ALWAYS_REDACTED_HEADERS: [&str; 4] = ["authorization", "proxy-authorization", "cookie", "set-cookie"];
static ACTIVE: AtomicBool = AtomicBool::new(false);
#[doc(hidden)]
pub fn maybe_active() -> bool {
ACTIVE.load(Ordering::Relaxed)
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub enum AnonymizeIp {
#[default]
Off,
Truncate,
Hash,
}
impl AnonymizeIp {
pub fn parse(value: &str) -> Option<AnonymizeIp> {
match value.trim().to_ascii_lowercase().as_str() {
"" | "off" | "false" | "none" => Some(AnonymizeIp::Off),
"truncate" | "true" | "on" => Some(AnonymizeIp::Truncate),
"hash" => Some(AnonymizeIp::Hash),
_ => None,
}
}
}
#[derive(Clone, Default, PartialEq)]
pub struct LogPrivacy {
pub anonymize_ip: AnonymizeIp,
pub ip_hash_key: Arc<[u8]>,
pub redact_headers: Vec<String>,
pub redact_query: Vec<String>,
}
impl fmt::Debug for LogPrivacy {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("LogPrivacy")
.field("anonymize_ip", &self.anonymize_ip)
.field("ip_hash_key", &if self.ip_hash_key.is_empty() { "(none)" } else { "(set)" })
.field("redact_headers", &self.redact_headers)
.field("redact_query", &self.redact_query)
.finish()
}
}
fn names(value: &str) -> Vec<String> {
value.split(',').map(|n| n.trim().to_ascii_lowercase()).filter(|n| !n.is_empty()).collect()
}
impl LogPrivacy {
pub fn from_env() -> LogPrivacy {
let read = |key: &str| std::env::var(key).unwrap_or_default();
let privacy = LogPrivacy {
anonymize_ip: AnonymizeIp::parse(&read(RWS_CONFIG_ACCESS_LOG_ANONYMIZE_IP)).unwrap_or(AnonymizeIp::Truncate),
ip_hash_key: read(RWS_CONFIG_ACCESS_LOG_IP_HASH_SECRET).trim().as_bytes().into(),
redact_headers: names(&read(RWS_CONFIG_LOG_REDACT_HEADERS)),
redact_query: names(&read(RWS_CONFIG_LOG_REDACT_QUERY)),
};
ACTIVE.store(!privacy.is_off(), Ordering::Relaxed);
privacy
}
pub fn is_off(&self) -> bool {
self.anonymize_ip == AnonymizeIp::Off && self.redact_headers.is_empty() && self.redact_query.is_empty()
}
pub fn write_ip(&self, out: &mut Vec<u8>, ip: IpAddr) {
use std::io::Write;
match self.anonymize_ip {
AnonymizeIp::Off => {
let _ = write!(out, "{ip}");
}
AnonymizeIp::Hash if !self.ip_hash_key.is_empty() => {
let text = ip.to_canonical().to_string();
let mac = crate::core::sha256::hmac_sha256(&self.ip_hash_key, text.as_bytes());
const HEX: &[u8; 16] = b"0123456789abcdef";
for b in &mac[..8] {
out.push(HEX[(b >> 4) as usize]);
out.push(HEX[(b & 15) as usize]);
}
}
AnonymizeIp::Truncate | AnonymizeIp::Hash => {
let _ = write!(out, "{}", truncate_ip(ip));
}
}
}
pub fn ip_string(&self, ip: IpAddr) -> String {
let mut out = Vec::with_capacity(40);
self.write_ip(&mut out, ip);
String::from_utf8(out).unwrap_or_default()
}
pub fn redacts_header(&self, name: &str) -> bool {
ALWAYS_REDACTED_HEADERS.iter().any(|h| h.eq_ignore_ascii_case(name))
|| self.redact_headers.iter().any(|h| h.eq_ignore_ascii_case(name))
}
pub fn redact_target<'a>(&self, target: &'a str, mut push: impl FnMut(&'a str)) {
let Some(q) = target.find('?').filter(|_| !self.redact_query.is_empty()) else {
push(target);
return;
};
let (head, rest) = target.split_at(q + 1);
let (query, fragment) = match rest.find('#') {
Some(h) => rest.split_at(h),
None => (rest, ""),
};
push(head);
for (i, pair) in query.split('&').enumerate() {
if i > 0 {
push("&");
}
match pair.split_once('=') {
Some((name, _)) if self.redact_query.iter().any(|n| n.eq_ignore_ascii_case(name)) => {
push(name);
push("=");
push(REDACTED);
}
_ => push(pair),
}
}
push(fragment);
}
pub fn redacted_target(&self, target: &str) -> String {
let mut out = String::with_capacity(target.len());
self.redact_target(target, |piece| out.push_str(piece));
out
}
}
pub fn truncate_ip(ip: IpAddr) -> IpAddr {
match ip.to_canonical() {
IpAddr::V4(v4) => {
let [a, b, c, _] = v4.octets();
IpAddr::from([a, b, c, 0])
}
IpAddr::V6(v6) => {
let s = v6.segments();
IpAddr::from([s[0], s[1], s[2], 0, 0, 0, 0, 0])
}
}
}
#[cfg(test)]
mod tests;