rama_http/layer/remove_header/
mod.rs1use rama_core::bytes::BytesMut;
6use rama_core::telemetry::tracing;
7use rama_http_headers::{Connection, HeaderMapExt};
8use rama_utils::str::{any_submatch_ignore_ascii_case, starts_with_ignore_ascii_case};
9
10use crate::{HeaderMap, HeaderName, HeaderValue, header};
11
12pub mod request;
13pub mod response;
14
15#[doc(inline)]
16pub use self::{
17 request::{RemoveRequestHeader, RemoveRequestHeaderLayer},
18 response::{RemoveResponseHeader, RemoveResponseHeaderLayer},
19};
20
21fn remove_headers_by_prefix(headers: &mut HeaderMap, prefix: &str) {
22 let keys: Vec<_> = headers
23 .keys()
24 .filter(|key| starts_with_ignore_ascii_case(key, prefix))
26 .cloned()
27 .collect();
28
29 for key in keys {
30 headers.remove(key);
31 }
32}
33
34fn remove_headers_by_exact_name(headers: &mut HeaderMap, name: &HeaderName) {
35 headers.remove(name);
36}
37
38pub fn remove_hop_by_hop_request_headers(headers: &mut HeaderMap) {
50 while let Some(c) = headers.typed_get::<Connection>() {
51 for header in c.iter_headers() {
52 while headers.remove(header).is_some() {
53 tracing::trace!(
54 "removed hop-by-hop request header listed in Connection header for name: {header}"
55 );
56 }
57 }
58 _ = headers.remove(header::CONNECTION);
59 }
60 for header in [
61 &header::CONNECTION,
62 &header::PROXY_CONNECTION,
63 &header::PROXY_AUTHORIZATION,
64 &header::KEEP_ALIVE,
65 &header::TE,
66 &header::TRAILER,
67 &header::TRANSFER_ENCODING,
68 &header::UPGRADE,
69 ] {
70 while headers.remove(header).is_some() {
71 tracing::trace!("removed hop-by-hop request header for name: {header}");
72 }
73 }
74}
75
76pub fn remove_forwarding_metadata_request_headers(headers: &mut HeaderMap) {
86 for header in [
87 &header::X_FORWARDED_FOR,
88 &header::X_FORWARDED_HOST,
89 &header::X_FORWARDED_PROTO,
90 &header::FORWARDED,
91 &header::VIA,
92 &header::CF_CONNECTING_IP,
93 &header::X_REAL_IP,
94 &header::X_CLIENT_IP,
95 &header::CLIENT_IP,
96 &header::TRUE_CLIENT_IP,
97 ] {
98 while headers.remove(header).is_some() {
99 tracing::trace!("removed forwarding metadata request header for name: {header}");
100 }
101 }
102}
103
104pub fn remove_hop_by_hop_response_headers(headers: &mut HeaderMap) {
112 while let Some(c) = headers.typed_get::<Connection>() {
113 for header in c.iter_headers() {
114 while headers.remove(header).is_some() {
115 tracing::trace!(
116 "removed hop-by-hop response header listed in Connection header for name: {header}"
117 );
118 }
119 }
120 _ = headers.remove(header::CONNECTION);
121 }
122 for header in [
123 &header::CONNECTION,
124 &header::KEEP_ALIVE,
125 &header::PROXY_AUTHENTICATE,
126 &header::TRAILER,
127 &header::TRANSFER_ENCODING,
128 &header::UPGRADE,
129 ] {
130 while headers.remove(header).is_some() {
131 tracing::trace!("removed hop-by-hop response header for name: {header}");
132 }
133 }
134}
135
136pub fn coalesce_cookie_headers(headers: &mut HeaderMap) {
143 let crate::header::Entry::Occupied(mut cookie_headers) = headers.entry(header::COOKIE) else {
144 return;
145 };
146
147 let Some((bytes_count, header_count, is_sensitive)) = cookie_headers
148 .iter()
149 .map(|value| (value.as_bytes().len(), 1usize, value.is_sensitive()))
150 .reduce(|a, b| (a.0 + b.0, a.1 + b.1, a.2 || b.2))
151 else {
152 return;
153 };
154 if header_count <= 1 {
155 return;
156 }
157
158 let mut buffer = BytesMut::with_capacity(bytes_count + ((header_count - 1) * 2));
159 let mut header_values = cookie_headers.iter();
160 if let Some(header_value) = header_values.next() {
161 buffer.extend_from_slice(header_value.as_bytes());
162 }
163 for header_value in header_values {
164 buffer.extend_from_slice(b"; ");
165 buffer.extend_from_slice(header_value.as_bytes());
166 }
167
168 let Ok(mut header_value) = HeaderValue::from_maybe_shared(buffer.freeze()) else {
170 tracing::error!("failed to coalesce valid Cookie header values");
171 return;
172 };
173 header_value.set_sensitive(is_sensitive);
174 cookie_headers.insert(header_value);
175}
176
177pub fn remove_illegal_h2_request_headers(headers: &mut HeaderMap) {
186 while let Some(c) = headers.typed_get::<Connection>() {
187 for header in c.iter_headers() {
188 while headers.remove(header).is_some() {
189 tracing::trace!(
190 header = %header,
191 "removed connection-specific request header listed in Connection header for name"
192 );
193 }
194 }
195 _ = headers.remove(header::CONNECTION);
196 }
197 for header in [
198 &header::CONNECTION,
199 &header::PROXY_CONNECTION,
200 &header::KEEP_ALIVE,
201 &header::TRANSFER_ENCODING,
202 &header::UPGRADE,
203 &header::SEC_WEBSOCKET_KEY,
204 &header::HOST,
205 ] {
206 while headers.remove(header).is_some() {
207 tracing::trace!(
208 header = %header,
209 "removed illegal (~http1) header from h2 request for name"
210 );
211 }
212 }
213
214 let te_is_legal = headers
217 .get_all(header::TE)
218 .iter()
219 .all(|v| v.as_bytes().trim_ascii().eq_ignore_ascii_case(b"trailers"));
220 if !te_is_legal {
221 while headers.remove(header::TE).is_some() {
222 tracing::trace!(
223 "removed illegal TE header (only `TE: trailers` is valid) from h2 request"
224 );
225 }
226 }
227}
228
229pub fn remove_illegal_h2_response_headers(headers: &mut HeaderMap) {
242 while let Some(c) = headers.typed_get::<Connection>() {
243 for header in c.iter_headers() {
244 while headers.remove(header).is_some() {
245 tracing::trace!(
246 header = %header,
247 "removed connection-specific response header listed in Connection header for name"
248 );
249 }
250 }
251 _ = headers.remove(header::CONNECTION);
252 }
253 for header in [
254 &header::CONNECTION,
255 &header::PROXY_CONNECTION,
256 &header::KEEP_ALIVE,
257 &header::TRANSFER_ENCODING,
258 &header::UPGRADE,
259 ] {
260 while headers.remove(header).is_some() {
261 tracing::trace!(
262 header = %header,
263 "removed illegal (~http1) header from h2 response for name"
264 );
265 }
266 }
267}
268
269pub fn remove_sensitive_request_headers(headers: &mut HeaderMap) {
279 for header in [
280 &header::AUTHORIZATION,
281 &header::PROXY_AUTHORIZATION,
282 &header::COOKIE,
283 ] {
284 while headers.remove(header).is_some() {
285 tracing::trace!("removed sensitive request header for name: {header}");
286 }
287 }
288 remove_headers_if(
289 headers,
290 |name, _value| is_sensitive_header_name(name),
291 "sensitive request header",
292 );
293}
294
295pub fn remove_sensitive_response_headers(headers: &mut HeaderMap) {
303 for header in [&header::SET_COOKIE] {
304 while headers.remove(header).is_some() {
305 tracing::trace!("removed sensitive response header for name: {header}");
306 }
307 }
308}
309
310pub fn remove_payload_metadata_headers(headers: &mut HeaderMap) {
318 for header in [
319 &header::CONTENT_ENCODING,
320 &header::TRANSFER_ENCODING,
321 &header::ACCEPT_RANGES,
322 &header::CONTENT_LENGTH,
323 ] {
324 while headers.remove(header).is_some() {
325 tracing::trace!("removed payload header for name: {header}");
326 }
327 }
328}
329
330pub fn remove_cache_validation_request_headers(headers: &mut HeaderMap) {
341 for header in [
342 &header::IF_NONE_MATCH,
343 &header::IF_MODIFIED_SINCE,
344 &header::IF_MATCH,
345 &header::IF_UNMODIFIED_SINCE,
346 &header::IF_RANGE,
347 &header::RANGE,
348 ] {
349 while headers.remove(header).is_some() {
350 tracing::trace!("removed cache validation request header for name: {header}");
351 }
352 }
353}
354
355pub fn remove_cache_validation_response_headers(headers: &mut HeaderMap) {
365 for header in [
366 &header::ETAG,
367 &header::LAST_MODIFIED,
368 &header::ACCEPT_RANGES,
369 &header::CONTENT_RANGE,
370 ] {
371 while headers.remove(header).is_some() {
372 tracing::trace!("removed cache validation response header for name: {header}");
373 }
374 }
375}
376
377pub fn remove_cache_policy_headers(headers: &mut HeaderMap) {
386 for header in [
387 &header::CACHE_CONTROL,
388 &header::PRAGMA,
389 &header::EXPIRES,
390 &header::AGE,
391 &header::WARNING,
392 ] {
393 while headers.remove(header).is_some() {
394 tracing::trace!("removed cache policy header for name: {header}");
395 }
396 }
397}
398
399#[inline(always)]
400fn is_sensitive_header_name(name: &HeaderName) -> bool {
401 any_submatch_ignore_ascii_case(
402 name.as_str(),
403 ["api-key", "auth-token", "access-token", "security-token"],
404 )
405}
406
407fn remove_headers_if<F>(headers: &mut HeaderMap, mut remove: F, log_context: &str)
408where
409 F: FnMut(&HeaderName, &HeaderValue) -> bool,
410{
411 loop {
412 let name_to_remove: Option<HeaderName> = headers
413 .iter()
414 .find_map(|(name, value)| remove(name, value).then(|| name.clone()));
415
416 let Some(name) = name_to_remove else { break };
417
418 while headers.remove(&name).is_some() {
419 tracing::trace!("{log_context}: removed header: {name}");
420 }
421 }
422}
423
424#[cfg(test)]
425mod tests {
426 use super::*;
427
428 const FORWARDING_METADATA_HEADERS: [&str; 10] = [
429 "x-forwarded-for",
430 "x-forwarded-host",
431 "x-forwarded-proto",
432 "forwarded",
433 "via",
434 "cf-connecting-ip",
435 "x-real-ip",
436 "x-client-ip",
437 "client-ip",
438 "true-client-ip",
439 ];
440
441 #[test]
442 fn hop_by_hop_request_cleanup_preserves_forwarding_metadata() {
443 let mut headers = HeaderMap::new();
444 for name in FORWARDING_METADATA_HEADERS {
445 headers.append(name, HeaderValue::from_static("metadata"));
446 }
447 headers.insert(header::CONNECTION, HeaderValue::from_static("x-hop"));
448 headers.insert("x-hop", HeaderValue::from_static("remove me"));
449 headers.insert(header::KEEP_ALIVE, HeaderValue::from_static("timeout=5"));
450 headers.insert(
451 header::PROXY_AUTHORIZATION,
452 HeaderValue::from_static("Basic dGVzdA=="),
453 );
454
455 remove_hop_by_hop_request_headers(&mut headers);
456
457 for name in FORWARDING_METADATA_HEADERS {
458 assert!(
459 headers.contains_key(name),
460 "expected {name} to be preserved"
461 );
462 }
463 for name in ["connection", "x-hop", "keep-alive", "proxy-authorization"] {
464 assert!(!headers.contains_key(name), "expected {name} to be removed");
465 }
466 }
467
468 #[test]
469 fn hop_by_hop_request_cleanup_removes_metadata_named_by_connection() {
470 let mut headers = HeaderMap::new();
471 headers.insert(header::CONNECTION, HeaderValue::from_static("forwarded"));
472 headers.insert(header::FORWARDED, HeaderValue::from_static("for=192.0.2.1"));
473 headers.insert(header::VIA, HeaderValue::from_static("1.1 proxy"));
474
475 remove_hop_by_hop_request_headers(&mut headers);
476
477 assert!(!headers.contains_key(header::FORWARDED));
478 assert!(headers.contains_key(header::VIA));
479 }
480
481 #[test]
482 fn forwarding_metadata_cleanup_is_independent() {
483 let mut headers = HeaderMap::new();
484 for name in FORWARDING_METADATA_HEADERS {
485 headers.append(name, HeaderValue::from_static("metadata"));
486 }
487 headers.insert(header::CONNECTION, HeaderValue::from_static("close"));
488
489 remove_forwarding_metadata_request_headers(&mut headers);
490
491 for name in FORWARDING_METADATA_HEADERS {
492 assert!(!headers.contains_key(name), "expected {name} to be removed");
493 }
494 assert!(headers.contains_key(header::CONNECTION));
495 }
496
497 #[test]
498 fn coalesce_cookies_preserves_order_and_sensitivity() {
499 let mut headers = HeaderMap::new();
500 headers.append(header::COOKIE, HeaderValue::from_static("a=1"));
501 let mut sensitive = HeaderValue::from_static("b=2");
502 sensitive.set_sensitive(true);
503 headers.append(header::COOKIE, sensitive);
504
505 coalesce_cookie_headers(&mut headers);
506
507 let cookies: Vec<_> = headers.get_all(header::COOKIE).iter().collect();
508 assert_eq!(cookies.len(), 1);
509 assert_eq!(cookies[0].as_bytes(), b"a=1; b=2");
510 assert!(cookies[0].is_sensitive());
511 }
512}