use http::HeaderName;
use http::header::{AUTHORIZATION, COOKIE, PROXY_AUTHORIZATION, SET_COOKIE};
use soaprs_core::{SoapError, SoapResult};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TelemetryPolicy {
pub enabled: bool,
pub span_name: Option<String>,
request_headers: Vec<HeaderName>,
response_headers: Vec<HeaderName>,
}
impl TelemetryPolicy {
pub const fn enabled() -> Self {
Self {
enabled: true,
span_name: None,
request_headers: Vec::new(),
response_headers: Vec::new(),
}
}
pub const fn disabled() -> Self {
Self {
enabled: false,
span_name: None,
request_headers: Vec::new(),
response_headers: Vec::new(),
}
}
pub fn span_name(mut self, span_name: impl Into<String>) -> SoapResult<Self> {
let span_name = span_name.into();
if span_name.trim().is_empty() || span_name.chars().any(char::is_control) {
return Err(SoapError::validation(
"telemetry span name is empty or contains control characters",
));
}
self.span_name = Some(span_name);
Ok(self)
}
pub fn record_request_header(mut self, header: HeaderName) -> SoapResult<Self> {
if sensitive_request_header(&header) {
return Err(SoapError::validation(format!(
"sensitive request header `{header}` cannot be recorded"
)));
}
push_once(&mut self.request_headers, header);
Ok(self)
}
pub fn record_response_header(mut self, header: HeaderName) -> SoapResult<Self> {
if header == SET_COOKIE {
return Err(SoapError::validation(
"sensitive response header `set-cookie` cannot be recorded",
));
}
push_once(&mut self.response_headers, header);
Ok(self)
}
pub fn request_headers(&self) -> &[HeaderName] {
&self.request_headers
}
pub fn response_headers(&self) -> &[HeaderName] {
&self.response_headers
}
pub fn validate(&self) -> SoapResult<()> {
if self.span_name.as_ref().is_some_and(|span_name| {
span_name.trim().is_empty() || span_name.chars().any(char::is_control)
}) {
return Err(SoapError::validation(
"telemetry span name is empty or contains control characters",
));
}
Ok(())
}
}
impl Default for TelemetryPolicy {
fn default() -> Self {
Self::enabled()
}
}
fn sensitive_request_header(header: &HeaderName) -> bool {
header == AUTHORIZATION
|| header == PROXY_AUTHORIZATION
|| header == COOKIE
|| header == SET_COOKIE
}
fn push_once(headers: &mut Vec<HeaderName>, header: HeaderName) {
if !headers.contains(&header) {
headers.push(header);
}
}
#[cfg(test)]
mod tests {
use http::header::{AUTHORIZATION, CONTENT_TYPE, SET_COOKIE};
use super::TelemetryPolicy;
#[test]
fn sensitive_headers_are_denied_from_telemetry_allowlists() {
assert!(
TelemetryPolicy::enabled()
.record_request_header(AUTHORIZATION)
.is_err()
);
assert!(
TelemetryPolicy::enabled()
.record_response_header(SET_COOKIE)
.is_err()
);
let policy = TelemetryPolicy::enabled().record_request_header(CONTENT_TYPE);
assert_eq!(
policy.ok().map(|value| value.request_headers().len()),
Some(1)
);
}
}