Skip to main content

wisegate_core/
headers.rs

1//! HTTP header constants for WiseGate.
2//!
3//! This module centralizes all HTTP header names used throughout the codebase,
4//! avoiding magic strings and ensuring consistency.
5
6/// X-Forwarded-For header - contains the originating client IP.
7pub const X_FORWARDED_FOR: &str = "x-forwarded-for";
8
9/// X-Real-IP header - injected by WiseGate for upstream services.
10pub const X_REAL_IP: &str = "x-real-ip";
11
12/// Forwarded header (RFC 7239) - standardized proxy header.
13pub const FORWARDED: &str = "forwarded";
14
15/// Authorization header (for Basic Auth).
16pub const AUTHORIZATION: &str = "authorization";
17
18/// WWW-Authenticate header (for 401 responses).
19pub const WWW_AUTHENTICATE: &str = "www-authenticate";
20
21/// Content-Type header.
22pub const CONTENT_TYPE: &str = "content-type";
23
24/// Host header.
25pub const HOST: &str = "host";
26
27/// Content-Length header.
28pub const CONTENT_LENGTH: &str = "content-length";
29
30/// Connection header (hop-by-hop).
31pub const CONNECTION: &str = "connection";
32
33/// Keep-Alive header (hop-by-hop).
34pub const KEEP_ALIVE: &str = "keep-alive";
35
36/// Proxy-Authenticate header (hop-by-hop).
37pub const PROXY_AUTHENTICATE: &str = "proxy-authenticate";
38
39/// Proxy-Authorization header (hop-by-hop).
40pub const PROXY_AUTHORIZATION: &str = "proxy-authorization";
41
42/// TE header (hop-by-hop).
43pub const TE: &str = "te";
44
45/// Trailer header (hop-by-hop, RFC 9110 section 7.6.1).
46pub const TRAILER: &str = "trailer";
47
48/// Former, misspelled name of [`TRAILER`]. `trailers` is a `TE` token, not a
49/// header field, so matching it never removed the `Trailer` header.
50#[deprecated(since = "0.13.0", note = "use `TRAILER`; the header is `Trailer`")]
51pub const TRAILERS: &str = "trailers";
52
53/// Transfer-Encoding header (hop-by-hop).
54pub const TRANSFER_ENCODING: &str = "transfer-encoding";
55
56/// Upgrade header (hop-by-hop).
57pub const UPGRADE: &str = "upgrade";
58
59/// List of all hop-by-hop headers that should not be forwarded.
60pub const HOP_BY_HOP_HEADERS: &[&str] = &[
61    CONNECTION,
62    KEEP_ALIVE,
63    PROXY_AUTHENTICATE,
64    PROXY_AUTHORIZATION,
65    TE,
66    TRAILER,
67    TRANSFER_ENCODING,
68    UPGRADE,
69];
70
71/// Check if a header is a hop-by-hop header that shouldn't be forwarded.
72///
73/// Comparison is case-insensitive (ASCII), avoiding allocations from `to_lowercase()`.
74///
75/// # Arguments
76///
77/// * `header_name` - The header name to check.
78///
79/// # Returns
80///
81/// `true` if the header is a hop-by-hop header, `false` otherwise.
82///
83/// # Example
84///
85/// ```
86/// use wisegate_core::headers::is_hop_by_hop;
87///
88/// assert!(is_hop_by_hop("connection"));
89/// assert!(is_hop_by_hop("Transfer-Encoding"));
90/// assert!(!is_hop_by_hop("content-type"));
91/// ```
92pub fn is_hop_by_hop(header_name: &str) -> bool {
93    HOP_BY_HOP_HEADERS
94        .iter()
95        .any(|h| h.eq_ignore_ascii_case(header_name))
96}
97
98/// Returns the header names listed in the `Connection` header(s), lowercased.
99///
100/// RFC 9110 section 7.6.1: a proxy must remove every field named in
101/// `Connection` before forwarding the message, in addition to the fixed
102/// hop-by-hop list.
103///
104/// # Example
105///
106/// ```
107/// use wisegate_core::headers::connection_nominated;
108///
109/// let mut headers = hyper::HeaderMap::new();
110/// headers.insert("connection", "keep-alive, X-Secret".parse().unwrap());
111/// assert_eq!(connection_nominated(&headers), vec!["keep-alive", "x-secret"]);
112/// ```
113pub fn connection_nominated(headers: &hyper::HeaderMap) -> Vec<String> {
114    headers
115        .get_all(CONNECTION)
116        .iter()
117        .filter_map(|value| value.to_str().ok())
118        .flat_map(|value| value.split(','))
119        .map(|token| token.trim().to_ascii_lowercase())
120        .filter(|token| !token.is_empty())
121        .collect()
122}
123
124/// Returns `true` if a header must not be forwarded: either a fixed
125/// hop-by-hop header or one nominated by `Connection` (see
126/// [`connection_nominated`]).
127pub fn is_hop_by_hop_for(header_name: &str, nominated: &[String]) -> bool {
128    is_hop_by_hop(header_name)
129        || nominated
130            .iter()
131            .any(|token| token.eq_ignore_ascii_case(header_name))
132}
133
134#[cfg(test)]
135mod tests {
136    use super::*;
137
138    #[test]
139    fn test_hop_by_hop_headers() {
140        assert!(is_hop_by_hop(CONNECTION));
141        assert!(is_hop_by_hop(KEEP_ALIVE));
142        assert!(is_hop_by_hop(PROXY_AUTHENTICATE));
143        assert!(is_hop_by_hop(PROXY_AUTHORIZATION));
144        assert!(is_hop_by_hop(TE));
145        assert!(is_hop_by_hop(TRAILER));
146        assert!(is_hop_by_hop("Trailer"));
147        assert!(is_hop_by_hop(TRANSFER_ENCODING));
148        assert!(is_hop_by_hop(UPGRADE));
149    }
150
151    #[test]
152    fn test_connection_nominated_reads_every_field() {
153        let mut headers = hyper::HeaderMap::new();
154        headers.append(CONNECTION, "close, X-Secret".parse().unwrap());
155        headers.append(CONNECTION, " x-other ,,".parse().unwrap());
156        assert_eq!(
157            connection_nominated(&headers),
158            vec!["close", "x-secret", "x-other"]
159        );
160    }
161
162    #[test]
163    fn test_is_hop_by_hop_for_nominated_headers() {
164        let nominated = vec!["x-secret".to_string()];
165        assert!(is_hop_by_hop_for("X-Secret", &nominated));
166        assert!(is_hop_by_hop_for(UPGRADE, &nominated));
167        assert!(!is_hop_by_hop_for("x-public", &nominated));
168    }
169
170    #[test]
171    fn test_not_hop_by_hop_headers() {
172        assert!(!is_hop_by_hop(CONTENT_TYPE));
173        assert!(!is_hop_by_hop(HOST));
174        assert!(!is_hop_by_hop(CONTENT_LENGTH));
175        assert!(!is_hop_by_hop(X_FORWARDED_FOR));
176        assert!(!is_hop_by_hop(X_REAL_IP));
177        assert!(!is_hop_by_hop(FORWARDED));
178        assert!(!is_hop_by_hop("authorization"));
179        assert!(!is_hop_by_hop("accept"));
180    }
181
182    #[test]
183    fn test_header_constants_lowercase() {
184        // All header constants should be lowercase for consistent matching
185        assert_eq!(X_FORWARDED_FOR, X_FORWARDED_FOR.to_lowercase());
186        assert_eq!(X_REAL_IP, X_REAL_IP.to_lowercase());
187        assert_eq!(FORWARDED, FORWARDED.to_lowercase());
188        assert_eq!(CONTENT_TYPE, CONTENT_TYPE.to_lowercase());
189        assert_eq!(HOST, HOST.to_lowercase());
190        assert_eq!(CONTENT_LENGTH, CONTENT_LENGTH.to_lowercase());
191    }
192}