use std::sync::Arc;
use rvoip_sip_core::types::headers::HeaderName;
pub const REDACTED_BODY_MARKER: &str =
rvoip_infra_common::events::cross_crate::SIP_TRACE_REDACTED_BODY;
#[derive(Clone, PartialEq, Eq)]
pub enum RedactionDecision {
Keep,
Redact(String),
Drop,
}
impl std::fmt::Debug for RedactionDecision {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Keep => formatter.write_str("Keep"),
Self::Redact(_) => formatter.write_str("Redact([redacted])"),
Self::Drop => formatter.write_str("Drop"),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum BodyRedactionDecision {
Keep,
Redact,
Drop,
}
pub trait TraceRedactor: Send + Sync + std::fmt::Debug {
fn redact(&self, header: &HeaderName, value: &str) -> RedactionDecision;
fn redact_body(&self, _content_type: Option<&str>) -> BodyRedactionDecision {
BodyRedactionDecision::Redact
}
fn allows_verbatim_trace(&self) -> bool {
false
}
}
#[derive(Clone, Debug, Default)]
pub struct DefaultTraceRedactor;
impl TraceRedactor for DefaultTraceRedactor {
fn redact(&self, header: &HeaderName, _value: &str) -> RedactionDecision {
match header {
HeaderName::CallId
| HeaderName::ContentLength
| HeaderName::ContentType
| HeaderName::CSeq
| HeaderName::MaxForwards
| HeaderName::Allow
| HeaderName::Expires
| HeaderName::MinExpires
| HeaderName::Supported
| HeaderName::RAck
| HeaderName::Accept
| HeaderName::AcceptEncoding
| HeaderName::ContentEncoding
| HeaderName::Require
| HeaderName::Timestamp
| HeaderName::Priority
| HeaderName::Date
| HeaderName::MimeVersion
| HeaderName::ProxyRequire
| HeaderName::Unsupported
| HeaderName::SessionExpires
| HeaderName::MinSE
| HeaderName::RSeq
| HeaderName::AllowEvents => RedactionDecision::Keep,
_ => RedactionDecision::Redact("<redacted>".to_string()),
}
}
}
pub fn default_trace_redactor() -> Arc<dyn TraceRedactor> {
Arc::new(DefaultTraceRedactor)
}
#[derive(Clone, Debug, Default)]
pub struct PassthroughRedactor;
impl TraceRedactor for PassthroughRedactor {
fn redact(&self, _header: &HeaderName, _value: &str) -> RedactionDecision {
RedactionDecision::Keep
}
fn redact_body(&self, _content_type: Option<&str>) -> BodyRedactionDecision {
BodyRedactionDecision::Keep
}
fn allows_verbatim_trace(&self) -> bool {
true
}
}
pub fn apply_redaction(
redactor: Option<&Arc<dyn TraceRedactor>>,
header: &HeaderName,
value: &str,
) -> Option<String> {
match redactor {
None => match DefaultTraceRedactor.redact(header, value) {
RedactionDecision::Keep => Some(value.to_string()),
RedactionDecision::Redact(replacement) => Some(replacement),
RedactionDecision::Drop => None,
},
Some(r) => match r.redact(header, value) {
RedactionDecision::Keep => Some(value.to_string()),
RedactionDecision::Redact(replacement) => Some(replacement),
RedactionDecision::Drop => None,
},
}
}
pub fn apply_message_redactor(redactor: &dyn TraceRedactor, raw: &str) -> String {
enum ContinuationDecision {
None,
Keep,
Redact(String),
Drop,
}
fn push_line_ending(out: &mut String, line: &str) {
if line.ends_with("\r\n") {
out.push_str("\r\n");
} else if line.ends_with('\n') {
out.push('\n');
}
}
if redactor.allows_verbatim_trace() {
return raw.to_string();
}
let mut out = String::with_capacity(raw.len());
let mut continuation = ContinuationDecision::None;
let mut content_type = None;
let mut offset = 0;
let mut first_line = true;
for line in raw.split_inclusive('\n') {
offset += line.len();
let trimmed = line.trim_end_matches('\n').trim_end_matches('\r');
if first_line {
out.push_str(
&rvoip_infra_common::events::cross_crate::redact_sip_trace_start_line(trimmed),
);
push_line_ending(&mut out, line);
first_line = false;
continue;
}
if trimmed.is_empty() {
out.push_str(line);
let body = &raw[offset..];
if !body.is_empty() {
match redactor.redact_body(content_type.as_deref()) {
BodyRedactionDecision::Keep => out.push_str(body),
BodyRedactionDecision::Redact => {
out.push_str(REDACTED_BODY_MARKER);
if body.ends_with("\r\n") {
out.push_str("\r\n");
} else if body.ends_with('\n') {
out.push('\n');
}
}
BodyRedactionDecision::Drop => {}
}
}
return out;
}
let bytes = trimmed.as_bytes();
if matches!(bytes.first(), Some(b' ' | b'\t')) {
match &continuation {
ContinuationDecision::None => {
let leading_whitespace_len = trimmed.len() - trimmed.trim_start().len();
out.push_str(&trimmed[..leading_whitespace_len]);
out.push_str(
rvoip_infra_common::events::cross_crate::SIP_TRACE_REDACTED_HEADER_VALUE,
);
push_line_ending(&mut out, line);
}
ContinuationDecision::Keep => out.push_str(line),
ContinuationDecision::Redact(replacement) => {
let leading_whitespace_len = trimmed.len() - trimmed.trim_start().len();
out.push_str(&trimmed[..leading_whitespace_len]);
out.push_str(replacement);
push_line_ending(&mut out, line);
}
ContinuationDecision::Drop => {}
}
continue;
}
let Some(colon) = trimmed.find(':') else {
out.push_str(rvoip_infra_common::events::cross_crate::SIP_TRACE_REDACTED_HEADER_VALUE);
push_line_ending(&mut out, line);
continuation = ContinuationDecision::Redact(
rvoip_infra_common::events::cross_crate::SIP_TRACE_REDACTED_HEADER_VALUE.into(),
);
continue;
};
let name = trimmed[..colon].trim();
let value = trimmed[colon + 1..].trim();
let header_name = name
.parse::<HeaderName>()
.unwrap_or_else(|_| HeaderName::Other(name.to_string()));
if header_name == HeaderName::ContentType {
content_type = Some(value.to_string());
}
match redactor.redact(&header_name, value) {
RedactionDecision::Keep => {
out.push_str(line);
continuation = ContinuationDecision::Keep;
}
RedactionDecision::Redact(replacement) => {
out.push_str(name);
out.push_str(": ");
out.push_str(&replacement);
push_line_ending(&mut out, line);
continuation = ContinuationDecision::Redact(replacement);
}
RedactionDecision::Drop => {
continuation = ContinuationDecision::Drop;
}
}
}
out
}