Release v0.6.0
This commit is contained in:
1 parent
7069ca9db7
commit
f49698bb5c
75 files changed
+11199
-7508
No files matched your search
@@ -22,3 +22,119 @@ pub fn get_client_ip(headers: &HeaderMap, connect_info: Option<ConnectInfo<Socke
|
||||
}
|
||||
"127.0.0.1".to_string()
|
||||
}
|
||||
|
||||
// Helper to safely extract hostname from a Host header, handling IPv6 and ports.
|
||||
pub(crate) fn extract_hostname(host_header: &str) -> &str {
|
||||
if host_header.starts_with('[') {
|
||||
if let Some(end_idx) = host_header.find(']') {
|
||||
return &host_header[1..end_idx];
|
||||
}
|
||||
}
|
||||
host_header.split(':').next().unwrap_or(host_header)
|
||||
}
|
||||
|
||||
/// Determines whether to set the `Secure` flag on a cookie based on deployment context.
|
||||
///
|
||||
/// Policy:
|
||||
/// 1. If X-Forwarded-Proto is explicitly "https", always enforce Secure=true.
|
||||
/// (We assume X-Forwarded-Proto is from a trusted proxy. Forged "https" only makes
|
||||
/// the cookie safer. We never weaken based on X-Forwarded-Proto=http).
|
||||
/// 2. If the exact Host is a local loopback (localhost, 127.0.0.1, ::1) and we are not
|
||||
/// explicitly proxied via HTTPS, disable Secure. This prevents browsers from dropping
|
||||
/// the cookie during local development over cleartext HTTP.
|
||||
/// 3. Otherwise, fall back to the global `cookie_secure` config (which defaults to true
|
||||
/// to keep production secure-by-default even if the proxy strips X-Forwarded-Proto).
|
||||
pub fn resolve_cookie_secure(config_secure: bool, headers: &HeaderMap) -> bool {
|
||||
// 1. Explicit HTTPS via reverse proxy
|
||||
if let Some(proto) = headers
|
||||
.get("x-forwarded-proto")
|
||||
.and_then(|v| v.to_str().ok())
|
||||
{
|
||||
if proto.eq_ignore_ascii_case("https") {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Exact loopback development (prevent cookie drop)
|
||||
if let Some(host_hdr) = headers.get("host").and_then(|h| h.to_str().ok()) {
|
||||
let hostname = extract_hostname(host_hdr);
|
||||
if matches!(hostname, "localhost" | "127.0.0.1" | "::1") {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Global config (normally true)
|
||||
config_secure
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use axum::http::HeaderValue;
|
||||
|
||||
#[test]
|
||||
fn test_extract_hostname() {
|
||||
assert_eq!(extract_hostname("localhost"), "localhost");
|
||||
assert_eq!(extract_hostname("localhost:8080"), "localhost");
|
||||
assert_eq!(extract_hostname("127.0.0.1"), "127.0.0.1");
|
||||
assert_eq!(extract_hostname("127.0.0.1:8080"), "127.0.0.1");
|
||||
assert_eq!(extract_hostname("[::1]"), "::1");
|
||||
assert_eq!(extract_hostname("[::1]:8080"), "::1");
|
||||
assert_eq!(extract_hostname("example.com"), "example.com");
|
||||
assert_eq!(extract_hostname("example.com:443"), "example.com");
|
||||
assert_eq!(
|
||||
extract_hostname("localhost.example.com"),
|
||||
"localhost.example.com"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_resolve_cookie_secure() {
|
||||
let mut headers = HeaderMap::new();
|
||||
|
||||
// No headers, global config is true -> Secure=true
|
||||
assert!(resolve_cookie_secure(true, &headers));
|
||||
// No headers, global config is false -> Secure=false
|
||||
assert!(!resolve_cookie_secure(false, &headers));
|
||||
|
||||
// Exact loopback matches -> Secure=false
|
||||
let loopback_hosts = [
|
||||
"localhost",
|
||||
"localhost:8080",
|
||||
"127.0.0.1",
|
||||
"127.0.0.1:8080",
|
||||
"[::1]",
|
||||
"[::1]:8080",
|
||||
];
|
||||
for host in loopback_hosts {
|
||||
headers.insert("host", HeaderValue::from_static(host));
|
||||
assert!(
|
||||
!resolve_cookie_secure(true, &headers),
|
||||
"Failed for host: {}",
|
||||
host
|
||||
);
|
||||
}
|
||||
|
||||
// Non-loopback localhost subdomains -> Secure=true (relying on config)
|
||||
let public_hosts = ["localhost.example.com", "127.0.0.2", "example.com"];
|
||||
for host in public_hosts {
|
||||
headers.insert("host", HeaderValue::from_static(host));
|
||||
assert!(
|
||||
resolve_cookie_secure(true, &headers),
|
||||
"Failed for host: {}",
|
||||
host
|
||||
);
|
||||
}
|
||||
|
||||
// X-Forwarded-Proto = https overrides loopback
|
||||
headers.insert("host", HeaderValue::from_static("localhost"));
|
||||
headers.insert("x-forwarded-proto", HeaderValue::from_static("https"));
|
||||
assert!(resolve_cookie_secure(true, &headers));
|
||||
assert!(resolve_cookie_secure(false, &headers)); // Overrides false config too
|
||||
|
||||
// X-Forwarded-Proto = http DOES NOT override global config
|
||||
headers.insert("host", HeaderValue::from_static("example.com"));
|
||||
headers.insert("x-forwarded-proto", HeaderValue::from_static("http"));
|
||||
assert!(resolve_cookie_secure(true, &headers)); // Still true because of config
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user