From a73321cb2790a9c5da97b616c67ebb27281e4aea Mon Sep 17 00:00:00 2001 From: Jun Kurihara Date: Fri, 4 Jul 2025 17:10:24 +0900 Subject: [PATCH 1/6] initial support forwarded header --- .gitignore | 1 + rpxy-lib/src/backend/upstream_opts.rs | 3 + .../handler_manipulate_messages.rs | 4 +- rpxy-lib/src/message_handler/utils_headers.rs | 140 +++++++++++++++++- 4 files changed, 142 insertions(+), 6 deletions(-) diff --git a/.gitignore b/.gitignore index 20b6fd3..6febdfa 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ .vscode .private +.tmp docker/log docker/cache docker/config diff --git a/rpxy-lib/src/backend/upstream_opts.rs b/rpxy-lib/src/backend/upstream_opts.rs index 68309ca..90ec7de 100644 --- a/rpxy-lib/src/backend/upstream_opts.rs +++ b/rpxy-lib/src/backend/upstream_opts.rs @@ -13,6 +13,8 @@ pub enum UpstreamOption { ForceHttp11Upstream, /// Force HTTP/2 upstream ForceHttp2Upstream, + /// Add RFC 7239 Forwarded header + ForwardedHeader, // TODO: Adds more options for heder override } impl TryFrom<&str> for UpstreamOption { @@ -24,6 +26,7 @@ impl TryFrom<&str> for UpstreamOption { "upgrade_insecure_requests" => Ok(Self::UpgradeInsecureRequests), "force_http11_upstream" => Ok(Self::ForceHttp11Upstream), "force_http2_upstream" => Ok(Self::ForceHttp2Upstream), + "forwarded_header" => Ok(Self::ForwardedHeader), _ => Err(RpxyError::UnsupportedUpstreamOption), } } diff --git a/rpxy-lib/src/message_handler/handler_manipulate_messages.rs b/rpxy-lib/src/message_handler/handler_manipulate_messages.rs index dc58486..1f925e2 100644 --- a/rpxy-lib/src/message_handler/handler_manipulate_messages.rs +++ b/rpxy-lib/src/message_handler/handler_manipulate_messages.rs @@ -87,7 +87,7 @@ where remove_connection_header(headers); // delete hop headers including header.connection remove_hop_header(headers); - // X-Forwarded-For + // X-Forwarded-For (and Forwarded if exists) add_forwarding_header(headers, client_addr, listen_addr, tls_enabled, &original_uri)?; // Add te: trailer if te_trailer @@ -126,7 +126,7 @@ where // apply upstream-specific headers given in upstream_option let headers = req.headers_mut(); - // apply upstream options to header + // apply upstream options to header, after X-Forwarded-For is added apply_upstream_options_to_header(headers, &upstream_chosen.uri, upstream_candidates)?; // update uri in request diff --git a/rpxy-lib/src/message_handler/utils_headers.rs b/rpxy-lib/src/message_handler/utils_headers.rs index fe351d9..f6efac2 100644 --- a/rpxy-lib/src/message_handler/utils_headers.rs +++ b/rpxy-lib/src/message_handler/utils_headers.rs @@ -96,6 +96,7 @@ fn override_host_header(headers: &mut HeaderMap, upstream_base_uri: &Uri) -> Res } /// Apply options to request header, which are specified in the configuration +/// This function is called after almost all other headers has been set and updated. pub(super) fn apply_upstream_options_to_header( headers: &mut HeaderMap, upstream_base_uri: &Uri, @@ -117,6 +118,22 @@ pub(super) fn apply_upstream_options_to_header( .entry(header::UPGRADE_INSECURE_REQUESTS) .or_insert(HeaderValue::from_bytes(b"1").unwrap()); } + UpstreamOption::ForwardedHeader => { + // This is called after X-Forwarded-For is added + // Generate RFC 7239 Forwarded header + let host = headers.get(header::HOST).and_then(|h| h.to_str().ok()).unwrap_or("unknown"); + let tls = upstream_base_uri.scheme_str() == Some("https"); + + match generate_forwarded_header(headers, tls, host) { + Ok(forwarded_value) => { + add_header_entry_overwrite_if_exist(headers, "forwarded", forwarded_value)?; + } + Err(e) => { + // Log warning but don't fail the request if Forwarded generation fails + warn!("Failed to generate Forwarded header: {}", e); + } + } + } _ => (), } } @@ -194,7 +211,9 @@ pub(super) fn make_cookie_single_line(headers: &mut HeaderMap) -> Result<()> { Ok(()) } -/// Add forwarding headers like `x-forwarded-for`. +/// Add or update forwarding headers like `x-forwarded-for`. +/// If only `forwarded` header exists, it will update `x-forwarded-for` with the proxy chain. +/// If both `x-forwarded-for` and `forwarded` headers exist, it will update `x-forwarded-for` first and then add `forwarded` header. pub(super) fn add_forwarding_header( headers: &mut HeaderMap, client_addr: &SocketAddr, @@ -202,10 +221,40 @@ pub(super) fn add_forwarding_header( tls: bool, uri_str: &str, ) -> Result<()> { - // default process - // optional process defined by upstream_option is applied in fn apply_upstream_options let canonical_client_addr = client_addr.to_canonical().ip().to_string(); - append_header_entry_with_comma(headers, "x-forwarded-for", &canonical_client_addr)?; + let has_forwarded = headers.contains_key("forwarded"); + let has_xff = headers.contains_key("x-forwarded-for"); + + // Handle incoming Forwarded header (Case 2: only Forwarded exists) + if has_forwarded && !has_xff { + // Extract proxy chain from Forwarded header and update X-Forwarded-For for consistency + update_xff_from_forwarded(headers, client_addr)?; + } else { + // Case 1: only X-Forwarded-For exists, or Case 3: both exist (conservative: use X-Forwarded-For) + // TODO: In future PR, implement proper RFC 7239 precedence + // where Forwarded header should take priority over X-Forwarded-For + // This requires careful testing to ensure no breaking changes + append_header_entry_with_comma(headers, "x-forwarded-for", &canonical_client_addr)?; + } + + // IMPORTANT: If Forwarded header exists, always update it for consistency + // This ensures headers remain consistent even when forwarded_header upstream option is not specified + if has_forwarded { + let host = headers + .get(header::HOST) + .and_then(|h| h.to_str().ok()) + .unwrap_or("unknown"); + + match generate_forwarded_header(headers, tls, host) { + Ok(forwarded_value) => { + add_header_entry_overwrite_if_exist(headers, "forwarded", forwarded_value)?; + } + Err(e) => { + // Log warning but don't fail the request if Forwarded generation fails + warn!("Failed to update existing Forwarded header for consistency: {}", e); + } + } + } // Single line cookie header // TODO: This should be only for HTTP/1.1. For 2+, this can be multi-lined. @@ -232,6 +281,89 @@ pub(super) fn add_forwarding_header( Ok(()) } +/// Extract proxy chain from existing Forwarded header +fn extract_forwarded_chain(headers: &HeaderMap) -> Vec { + headers + .get("forwarded") + .and_then(|h| h.to_str().ok()) + .map(|forwarded_str| { + // Parse Forwarded header entries (comma-separated) + forwarded_str + .split(',') + .flat_map(|entry| entry.split(';')) + .map(str::trim) + .filter_map(|param| param.strip_prefix("for=")) + .map(|for_value| { + // Remove quotes from IPv6 addresses for consistency with X-Forwarded-For + if let Some(ipv6) = for_value.strip_prefix("\"[").and_then(|s| s.strip_suffix("]\"")) { + ipv6.to_string() + } else { + for_value.to_string() + } + }) + .collect() + }) + .unwrap_or_default() +} + +/// Update X-Forwarded-For with proxy chain from Forwarded header for consistency +fn update_xff_from_forwarded(headers: &mut HeaderMap, client_addr: &SocketAddr) -> Result<()> { + let forwarded_chain = extract_forwarded_chain(headers); + + if !forwarded_chain.is_empty() { + // Replace X-Forwarded-For with the chain from Forwarded header + headers.remove("x-forwarded-for"); + for ip in forwarded_chain { + append_header_entry_with_comma(headers, "x-forwarded-for", &ip)?; + } + } + + // Append current client IP (standard behavior) + let canonical_client_addr = client_addr.to_canonical().ip().to_string(); + append_header_entry_with_comma(headers, "x-forwarded-for", &canonical_client_addr)?; + + Ok(()) +} + +/// Generate RFC 7239 Forwarded header from X-Forwarded-For +/// This function assumes that the X-Forwarded-For header is present and well-formed. +fn generate_forwarded_header(headers: &HeaderMap, tls: bool, host: &str) -> Result { + let for_values = headers + .get("x-forwarded-for") + .and_then(|h| h.to_str().ok()) + .map(|xff_str| { + xff_str + .split(',') + .map(str::trim) + .filter(|ip| !ip.is_empty()) + .map(|ip| { + // Format IP according to RFC 7239 (quote IPv6) + if ip.contains(':') { + format!("\"[{}]\"", ip) + } else { + ip.to_string() + } + }) + .collect::>() + .join(",for=") + }) + .unwrap_or_default(); + + if for_values.is_empty() { + return Err(anyhow!("No X-Forwarded-For header found for Forwarded generation")); + } + + // Build forwarded header value + let forwarded_value = format!( + "for={};proto={};host={}", + for_values, + if tls { "https" } else { "http" }, + host + ); + + Ok(forwarded_value) +} + /// Remove connection header pub(super) fn remove_connection_header(headers: &mut HeaderMap) { if let Some(values) = headers.get(header::CONNECTION) { From 03bfd466bf0b1ce5598d706d45012872d7e525a1 Mon Sep 17 00:00:00 2001 From: Jun Kurihara Date: Fri, 4 Jul 2025 17:58:41 +0900 Subject: [PATCH 2/6] wip: still bug for forwarde-host param --- CHANGELOG.md | 4 ++++ config-example.toml | 1 + .../message_handler/handler_manipulate_messages.rs | 12 ++++++++++++ rpxy-lib/src/message_handler/utils_headers.rs | 8 +++----- 4 files changed, 20 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a76b24d..d7a06b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## 0.10.1 or 0.11.0 (Unreleased) +### Improvement + +- Feat: Support `Forwarded` header in addition to `X-Forwarded-For` header. This is to support the standard forwarding header for reverse proxy applications (RFC 7239). Use the `forwarded_header` upstream option to enable this feature. + ## 0.10.0 ### Important Changes diff --git a/config-example.toml b/config-example.toml index 8ed7621..8e08318 100644 --- a/config-example.toml +++ b/config-example.toml @@ -84,6 +84,7 @@ upstream_options = [ "upgrade_insecure_requests", "force_http11_upstream", "set_upstream_host", # overwrite HOST value with upstream hostname (like www.yahoo.com) + "forwarded_header" # add Forwarded header ] ###################################################################### diff --git a/rpxy-lib/src/message_handler/handler_manipulate_messages.rs b/rpxy-lib/src/message_handler/handler_manipulate_messages.rs index 1f925e2..9f74fe8 100644 --- a/rpxy-lib/src/message_handler/handler_manipulate_messages.rs +++ b/rpxy-lib/src/message_handler/handler_manipulate_messages.rs @@ -98,6 +98,18 @@ where // by default, add "host" header of original server_name if not exist if req.headers().get(header::HOST).is_none() { let org_host = req.uri().host().ok_or_else(|| anyhow!("Invalid request"))?.to_owned(); + // Omit port 80 if !tls_enabled, omit port 443 if tls_enabled + let org_host = req + .uri() + .port_u16() + .map(|port| { + if (tls_enabled && port == 443) || (!tls_enabled && port == 80) { + org_host.clone() + } else { + format!("{}:{}", org_host, port) + } + }) + .unwrap_or(org_host); req.headers_mut().insert(header::HOST, HeaderValue::from_str(&org_host)?); }; diff --git a/rpxy-lib/src/message_handler/utils_headers.rs b/rpxy-lib/src/message_handler/utils_headers.rs index f6efac2..61a1d50 100644 --- a/rpxy-lib/src/message_handler/utils_headers.rs +++ b/rpxy-lib/src/message_handler/utils_headers.rs @@ -121,6 +121,7 @@ pub(super) fn apply_upstream_options_to_header( UpstreamOption::ForwardedHeader => { // This is called after X-Forwarded-For is added // Generate RFC 7239 Forwarded header + // TODO: host is generated from x-original-uri let host = headers.get(header::HOST).and_then(|h| h.to_str().ok()).unwrap_or("unknown"); let tls = upstream_base_uri.scheme_str() == Some("https"); @@ -240,11 +241,8 @@ pub(super) fn add_forwarding_header( // IMPORTANT: If Forwarded header exists, always update it for consistency // This ensures headers remain consistent even when forwarded_header upstream option is not specified if has_forwarded { - let host = headers - .get(header::HOST) - .and_then(|h| h.to_str().ok()) - .unwrap_or("unknown"); - + let host = headers.get(header::HOST).and_then(|h| h.to_str().ok()).unwrap_or("unknown"); + match generate_forwarded_header(headers, tls, host) { Ok(forwarded_value) => { add_header_entry_overwrite_if_exist(headers, "forwarded", forwarded_value)?; From 5c9bb200e02a48c008d02fb0d1589e619a1158ba Mon Sep 17 00:00:00 2001 From: Jun Kurihara Date: Sat, 5 Jul 2025 00:26:15 +0900 Subject: [PATCH 3/6] feat: implement forwarded header handling --- config-example.toml | 4 +-- .../handler_manipulate_messages.rs | 16 ++-------- rpxy-lib/src/message_handler/utils_headers.rs | 32 +++++++++++++------ 3 files changed, 26 insertions(+), 26 deletions(-) diff --git a/config-example.toml b/config-example.toml index 8e08318..e1c71cd 100644 --- a/config-example.toml +++ b/config-example.toml @@ -83,8 +83,8 @@ load_balance = "random" # or "round_robin" or "sticky" (sticky session) or "none upstream_options = [ "upgrade_insecure_requests", "force_http11_upstream", - "set_upstream_host", # overwrite HOST value with upstream hostname (like www.yahoo.com) - "forwarded_header" # add Forwarded header + "set_upstream_host", # overwrite HOST value with upstream hostname (like www.yahoo.com) + "forwarded_header" # add Forwarded header (by default, this is not added. However, if the incoming request has Forwarded header, it would be preserved and updated) ] ###################################################################### diff --git a/rpxy-lib/src/message_handler/handler_manipulate_messages.rs b/rpxy-lib/src/message_handler/handler_manipulate_messages.rs index 9f74fe8..08227f7 100644 --- a/rpxy-lib/src/message_handler/handler_manipulate_messages.rs +++ b/rpxy-lib/src/message_handler/handler_manipulate_messages.rs @@ -81,7 +81,7 @@ where .unwrap_or(false) }; - let original_uri = req.uri().to_string(); + let original_uri = req.uri().clone(); let headers = req.headers_mut(); // delete headers specified in header.connection remove_connection_header(headers); @@ -98,18 +98,6 @@ where // by default, add "host" header of original server_name if not exist if req.headers().get(header::HOST).is_none() { let org_host = req.uri().host().ok_or_else(|| anyhow!("Invalid request"))?.to_owned(); - // Omit port 80 if !tls_enabled, omit port 443 if tls_enabled - let org_host = req - .uri() - .port_u16() - .map(|port| { - if (tls_enabled && port == 443) || (!tls_enabled && port == 80) { - org_host.clone() - } else { - format!("{}:{}", org_host, port) - } - }) - .unwrap_or(org_host); req.headers_mut().insert(header::HOST, HeaderValue::from_str(&org_host)?); }; @@ -139,7 +127,7 @@ where // apply upstream-specific headers given in upstream_option let headers = req.headers_mut(); // apply upstream options to header, after X-Forwarded-For is added - apply_upstream_options_to_header(headers, &upstream_chosen.uri, upstream_candidates)?; + apply_upstream_options_to_header(headers, &upstream_chosen.uri, upstream_candidates, &original_uri)?; // update uri in request ensure!( diff --git a/rpxy-lib/src/message_handler/utils_headers.rs b/rpxy-lib/src/message_handler/utils_headers.rs index 61a1d50..7f84494 100644 --- a/rpxy-lib/src/message_handler/utils_headers.rs +++ b/rpxy-lib/src/message_handler/utils_headers.rs @@ -102,6 +102,7 @@ pub(super) fn apply_upstream_options_to_header( upstream_base_uri: &Uri, // _client_addr: &SocketAddr, upstream: &UpstreamCandidates, + original_uri: &Uri, ) -> Result<()> { for opt in upstream.options.iter() { match opt { @@ -121,11 +122,9 @@ pub(super) fn apply_upstream_options_to_header( UpstreamOption::ForwardedHeader => { // This is called after X-Forwarded-For is added // Generate RFC 7239 Forwarded header - // TODO: host is generated from x-original-uri - let host = headers.get(header::HOST).and_then(|h| h.to_str().ok()).unwrap_or("unknown"); let tls = upstream_base_uri.scheme_str() == Some("https"); - match generate_forwarded_header(headers, tls, host) { + match generate_forwarded_header(headers, tls, original_uri) { Ok(forwarded_value) => { add_header_entry_overwrite_if_exist(headers, "forwarded", forwarded_value)?; } @@ -220,7 +219,7 @@ pub(super) fn add_forwarding_header( client_addr: &SocketAddr, listen_addr: &SocketAddr, tls: bool, - uri_str: &str, + original_uri: &Uri, ) -> Result<()> { let canonical_client_addr = client_addr.to_canonical().ip().to_string(); let has_forwarded = headers.contains_key("forwarded"); @@ -241,9 +240,7 @@ pub(super) fn add_forwarding_header( // IMPORTANT: If Forwarded header exists, always update it for consistency // This ensures headers remain consistent even when forwarded_header upstream option is not specified if has_forwarded { - let host = headers.get(header::HOST).and_then(|h| h.to_str().ok()).unwrap_or("unknown"); - - match generate_forwarded_header(headers, tls, host) { + match generate_forwarded_header(headers, tls, original_uri) { Ok(forwarded_value) => { add_header_entry_overwrite_if_exist(headers, "forwarded", forwarded_value)?; } @@ -272,7 +269,7 @@ pub(super) fn add_forwarding_header( // x-forwarded-ssl add_header_entry_overwrite_if_exist(headers, "x-forwarded-ssl", if tls { "on" } else { "off" })?; // x-original-uri - add_header_entry_overwrite_if_exist(headers, "x-original-uri", uri_str.to_string())?; + add_header_entry_overwrite_if_exist(headers, "x-original-uri", original_uri.to_string())?; // proxy add_header_entry_overwrite_if_exist(headers, "proxy", "")?; @@ -325,7 +322,7 @@ fn update_xff_from_forwarded(headers: &mut HeaderMap, client_addr: &SocketAddr) /// Generate RFC 7239 Forwarded header from X-Forwarded-For /// This function assumes that the X-Forwarded-For header is present and well-formed. -fn generate_forwarded_header(headers: &HeaderMap, tls: bool, host: &str) -> Result { +fn generate_forwarded_header(headers: &HeaderMap, tls: bool, original_uri: &Uri) -> Result { let for_values = headers .get("x-forwarded-for") .and_then(|h| h.to_str().ok()) @@ -356,12 +353,27 @@ fn generate_forwarded_header(headers: &HeaderMap, tls: bool, host: &str) -> Resu "for={};proto={};host={}", for_values, if tls { "https" } else { "http" }, - host + host_from_uri(original_uri)? ); Ok(forwarded_value) } +#[inline] +/// Extract host from URI +fn host_from_uri(uri: &Uri) -> Result { + uri + .host() + .map(|host| { + if let Some(port) = uri.port_u16() { + format!("{}:{}", host, port) + } else { + host.to_string() + } + }) + .ok_or_else(|| anyhow!("No host found in URI")) +} + /// Remove connection header pub(super) fn remove_connection_header(headers: &mut HeaderMap) { if let Some(values) = headers.get(header::CONNECTION) { From 8e61098753e32c8a0f50edbd8733589f971d4d25 Mon Sep 17 00:00:00 2001 From: Jun Kurihara Date: Sat, 5 Jul 2025 00:34:41 +0900 Subject: [PATCH 4/6] update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d7a06b8..c26304c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### Improvement - Feat: Support `Forwarded` header in addition to `X-Forwarded-For` header. This is to support the standard forwarding header for reverse proxy applications (RFC 7239). Use the `forwarded_header` upstream option to enable this feature. + By default, it is not appended to the outgoing header. However, if the incoming request has the forwarded header, it would be preserved and updated simultaneously with `x-forwarded-for` header. if both forwarded and x-forwarded-for headers exists (and they are inconsistent), x-forwarded-for is prioritized. This means that x-forwarded-for is first updated and it is then copied (overridden) to `for` param of forwarded header. ## 0.10.0 From ba06d8d4ebfcc510f2af421ce9d489233059d4b3 Mon Sep 17 00:00:00 2001 From: Jun Kurihara Date: Sat, 5 Jul 2025 12:08:13 +0900 Subject: [PATCH 5/6] refactor --- rpxy-lib/src/message_handler/utils_headers.rs | 37 +++++++++++-------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/rpxy-lib/src/message_handler/utils_headers.rs b/rpxy-lib/src/message_handler/utils_headers.rs index 7f84494..c4a0b75 100644 --- a/rpxy-lib/src/message_handler/utils_headers.rs +++ b/rpxy-lib/src/message_handler/utils_headers.rs @@ -12,6 +12,13 @@ use std::{borrow::Cow, net::SocketAddr}; use crate::backend::{LoadBalanceContext, StickyCookie, StickyCookieValue}; // use crate::backend::{UpstreamGroup, UpstreamOption}; +const X_FORWARDED_FOR: &str = "x-forwarded-for"; +const X_FORWARDED_PROTO: &str = "x-forwarded-proto"; +const X_FORWARDED_PORT: &str = "x-forwarded-port"; +const X_FORWARDED_SSL: &str = "x-forwarded-ssl"; +const X_ORIGINAL_URI: &str = "x-original-uri"; +const X_REAL_IP: &str = "x-real-ip"; + // //////////////////////////////////////////////////// // // Functions to manipulate headers #[cfg(feature = "sticky-cookie")] @@ -126,7 +133,7 @@ pub(super) fn apply_upstream_options_to_header( match generate_forwarded_header(headers, tls, original_uri) { Ok(forwarded_value) => { - add_header_entry_overwrite_if_exist(headers, "forwarded", forwarded_value)?; + add_header_entry_overwrite_if_exist(headers, header::FORWARDED.as_str(), forwarded_value)?; } Err(e) => { // Log warning but don't fail the request if Forwarded generation fails @@ -222,8 +229,8 @@ pub(super) fn add_forwarding_header( original_uri: &Uri, ) -> Result<()> { let canonical_client_addr = client_addr.to_canonical().ip().to_string(); - let has_forwarded = headers.contains_key("forwarded"); - let has_xff = headers.contains_key("x-forwarded-for"); + let has_forwarded = headers.contains_key(header::FORWARDED); + let has_xff = headers.contains_key(X_FORWARDED_FOR); // Handle incoming Forwarded header (Case 2: only Forwarded exists) if has_forwarded && !has_xff { @@ -234,7 +241,7 @@ pub(super) fn add_forwarding_header( // TODO: In future PR, implement proper RFC 7239 precedence // where Forwarded header should take priority over X-Forwarded-For // This requires careful testing to ensure no breaking changes - append_header_entry_with_comma(headers, "x-forwarded-for", &canonical_client_addr)?; + append_header_entry_with_comma(headers, X_FORWARDED_FOR, &canonical_client_addr)?; } // IMPORTANT: If Forwarded header exists, always update it for consistency @@ -242,7 +249,7 @@ pub(super) fn add_forwarding_header( if has_forwarded { match generate_forwarded_header(headers, tls, original_uri) { Ok(forwarded_value) => { - add_header_entry_overwrite_if_exist(headers, "forwarded", forwarded_value)?; + add_header_entry_overwrite_if_exist(headers, header::FORWARDED.as_str(), forwarded_value)?; } Err(e) => { // Log warning but don't fail the request if Forwarded generation fails @@ -258,18 +265,18 @@ pub(super) fn add_forwarding_header( /////////// As Nginx // If we receive X-Forwarded-Proto, pass it through; otherwise, pass along the // scheme used to connect to this server - add_header_entry_if_not_exist(headers, "x-forwarded-proto", if tls { "https" } else { "http" })?; + add_header_entry_if_not_exist(headers, X_FORWARDED_PROTO, if tls { "https" } else { "http" })?; // If we receive X-Forwarded-Port, pass it through; otherwise, pass along the // server port the client connected to - add_header_entry_if_not_exist(headers, "x-forwarded-port", listen_addr.port().to_string())?; + add_header_entry_if_not_exist(headers, X_FORWARDED_PORT, listen_addr.port().to_string())?; /////////// As Nginx-Proxy // x-real-ip - add_header_entry_overwrite_if_exist(headers, "x-real-ip", canonical_client_addr)?; + add_header_entry_overwrite_if_exist(headers, X_REAL_IP, canonical_client_addr)?; // x-forwarded-ssl - add_header_entry_overwrite_if_exist(headers, "x-forwarded-ssl", if tls { "on" } else { "off" })?; + add_header_entry_overwrite_if_exist(headers, X_FORWARDED_SSL, if tls { "on" } else { "off" })?; // x-original-uri - add_header_entry_overwrite_if_exist(headers, "x-original-uri", original_uri.to_string())?; + add_header_entry_overwrite_if_exist(headers, X_ORIGINAL_URI, original_uri.to_string())?; // proxy add_header_entry_overwrite_if_exist(headers, "proxy", "")?; @@ -279,7 +286,7 @@ pub(super) fn add_forwarding_header( /// Extract proxy chain from existing Forwarded header fn extract_forwarded_chain(headers: &HeaderMap) -> Vec { headers - .get("forwarded") + .get(header::FORWARDED) .and_then(|h| h.to_str().ok()) .map(|forwarded_str| { // Parse Forwarded header entries (comma-separated) @@ -307,15 +314,15 @@ fn update_xff_from_forwarded(headers: &mut HeaderMap, client_addr: &SocketAddr) if !forwarded_chain.is_empty() { // Replace X-Forwarded-For with the chain from Forwarded header - headers.remove("x-forwarded-for"); + headers.remove(X_FORWARDED_FOR); for ip in forwarded_chain { - append_header_entry_with_comma(headers, "x-forwarded-for", &ip)?; + append_header_entry_with_comma(headers, X_FORWARDED_FOR, &ip)?; } } // Append current client IP (standard behavior) let canonical_client_addr = client_addr.to_canonical().ip().to_string(); - append_header_entry_with_comma(headers, "x-forwarded-for", &canonical_client_addr)?; + append_header_entry_with_comma(headers, X_FORWARDED_FOR, &canonical_client_addr)?; Ok(()) } @@ -324,7 +331,7 @@ fn update_xff_from_forwarded(headers: &mut HeaderMap, client_addr: &SocketAddr) /// This function assumes that the X-Forwarded-For header is present and well-formed. fn generate_forwarded_header(headers: &HeaderMap, tls: bool, original_uri: &Uri) -> Result { let for_values = headers - .get("x-forwarded-for") + .get(X_FORWARDED_FOR) .and_then(|h| h.to_str().ok()) .map(|xff_str| { xff_str From 42464d1d49f0cf281721751766957ec63a59ecd0 Mon Sep 17 00:00:00 2001 From: Jun Kurihara Date: Sat, 5 Jul 2025 13:21:20 +0900 Subject: [PATCH 6/6] simplified http_log internal routine --- rpxy-lib/src/message_handler/http_log.rs | 91 ++++++++++++++++--- rpxy-lib/src/message_handler/utils_headers.rs | 31 ++++--- 2 files changed, 94 insertions(+), 28 deletions(-) diff --git a/rpxy-lib/src/message_handler/http_log.rs b/rpxy-lib/src/message_handler/http_log.rs index 40d13cc..5ae302e 100644 --- a/rpxy-lib/src/message_handler/http_log.rs +++ b/rpxy-lib/src/message_handler/http_log.rs @@ -1,5 +1,5 @@ use super::canonical_address::ToCanonical; -use crate::log::*; +use crate::{log::*, message_handler::utils_headers}; use http::header; use std::net::SocketAddr; @@ -12,10 +12,11 @@ pub struct HttpMessageLog { pub host: String, pub p_and_q: String, pub version: http::Version, - pub uri_scheme: String, - pub uri_host: String, + pub scheme: String, + pub path: String, pub ua: String, pub xff: String, + pub forwarded: String, pub status: String, pub upstream: String, } @@ -29,17 +30,21 @@ impl From<&http::Request> for HttpMessageLog { .map_or_else(|| "", |s| s.to_str().unwrap_or("")) .to_string() }; + let host = + utils_headers::host_from_uri_or_host_header(req.uri(), req.headers().get(header::HOST).cloned()).unwrap_or_default(); + Self { // tls_server_name: "".to_string(), client_addr: "".to_string(), method: req.method().to_string(), - host: header_mapper(header::HOST), + host, p_and_q: req.uri().path_and_query().map_or_else(|| "", |v| v.as_str()).to_string(), version: req.version(), - uri_scheme: req.uri().scheme_str().unwrap_or("").to_string(), - uri_host: req.uri().host().unwrap_or("").to_string(), + scheme: req.uri().scheme_str().unwrap_or("").to_string(), + path: req.uri().path().to_string(), ua: header_mapper(header::USER_AGENT), xff: header_mapper(header::HeaderName::from_static("x-forwarded-for")), + forwarded: header_mapper(header::FORWARDED), status: "".to_string(), upstream: "".to_string(), } @@ -48,26 +53,29 @@ impl From<&http::Request> for HttpMessageLog { impl std::fmt::Display for HttpMessageLog { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let forwarded_part = if !self.forwarded.is_empty() { + format!(" \"{}\"", self.forwarded) + } else { + "".to_string() + }; + write!( f, - "{} <- {} -- {} {} {:?} -- {} -- {} \"{}\", \"{}\" \"{}\"", - if !self.host.is_empty() { - self.host.as_str() - } else { - self.uri_host.as_str() - }, + "{} <- {} -- {} {} {:?} -- {} -- {} \"{}\", \"{}\"{} \"{}\"", + self.host, self.client_addr, self.method, self.p_and_q, self.version, self.status, - if !self.uri_scheme.is_empty() && !self.uri_host.is_empty() { - format!("{}://{}", self.uri_scheme, self.uri_host) + if !self.scheme.is_empty() && !self.host.is_empty() { + format!("{}://{}{}", self.scheme, self.host, self.path) } else { - "".to_string() + self.path.clone() }, self.ua, self.xff, + forwarded_part, self.upstream ) } @@ -102,3 +110,56 @@ impl HttpMessageLog { ); } } + +#[cfg(test)] +mod tests { + use super::*; + use http::{Method, Version}; + + #[test] + fn test_log_format_without_forwarded() { + let log = HttpMessageLog { + client_addr: "192.168.1.1:8080".to_string(), + method: Method::GET.to_string(), + host: "example.com".to_string(), + p_and_q: "/path?query=value".to_string(), + version: Version::HTTP_11, + scheme: "https".to_string(), + path: "/path".to_string(), + ua: "Mozilla/5.0".to_string(), + xff: "10.0.0.1".to_string(), + forwarded: "".to_string(), + status: "200".to_string(), + upstream: "https://backend.example.com".to_string(), + }; + + let formatted = format!("{}", log); + assert!(!formatted.contains(" \"\"")); + assert!(formatted.contains("\"Mozilla/5.0\", \"10.0.0.1\" \"https://backend.example.com\"")); + } + + #[test] + fn test_log_format_with_forwarded() { + let log = HttpMessageLog { + client_addr: "192.168.1.1:8080".to_string(), + method: Method::GET.to_string(), + host: "example.com".to_string(), + p_and_q: "/path?query=value".to_string(), + version: Version::HTTP_11, + scheme: "https".to_string(), + path: "/path".to_string(), + ua: "Mozilla/5.0".to_string(), + xff: "10.0.0.1".to_string(), + forwarded: "for=192.0.2.60;proto=http;by=203.0.113.43".to_string(), + status: "200".to_string(), + upstream: "https://backend.example.com".to_string(), + }; + + let formatted = format!("{}", log); + assert!(formatted.contains(" \"for=192.0.2.60;proto=http;by=203.0.113.43\"")); + assert!( + formatted + .contains("\"Mozilla/5.0\", \"10.0.0.1\" \"for=192.0.2.60;proto=http;by=203.0.113.43\" \"https://backend.example.com\"") + ); + } +} diff --git a/rpxy-lib/src/message_handler/utils_headers.rs b/rpxy-lib/src/message_handler/utils_headers.rs index c4a0b75..ddc68ba 100644 --- a/rpxy-lib/src/message_handler/utils_headers.rs +++ b/rpxy-lib/src/message_handler/utils_headers.rs @@ -360,25 +360,30 @@ fn generate_forwarded_header(headers: &HeaderMap, tls: bool, original_uri: &Uri) "for={};proto={};host={}", for_values, if tls { "https" } else { "http" }, - host_from_uri(original_uri)? + host_from_uri_or_host_header(original_uri, headers.get(header::HOST).cloned())? ); Ok(forwarded_value) } -#[inline] /// Extract host from URI -fn host_from_uri(uri: &Uri) -> Result { - uri - .host() - .map(|host| { - if let Some(port) = uri.port_u16() { - format!("{}:{}", host, port) - } else { - host.to_string() - } - }) - .ok_or_else(|| anyhow!("No host found in URI")) +pub(super) fn host_from_uri_or_host_header(uri: &Uri, host_header_value: Option) -> Result { + // Prioritize uri host over host header + let uri_host = uri.host().map(|host| { + if let Some(port) = uri.port_u16() { + format!("{}:{}", host, port) + } else { + host.to_string() + } + }); + if let Some(host) = uri_host { + return Ok(host); + } + // If uri host is not available, use host header + host_header_value + .map(|h| h.to_str().map(|s| s.to_string())) + .transpose()? + .ok_or_else(|| anyhow!("No host found in URI or Host header")) } /// Remove connection header