1use crate::config::{HeaderConfig, HeaderOps};
11use crate::shadow::is_sensitive_header;
12use bytes::Bytes;
13use http::header::{HeaderName, HeaderValue};
14use pingora_http::{RequestHeader, ResponseHeader};
15use tracing::debug;
16
17#[inline]
22fn redact_for_log(name: &str, value: &str) -> String {
23 if is_sensitive_header(name) {
24 "[REDACTED]".to_string()
25 } else {
26 value.to_string()
27 }
28}
29
30#[derive(Debug, Clone, Default)]
31pub struct CompiledHeaderOps {
32 pub(crate) add: Vec<CompiledHeaderValue>,
33 pub(crate) set: Vec<CompiledHeaderValue>,
34 pub(crate) remove: Vec<HeaderName>,
35}
36
37#[derive(Debug, Clone, Default)]
38pub struct CompiledHeaderConfig {
39 pub(crate) request: CompiledHeaderOps,
40 pub(crate) response: CompiledHeaderOps,
41}
42
43impl CompiledHeaderConfig {
44 pub fn request(&self) -> &CompiledHeaderOps {
46 &self.request
47 }
48
49 pub fn response(&self) -> &CompiledHeaderOps {
51 &self.response
52 }
53}
54
55#[derive(Debug, Clone)]
56struct CompiledHeaderValue {
57 name: Bytes,
58 value: HeaderValue,
59}
60
61impl CompiledHeaderOps {
62 fn with_capacity(add: usize, set: usize, remove: usize) -> Self {
63 Self {
64 add: Vec::with_capacity(add),
65 set: Vec::with_capacity(set),
66 remove: Vec::with_capacity(remove),
67 }
68 }
69}
70
71impl HeaderConfig {
72 pub fn compile(&self) -> CompiledHeaderConfig {
73 CompiledHeaderConfig {
74 request: self.request.compile(),
75 response: self.response.compile(),
76 }
77 }
78}
79
80impl HeaderOps {
81 pub fn compile(&self) -> CompiledHeaderOps {
82 let mut compiled =
83 CompiledHeaderOps::with_capacity(self.add.len(), self.set.len(), self.remove.len());
84
85 for name in &self.remove {
86 match HeaderName::from_bytes(name.as_bytes()) {
87 Ok(header_name) => compiled.remove.push(header_name),
88 Err(err) => debug!("Invalid remove header name '{}': {}", name, err),
89 }
90 }
91
92 compiled.set = compile_header_entries(&self.set, "set");
93 compiled.add = compile_header_entries(&self.add, "add");
94
95 compiled
96 }
97}
98
99fn compile_header_entries(
100 entries: &std::collections::HashMap<String, String>,
101 op: &'static str,
102) -> Vec<CompiledHeaderValue> {
103 let mut compiled = Vec::with_capacity(entries.len());
104
105 for (name, value) in entries {
106 if let Err(err) = HeaderName::from_bytes(name.as_bytes()) {
107 debug!("Invalid {} header name '{}': {}", op, name, err);
108 continue;
109 }
110
111 match HeaderValue::from_str(value) {
112 Ok(header_value) => compiled.push(CompiledHeaderValue {
113 name: Bytes::copy_from_slice(name.as_bytes()),
114 value: header_value,
115 }),
116 Err(err) => debug!("Invalid {} header value for '{}': {}", op, name, err),
117 }
118 }
119
120 compiled
121}
122
123#[inline]
124fn header_name_for_log(name: &Bytes) -> &str {
125 std::str::from_utf8(name.as_ref()).unwrap_or("<invalid>")
126}
127
128#[inline]
129fn header_value_for_log(value: &HeaderValue) -> &str {
130 value.to_str().unwrap_or("<binary>")
131}
132
133pub fn apply_request_headers(header: &mut RequestHeader, ops: &CompiledHeaderOps) {
135 for name in &ops.remove {
137 if header.remove_header(name).is_some() {
138 debug!("Removed request header: {}", name.as_str());
139 }
140 }
141
142 for entry in &ops.set {
144 let name = header_name_for_log(&entry.name);
145 if let Err(e) = header.insert_header(entry.name.clone(), entry.value.clone()) {
146 debug!("Failed to set request header {}: {}", name, e);
147 } else {
148 let value = header_value_for_log(&entry.value);
150 debug!(
151 "Set request header: {} = {}",
152 name,
153 redact_for_log(name, value)
154 );
155 }
156 }
157
158 for entry in &ops.add {
160 let name = header_name_for_log(&entry.name);
161 if let Err(e) = header.append_header(entry.name.clone(), entry.value.clone()) {
162 debug!("Failed to add request header {}: {}", name, e);
163 } else {
164 let value = header_value_for_log(&entry.value);
166 debug!(
167 "Added request header: {} = {}",
168 name,
169 redact_for_log(name, value)
170 );
171 }
172 }
173}
174
175pub fn apply_response_headers(header: &mut ResponseHeader, ops: &CompiledHeaderOps) {
177 for name in &ops.remove {
179 if header.remove_header(name).is_some() {
180 debug!("Removed response header: {}", name.as_str());
181 }
182 }
183
184 for entry in &ops.set {
186 let name = header_name_for_log(&entry.name);
187 if let Err(e) = header.insert_header(entry.name.clone(), entry.value.clone()) {
188 debug!("Failed to set response header {}: {}", name, e);
189 } else {
190 let value = header_value_for_log(&entry.value);
192 debug!(
193 "Set response header: {} = {}",
194 name,
195 redact_for_log(name, value)
196 );
197 }
198 }
199
200 for entry in &ops.add {
202 let name = header_name_for_log(&entry.name);
203 if let Err(e) = header.append_header(entry.name.clone(), entry.value.clone()) {
204 debug!("Failed to add response header {}: {}", name, e);
205 } else {
206 let value = header_value_for_log(&entry.value);
208 debug!(
209 "Added response header: {} = {}",
210 name,
211 redact_for_log(name, value)
212 );
213 }
214 }
215}
216
217#[inline]
218fn ensure_response_header(header: &mut ResponseHeader, name: &'static str, value: &'static str) {
219 if header.headers.get(name).is_some() {
220 return;
221 }
222
223 if let Err(err) = header.insert_header(name, value) {
224 debug!("Failed to set security header {}: {}", name, err);
225 }
226}
227
228pub fn apply_security_response_headers(header: &mut ResponseHeader, is_https: bool) {
234 if is_https {
236 ensure_response_header(
237 header,
238 "strict-transport-security",
239 "max-age=31536000; includeSubDomains",
240 );
241 }
242
243 ensure_response_header(header, "x-content-type-options", "nosniff");
244 ensure_response_header(header, "x-frame-options", "DENY");
245 ensure_response_header(header, "referrer-policy", "strict-origin-when-cross-origin");
246 ensure_response_header(
247 header,
248 "permissions-policy",
249 "accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()",
250 );
251}
252
253#[cfg(test)]
254mod tests {
255 use super::*;
256 use pingora_http::RequestHeader;
257
258 #[test]
259 fn test_redact_sensitive_headers() {
260 assert_eq!(
262 redact_for_log("Authorization", "Bearer secret-token"),
263 "[REDACTED]"
264 );
265 assert_eq!(
266 redact_for_log("authorization", "Basic dXNlcjpwYXNz"),
267 "[REDACTED]"
268 );
269 assert_eq!(redact_for_log("Cookie", "session=abc123"), "[REDACTED]");
270 assert_eq!(redact_for_log("X-Api-Key", "sk-live-12345"), "[REDACTED]");
271 assert_eq!(
272 redact_for_log("X-Auth-Token", "auth-token-value"),
273 "[REDACTED]"
274 );
275 assert_eq!(redact_for_log("X-CSRF-Token", "csrf123"), "[REDACTED]");
276 }
277
278 #[test]
279 fn test_redact_non_sensitive_headers() {
280 assert_eq!(
282 redact_for_log("Content-Type", "application/json"),
283 "application/json"
284 );
285 assert_eq!(redact_for_log("Accept", "text/html"), "text/html");
286 assert_eq!(redact_for_log("User-Agent", "Mozilla/5.0"), "Mozilla/5.0");
287 assert_eq!(redact_for_log("X-Request-Id", "req-123"), "req-123");
288 assert_eq!(redact_for_log("Cache-Control", "no-cache"), "no-cache");
289 }
290
291 #[test]
292 fn test_redact_case_insensitive() {
293 assert_eq!(redact_for_log("AUTHORIZATION", "token"), "[REDACTED]");
295 assert_eq!(redact_for_log("Authorization", "token"), "[REDACTED]");
296 assert_eq!(redact_for_log("authorization", "token"), "[REDACTED]");
297 assert_eq!(redact_for_log("COOKIE", "value"), "[REDACTED]");
298 assert_eq!(redact_for_log("Cookie", "value"), "[REDACTED]");
299 assert_eq!(redact_for_log("cookie", "value"), "[REDACTED]");
300 }
301
302 #[test]
303 fn test_compile_header_ops_skips_invalid_entries() {
304 let mut ops = HeaderOps::default();
305 ops.add
306 .insert("Bad Header".to_string(), "value".to_string());
307 ops.set.insert("X-Good".to_string(), "ok".to_string());
308 ops.remove.push("Another Bad Header".to_string());
309
310 let compiled = ops.compile();
311
312 assert_eq!(compiled.add.len(), 0);
313 assert_eq!(compiled.set.len(), 1);
314 assert_eq!(compiled.remove.len(), 0);
315 }
316
317 #[test]
318 fn test_apply_compiled_request_headers() {
319 let mut ops = HeaderOps::default();
320 ops.add.insert("X-Added".to_string(), "value".to_string());
321 ops.set.insert("X-Set".to_string(), "set-value".to_string());
322 ops.remove.push("X-Remove".to_string());
323
324 let compiled = ops.compile();
325 let mut header = RequestHeader::build("GET", b"/", None).unwrap();
326 header.insert_header("X-Remove", "bye").unwrap();
327
328 apply_request_headers(&mut header, &compiled);
329
330 assert!(header.headers.get("x-remove").is_none());
331 assert_eq!(
332 header.headers.get("x-added").unwrap().to_str().unwrap(),
333 "value"
334 );
335 assert_eq!(
336 header.headers.get("x-set").unwrap().to_str().unwrap(),
337 "set-value"
338 );
339 }
340
341 #[test]
342 fn test_set_header_overwrites_existing_response_header() {
343 let mut ops = HeaderOps::default();
344 ops.set
345 .insert("x-custom".to_string(), "new-value".to_string());
346
347 let compiled = ops.compile();
348 let mut resp = ResponseHeader::build(200, None).unwrap();
349 resp.insert_header("x-custom", "old-value").unwrap();
350
351 apply_response_headers(&mut resp, &compiled);
352
353 let values: Vec<&str> = resp
355 .headers
356 .get_all("x-custom")
357 .iter()
358 .map(|v| v.to_str().unwrap())
359 .collect();
360 assert_eq!(values, vec!["new-value"]);
361 }
362
363 #[test]
364 fn test_add_header_appends_to_existing_response_header() {
365 let mut ops = HeaderOps::default();
366 ops.add.insert("x-custom".to_string(), "second".to_string());
367
368 let compiled = ops.compile();
369 let mut resp = ResponseHeader::build(200, None).unwrap();
370 resp.insert_header("x-custom", "first").unwrap();
371
372 apply_response_headers(&mut resp, &compiled);
373
374 let values: Vec<&str> = resp
376 .headers
377 .get_all("x-custom")
378 .iter()
379 .map(|v| v.to_str().unwrap())
380 .collect();
381 assert_eq!(values.len(), 2);
382 assert!(values.contains(&"first"));
383 assert!(values.contains(&"second"));
384 }
385
386 #[test]
387 fn test_remove_header_removes_response_header() {
388 let mut ops = HeaderOps::default();
389 ops.remove.push("x-unwanted".to_string());
390
391 let compiled = ops.compile();
392 let mut resp = ResponseHeader::build(200, None).unwrap();
393 resp.insert_header("x-unwanted", "bye").unwrap();
394 resp.insert_header("x-keep", "stay").unwrap();
395
396 apply_response_headers(&mut resp, &compiled);
397
398 assert!(resp.headers.get("x-unwanted").is_none());
399 assert_eq!(
400 resp.headers.get("x-keep").unwrap().to_str().unwrap(),
401 "stay"
402 );
403 }
404
405 #[test]
406 fn test_response_header_ops_order_remove_then_set_then_add() {
407 let mut ops = HeaderOps::default();
410 ops.remove.push("x-replaced".to_string());
411 ops.set
412 .insert("x-replaced".to_string(), "set-after-remove".to_string());
413
414 let compiled = ops.compile();
415 let mut resp = ResponseHeader::build(200, None).unwrap();
416 resp.insert_header("x-replaced", "original").unwrap();
417
418 apply_response_headers(&mut resp, &compiled);
419
420 assert_eq!(
422 resp.headers.get("x-replaced").unwrap().to_str().unwrap(),
423 "set-after-remove"
424 );
425 }
426
427 #[test]
428 fn test_set_header_creates_new_response_header() {
429 let mut ops = HeaderOps::default();
430 ops.set
431 .insert("x-new-header".to_string(), "fresh".to_string());
432
433 let compiled = ops.compile();
434 let mut resp = ResponseHeader::build(200, None).unwrap();
435
436 assert!(resp.headers.get("x-new-header").is_none());
438
439 apply_response_headers(&mut resp, &compiled);
440
441 assert_eq!(
442 resp.headers.get("x-new-header").unwrap().to_str().unwrap(),
443 "fresh"
444 );
445 }
446
447 #[test]
448 fn test_remove_nonexistent_response_header_is_noop() {
449 let mut ops = HeaderOps::default();
450 ops.remove.push("x-does-not-exist".to_string());
451
452 let compiled = ops.compile();
453 let mut resp = ResponseHeader::build(200, None).unwrap();
454 resp.insert_header("x-keep", "kept").unwrap();
455
456 apply_response_headers(&mut resp, &compiled);
457
458 assert_eq!(
460 resp.headers.get("x-keep").unwrap().to_str().unwrap(),
461 "kept"
462 );
463 }
464
465 #[test]
466 fn test_apply_security_response_headers_sets_missing_only() {
467 let mut resp = ResponseHeader::build(200, None).unwrap();
468
469 apply_security_response_headers(&mut resp, false);
470 assert!(resp.headers.get("strict-transport-security").is_none());
471 assert_eq!(
472 resp.headers
473 .get("x-content-type-options")
474 .unwrap()
475 .to_str()
476 .unwrap(),
477 "nosniff"
478 );
479 assert_eq!(
480 resp.headers
481 .get("x-frame-options")
482 .unwrap()
483 .to_str()
484 .unwrap(),
485 "DENY"
486 );
487 assert_eq!(
488 resp.headers
489 .get("referrer-policy")
490 .unwrap()
491 .to_str()
492 .unwrap(),
493 "strict-origin-when-cross-origin"
494 );
495 assert!(resp.headers.get("permissions-policy").is_some());
496
497 resp.insert_header("x-frame-options", "SAMEORIGIN").unwrap();
499 apply_security_response_headers(&mut resp, true);
500 assert_eq!(
501 resp.headers
502 .get("x-frame-options")
503 .unwrap()
504 .to_str()
505 .unwrap(),
506 "SAMEORIGIN"
507 );
508 assert!(resp.headers.get("strict-transport-security").is_some());
509 }
510}