Skip to main content

pingap_core/
http_header.rs

1// Copyright 2024-2025 Tree xie.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15// Import necessary modules and types from supervisors and external crates.
16use super::{Ctx, get_hostname};
17use ahash::AHashSet;
18use arc_swap::ArcSwapOption;
19use bytes::BytesMut;
20use http::header;
21use http::{HeaderName, HeaderValue};
22use ipnet::IpNet;
23use pingora::http::RequestHeader;
24use pingora::proxy::Session;
25use snafu::{ResultExt, Snafu};
26use std::borrow::Cow;
27use std::fmt::Write;
28use std::net::IpAddr;
29use std::str::FromStr;
30use std::sync::Arc;
31use std::sync::atomic::{AtomicBool, Ordering};
32
33// Define string constants for commonly used HTTP header names.
34const HTTP_HEADER_X_FORWARDED_FOR: &str = "x-forwarded-for";
35const HTTP_HEADER_X_REAL_IP: &str = "x-real-ip";
36
37// Define byte slice constants for special variable tags used in header value processing.
38// These are matched against the raw bytes of a header value.
39pub const HOST_NAME_TAG: &[u8] = b"$hostname";
40const HOST_TAG: &[u8] = b"$host";
41const SCHEME_TAG: &[u8] = b"$scheme";
42const REMOTE_ADDR_TAG: &[u8] = b"$remote_addr";
43const REMOTE_PORT_TAG: &[u8] = b"$remote_port";
44const SERVER_ADDR_TAG: &[u8] = b"$server_addr";
45const SERVER_PORT_TAG: &[u8] = b"$server_port";
46const PROXY_ADD_FORWARDED_TAG: &[u8] = b"$proxy_add_x_forwarded_for";
47const UPSTREAM_ADDR_TAG: &[u8] = b"$upstream_addr";
48const JA4_TAG: &[u8] = b"$ja4";
49
50// Define static HeaderValues for HTTP and HTTPS schemes to avoid re-creation.
51static SCHEME_HTTPS: HeaderValue = HeaderValue::from_static("https");
52static SCHEME_HTTP: HeaderValue = HeaderValue::from_static("http");
53
54/// Defines the custom error types for this module using the snafu crate.
55#[derive(Debug, Snafu)]
56pub enum Error {
57    /// Error for when a string cannot be parsed into a valid HeaderValue.
58    #[snafu(display("invalid header value: {value} - {source}"))]
59    InvalidHeaderValue {
60        value: String,
61        source: header::InvalidHeaderValue,
62    },
63    /// Error for when a string cannot be parsed into a valid HeaderName.
64    #[snafu(display("invalid header name: {value} - {source}"))]
65    InvalidHeaderName {
66        value: String,
67        source: header::InvalidHeaderName,
68    },
69}
70/// A convenient type alias for `Result` with the module's `Error` type.
71type Result<T, E = Error> = std::result::Result<T, E>;
72
73/// A type alias for a tuple representing an HTTP header.
74pub type HttpHeader = (HeaderName, HeaderValue);
75
76/// Gets the request host by checking the URI first, then falling back to the "Host" header.
77///
78/// This function follows the common practice of prioritizing the host from the absolute URI
79/// (e.g., in `GET http://example.com/path HTTP/1.1`) over the `Host` header field.
80pub fn get_host(header: &RequestHeader) -> Option<&str> {
81    // First, try to get the host directly from the parsed URI.
82    // http2 will always have a host in the uri
83    if let Some(host) = header.uri.host() {
84        return Some(host);
85    }
86    // If not in the URI, fall back to the "Host" header.
87    header
88        .headers
89        .get(http::header::HOST)
90        // Convert the header value to a string slice.
91        .and_then(|value| value.to_str().ok())
92        .map(strip_port)
93}
94
95/// Drops the `:port` suffix of a `Host` value. An IPv6 literal keeps its
96/// brackets, and the colons inside them are not a port separator:
97/// `[::1]:8080` is `[::1]`, the same form `Uri::host` reports.
98fn strip_port(host: &str) -> &str {
99    if host.starts_with('[') {
100        return match host.find(']') {
101            Some(end) => &host[..=end],
102            None => host,
103        };
104    }
105    host.split(':').next().unwrap_or(host)
106}
107
108/// Converts a single string in "name: value" format into an `HttpHeader` tuple.
109///
110/// This is a utility function for parsing header configurations. It trims whitespace
111/// from both the name and the value.
112pub fn convert_header(value: &str) -> Result<Option<HttpHeader>> {
113    // `split_once` is an efficient way to split the string into two parts at the first colon.
114    value
115        .split_once(':')
116        // If a colon exists, map the key and value parts.
117        .map(|(k, v)| {
118            // Parse the trimmed key into a HeaderName, wrapping errors.
119            let name = HeaderName::from_str(k.trim())
120                .context(InvalidHeaderNameSnafu { value: k })?;
121            // Parse the trimmed value into a HeaderValue, wrapping errors.
122            let value = HeaderValue::from_str(v.trim())
123                .context(InvalidHeaderValueSnafu { value: v })?;
124            // If both parsing steps succeed, return the header tuple.
125            Ok(Some((name, value)))
126        })
127        // If `split_once` returns None (no colon), default to `Ok(None)`.
128        .unwrap_or(Ok(None))
129}
130
131/// Converts a slice of strings into a `Vec` of `HttpHeader`s.
132///
133/// This function iterates over a list of header strings and uses `convert_header`
134/// on each, collecting the valid results into a vector.
135pub fn convert_headers(header_values: &[String]) -> Result<Vec<HttpHeader>> {
136    header_values
137        .iter()
138        // `filter_map` is used to iterate, convert, and filter out `None` results elegantly.
139        // `transpose` flips `Option<Result<T>>` to `Result<Option<T>>`, which is what `filter_map` expects.
140        .filter_map(|item| convert_header(item).transpose())
141        // `collect` gathers the `Result<HttpHeader>` items. If any item is an `Err`, `collect` will return that `Err`.
142        .collect()
143}
144
145// Define common, pre-built HTTP headers as static constants for reuse and performance.
146pub static HTTP_HEADER_NO_STORE: HttpHeader = (
147    header::CACHE_CONTROL,
148    HeaderValue::from_static("private, no-store"),
149);
150pub static HTTP_HEADER_NO_CACHE: HttpHeader = (
151    header::CACHE_CONTROL,
152    HeaderValue::from_static("private, no-cache"),
153);
154pub static HTTP_HEADER_CONTENT_JSON: HttpHeader = (
155    header::CONTENT_TYPE,
156    HeaderValue::from_static("application/json; charset=utf-8"),
157);
158pub static HTTP_HEADER_CONTENT_HTML: HttpHeader = (
159    header::CONTENT_TYPE,
160    HeaderValue::from_static("text/html; charset=utf-8"),
161);
162pub static HTTP_HEADER_CONTENT_TEXT: HttpHeader = (
163    header::CONTENT_TYPE,
164    HeaderValue::from_static("text/plain; charset=utf-8"),
165);
166pub static HTTP_HEADER_TRANSFER_CHUNKED: HttpHeader = (
167    header::TRANSFER_ENCODING,
168    HeaderValue::from_static("chunked"),
169);
170pub static HTTP_HEADER_NAME_X_REQUEST_ID: HeaderName =
171    HeaderName::from_static("x-request-id");
172
173/// Resolves a header value that cannot change for the life of the process -
174/// `$hostname` and `$<ENV_VAR>` - so it can be stored as a plain value when
175/// the configuration is loaded instead of being looked up on every request
176/// (`std::env::var` takes a process-wide lock and allocates each time).
177/// Values that depend on the request (`$host`, `$http_*`, `:key`, ...) and
178/// plain values come back unchanged, as does a `$NAME` whose variable is not
179/// set, so the request path treats it exactly as before.
180pub fn resolve_static_header_value(value: HeaderValue) -> HeaderValue {
181    let buf = value.as_bytes();
182    if buf.first() != Some(&b'$') {
183        return value;
184    }
185    if buf == HOST_NAME_TAG {
186        return HeaderValue::from_str(get_hostname()).unwrap_or(value);
187    }
188    let request_tags: [&[u8]; 9] = [
189        HOST_TAG,
190        SCHEME_TAG,
191        REMOTE_ADDR_TAG,
192        REMOTE_PORT_TAG,
193        SERVER_ADDR_TAG,
194        SERVER_PORT_TAG,
195        PROXY_ADD_FORWARDED_TAG,
196        UPSTREAM_ADDR_TAG,
197        JA4_TAG,
198    ];
199    if request_tags.contains(&buf) || buf.starts_with(b"$http_") {
200        return value;
201    }
202    let Ok(name) = std::str::from_utf8(&buf[1..]) else {
203        return value;
204    };
205    match std::env::var(name) {
206        Ok(env_value) => HeaderValue::from_str(&env_value).unwrap_or(value),
207        Err(_) => value,
208    }
209}
210
211/// Processes a `HeaderValue` that may contain a special dynamic variable (e.g., `$host`).
212/// It replaces the variable with its corresponding runtime value.
213#[inline]
214pub fn convert_header_value(
215    value: &HeaderValue,
216    session: &Session,
217    ctx: &Ctx,
218) -> Option<HeaderValue> {
219    // Work with the raw byte representation of the header value for efficient matching.
220    let buf = value.as_bytes();
221
222    // Perform a quick check for the special variable prefix ('$' or ':') to exit early
223    // for normal header values, which is the most common case.
224    if buf.is_empty() || !(buf[0] == b'$' || buf[0] == b':') {
225        return None;
226    }
227
228    // A helper closure to reduce boilerplate when converting a string slice to a HeaderValue.
229    let to_header_value = |s: &str| HeaderValue::from_str(s).ok();
230
231    // Match the entire byte slice against the predefined variable tags.
232    match buf {
233        HOST_TAG => get_host(session.req_header()).and_then(to_header_value),
234        SCHEME_TAG => Some(if ctx.conn.tls_version.is_some() {
235            SCHEME_HTTPS.clone()
236        } else {
237            SCHEME_HTTP.clone()
238        }),
239        HOST_NAME_TAG => to_header_value(get_hostname()),
240        REMOTE_ADDR_TAG => {
241            ctx.conn.remote_addr.as_deref().and_then(to_header_value)
242        },
243        REMOTE_PORT_TAG => ctx.conn.remote_port.and_then(|p| {
244            // Use `itoa` to format the integer directly into a valid header value
245            // without creating an intermediate `String`.
246            HeaderValue::from_str(itoa::Buffer::new().format(p)).ok()
247        }),
248        SERVER_ADDR_TAG => {
249            ctx.conn.server_addr.as_deref().and_then(to_header_value)
250        },
251        SERVER_PORT_TAG => ctx.conn.server_port.and_then(|p| {
252            HeaderValue::from_str(itoa::Buffer::new().format(p)).ok()
253        }),
254        UPSTREAM_ADDR_TAG => {
255            if !ctx.upstream.address.is_empty() {
256                to_header_value(&ctx.upstream.address)
257            } else {
258                None
259            }
260        },
261        JA4_TAG => ctx
262            .conn
263            .ja4
264            .as_deref()
265            .and_then(|fingerprint| to_header_value(fingerprint.ja4())),
266        PROXY_ADD_FORWARDED_TAG => {
267            ctx.conn.remote_addr.as_deref().and_then(|remote_addr| {
268                // Build the new `x-forwarded-for` value efficiently using `BytesMut` to avoid `format!`.
269                let existing = session.get_header(HTTP_HEADER_X_FORWARDED_FOR);
270                let capacity =
271                    existing.map(|v| v.as_bytes().len() + 2).unwrap_or(0)
272                        + remote_addr.len();
273                let mut value_buf = BytesMut::with_capacity(capacity);
274                if let Some(existing) = existing {
275                    value_buf.extend_from_slice(existing.as_bytes());
276                    value_buf.extend_from_slice(b", ");
277                }
278                value_buf.extend_from_slice(remote_addr.as_bytes());
279                HeaderValue::from_bytes(&value_buf).ok()
280            })
281        },
282        // If no predefined tag matches, it might be a different type of variable (e.g., `$http_...`).
283        _ => handle_special_headers(buf, session, ctx),
284    }
285}
286
287/// A helper function to handle more complex or less common special header variables.
288/// This function is called as a fallback from `convert_header_value`.
289#[inline]
290fn handle_special_headers(
291    buf: &[u8],
292    session: &Session,
293    ctx: &Ctx,
294) -> Option<HeaderValue> {
295    // Handle variables that reference other request headers, like `$http_user_agent`.
296    if let Some(name) = buf.strip_prefix(b"$http_") {
297        let key = std::str::from_utf8(name).ok()?;
298        // nginx spelling: `$http_user_agent` names `User-Agent`. Header names
299        // never contain underscores, so mapping them is unambiguous.
300        if key.contains('_') {
301            return session.get_header(key.replace('_', "-")).cloned();
302        }
303        return session.get_header(key).cloned();
304    }
305    // Handle variables that reference environment variables, like `$PATH`.
306    if buf.starts_with(b"$") {
307        let var_name = std::str::from_utf8(&buf[1..]).ok()?;
308        // Look up the environment variable and convert its value to a HeaderValue.
309        return std::env::var(var_name)
310            .ok()
311            .and_then(|v| HeaderValue::from_str(&v).ok());
312    }
313    // Handle variables that reference fields in the `Ctx` struct, like `:connection_id`.
314    if buf.starts_with(b":") {
315        let key = std::str::from_utf8(&buf[1..]).ok()?;
316        // Use `append_log_value` to get the string representation of the context field.
317        let mut value = BytesMut::with_capacity(20);
318        ctx.append_log_value(&mut value, key);
319        if !value.is_empty() {
320            // Convert the resulting bytes to a HeaderValue.
321            return HeaderValue::from_bytes(&value).ok();
322        }
323    }
324    // If no pattern matches, return None.
325    None
326}
327
328/// Gets the remote address (IP and port) from the session.
329pub fn get_remote_addr(session: &Session) -> Option<(String, u16)> {
330    session
331        .client_addr()
332        // Ensure the address is an IP address (v4 or v6).
333        .and_then(|addr| addr.as_inet())
334        // Map it to a tuple of (String, u16).
335        .map(|addr| (addr.ip().to_string(), addr.port()))
336}
337
338/// Parsed trusted downstream proxy addresses: individual IPs plus CIDR
339/// networks. Kept small and local since it is only used by `get_client_ip`.
340struct TrustedProxies {
341    nets: Vec<IpNet>,
342    ips: AHashSet<IpAddr>,
343}
344
345impl TrustedProxies {
346    /// Parses a list of IPs / CIDR ranges. Invalid entries are ignored (a
347    /// mistyped proxy simply fails closed: its forwarded headers are dropped).
348    fn parse(values: &[String]) -> Self {
349        let mut nets = Vec::new();
350        let mut ips = AHashSet::new();
351        for item in values {
352            if let Ok(net) = IpNet::from_str(item) {
353                nets.push(net);
354            } else if let Ok(ip) = IpAddr::from_str(item) {
355                ips.insert(ip);
356            }
357        }
358        Self { nets, ips }
359    }
360
361    /// Returns true if `peer` is one of the trusted proxies.
362    fn contains(&self, peer: IpAddr) -> bool {
363        self.ips.contains(&peer)
364            || self.nets.iter().any(|net| net.contains(&peer))
365    }
366}
367
368// Trusted downstream proxies. When configured, the forwarded client-IP headers
369// (`X-Forwarded-For` / `X-Real-IP`) are only honoured for connections whose
370// direct TCP peer is one of these addresses; a client connecting directly must
371// not be able to spoof its IP for IP-based access control, rate limiting, etc.
372// A cheap atomic flag keeps the common "not configured" path to one load;
373// the configured path reads the list through an `ArcSwap`, so a reload never
374// makes a request wait on a lock.
375static TRUSTED_PROXIES_ENABLED: AtomicBool = AtomicBool::new(false);
376static TRUSTED_PROXIES: ArcSwapOption<TrustedProxies> =
377    ArcSwapOption::const_empty();
378
379/// Sets the trusted downstream proxy addresses (individual IPs or CIDR ranges).
380///
381/// When a non-empty list is configured, forwarded headers are only trusted for
382/// connections coming directly from one of these addresses. Passing `None` or
383/// an empty list restores the default behaviour of trusting forwarded headers
384/// unconditionally (backwards compatible). Safe to call repeatedly on reload.
385pub fn set_trusted_proxies(proxies: &Option<Vec<String>>) {
386    let parsed = match proxies {
387        Some(list) if !list.is_empty() => Some(TrustedProxies::parse(list)),
388        _ => None,
389    };
390    let enabled = parsed.is_some();
391    TRUSTED_PROXIES.store(parsed.map(Arc::new));
392    // Set last: a request that sees the flag also sees the list.
393    TRUSTED_PROXIES_ENABLED.store(enabled, Ordering::Relaxed);
394}
395
396/// Returns true if the direct peer address is a configured trusted proxy.
397fn is_trusted_proxy(peer: IpAddr) -> bool {
398    TRUSTED_PROXIES
399        .load()
400        .as_ref()
401        .is_some_and(|trusted| trusted.contains(peer))
402}
403
404/// Ensures `ctx.conn.client_ip` is populated and returns a borrowed reference.
405///
406/// Prefer this on the request path over calling [`get_client_ip`] repeatedly —
407/// the first call allocates once and subsequent callers reuse the cached value.
408#[inline]
409pub fn ensure_client_ip<'a>(session: &Session, ctx: &'a mut Ctx) -> &'a str {
410    if ctx.conn.client_ip.is_none() {
411        ctx.conn.client_ip = Some(get_client_ip(session));
412    }
413    // Just inserted or already present — never None after the block above.
414    ctx.conn.client_ip.as_deref().unwrap_or_default()
415}
416
417/// Gets the client's IP address.
418///
419/// When trusted proxies are configured, `X-Forwarded-For` / `X-Real-IP` are
420/// only honoured if the direct TCP peer is a trusted proxy; otherwise the
421/// peer's own address is returned. When no trusted proxies are configured the
422/// lookup order is:
423/// 1. `X-Forwarded-For` (taking the first IP in the list)
424/// 2. `X-Real-IP`
425/// 3. The remote address of the direct TCP connection
426pub fn get_client_ip(session: &Session) -> String {
427    // When trusted proxies are configured, a direct (untrusted) peer's
428    // forwarded headers must be ignored to prevent client-IP spoofing.
429    if TRUSTED_PROXIES_ENABLED.load(Ordering::Relaxed) {
430        // Compare the address itself; formatting it only to parse it back
431        // would cost an allocation per request.
432        let peer = session
433            .client_addr()
434            .and_then(|addr| addr.as_inet())
435            .map(|addr| addr.ip());
436        match peer {
437            Some(ip) if is_trusted_proxy(ip) => {},
438            Some(ip) => return ip.to_string(),
439            None => return String::new(),
440        }
441    }
442    // 1. Check `X-Forwarded-For`.
443    if let Some(value) = session.get_header(HTTP_HEADER_X_FORWARDED_FOR) {
444        // Efficiently take the first IP without creating an intermediate Vec.
445        if let Ok(s) = value.to_str()
446            && let Some(ip) = s.split(',').next()
447        {
448            let trimmed_ip = ip.trim();
449            if !trimmed_ip.is_empty() {
450                return trimmed_ip.to_string();
451            }
452        }
453    }
454    // 2. Check `X-Real-IP`.
455    if let Some(value) = session.get_header(HTTP_HEADER_X_REAL_IP) {
456        return value.to_str().unwrap_or_default().to_string();
457    }
458    // 3. Fall back to the direct connection's remote address.
459    if let Some((addr, _)) = get_remote_addr(session) {
460        return addr;
461    }
462    // If all checks fail, return an empty string.
463    "".to_string()
464}
465
466/// A convenient helper to get a header value as a `&str` from a `RequestHeader`.
467pub fn get_req_header_value<'a>(
468    req_header: &'a RequestHeader,
469    key: &str,
470) -> Option<&'a str> {
471    // Get the header by its key.
472    if let Some(value) = req_header.headers.get(key) {
473        // Try to convert it to a string slice. Fails if the value is not valid UTF-8.
474        if let Ok(value) = value.to_str() {
475            return Some(value);
476        }
477    }
478    None
479}
480
481/// Parses the "Cookie" header to find the value of a specific cookie.
482pub fn get_cookie_value<'a>(
483    req_header: &'a RequestHeader,
484    cookie_name: &str,
485) -> Option<&'a str> {
486    // First, get the entire "Cookie" header string. The '?' operator will short-circuit if it's not present.
487    get_req_header_value(req_header, "cookie")?
488        // Split the string into individual cookies.
489        .split(';')
490        // `find_map` is an efficient way to find the first cookie that matches our criteria.
491        .find_map(|item| {
492            // This chained logic attempts to quickly find a match.
493            // It's more complex to handle cases like "key=value" vs "key=" correctly.
494            item.trim()
495                .strip_prefix(cookie_name)?
496                .strip_prefix('=')
497                .or_else(|| {
498                    // Fallback logic to ensure the cookie name is an exact match.
499                    let (k, v) = item.split_once('=')?;
500                    if k.trim() == cookie_name {
501                        Some(v.trim())
502                    } else {
503                        None
504                    }
505                })
506        })
507}
508
509/// Gets the value of a specific query parameter from the request URI.
510pub fn get_query_value<'a>(
511    req_header: &'a RequestHeader,
512    name: &str,
513) -> Option<&'a str> {
514    // Get the query string from the URI, exiting if it doesn't exist.
515    req_header
516        .uri
517        .query()?
518        // Split the query string into key-value pairs.
519        .split('&')
520        // `find_map` efficiently searches for the first pair where the key matches.
521        .find_map(|item| {
522            // Split the pair into key and value.
523            let (k, v) = item.split_once('=')?;
524            // If the key matches, return the value.
525            if k == name { Some(v) } else { None }
526        })
527}
528
529/// Removes a specific query parameter from the request header's URI.
530///
531/// This function modifies the `req_header` in place.
532pub fn remove_query_from_header(
533    req_header: &mut RequestHeader,
534    name: &str,
535) -> Result<(), http::uri::InvalidUri> {
536    // If there is no query string, there is nothing to do.
537    let Some(query_str) = req_header.uri.query() else {
538        return Ok(());
539    };
540
541    // Pre-allocate a String with enough capacity to hold the new query string,
542    // which is a performance optimization to avoid reallocations.
543    let mut new_query = String::with_capacity(query_str.len());
544
545    // Iterate over each key-value pair in the original query string.
546    for item in query_str.split('&') {
547        // Get the key part of the pair.
548        let key = item.split('=').next().unwrap_or(item);
549
550        // If the key is not the one we want to remove, keep the item.
551        if key != name {
552            // If the new query string is not empty, add a separator first.
553            if !new_query.is_empty() {
554                new_query.push('&');
555            }
556            // Append the original "key=value" slice, which is allocation-free.
557            new_query.push_str(item);
558        }
559    }
560
561    // Reconstruct the URI from its path and the new query string.
562    let path = req_header.uri.path();
563    // Use `Cow` (Clone-on-Write) to avoid allocating a new String for the path if the query is empty.
564    let new_uri_str = if new_query.is_empty() {
565        // If the new query is empty, the new URI is just the path. Borrow it.
566        Cow::Borrowed(path)
567    } else {
568        // If the new query is not empty, build a new String. Own it.
569        let mut s = String::with_capacity(path.len() + 1 + new_query.len());
570        // `write!` is an efficient way to format into an existing String buffer.
571        let _ = write!(&mut s, "{path}?{new_query}");
572        Cow::Owned(s)
573    };
574
575    // Parse the newly constructed string into a `http::Uri`.
576    let new_uri = http::Uri::from_str(&new_uri_str)?;
577    // Update the request header with the new URI.
578    req_header.set_uri(new_uri);
579
580    Ok(())
581}
582
583#[cfg(test)]
584mod tests {
585    use super::*;
586    use crate::{ConnectionInfo, UpstreamInfo, new_test_session};
587    use pretty_assertions::assert_eq;
588
589    #[test]
590    fn test_convert_headers() {
591        let headers = convert_headers(&[
592            "Content-Type: application/octet-stream".to_string(),
593            "X-Server: $hostname".to_string(),
594            "X-User: $USER".to_string(),
595        ])
596        .unwrap();
597        assert_eq!(3, headers.len());
598        assert_eq!("content-type", headers[0].0.to_string());
599        assert_eq!("application/octet-stream", headers[0].1.to_str().unwrap());
600        assert_eq!("x-server", headers[1].0.to_string());
601        assert_eq!(false, headers[1].1.to_str().unwrap().is_empty());
602        assert_eq!("x-user", headers[2].0.to_string());
603        assert_eq!(false, headers[2].1.to_str().unwrap().is_empty());
604    }
605
606    #[test]
607    fn test_static_value() {
608        assert_eq!(
609            "cache-control: private, no-store",
610            format!(
611                "{}: {}",
612                HTTP_HEADER_NO_STORE.0.to_string(),
613                HTTP_HEADER_NO_STORE.1.to_str().unwrap_or_default()
614            )
615        );
616
617        assert_eq!(
618            "cache-control: private, no-cache",
619            format!(
620                "{}: {}",
621                HTTP_HEADER_NO_CACHE.0.to_string(),
622                HTTP_HEADER_NO_CACHE.1.to_str().unwrap_or_default()
623            )
624        );
625
626        assert_eq!(
627            "content-type: application/json; charset=utf-8",
628            format!(
629                "{}: {}",
630                HTTP_HEADER_CONTENT_JSON.0.to_string(),
631                HTTP_HEADER_CONTENT_JSON.1.to_str().unwrap_or_default()
632            )
633        );
634
635        assert_eq!(
636            "content-type: text/html; charset=utf-8",
637            format!(
638                "{}: {}",
639                HTTP_HEADER_CONTENT_HTML.0.to_string(),
640                HTTP_HEADER_CONTENT_HTML.1.to_str().unwrap_or_default()
641            )
642        );
643
644        assert_eq!(
645            "transfer-encoding: chunked",
646            format!(
647                "{}: {}",
648                HTTP_HEADER_TRANSFER_CHUNKED.0.to_string(),
649                HTTP_HEADER_TRANSFER_CHUNKED.1.to_str().unwrap_or_default()
650            )
651        );
652
653        assert_eq!("x-request-id", HTTP_HEADER_NAME_X_REQUEST_ID.to_string());
654
655        assert_eq!(
656            "content-type: text/plain; charset=utf-8",
657            format!(
658                "{}: {}",
659                HTTP_HEADER_CONTENT_TEXT.0.to_string(),
660                HTTP_HEADER_CONTENT_TEXT.1.to_str().unwrap_or_default()
661            )
662        );
663    }
664
665    #[tokio::test]
666    async fn test_convert_header_value() {
667        let session =
668            new_test_session(&["Host: pingap.io"], "/vicanso/pingap?size=1")
669                .await;
670        let default_state = Ctx {
671            upstream: UpstreamInfo {
672                address: "10.1.1.3:4123".to_string(),
673                ..Default::default()
674            },
675            conn: ConnectionInfo {
676                id: 102,
677                remote_addr: Some("10.1.1.1".to_string()),
678                remote_port: Some(6000),
679                server_addr: Some("10.1.1.2".to_string()),
680                server_port: Some(6001),
681                tls_version: Some("tls1.3".into()),
682                ..Default::default()
683            },
684            ..Default::default()
685        };
686
687        let value = convert_header_value(
688            &HeaderValue::from_str("$host").unwrap(),
689            &session,
690            &Ctx {
691                ..Default::default()
692            },
693        );
694        assert_eq!(true, value.is_some());
695        assert_eq!("pingap.io", value.unwrap().to_str().unwrap());
696
697        let value = convert_header_value(
698            &HeaderValue::from_str("$scheme").unwrap(),
699            &session,
700            &Ctx {
701                ..Default::default()
702            },
703        );
704        assert_eq!(true, value.is_some());
705        assert_eq!("http", value.unwrap().to_str().unwrap());
706        let value = convert_header_value(
707            &HeaderValue::from_str("$scheme").unwrap(),
708            &session,
709            &default_state,
710        );
711        assert_eq!(true, value.is_some());
712        assert_eq!("https", value.unwrap().to_str().unwrap());
713
714        let value = convert_header_value(
715            &HeaderValue::from_str("$remote_addr").unwrap(),
716            &session,
717            &default_state,
718        );
719        assert_eq!(true, value.is_some());
720        assert_eq!("10.1.1.1", value.unwrap().to_str().unwrap());
721
722        let value = convert_header_value(
723            &HeaderValue::from_str("$remote_port").unwrap(),
724            &session,
725            &default_state,
726        );
727        assert_eq!(true, value.is_some());
728        assert_eq!("6000", value.unwrap().to_str().unwrap());
729
730        let value = convert_header_value(
731            &HeaderValue::from_str("$server_addr").unwrap(),
732            &session,
733            &default_state,
734        );
735        assert_eq!(true, value.is_some());
736        assert_eq!("10.1.1.2", value.unwrap().to_str().unwrap());
737
738        let value = convert_header_value(
739            &HeaderValue::from_str("$server_port").unwrap(),
740            &session,
741            &default_state,
742        );
743        assert_eq!(true, value.is_some());
744        assert_eq!("6001", value.unwrap().to_str().unwrap());
745
746        let value = convert_header_value(
747            &HeaderValue::from_str("$upstream_addr").unwrap(),
748            &session,
749            &default_state,
750        );
751        assert_eq!(true, value.is_some());
752        assert_eq!("10.1.1.3:4123", value.unwrap().to_str().unwrap());
753
754        let value = convert_header_value(
755            &HeaderValue::from_str(":connection_id").unwrap(),
756            &session,
757            &default_state,
758        );
759        assert_eq!(true, value.is_some());
760        assert_eq!("102", value.unwrap().to_str().unwrap());
761
762        let session = new_test_session(
763            &["X-Forwarded-For: 1.1.1.1, 2.2.2.2"],
764            "/vicanso/pingap?size=1",
765        )
766        .await;
767        let value = convert_header_value(
768            &HeaderValue::from_str("$proxy_add_x_forwarded_for").unwrap(),
769            &session,
770            &Ctx {
771                conn: ConnectionInfo {
772                    remote_addr: Some("10.1.1.1".to_string()),
773                    ..Default::default()
774                },
775                ..Default::default()
776            },
777        );
778        assert_eq!(true, value.is_some());
779        assert_eq!(
780            "1.1.1.1, 2.2.2.2, 10.1.1.1",
781            value.unwrap().to_str().unwrap()
782        );
783
784        let session = new_test_session(&[""], "/vicanso/pingap?size=1").await;
785        let value = convert_header_value(
786            &HeaderValue::from_str("$proxy_add_x_forwarded_for").unwrap(),
787            &session,
788            &Ctx {
789                conn: ConnectionInfo {
790                    remote_addr: Some("10.1.1.1".to_string()),
791                    ..Default::default()
792                },
793                ..Default::default()
794            },
795        );
796        assert_eq!(true, value.is_some());
797        assert_eq!("10.1.1.1", value.unwrap().to_str().unwrap());
798
799        let session = new_test_session(&[""], "/vicanso/pingap?size=1").await;
800        let value = convert_header_value(
801            &HeaderValue::from_str("$upstream_addr").unwrap(),
802            &session,
803            &Ctx {
804                upstream: UpstreamInfo {
805                    address: "10.1.1.1:8001".to_string(),
806                    ..Default::default()
807                },
808                ..Default::default()
809            },
810        );
811        assert_eq!(true, value.is_some());
812        assert_eq!("10.1.1.1:8001", value.unwrap().to_str().unwrap());
813
814        // `$ja4` and the log-field form `:ja4` read the connection's
815        // fingerprint; without one there is nothing to set.
816        let session = new_test_session(&[""], "/vicanso/pingap?size=1").await;
817        let mut ctx = Ctx::default();
818        for tag in ["$ja4", ":ja4"] {
819            let value = convert_header_value(
820                &HeaderValue::from_str(tag).unwrap(),
821                &session,
822                &ctx,
823            );
824            assert_eq!(true, value.is_none(), "{tag}");
825        }
826        ctx.conn.ja4 = Some(std::sync::Arc::new(
827            crate::Ja4Fingerprint::from_client_hello(
828                &crate::ja4::testing::spec_example_body(),
829            )
830            .unwrap(),
831        ));
832        for tag in ["$ja4", ":ja4"] {
833            let value = convert_header_value(
834                &HeaderValue::from_str(tag).unwrap(),
835                &session,
836                &ctx,
837            );
838            assert_eq!(
839                "t13d1516h2_8daaf6152771_e5627efa2ab1",
840                value.unwrap().to_str().unwrap(),
841                "{tag}"
842            );
843        }
844        // Never resolved as an environment variable at load time.
845        assert_eq!(
846            "$ja4",
847            resolve_static_header_value(HeaderValue::from_static("$ja4"))
848        );
849
850        let session = new_test_session(
851            &["Origin: https://github.com"],
852            "/vicanso/pingap?size=1",
853        )
854        .await;
855        let value = convert_header_value(
856            &HeaderValue::from_str("$http_origin").unwrap(),
857            &session,
858            &Ctx::default(),
859        );
860        assert_eq!(true, value.is_some());
861        assert_eq!("https://github.com", value.unwrap().to_str().unwrap());
862
863        let session = new_test_session(
864            &["Origin: https://github.com"],
865            "/vicanso/pingap?size=1",
866        )
867        .await;
868        let value = convert_header_value(
869            &HeaderValue::from_str("$hostname").unwrap(),
870            &session,
871            &Ctx::default(),
872        );
873        assert_eq!(true, value.is_some());
874
875        let session = new_test_session(
876            &["Origin: https://github.com"],
877            "/vicanso/pingap?size=1",
878        )
879        .await;
880        let value = convert_header_value(
881            &HeaderValue::from_str("$HOME").unwrap(),
882            &session,
883            &Ctx::default(),
884        );
885        assert_eq!(true, value.is_some());
886
887        let session = new_test_session(
888            &["Origin: https://github.com"],
889            "/vicanso/pingap?size=1",
890        )
891        .await;
892        let value = convert_header_value(
893            &HeaderValue::from_str("UUID").unwrap(),
894            &session,
895            &Ctx::default(),
896        );
897        assert_eq!(false, value.is_some());
898    }
899
900    #[tokio::test]
901    async fn test_get_host() {
902        let session =
903            new_test_session(&["Host: pingap.io"], "/vicanso/pingap?size=1")
904                .await;
905        assert_eq!(get_host(session.req_header()), Some("pingap.io"));
906    }
907
908    #[test]
909    fn test_remove_query_from_header() {
910        let mut req =
911            RequestHeader::build("GET", b"/?apikey=123", None).unwrap();
912        remove_query_from_header(&mut req, "apikey").unwrap();
913        assert_eq!("/", req.uri.to_string());
914
915        let mut req =
916            RequestHeader::build("GET", b"/?apikey=123&name=pingap", None)
917                .unwrap();
918        remove_query_from_header(&mut req, "apikey").unwrap();
919        assert_eq!("/?name=pingap", req.uri.to_string());
920    }
921
922    #[tokio::test]
923    async fn test_get_client_ip() {
924        let session = new_test_session(
925            &["X-Forwarded-For:192.168.1.1"],
926            "/vicanso/pingap?size=1",
927        )
928        .await;
929        assert_eq!(get_client_ip(&session), "192.168.1.1");
930
931        let session = new_test_session(
932            &["X-Real-Ip:192.168.1.2"],
933            "/vicanso/pingap?size=1",
934        )
935        .await;
936        assert_eq!(get_client_ip(&session), "192.168.1.2");
937
938        // With trusted proxies configured, a forwarded header from an untrusted
939        // direct peer (the mock session has no trusted peer address) must be
940        // ignored instead of being taken at face value.
941        set_trusted_proxies(&Some(vec!["10.0.0.0/8".to_string()]));
942        let session = new_test_session(
943            &["X-Forwarded-For:192.168.1.1"],
944            "/vicanso/pingap?size=1",
945        )
946        .await;
947        assert_ne!(get_client_ip(&session), "192.168.1.1");
948
949        // Restoring the default trusts forwarded headers again.
950        set_trusted_proxies(&None);
951        let session = new_test_session(
952            &["X-Forwarded-For:192.168.1.1"],
953            "/vicanso/pingap?size=1",
954        )
955        .await;
956        assert_eq!(get_client_ip(&session), "192.168.1.1");
957    }
958
959    #[tokio::test]
960    async fn test_get_header_value() {
961        let session =
962            new_test_session(&["Host: pingap.io"], "/vicanso/pingap?size=1")
963                .await;
964        assert_eq!(
965            get_req_header_value(session.req_header(), "Host"),
966            Some("pingap.io")
967        );
968    }
969
970    #[tokio::test]
971    async fn test_get_cookie_value() {
972        let session = new_test_session(
973            &["Cookie: name=pingap"],
974            "/vicanso/pingap?size=1",
975        )
976        .await;
977        assert_eq!(
978            get_cookie_value(session.req_header(), "name"),
979            Some("pingap")
980        );
981    }
982
983    #[tokio::test]
984    async fn test_get_query_value() {
985        let session = new_test_session(
986            &["X-Forwarded-For:192.168.1.1"],
987            "/vicanso/pingap?size=1",
988        )
989        .await;
990        assert_eq!(get_query_value(session.req_header(), "size"), Some("1"));
991    }
992
993    /// Tests `convert_header` with edge cases like empty strings,
994    /// strings without colons, and invalid header names/values.
995    #[test]
996    fn test_convert_header_edge_cases() {
997        // Empty string should result in Ok(None).
998        assert!(convert_header("").unwrap().is_none());
999        // String without a colon should result in Ok(None).
1000        assert!(convert_header("no-colon").unwrap().is_none());
1001        // Invalid header name should result in an error.
1002        assert!(convert_header("Invalid Name: value").is_err());
1003        // Invalid header value (with newline) should result in an error.
1004        assert!(convert_header("Valid-Name: invalid\r\nvalue").is_err());
1005    }
1006
1007    /// Tests `get_host` logic with different request formats.
1008    #[test]
1009    fn test_get_host_variants() {
1010        // Case 1: Host is in the URI authority.
1011        let uri_string = "http://user:pass@authority.com/path";
1012
1013        // 使用 .parse() 或 from_str 来创建 Uri
1014        let uri = http::Uri::from_str(uri_string).unwrap();
1015        let mut req_with_authority =
1016            RequestHeader::build("GET", b"/path", None).unwrap();
1017        req_with_authority.set_uri(uri);
1018        assert_eq!(get_host(&req_with_authority), Some("authority.com"));
1019
1020        // Case 2: Host is in the "Host" header.
1021        let mut req_with_host_header =
1022            RequestHeader::build("GET", b"/path", None).unwrap();
1023        req_with_host_header
1024            .insert_header("Host", "header-host.com:8080")
1025            .unwrap();
1026        assert_eq!(get_host(&req_with_host_header), Some("header-host.com"));
1027
1028        // Case 3: No host information available.
1029        let req_no_host = RequestHeader::build("GET", b"/path", None).unwrap();
1030        assert_eq!(get_host(&req_no_host), None);
1031
1032        // Case 4: IPv6 literals keep their brackets and lose only the port.
1033        for (host, expected) in [
1034            ("[::1]:8080", "[::1]"),
1035            ("[::1]", "[::1]"),
1036            ("[2001:db8::1]:443", "[2001:db8::1]"),
1037            ("example.com", "example.com"),
1038            ("example.com:8080", "example.com"),
1039        ] {
1040            let mut req = RequestHeader::build("GET", b"/path", None).unwrap();
1041            req.insert_header("Host", host).unwrap();
1042            assert_eq!(get_host(&req), Some(expected), "{host}");
1043        }
1044    }
1045
1046    #[test]
1047    fn test_resolve_static_header_value() {
1048        let resolve = |value: &str| {
1049            resolve_static_header_value(HeaderValue::from_str(value).unwrap())
1050        };
1051        assert_eq!(get_hostname(), resolve("$hostname").to_str().unwrap());
1052        let home = std::env::var("HOME").unwrap();
1053        assert_eq!(home, resolve("$HOME").to_str().unwrap());
1054        // Request-time variables, plain values and unset variables are left
1055        // for the request path.
1056        for value in [
1057            "$host",
1058            "$remote_addr",
1059            "$proxy_add_x_forwarded_for",
1060            "$http_origin",
1061            ":connection_id",
1062            "plain",
1063            "$PINGAP_TEST_UNSET_VARIABLE",
1064        ] {
1065            assert_eq!(value, resolve(value).to_str().unwrap());
1066        }
1067    }
1068
1069    /// `$http_<name>` accepts the nginx spelling with underscores.
1070    #[tokio::test]
1071    async fn test_http_variable_with_underscores() {
1072        let session = new_test_session(
1073            &["User-Agent: pingap/1", "X-Env-Name: prod"],
1074            "/",
1075        )
1076        .await;
1077        for (variable, expected) in [
1078            ("$http_user_agent", Some("pingap/1")),
1079            ("$http_user-agent", Some("pingap/1")),
1080            ("$http_x_env_name", Some("prod")),
1081            ("$http_missing_header", None),
1082        ] {
1083            let value = convert_header_value(
1084                &HeaderValue::from_str(variable).unwrap(),
1085                &session,
1086                &Ctx::default(),
1087            );
1088            assert_eq!(
1089                expected,
1090                value.as_ref().and_then(|v| v.to_str().ok()),
1091                "{variable}"
1092            );
1093        }
1094    }
1095
1096    /// Tests `get_cookie_value` with multiple cookies and edge cases.
1097    #[test]
1098    fn test_get_cookie_value_advanced() {
1099        let mut req = RequestHeader::build("GET", b"/", None).unwrap();
1100        req.insert_header("Cookie", "id=123; session=abc; theme=dark")
1101            .unwrap();
1102
1103        assert_eq!(get_cookie_value(&req, "session"), Some("abc"));
1104        assert_eq!(get_cookie_value(&req, "id"), Some("123"));
1105        assert_eq!(get_cookie_value(&req, "theme"), Some("dark"));
1106        // Test for a non-existent cookie.
1107        assert_eq!(get_cookie_value(&req, "lang"), None);
1108        // Test for a cookie name that is a prefix of another.
1109        assert_eq!(get_cookie_value(&req, "the"), None);
1110    }
1111
1112    #[test]
1113    fn test_remove_query_from_header_variants() {
1114        // Case 1: Remove the only query param.
1115        let mut req =
1116            RequestHeader::build("GET", b"/path?key=val", None).unwrap();
1117        remove_query_from_header(&mut req, "key").unwrap();
1118        assert_eq!(req.uri.to_string(), "/path");
1119
1120        // Case 2: Remove the first of multiple params.
1121        let mut req =
1122            RequestHeader::build("GET", b"/path?key1=val1&key2=val2", None)
1123                .unwrap();
1124        remove_query_from_header(&mut req, "key1").unwrap();
1125        assert_eq!(req.uri.to_string(), "/path?key2=val2");
1126
1127        // Case 3: Remove the last of multiple params.
1128        let mut req =
1129            RequestHeader::build("GET", b"/path?key1=val1&key2=val2", None)
1130                .unwrap();
1131        remove_query_from_header(&mut req, "key2").unwrap();
1132        assert_eq!(req.uri.to_string(), "/path?key1=val1");
1133
1134        // Case 4: Remove a middle param.
1135        let mut req =
1136            RequestHeader::build("GET", b"/path?key1=v1&key2=v2&key3=v3", None)
1137                .unwrap();
1138        remove_query_from_header(&mut req, "key2").unwrap();
1139        assert_eq!(req.uri.to_string(), "/path?key1=v1&key3=v3");
1140
1141        // Case 5: Param to remove is not present.
1142        let mut req =
1143            RequestHeader::build("GET", b"/path?key=val", None).unwrap();
1144        remove_query_from_header(&mut req, "nonexistent").unwrap();
1145        assert_eq!(req.uri.to_string(), "/path?key=val");
1146
1147        // Case 6: No query string to begin with.
1148        let mut req = RequestHeader::build("GET", b"/path", None).unwrap();
1149        remove_query_from_header(&mut req, "key").unwrap();
1150        assert_eq!(req.uri.to_string(), "/path");
1151    }
1152}