1use http::HeaderName;
4use http::header::{AUTHORIZATION, COOKIE, PROXY_AUTHORIZATION, SET_COOKIE};
5use soaprs_core::{SoapError, SoapResult};
6
7#[derive(Debug, Clone, PartialEq, Eq)]
9pub struct TelemetryPolicy {
10 pub enabled: bool,
12 pub span_name: Option<String>,
14 request_headers: Vec<HeaderName>,
15 response_headers: Vec<HeaderName>,
16}
17
18impl TelemetryPolicy {
19 pub const fn enabled() -> Self {
21 Self {
22 enabled: true,
23 span_name: None,
24 request_headers: Vec::new(),
25 response_headers: Vec::new(),
26 }
27 }
28
29 pub const fn disabled() -> Self {
31 Self {
32 enabled: false,
33 span_name: None,
34 request_headers: Vec::new(),
35 response_headers: Vec::new(),
36 }
37 }
38
39 pub fn span_name(mut self, span_name: impl Into<String>) -> SoapResult<Self> {
41 let span_name = span_name.into();
42 if span_name.trim().is_empty() || span_name.chars().any(char::is_control) {
43 return Err(SoapError::validation(
44 "telemetry span name is empty or contains control characters",
45 ));
46 }
47 self.span_name = Some(span_name);
48 Ok(self)
49 }
50
51 pub fn record_request_header(mut self, header: HeaderName) -> SoapResult<Self> {
53 if sensitive_request_header(&header) {
54 return Err(SoapError::validation(format!(
55 "sensitive request header `{header}` cannot be recorded"
56 )));
57 }
58 push_once(&mut self.request_headers, header);
59 Ok(self)
60 }
61
62 pub fn record_response_header(mut self, header: HeaderName) -> SoapResult<Self> {
64 if header == SET_COOKIE {
65 return Err(SoapError::validation(
66 "sensitive response header `set-cookie` cannot be recorded",
67 ));
68 }
69 push_once(&mut self.response_headers, header);
70 Ok(self)
71 }
72
73 pub fn request_headers(&self) -> &[HeaderName] {
75 &self.request_headers
76 }
77
78 pub fn response_headers(&self) -> &[HeaderName] {
80 &self.response_headers
81 }
82
83 pub fn validate(&self) -> SoapResult<()> {
85 if self.span_name.as_ref().is_some_and(|span_name| {
86 span_name.trim().is_empty() || span_name.chars().any(char::is_control)
87 }) {
88 return Err(SoapError::validation(
89 "telemetry span name is empty or contains control characters",
90 ));
91 }
92 Ok(())
93 }
94}
95
96impl Default for TelemetryPolicy {
97 fn default() -> Self {
98 Self::enabled()
99 }
100}
101
102fn sensitive_request_header(header: &HeaderName) -> bool {
103 header == AUTHORIZATION
104 || header == PROXY_AUTHORIZATION
105 || header == COOKIE
106 || header == SET_COOKIE
107}
108
109fn push_once(headers: &mut Vec<HeaderName>, header: HeaderName) {
110 if !headers.contains(&header) {
111 headers.push(header);
112 }
113}
114
115#[cfg(test)]
116mod tests {
117 use http::header::{AUTHORIZATION, CONTENT_TYPE, SET_COOKIE};
118
119 use super::TelemetryPolicy;
120
121 #[test]
122 fn sensitive_headers_are_denied_from_telemetry_allowlists() {
123 assert!(
124 TelemetryPolicy::enabled()
125 .record_request_header(AUTHORIZATION)
126 .is_err()
127 );
128 assert!(
129 TelemetryPolicy::enabled()
130 .record_response_header(SET_COOKIE)
131 .is_err()
132 );
133 let policy = TelemetryPolicy::enabled().record_request_header(CONTENT_TYPE);
134 assert_eq!(
135 policy.ok().map(|value| value.request_headers().len()),
136 Some(1)
137 );
138 }
139}