use super::{Ctx, get_hostname};
use ahash::AHashSet;
use arc_swap::ArcSwapOption;
use bytes::BytesMut;
use http::header;
use http::{HeaderName, HeaderValue};
use ipnet::IpNet;
use pingora::http::RequestHeader;
use pingora::proxy::Session;
use snafu::{ResultExt, Snafu};
use std::borrow::Cow;
use std::net::{IpAddr, SocketAddr};
use std::str::FromStr;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
const HTTP_HEADER_X_FORWARDED_FOR: &str = "x-forwarded-for";
const HTTP_HEADER_X_REAL_IP: &str = "x-real-ip";
pub const HOST_NAME_TAG: &[u8] = b"$hostname";
const HOST_TAG: &[u8] = b"$host";
const SCHEME_TAG: &[u8] = b"$scheme";
const REMOTE_ADDR_TAG: &[u8] = b"$remote_addr";
const REMOTE_PORT_TAG: &[u8] = b"$remote_port";
const SERVER_ADDR_TAG: &[u8] = b"$server_addr";
const SERVER_PORT_TAG: &[u8] = b"$server_port";
const PROXY_ADD_FORWARDED_TAG: &[u8] = b"$proxy_add_x_forwarded_for";
const UPSTREAM_ADDR_TAG: &[u8] = b"$upstream_addr";
const JA4_TAG: &[u8] = b"$ja4";
const CLIENT_IP_TAG: &[u8] = b"$client_ip";
const FORWARDED_PROTO_TAG: &[u8] = b"$forwarded_proto";
const FORWARDED_HOST_TAG: &[u8] = b"$forwarded_host";
const FORWARDED_PORT_TAG: &[u8] = b"$forwarded_port";
const TLS_CLIENT_SUBJECT_TAG: &[u8] = b"$tls_client_subject";
const TLS_CLIENT_FINGERPRINT_TAG: &[u8] = b"$tls_client_fingerprint";
const TLS_CLIENT_SERIAL_TAG: &[u8] = b"$tls_client_serial";
const TLS_CLIENT_VERIFIED_TAG: &[u8] = b"$tls_client_verified";
static SCHEME_HTTPS: HeaderValue = HeaderValue::from_static("https");
static SCHEME_HTTP: HeaderValue = HeaderValue::from_static("http");
#[derive(Debug, Snafu)]
pub enum Error {
#[snafu(display("invalid header value: {value} - {source}"))]
InvalidHeaderValue {
value: String,
source: header::InvalidHeaderValue,
},
#[snafu(display("invalid header name: {value} - {source}"))]
InvalidHeaderName {
value: String,
source: header::InvalidHeaderName,
},
}
type Result<T, E = Error> = std::result::Result<T, E>;
pub type HttpHeader = (HeaderName, HeaderValue);
pub fn get_host(header: &RequestHeader) -> Option<&str> {
get_request_host(header).map(strip_root_label)
}
pub fn get_request_host(header: &RequestHeader) -> Option<&str> {
match header.uri.host() {
Some(host) => Some(host),
None => header
.headers
.get(http::header::HOST)
.and_then(|value| value.to_str().ok())
.map(strip_port),
}
}
#[inline]
pub fn strip_root_label(host: &str) -> &str {
match host.strip_suffix('.') {
Some(name) if !name.is_empty() => name,
_ => host,
}
}
pub fn merge_cookie_headers(header: &mut RequestHeader) {
let mut values = header.headers.get_all(http::header::COOKIE).iter();
let (Some(first), Some(second)) = (values.next(), values.next()) else {
return;
};
let mut merged = Vec::with_capacity(
first.len() + second.len() + 2 + values.size_hint().0 * 16,
);
merged.extend_from_slice(first.as_bytes());
for value in std::iter::once(second).chain(values) {
merged.extend_from_slice(b"; ");
merged.extend_from_slice(value.as_bytes());
}
if let Ok(value) = HeaderValue::from_bytes(&merged) {
let _ = header.insert_header(http::header::COOKIE, value);
}
}
fn strip_port(host: &str) -> &str {
if host.starts_with('[') {
return match host.find(']') {
Some(end) => &host[..=end],
None => host,
};
}
host.split(':').next().unwrap_or(host)
}
pub fn convert_header(value: &str) -> Result<Option<HttpHeader>> {
value
.split_once(':')
.map(|(k, v)| {
let name = HeaderName::from_str(k.trim())
.context(InvalidHeaderNameSnafu { value: k })?;
let value = HeaderValue::from_str(v.trim())
.context(InvalidHeaderValueSnafu { value: v })?;
Ok(Some((name, value)))
})
.unwrap_or(Ok(None))
}
pub fn convert_headers(header_values: &[String]) -> Result<Vec<HttpHeader>> {
header_values
.iter()
.filter_map(|item| convert_header(item).transpose())
.collect()
}
pub static HTTP_HEADER_NO_STORE: HttpHeader = (
header::CACHE_CONTROL,
HeaderValue::from_static("private, no-store"),
);
pub static HTTP_HEADER_NO_CACHE: HttpHeader = (
header::CACHE_CONTROL,
HeaderValue::from_static("private, no-cache"),
);
pub static HTTP_HEADER_CONTENT_JSON: HttpHeader = (
header::CONTENT_TYPE,
HeaderValue::from_static("application/json; charset=utf-8"),
);
pub static HTTP_HEADER_CONTENT_HTML: HttpHeader = (
header::CONTENT_TYPE,
HeaderValue::from_static("text/html; charset=utf-8"),
);
pub static HTTP_HEADER_CONTENT_TEXT: HttpHeader = (
header::CONTENT_TYPE,
HeaderValue::from_static("text/plain; charset=utf-8"),
);
pub static HTTP_HEADER_TRANSFER_CHUNKED: HttpHeader = (
header::TRANSFER_ENCODING,
HeaderValue::from_static("chunked"),
);
pub static HTTP_HEADER_NAME_X_REQUEST_ID: HeaderName =
HeaderName::from_static("x-request-id");
pub fn resolve_static_header_value(value: HeaderValue) -> HeaderValue {
let buf = value.as_bytes();
if buf.first() != Some(&b'$') {
return value;
}
if buf == HOST_NAME_TAG {
return HeaderValue::from_str(get_hostname()).unwrap_or(value);
}
let request_tags: [&[u8]; 17] = [
HOST_TAG,
SCHEME_TAG,
REMOTE_ADDR_TAG,
REMOTE_PORT_TAG,
SERVER_ADDR_TAG,
SERVER_PORT_TAG,
PROXY_ADD_FORWARDED_TAG,
UPSTREAM_ADDR_TAG,
JA4_TAG,
CLIENT_IP_TAG,
FORWARDED_PROTO_TAG,
FORWARDED_HOST_TAG,
FORWARDED_PORT_TAG,
TLS_CLIENT_SUBJECT_TAG,
TLS_CLIENT_FINGERPRINT_TAG,
TLS_CLIENT_SERIAL_TAG,
TLS_CLIENT_VERIFIED_TAG,
];
if request_tags.contains(&buf) || buf.starts_with(b"$http_") {
return value;
}
let Ok(name) = std::str::from_utf8(&buf[1..]) else {
return value;
};
match std::env::var(name) {
Ok(env_value) => HeaderValue::from_str(&env_value).unwrap_or(value),
Err(_) => value,
}
}
fn tls_client_value(
ctx: &Ctx,
field: impl Fn(&crate::TlsClientCert) -> &str,
) -> HeaderValue {
ctx.conn
.tls_client_cert
.as_deref()
.and_then(|cert| HeaderValue::from_str(field(cert)).ok())
.unwrap_or_else(|| HeaderValue::from_static(""))
}
#[inline]
pub fn convert_header_value(
value: &HeaderValue,
session: &Session,
ctx: &Ctx,
) -> Option<HeaderValue> {
let buf = value.as_bytes();
if buf.is_empty() || !(buf[0] == b'$' || buf[0] == b':') {
return None;
}
let to_header_value = |s: &str| HeaderValue::from_str(s).ok();
match buf {
HOST_TAG => get_host(session.req_header()).and_then(to_header_value),
SCHEME_TAG => Some(if ctx.conn.tls_version.is_some() {
SCHEME_HTTPS.clone()
} else {
SCHEME_HTTP.clone()
}),
HOST_NAME_TAG => to_header_value(get_hostname()),
REMOTE_ADDR_TAG => {
ctx.conn.remote_addr.as_deref().and_then(to_header_value)
},
REMOTE_PORT_TAG => ctx.conn.remote_port.and_then(|p| {
HeaderValue::from_str(itoa::Buffer::new().format(p)).ok()
}),
SERVER_ADDR_TAG => {
ctx.conn.server_addr.as_deref().and_then(to_header_value)
},
SERVER_PORT_TAG => ctx.conn.server_port.and_then(|p| {
HeaderValue::from_str(itoa::Buffer::new().format(p)).ok()
}),
UPSTREAM_ADDR_TAG => {
if !ctx.upstream.address.is_empty() {
to_header_value(&ctx.upstream.address)
} else {
None
}
},
JA4_TAG => ctx
.conn
.ja4
.as_deref()
.and_then(|fingerprint| to_header_value(fingerprint.ja4())),
CLIENT_IP_TAG => {
if has_trusted_proxies() {
match ctx.conn.client_ip.as_deref() {
Some(client_ip) => to_header_value(client_ip),
None => to_header_value(&get_client_ip(session)),
}
} else {
ctx.conn.remote_addr.as_deref().and_then(to_header_value)
}
},
FORWARDED_PROTO_TAG => Some(if client_used_https(session, ctx) {
SCHEME_HTTPS.clone()
} else {
SCHEME_HTTP.clone()
}),
FORWARDED_HOST_TAG => {
forwarded_by_trusted_proxy(session, &HTTP_HEADER_X_FORWARDED_HOST)
.filter(|host| is_host_name(host))
.and_then(to_header_value)
.or_else(|| {
get_host(session.req_header()).and_then(to_header_value)
})
},
TLS_CLIENT_SUBJECT_TAG => {
Some(tls_client_value(ctx, |cert| cert.subject.as_str()))
},
TLS_CLIENT_FINGERPRINT_TAG => {
Some(tls_client_value(ctx, |cert| cert.fingerprint.as_str()))
},
TLS_CLIENT_SERIAL_TAG => {
Some(tls_client_value(ctx, |cert| cert.serial.as_str()))
},
TLS_CLIENT_VERIFIED_TAG => Some(HeaderValue::from_static(
if ctx.conn.tls_client_cert.is_some() {
"true"
} else {
"false"
},
)),
FORWARDED_PORT_TAG => {
let forwarded = forwarded_by_trusted_proxy(
session,
&HTTP_HEADER_X_FORWARDED_PORT,
)
.and_then(|port| port.parse::<u16>().ok())
.filter(|port| *port > 0)
.or_else(|| {
forwarded_scheme(session)
.map(|https| if https { 443 } else { 80 })
})
.or(ctx.conn.server_port);
forwarded.and_then(|port| {
HeaderValue::from_str(itoa::Buffer::new().format(port)).ok()
})
},
PROXY_ADD_FORWARDED_TAG => {
ctx.conn.remote_addr.as_deref().and_then(|remote_addr| {
let existing = session
.req_header()
.headers
.get_all(HTTP_HEADER_X_FORWARDED_FOR);
let capacity = existing
.iter()
.map(|v| v.as_bytes().len() + 2)
.sum::<usize>()
+ remote_addr.len();
let mut value_buf = BytesMut::with_capacity(capacity);
for value in existing.iter() {
value_buf.extend_from_slice(value.as_bytes());
value_buf.extend_from_slice(b", ");
}
value_buf.extend_from_slice(remote_addr.as_bytes());
HeaderValue::from_bytes(&value_buf).ok()
})
},
_ => handle_special_headers(buf, session, ctx),
}
}
#[inline]
fn handle_special_headers(
buf: &[u8],
session: &Session,
ctx: &Ctx,
) -> Option<HeaderValue> {
if let Some(name) = buf.strip_prefix(b"$http_") {
let key = std::str::from_utf8(name).ok()?;
if key.contains('_') {
return session.get_header(key.replace('_', "-")).cloned();
}
return session.get_header(key).cloned();
}
if buf.starts_with(b"$") {
let var_name = std::str::from_utf8(&buf[1..]).ok()?;
return std::env::var(var_name)
.ok()
.and_then(|v| HeaderValue::from_str(&v).ok());
}
if buf.starts_with(b":") {
let key = std::str::from_utf8(&buf[1..]).ok()?;
let mut value = BytesMut::with_capacity(20);
ctx.append_log_value(&mut value, key);
if !value.is_empty() {
return HeaderValue::from_bytes(&value).ok();
}
}
None
}
pub fn get_remote_addr(session: &Session) -> Option<(String, u16)> {
session
.client_addr()
.and_then(|addr| addr.as_inet())
.map(|addr| (addr.ip().to_canonical().to_string(), addr.port()))
}
struct TrustedProxies {
nets: Vec<IpNet>,
ips: AHashSet<IpAddr>,
}
impl TrustedProxies {
fn parse(values: &[String]) -> Self {
let mut nets = Vec::new();
let mut ips = AHashSet::new();
for item in values {
if let Ok(net) = IpNet::from_str(item) {
nets.push(net);
} else if let Ok(ip) = IpAddr::from_str(item) {
ips.insert(ip.to_canonical());
}
}
Self { nets, ips }
}
fn contains(&self, peer: IpAddr) -> bool {
let peer = peer.to_canonical();
self.ips.contains(&peer)
|| self.nets.iter().any(|net| net.contains(&peer))
}
}
static TRUSTED_PROXIES_ENABLED: AtomicBool = AtomicBool::new(false);
static TRUSTED_PROXIES: ArcSwapOption<TrustedProxies> =
ArcSwapOption::const_empty();
static HTTP_HEADER_X_FORWARDED_PROTO: HeaderName =
HeaderName::from_static("x-forwarded-proto");
static HTTP_HEADER_X_FORWARDED_HOST: HeaderName =
HeaderName::from_static("x-forwarded-host");
static HTTP_HEADER_X_FORWARDED_PORT: HeaderName =
HeaderName::from_static("x-forwarded-port");
pub fn peer_is_trusted_proxy(session: &Session) -> bool {
if !has_trusted_proxies() {
return false;
}
let Some(peer) = session
.client_addr()
.and_then(|addr| addr.as_inet())
.map(|addr| addr.ip())
else {
return false;
};
TRUSTED_PROXIES
.load()
.as_ref()
.is_some_and(|trusted| trusted.contains(peer))
}
fn first_forwarded_entry<'a>(
values: impl Iterator<Item = &'a HeaderValue>,
) -> Option<&'a str> {
values
.filter_map(|value| value.to_str().ok())
.flat_map(|value| value.split(','))
.map(str::trim)
.find(|entry| !entry.is_empty())
}
fn forwarded_by_trusted_proxy<'a>(
session: &'a Session,
name: &HeaderName,
) -> Option<&'a str> {
let entry = first_forwarded_entry(
session.req_header().headers.get_all(name).iter(),
)?;
peer_is_trusted_proxy(session).then_some(entry)
}
fn forwarded_scheme(session: &Session) -> Option<bool> {
let scheme =
forwarded_by_trusted_proxy(session, &HTTP_HEADER_X_FORWARDED_PROTO)?;
if scheme.eq_ignore_ascii_case("https") {
Some(true)
} else if scheme.eq_ignore_ascii_case("http") {
Some(false)
} else {
None
}
}
pub fn client_used_https(session: &Session, ctx: &Ctx) -> bool {
forwarded_scheme(session).unwrap_or(ctx.conn.tls_version.is_some())
}
fn is_host_name(value: &str) -> bool {
value
.parse::<http::uri::Authority>()
.is_ok_and(|authority| {
!authority.host().is_empty() && !authority.as_str().contains('@')
})
}
pub fn set_trusted_proxies(proxies: &Option<Vec<String>>) {
let parsed = match proxies {
Some(list) if !list.is_empty() => Some(TrustedProxies::parse(list)),
_ => None,
};
let enabled = parsed.is_some();
TRUSTED_PROXIES.store(parsed.map(Arc::new));
TRUSTED_PROXIES_ENABLED.store(enabled, Ordering::Relaxed);
}
fn parse_forwarded_ip(value: &str) -> Option<IpAddr> {
let ip = if let Ok(ip) = IpAddr::from_str(value) {
ip
} else if let Ok(addr) = SocketAddr::from_str(value) {
addr.ip()
} else {
value
.strip_prefix('[')
.and_then(|value| value.strip_suffix(']'))
.and_then(|value| IpAddr::from_str(value).ok())?
};
Some(ip.to_canonical())
}
fn forwarded_client_ip<'a>(
values: impl DoubleEndedIterator<Item = &'a HeaderValue>,
trusted: &TrustedProxies,
) -> Option<String> {
let mut first_proxy = None;
for value in values.rev() {
for item in value.as_bytes().rsplit(|b| *b == b',') {
let item = item.trim_ascii();
if item.is_empty() {
continue;
}
let Ok(item) = std::str::from_utf8(item) else {
return Some(String::from_utf8_lossy(item).into_owned());
};
match parse_forwarded_ip(item) {
Some(ip) if trusted.contains(ip) => first_proxy = Some(ip),
Some(ip) => return Some(ip.to_string()),
None => return Some(item.to_string()),
}
}
}
first_proxy.map(|ip| ip.to_string())
}
#[inline]
pub fn has_trusted_proxies() -> bool {
TRUSTED_PROXIES_ENABLED.load(Ordering::Relaxed)
}
pub fn is_trusted_proxy(ip: IpAddr) -> bool {
if !has_trusted_proxies() {
return false;
}
TRUSTED_PROXIES
.load()
.as_ref()
.is_some_and(|trusted| trusted.contains(ip.to_canonical()))
}
#[inline]
pub fn ensure_verified_client_ip<'a>(
session: &Session,
ctx: &'a mut Ctx,
) -> &'a str {
if has_trusted_proxies() {
return ensure_client_ip(session, ctx);
}
ctx.conn.remote_addr.as_deref().unwrap_or_default()
}
#[inline]
pub fn ensure_client_ip<'a>(session: &Session, ctx: &'a mut Ctx) -> &'a str {
if ctx.conn.client_ip.is_none() {
ctx.conn.client_ip = Some(get_client_ip(session));
}
ctx.conn.client_ip.as_deref().unwrap_or_default()
}
pub fn get_client_ip(session: &Session) -> String {
if TRUSTED_PROXIES_ENABLED.load(Ordering::Relaxed) {
let Some(peer) = session
.client_addr()
.and_then(|addr| addr.as_inet())
.map(|addr| addr.ip().to_canonical())
else {
return String::new();
};
let trusted = TRUSTED_PROXIES.load();
let Some(trusted) =
trusted.as_ref().filter(|trusted| trusted.contains(peer))
else {
return peer.to_string();
};
let headers = &session.req_header().headers;
if let Some(ip) = forwarded_client_ip(
headers.get_all(HTTP_HEADER_X_FORWARDED_FOR).iter(),
trusted,
) {
return ip;
}
if let Some(ip) = headers
.get(HTTP_HEADER_X_REAL_IP)
.and_then(|value| value.to_str().ok())
.map(|value| value.trim())
.filter(|value| !value.is_empty())
{
return ip.to_string();
}
return peer.to_string();
}
if let Some(value) = session.get_header(HTTP_HEADER_X_FORWARDED_FOR) {
if let Ok(s) = value.to_str()
&& let Some(ip) = s.split(',').next()
{
let trimmed_ip = ip.trim();
if !trimmed_ip.is_empty() {
return trimmed_ip.to_string();
}
}
}
if let Some(value) = session.get_header(HTTP_HEADER_X_REAL_IP) {
return value.to_str().unwrap_or_default().to_string();
}
if let Some((addr, _)) = get_remote_addr(session) {
return addr;
}
"".to_string()
}
pub fn get_req_header_value<'a>(
req_header: &'a RequestHeader,
key: &str,
) -> Option<&'a str> {
if let Some(value) = req_header.headers.get(key) {
if let Ok(value) = value.to_str() {
return Some(value);
}
}
None
}
pub fn get_cookie_value<'a>(
req_header: &'a RequestHeader,
cookie_name: &str,
) -> Option<&'a str> {
get_req_header_value(req_header, "cookie")?
.split(';')
.find_map(|item| {
item.trim()
.strip_prefix(cookie_name)?
.strip_prefix('=')
.or_else(|| {
let (k, v) = item.split_once('=')?;
if k.trim() == cookie_name {
Some(v.trim())
} else {
None
}
})
})
}
pub fn get_query_value<'a>(
req_header: &'a RequestHeader,
name: &str,
) -> Option<&'a str> {
req_header
.uri
.query()?
.split('&')
.find_map(|item| {
let (k, v) = item.split_once('=')?;
if k == name { Some(v) } else { None }
})
}
pub fn decode_query_value(raw: &str) -> Cow<'_, str> {
if !raw.contains('%') {
return Cow::Borrowed(raw);
}
urlencoding::decode(raw).unwrap_or(Cow::Borrowed(raw))
}
pub fn remove_query_from_header(
req_header: &mut RequestHeader,
name: &str,
) -> Result<(), http::Error> {
let Some(query_str) = req_header.uri.query() else {
return Ok(());
};
let mut new_query = String::with_capacity(query_str.len());
for item in query_str.split('&') {
let key = item.split('=').next().unwrap_or(item);
if key != name {
if !new_query.is_empty() {
new_query.push('&');
}
new_query.push_str(item);
}
}
let path = req_header.uri.path();
let mut path_and_query =
String::with_capacity(path.len() + 1 + new_query.len());
path_and_query.push_str(path);
if !new_query.is_empty() {
path_and_query.push('?');
path_and_query.push_str(&new_query);
}
set_path_and_query(req_header, &path_and_query)
}
fn path_needs_normalizing(path: &[u8]) -> bool {
let mut after_slash = false;
for (index, byte) in path.iter().enumerate() {
match byte {
b'%' | b';' | b'\\' => return true,
b'/' if after_slash => return true,
b'.' if after_slash => {
let rest = &path[index + 1..];
let rest = rest.strip_prefix(b".").unwrap_or(rest);
if rest.first().is_none_or(|next| *next == b'/') {
return true;
}
},
_ => {},
}
after_slash = *byte == b'/';
}
false
}
pub fn normalize_path(path: &str) -> Cow<'_, str> {
match normalized_path_bytes(path) {
None => Cow::Borrowed(path),
Some(normalized) => {
Cow::Owned(String::from_utf8_lossy(&normalized).into_owned())
},
}
}
pub fn canonical_path(path: &str) -> Cow<'_, str> {
let Some(normalized) = normalized_path_bytes(path) else {
return Cow::Borrowed(path);
};
let mut encoded = String::with_capacity(normalized.len() + 8);
for byte in normalized {
let plain = byte.is_ascii_alphanumeric()
|| matches!(
byte,
b'/' | b'-'
| b'.'
| b'_'
| b'~'
| b'!'
| b'$'
| b'&'
| b'\''
| b'('
| b')'
| b'*'
| b'+'
| b','
| b';'
| b'='
| b':'
| b'@'
);
if plain {
encoded.push(byte as char);
} else {
encoded.push('%');
encoded.push(char::from(b"0123456789ABCDEF"[(byte >> 4) as usize]));
encoded.push(char::from(b"0123456789ABCDEF"[(byte & 15) as usize]));
}
}
Cow::Owned(encoded)
}
pub fn has_dot_segments(path: &str) -> bool {
normalized_path(path).is_some_and(|(_, dot_segments)| dot_segments)
}
fn normalized_path_bytes(path: &str) -> Option<Vec<u8>> {
normalized_path(path).map(|(bytes, _)| bytes)
}
fn normalized_path(path: &str) -> Option<(Vec<u8>, bool)> {
let bytes = path.as_bytes();
if !path.starts_with('/') || !path_needs_normalizing(bytes) {
return None;
}
let hex = |byte: Option<&u8>| {
byte.and_then(|byte| (*byte as char).to_digit(16))
.map(|value| value as u8)
};
let mut decoded = Vec::with_capacity(bytes.len());
let mut index = 0;
while index < bytes.len() {
if bytes[index] == b'%'
&& let (Some(high), Some(low)) =
(hex(bytes.get(index + 1)), hex(bytes.get(index + 2)))
{
decoded.push(high << 4 | low);
index += 3;
} else {
decoded.push(bytes[index]);
index += 1;
}
}
let mut segments: Vec<&[u8]> = vec![];
let mut dot_segments = false;
let mut trailing_slash = false;
for segment in decoded.split(|byte| matches!(byte, b'/' | b'\\')) {
trailing_slash = true;
let name = segment
.split(|byte| *byte == b';')
.next()
.unwrap_or(segment);
match name {
b"" => {},
b"." => dot_segments = true,
b".." => {
dot_segments = true;
segments.pop();
},
_ => {
segments.push(name);
trailing_slash = false;
},
}
}
let mut normalized = Vec::with_capacity(decoded.len());
for segment in segments.iter() {
normalized.push(b'/');
normalized.extend_from_slice(segment);
}
if trailing_slash || normalized.is_empty() {
normalized.push(b'/');
}
Some((normalized, dot_segments))
}
pub fn set_path_and_query(
req_header: &mut RequestHeader,
path_and_query: &str,
) -> Result<(), http::Error> {
if req_header.uri.authority().is_none() {
req_header.set_uri(http::Uri::from_str(path_and_query)?);
return Ok(());
}
let path_and_query = http::uri::PathAndQuery::from_str(path_and_query)?;
let mut parts = req_header.uri.clone().into_parts();
parts.path_and_query = Some(path_and_query);
req_header.set_uri(http::Uri::from_parts(parts)?);
Ok(())
}
pub fn protect_from_connection_header(
req: &mut RequestHeader,
names: &[HeaderName],
) {
if names.is_empty() || !req.headers.contains_key(header::CONNECTION) {
return;
}
let nominated = |token: &str| {
names
.iter()
.any(|name| name.as_str().eq_ignore_ascii_case(token))
};
let mut kept = vec![];
let mut removed = false;
for value in req.headers.get_all(header::CONNECTION).iter() {
let Ok(value) = value.to_str() else {
return;
};
for token in value.split(',').map(str::trim) {
if token.is_empty() {
continue;
}
if nominated(token) {
removed = true;
} else {
kept.push(token);
}
}
}
if !removed {
return;
}
let kept = kept.join(", ");
req.remove_header(&header::CONNECTION);
if !kept.is_empty() {
let _ = req.insert_header(header::CONNECTION, kept);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
ConnectionInfo, TlsClientCert, UpstreamInfo, new_test_session,
};
use pretty_assertions::assert_eq;
#[test]
fn test_protect_from_connection_header() {
let protected = [
HeaderName::from_static("x-real-ip"),
HeaderName::from_static("x-user-id"),
];
let after = |connection: &[&str]| {
let mut req = RequestHeader::build("GET", b"/", None).unwrap();
for value in connection {
req.append_header(header::CONNECTION, *value).unwrap();
}
protect_from_connection_header(&mut req, &protected);
req.headers
.get_all(header::CONNECTION)
.iter()
.map(|value| value.to_str().unwrap().to_string())
.collect::<Vec<_>>()
};
assert_eq!(Vec::<String>::new(), after(&["X-Real-IP"]));
assert_eq!(vec!["keep-alive"], after(&["keep-alive, x-real-ip"]));
assert_eq!(
vec!["keep-alive, upgrade, X-Other"],
after(&[" keep-alive ,X-USER-ID,upgrade", "x-real-ip, X-Other"])
);
assert_eq!(
vec!["keep-alive , Upgrade"],
after(&["keep-alive , Upgrade"])
);
assert_eq!(vec!["close", "X-Other"], after(&["close", "X-Other"]));
assert_eq!(Vec::<String>::new(), after(&[]));
assert_eq!(vec!["x-real-ip-2"], after(&["x-real-ip-2"]));
}
#[test]
fn test_convert_headers() {
let headers = convert_headers(&[
"Content-Type: application/octet-stream".to_string(),
"X-Server: $hostname".to_string(),
"X-User: $USER".to_string(),
])
.unwrap();
assert_eq!(3, headers.len());
assert_eq!("content-type", headers[0].0.to_string());
assert_eq!("application/octet-stream", headers[0].1.to_str().unwrap());
assert_eq!("x-server", headers[1].0.to_string());
assert_eq!(false, headers[1].1.to_str().unwrap().is_empty());
assert_eq!("x-user", headers[2].0.to_string());
assert_eq!(false, headers[2].1.to_str().unwrap().is_empty());
}
#[test]
fn test_static_value() {
assert_eq!(
"cache-control: private, no-store",
format!(
"{}: {}",
HTTP_HEADER_NO_STORE.0.to_string(),
HTTP_HEADER_NO_STORE.1.to_str().unwrap_or_default()
)
);
assert_eq!(
"cache-control: private, no-cache",
format!(
"{}: {}",
HTTP_HEADER_NO_CACHE.0.to_string(),
HTTP_HEADER_NO_CACHE.1.to_str().unwrap_or_default()
)
);
assert_eq!(
"content-type: application/json; charset=utf-8",
format!(
"{}: {}",
HTTP_HEADER_CONTENT_JSON.0.to_string(),
HTTP_HEADER_CONTENT_JSON.1.to_str().unwrap_or_default()
)
);
assert_eq!(
"content-type: text/html; charset=utf-8",
format!(
"{}: {}",
HTTP_HEADER_CONTENT_HTML.0.to_string(),
HTTP_HEADER_CONTENT_HTML.1.to_str().unwrap_or_default()
)
);
assert_eq!(
"transfer-encoding: chunked",
format!(
"{}: {}",
HTTP_HEADER_TRANSFER_CHUNKED.0.to_string(),
HTTP_HEADER_TRANSFER_CHUNKED.1.to_str().unwrap_or_default()
)
);
assert_eq!("x-request-id", HTTP_HEADER_NAME_X_REQUEST_ID.to_string());
assert_eq!(
"content-type: text/plain; charset=utf-8",
format!(
"{}: {}",
HTTP_HEADER_CONTENT_TEXT.0.to_string(),
HTTP_HEADER_CONTENT_TEXT.1.to_str().unwrap_or_default()
)
);
}
#[tokio::test]
async fn test_tls_client_header_values() {
let session = new_test_session(&["Host: pingap.io"], "/").await;
let value = |ctx: &Ctx, tag: &str| {
convert_header_value(
&HeaderValue::from_str(tag).unwrap(),
&session,
ctx,
)
.map(|value| value.to_str().unwrap().to_string())
};
let mut ctx = Ctx::default();
for tag in [
"$tls_client_subject",
"$tls_client_fingerprint",
"$tls_client_serial",
] {
assert_eq!(Some("".to_string()), value(&ctx, tag), "{tag}");
}
assert_eq!(
Some("false".to_string()),
value(&ctx, "$tls_client_verified")
);
ctx.conn.tls_client_cert = Some(std::sync::Arc::new(TlsClientCert {
subject: "O=Example, CN=device-42".to_string(),
fingerprint: "b545db7a".to_string(),
serial: "3429".to_string(),
}));
assert_eq!(
Some("O=Example, CN=device-42".to_string()),
value(&ctx, "$tls_client_subject")
);
assert_eq!(
Some("b545db7a".to_string()),
value(&ctx, "$tls_client_fingerprint")
);
assert_eq!(Some("3429".to_string()), value(&ctx, "$tls_client_serial"));
assert_eq!(
Some("true".to_string()),
value(&ctx, "$tls_client_verified")
);
let tag = HeaderValue::from_static("$tls_client_subject");
assert_eq!(tag.clone(), resolve_static_header_value(tag));
}
#[tokio::test]
async fn test_convert_header_value() {
let session =
new_test_session(&["Host: pingap.io"], "/vicanso/pingap?size=1")
.await;
let default_state = Ctx {
upstream: UpstreamInfo {
address: "10.1.1.3:4123".to_string(),
..Default::default()
},
conn: ConnectionInfo {
id: 102,
remote_addr: Some("10.1.1.1".to_string()),
remote_port: Some(6000),
server_addr: Some("10.1.1.2".to_string()),
server_port: Some(6001),
tls_version: Some("tls1.3".into()),
..Default::default()
},
..Default::default()
};
let value = convert_header_value(
&HeaderValue::from_str("$host").unwrap(),
&session,
&Ctx {
..Default::default()
},
);
assert_eq!(true, value.is_some());
assert_eq!("pingap.io", value.unwrap().to_str().unwrap());
let value = convert_header_value(
&HeaderValue::from_str("$scheme").unwrap(),
&session,
&Ctx {
..Default::default()
},
);
assert_eq!(true, value.is_some());
assert_eq!("http", value.unwrap().to_str().unwrap());
let value = convert_header_value(
&HeaderValue::from_str("$scheme").unwrap(),
&session,
&default_state,
);
assert_eq!(true, value.is_some());
assert_eq!("https", value.unwrap().to_str().unwrap());
let value = convert_header_value(
&HeaderValue::from_str("$remote_addr").unwrap(),
&session,
&default_state,
);
assert_eq!(true, value.is_some());
assert_eq!("10.1.1.1", value.unwrap().to_str().unwrap());
let value = convert_header_value(
&HeaderValue::from_str("$remote_port").unwrap(),
&session,
&default_state,
);
assert_eq!(true, value.is_some());
assert_eq!("6000", value.unwrap().to_str().unwrap());
let value = convert_header_value(
&HeaderValue::from_str("$server_addr").unwrap(),
&session,
&default_state,
);
assert_eq!(true, value.is_some());
assert_eq!("10.1.1.2", value.unwrap().to_str().unwrap());
let value = convert_header_value(
&HeaderValue::from_str("$server_port").unwrap(),
&session,
&default_state,
);
assert_eq!(true, value.is_some());
assert_eq!("6001", value.unwrap().to_str().unwrap());
let value = convert_header_value(
&HeaderValue::from_str("$upstream_addr").unwrap(),
&session,
&default_state,
);
assert_eq!(true, value.is_some());
assert_eq!("10.1.1.3:4123", value.unwrap().to_str().unwrap());
let value = convert_header_value(
&HeaderValue::from_str(":connection_id").unwrap(),
&session,
&default_state,
);
assert_eq!(true, value.is_some());
assert_eq!("102", value.unwrap().to_str().unwrap());
let session = new_test_session(
&["X-Forwarded-For: 1.1.1.1, 2.2.2.2"],
"/vicanso/pingap?size=1",
)
.await;
let value = convert_header_value(
&HeaderValue::from_str("$proxy_add_x_forwarded_for").unwrap(),
&session,
&Ctx {
conn: ConnectionInfo {
remote_addr: Some("10.1.1.1".to_string()),
..Default::default()
},
..Default::default()
},
);
assert_eq!(true, value.is_some());
assert_eq!(
"1.1.1.1, 2.2.2.2, 10.1.1.1",
value.unwrap().to_str().unwrap()
);
let session = new_test_session(&[""], "/vicanso/pingap?size=1").await;
let value = convert_header_value(
&HeaderValue::from_str("$proxy_add_x_forwarded_for").unwrap(),
&session,
&Ctx {
conn: ConnectionInfo {
remote_addr: Some("10.1.1.1".to_string()),
..Default::default()
},
..Default::default()
},
);
assert_eq!(true, value.is_some());
assert_eq!("10.1.1.1", value.unwrap().to_str().unwrap());
let session = new_test_session(&[""], "/vicanso/pingap?size=1").await;
let value = convert_header_value(
&HeaderValue::from_str("$upstream_addr").unwrap(),
&session,
&Ctx {
upstream: UpstreamInfo {
address: "10.1.1.1:8001".to_string(),
..Default::default()
},
..Default::default()
},
);
assert_eq!(true, value.is_some());
assert_eq!("10.1.1.1:8001", value.unwrap().to_str().unwrap());
let session = new_test_session(&[""], "/vicanso/pingap?size=1").await;
let mut ctx = Ctx::default();
for tag in ["$ja4", ":ja4"] {
let value = convert_header_value(
&HeaderValue::from_str(tag).unwrap(),
&session,
&ctx,
);
assert_eq!(true, value.is_none(), "{tag}");
}
ctx.conn.ja4 = Some(std::sync::Arc::new(
crate::Ja4Fingerprint::from_client_hello(
&crate::ja4::testing::spec_example_body(),
)
.unwrap(),
));
for tag in ["$ja4", ":ja4"] {
let value = convert_header_value(
&HeaderValue::from_str(tag).unwrap(),
&session,
&ctx,
);
assert_eq!(
"t13d1516h2_8daaf6152771_e5627efa2ab1",
value.unwrap().to_str().unwrap(),
"{tag}"
);
}
assert_eq!(
"$ja4",
resolve_static_header_value(HeaderValue::from_static("$ja4"))
);
let session = new_test_session(
&["Origin: https://github.com"],
"/vicanso/pingap?size=1",
)
.await;
let value = convert_header_value(
&HeaderValue::from_str("$http_origin").unwrap(),
&session,
&Ctx::default(),
);
assert_eq!(true, value.is_some());
assert_eq!("https://github.com", value.unwrap().to_str().unwrap());
let session = new_test_session(
&["Origin: https://github.com"],
"/vicanso/pingap?size=1",
)
.await;
let value = convert_header_value(
&HeaderValue::from_str("$hostname").unwrap(),
&session,
&Ctx::default(),
);
assert_eq!(true, value.is_some());
let session = new_test_session(
&["Origin: https://github.com"],
"/vicanso/pingap?size=1",
)
.await;
let value = convert_header_value(
&HeaderValue::from_str("$HOME").unwrap(),
&session,
&Ctx::default(),
);
assert_eq!(true, value.is_some());
let session = new_test_session(
&["Origin: https://github.com"],
"/vicanso/pingap?size=1",
)
.await;
let value = convert_header_value(
&HeaderValue::from_str("UUID").unwrap(),
&session,
&Ctx::default(),
);
assert_eq!(false, value.is_some());
}
#[tokio::test]
async fn test_get_host() {
let session =
new_test_session(&["Host: pingap.io"], "/vicanso/pingap?size=1")
.await;
assert_eq!(get_host(session.req_header()), Some("pingap.io"));
}
#[test]
fn test_remove_query_from_header() {
let mut req =
RequestHeader::build("GET", b"/?apikey=123", None).unwrap();
remove_query_from_header(&mut req, "apikey").unwrap();
assert_eq!("/", req.uri.to_string());
let mut req =
RequestHeader::build("GET", b"/?apikey=123&name=pingap", None)
.unwrap();
remove_query_from_header(&mut req, "apikey").unwrap();
assert_eq!("/?name=pingap", req.uri.to_string());
}
#[tokio::test]
async fn test_get_client_ip() {
let session = new_test_session(
&["X-Forwarded-For:192.168.1.1"],
"/vicanso/pingap?size=1",
)
.await;
assert_eq!(get_client_ip(&session), "192.168.1.1");
let session = new_test_session(
&["X-Real-Ip:192.168.1.2"],
"/vicanso/pingap?size=1",
)
.await;
assert_eq!(get_client_ip(&session), "192.168.1.2");
let claims = [
"Host: pingap.io",
"X-Forwarded-For: 192.168.1.1",
"X-Forwarded-Proto: https",
"X-Forwarded-Host: other.example",
];
let session = new_test_session(&claims, "/").await;
let mut ctx = Ctx::default();
ctx.conn.remote_addr = Some("10.1.1.1".to_string());
let resolve = |name: &'static str, session: &Session, ctx: &Ctx| {
convert_header_value(&HeaderValue::from_static(name), session, ctx)
.map(|value| value.to_str().unwrap().to_string())
};
assert_eq!(false, peer_is_trusted_proxy(&session));
ctx.conn.server_port = Some(8080);
for (name, expected) in [
("$client_ip", "10.1.1.1"),
("$forwarded_proto", "http"),
("$forwarded_host", "pingap.io"),
("$forwarded_port", "8080"),
] {
assert_eq!(
Some(expected.to_string()),
resolve(name, &session, &ctx),
"{name}"
);
}
ctx.conn.tls_version = Some(Cow::Borrowed("TLSv1.3"));
assert_eq!(
Some("https".to_string()),
resolve("$forwarded_proto", &session, &ctx)
);
for name in [
"$client_ip",
"$forwarded_proto",
"$forwarded_host",
"$forwarded_port",
] {
assert_eq!(
name,
resolve_static_header_value(HeaderValue::from_static(name))
);
}
let entry = |lines: &[&'static str]| {
let values: Vec<HeaderValue> = lines
.iter()
.map(|line| HeaderValue::from_static(line))
.collect();
first_forwarded_entry(values.iter()).map(str::to_string)
};
assert_eq!(Some("https".to_string()), entry(&["https, http"]));
assert_eq!(Some("https".to_string()), entry(&[" , https", "http"]));
assert_eq!(Some("https".to_string()), entry(&["https", "http"]));
assert_eq!(None, entry(&[]));
assert_eq!(None, entry(&[" , "]));
for host in ["shop.example.com", "shop.example.com:8443", "[::1]:80"] {
assert_eq!(true, is_host_name(host), "{host}");
}
for host in [
"",
"user@shop.example.com",
"shop.example.com/path",
"shop example.com",
"evil.example\r\nx: 1",
] {
assert_eq!(false, is_host_name(host), "{host}");
}
set_trusted_proxies(&Some(vec!["10.0.0.0/8".to_string()]));
let session = new_test_session(&claims, "/").await;
assert_eq!(false, peer_is_trusted_proxy(&session));
ctx.conn.tls_version = None;
assert_eq!(
Some("http".to_string()),
resolve("$forwarded_proto", &session, &ctx)
);
assert_eq!(
Some("pingap.io".to_string()),
resolve("$forwarded_host", &session, &ctx)
);
ctx.conn.client_ip = Some("203.0.113.9".to_string());
assert_eq!(
Some("203.0.113.9".to_string()),
resolve("$client_ip", &session, &ctx)
);
let session = new_test_session(
&["X-Forwarded-For:192.168.1.1"],
"/vicanso/pingap?size=1",
)
.await;
assert_ne!(get_client_ip(&session), "192.168.1.1");
set_trusted_proxies(&None);
let session = new_test_session(
&["X-Forwarded-For:192.168.1.1"],
"/vicanso/pingap?size=1",
)
.await;
assert_eq!(get_client_ip(&session), "192.168.1.1");
}
#[test]
fn test_forwarded_client_ip() {
let trusted = TrustedProxies::parse(&[
"10.0.0.0/8".to_string(),
"192.168.1.1".to_string(),
"fd00::/8".to_string(),
]);
let client_ip = |lines: &[&str]| {
let values: Vec<HeaderValue> = lines
.iter()
.map(|line| HeaderValue::from_str(line).unwrap())
.collect();
forwarded_client_ip(values.iter(), &trusted)
};
let ip = |value: &str| Some(value.to_string());
assert_eq!(ip("9.9.9.9"), client_ip(&["9.9.9.9"]));
assert_eq!(ip("9.9.9.9"), client_ip(&["6.6.6.6, 9.9.9.9"]));
assert_eq!(ip("9.9.9.9"), client_ip(&["6.6.6.6,10.1.1.1 , 9.9.9.9"]));
assert_eq!(
ip("9.9.9.9"),
client_ip(&["6.6.6.6, 9.9.9.9, 10.0.0.2, 192.168.1.1"])
);
assert_eq!(ip("2001:db8::1"), client_ip(&["2001:db8::1, fd00::2"]));
assert_eq!(
ip("9.9.9.9"),
client_ip(&["6.6.6.6", "9.9.9.9", "10.0.0.2"])
);
assert_eq!(ip("9.9.9.9"), client_ip(&["6.6.6.6, 9.9.9.9", "10.0.0.2"]));
assert_eq!(ip("10.0.0.3"), client_ip(&["10.0.0.3, 10.0.0.2"]));
assert_eq!(ip("9.9.9.9"), client_ip(&["9.9.9.9:4321, 10.0.0.2:80"]));
assert_eq!(
ip("2001:db8::1"),
client_ip(&["[2001:db8::1]:4321, [fd00::2]"])
);
assert_eq!(ip("unknown"), client_ip(&["6.6.6.6, unknown, 10.0.0.2"]));
assert_eq!(None, client_ip(&[]));
assert_eq!(None, client_ip(&[" , "]));
assert_eq!(
ip("9.9.9.9"),
client_ip(&["6.6.6.6, ::ffff:9.9.9.9, ::ffff:10.0.0.2"])
);
assert_eq!(true, trusted.contains("::ffff:10.1.2.3".parse().unwrap()));
assert_eq!(
true,
trusted.contains("::ffff:192.168.1.1".parse().unwrap())
);
assert_eq!(false, trusted.contains("::ffff:9.9.9.9".parse().unwrap()));
let mapped = TrustedProxies::parse(&["::ffff:172.16.0.1".to_string()]);
assert_eq!(true, mapped.contains("172.16.0.1".parse().unwrap()));
let line = HeaderValue::from_bytes(b"\xff\xfe, 9.9.9.9").unwrap();
assert_eq!(
ip("9.9.9.9"),
forwarded_client_ip([&line].into_iter(), &trusted)
);
let line = HeaderValue::from_bytes(b"6.6.6.6, \xff, 9.9.9.9, 10.0.0.2")
.unwrap();
assert_eq!(
ip("9.9.9.9"),
forwarded_client_ip([&line].into_iter(), &trusted)
);
}
#[tokio::test]
async fn test_get_header_value() {
let session =
new_test_session(&["Host: pingap.io"], "/vicanso/pingap?size=1")
.await;
assert_eq!(
get_req_header_value(session.req_header(), "Host"),
Some("pingap.io")
);
}
#[tokio::test]
async fn test_get_cookie_value() {
let session = new_test_session(
&["Cookie: name=pingap"],
"/vicanso/pingap?size=1",
)
.await;
assert_eq!(
get_cookie_value(session.req_header(), "name"),
Some("pingap")
);
}
#[tokio::test]
async fn test_get_query_value() {
let session = new_test_session(
&["X-Forwarded-For:192.168.1.1"],
"/vicanso/pingap?size=1",
)
.await;
assert_eq!(get_query_value(session.req_header(), "size"), Some("1"));
}
#[test]
fn test_convert_header_edge_cases() {
assert!(convert_header("").unwrap().is_none());
assert!(convert_header("no-colon").unwrap().is_none());
assert!(convert_header("Invalid Name: value").is_err());
assert!(convert_header("Valid-Name: invalid\r\nvalue").is_err());
}
#[test]
fn test_get_host_variants() {
let uri_string = "http://user:pass@authority.com/path";
let uri = http::Uri::from_str(uri_string).unwrap();
let mut req_with_authority =
RequestHeader::build("GET", b"/path", None).unwrap();
req_with_authority.set_uri(uri);
assert_eq!(get_host(&req_with_authority), Some("authority.com"));
let mut req_with_host_header =
RequestHeader::build("GET", b"/path", None).unwrap();
req_with_host_header
.insert_header("Host", "header-host.com:8080")
.unwrap();
assert_eq!(get_host(&req_with_host_header), Some("header-host.com"));
let req_no_host = RequestHeader::build("GET", b"/path", None).unwrap();
assert_eq!(get_host(&req_no_host), None);
for (host, expected) in [
("[::1]:8080", "[::1]"),
("[::1]", "[::1]"),
("[2001:db8::1]:443", "[2001:db8::1]"),
("example.com", "example.com"),
("example.com:8080", "example.com"),
] {
let mut req = RequestHeader::build("GET", b"/path", None).unwrap();
req.insert_header("Host", host).unwrap();
assert_eq!(get_host(&req), Some(expected), "{host}");
}
}
#[test]
fn test_get_host_drops_the_root_label() {
for (host, expected) in [
("admin.example.com.", "admin.example.com"),
("admin.example.com.:8443", "admin.example.com"),
("Admin.Example.com.", "Admin.Example.com"),
("admin.example.com", "admin.example.com"),
("admin.example.com..", "admin.example.com."),
(".", "."),
] {
let mut req = RequestHeader::build("GET", b"/path", None).unwrap();
req.insert_header("Host", host).unwrap();
assert_eq!(get_host(&req), Some(expected), "{host}");
}
let mut req = RequestHeader::build("GET", b"/", None).unwrap();
req.set_uri(http::Uri::from_static("https://admin.example.com./a"));
assert_eq!(get_host(&req), Some("admin.example.com"));
assert_eq!(get_request_host(&req), Some("admin.example.com."));
}
#[test]
fn test_cache_key_keeps_the_host_as_sent() {
let key = |host: &str| {
let mut req = RequestHeader::build("GET", b"/a", None).unwrap();
req.insert_header("Host", host).unwrap();
let ctx = Ctx {
cache: Some(Default::default()),
..Default::default()
};
format!("{:?}", crate::get_cache_key(&ctx, "GET", &req))
};
assert_ne!(key("example.com"), key("example.com."));
assert_eq!(key("example.com"), key("EXAMPLE.com:8080"));
}
#[test]
fn test_merge_cookie_headers() {
let cookies = |values: &[&str]| {
let mut req = RequestHeader::build("GET", b"/", None).unwrap();
for value in values {
req.append_header("cookie", *value).unwrap();
}
merge_cookie_headers(&mut req);
let merged: Vec<String> = req
.headers
.get_all(http::header::COOKIE)
.iter()
.map(|value| value.to_str().unwrap().to_string())
.collect();
let c = get_cookie_value(&req, "c").map(|v| v.to_string());
(merged, c)
};
assert_eq!(
(vec!["a=1; b=2; c=3".to_string()], Some("3".to_string())),
cookies(&["a=1", "b=2", "c=3"])
);
assert_eq!(
(vec!["a=1; b=2; c=3".to_string()], Some("3".to_string())),
cookies(&["a=1; b=2", "c=3"])
);
assert_eq!(
(vec!["a=1; c=3".to_string()], Some("3".to_string())),
cookies(&["a=1; c=3"])
);
assert_eq!((vec![], None), cookies(&[]));
}
#[tokio::test]
async fn test_proxy_add_x_forwarded_for_keeps_every_line() {
let session = new_test_session(
&["X-Forwarded-For: 6.6.6.6", "X-Forwarded-For: 10.0.0.9"],
"/",
)
.await;
let value = convert_header_value(
&HeaderValue::from_str("$proxy_add_x_forwarded_for").unwrap(),
&session,
&Ctx {
conn: ConnectionInfo {
remote_addr: Some("10.0.0.1".to_string()),
..Default::default()
},
..Default::default()
},
);
assert_eq!(
"6.6.6.6, 10.0.0.9, 10.0.0.1",
value.unwrap().to_str().unwrap()
);
}
#[test]
fn test_resolve_static_header_value() {
let resolve = |value: &str| {
resolve_static_header_value(HeaderValue::from_str(value).unwrap())
};
assert_eq!(get_hostname(), resolve("$hostname").to_str().unwrap());
let home = std::env::var("HOME").unwrap();
assert_eq!(home, resolve("$HOME").to_str().unwrap());
for value in [
"$host",
"$remote_addr",
"$proxy_add_x_forwarded_for",
"$http_origin",
":connection_id",
"plain",
"$PINGAP_TEST_UNSET_VARIABLE",
] {
assert_eq!(value, resolve(value).to_str().unwrap());
}
}
#[tokio::test]
async fn test_http_variable_with_underscores() {
let session = new_test_session(
&["User-Agent: pingap/1", "X-Env-Name: prod"],
"/",
)
.await;
for (variable, expected) in [
("$http_user_agent", Some("pingap/1")),
("$http_user-agent", Some("pingap/1")),
("$http_x_env_name", Some("prod")),
("$http_missing_header", None),
] {
let value = convert_header_value(
&HeaderValue::from_str(variable).unwrap(),
&session,
&Ctx::default(),
);
assert_eq!(
expected,
value.as_ref().and_then(|v| v.to_str().ok()),
"{variable}"
);
}
}
#[test]
fn test_get_cookie_value_advanced() {
let mut req = RequestHeader::build("GET", b"/", None).unwrap();
req.insert_header("Cookie", "id=123; session=abc; theme=dark")
.unwrap();
assert_eq!(get_cookie_value(&req, "session"), Some("abc"));
assert_eq!(get_cookie_value(&req, "id"), Some("123"));
assert_eq!(get_cookie_value(&req, "theme"), Some("dark"));
assert_eq!(get_cookie_value(&req, "lang"), None);
assert_eq!(get_cookie_value(&req, "the"), None);
}
#[test]
fn test_canonical_path() {
for path in ["/", "/users/x", "/a/b.c/d", "relative"] {
assert_eq!(
true,
matches!(canonical_path(path), Cow::Borrowed(_)),
"{path}"
);
}
for (path, expected) in [
("/%75sers/x", "/users/x"),
("/users/../other", "/other"),
("//users/./x/", "/users/x/"),
("/users;v=1/x", "/users/x"),
("/a%20b", "/a%20b"),
("/a%3fb", "/a%3Fb"),
("/a%23b", "/a%23b"),
("/a%2fb", "/a/b"),
("/100%25", "/100%25"),
("/%2561", "/%2561"),
("/caf%c3%a9", "/caf%C3%A9"), ("/%ff%00", "/%FF%00"),
] {
assert_eq!(expected, canonical_path(path), "{path}");
}
}
#[test]
fn test_has_dot_segments() {
for path in [
"/a/../b",
"/a/./b",
"/a/..",
"/a/%2e%2e/b",
"/a/%2E./b",
"/a/..;x=1/b",
"/a%2F..%2Fb",
"/a%5c..%5cb",
"/a\\..\\b",
"/a/.;/b",
] {
assert_eq!(true, has_dot_segments(path), "{path}");
}
for path in [
"/",
"/a/b",
"/a..b/c",
"/a/..b/c..",
"/a/.../b",
"/a%2Fb",
"/a;x=../b",
"/a/b;jsessionid=1",
"//a//b",
"/%2e%2ea/b",
] {
assert_eq!(false, has_dot_segments(path), "{path}");
}
}
#[test]
fn test_normalize_path() {
for path in [
"/",
"/admin",
"/admin/",
"/a/b.c/d",
"/.well-known/acme-challenge/token",
"/.env",
"/a/..b/c",
"/a/b../c",
"/a/...",
"*",
"",
] {
assert_eq!(
true,
matches!(normalize_path(path), Cow::Borrowed(value) if value == path),
"{path}"
);
}
for (path, expected) in [
("/%61dmin", "/admin"),
("/%61dmin/users", "/admin/users"),
("//admin", "/admin"),
("/./admin", "/admin"),
("/public/../admin", "/admin"),
("/public/%2e%2e/admin", "/admin"),
("/public/%2E%2E%2Fadmin", "/admin"),
("/public/..%2fadmin/x", "/admin/x"),
("/../../admin", "/admin"),
("/..", "/"),
("/.", "/"),
("//", "/"),
("/admin//", "/admin/"),
("/admin/.", "/admin/"),
("/admin/x/..", "/admin/"),
("/admin/x/../", "/admin/"),
("/%2561dmin", "/%61dmin"),
("/100%", "/100%"),
("/a%zzb", "/a%zzb"),
("/a%4", "/a%4"),
("/%E6%96%87%E6%A1%A3/menu", "/文档/menu"),
("/a%20b", "/a b"),
("/public/..;/admin", "/admin"),
("/public/..;x=1/admin", "/admin"),
("/public/.;/admin", "/public/admin"),
("/public/%2e%2e%3b/admin", "/admin"),
("/api;v=1/admin", "/api/admin"),
("/admin;jsessionid=A1/users", "/admin/users"),
("/login;jsessionid=A1", "/login"),
("/a/;x/b", "/a/b"),
("/a/b;x/", "/a/b/"),
("/a/;x", "/a/"),
("/public\\..\\admin", "/admin"),
("/public/..%5cadmin", "/admin"),
("/a\\b", "/a/b"),
("/a\\\\b\\", "/a/b/"),
] {
assert_eq!(expected, normalize_path(path), "{path}");
}
assert_eq!("/\u{fffd}/x", normalize_path("/%ff/x"));
}
#[test]
fn test_path_and_query_changes_keep_the_authority() {
let mut req = RequestHeader::build("GET", b"/", None).unwrap();
req.set_uri(http::Uri::from_static(
"https://example.com/api/users?apikey=1&page=2",
));
remove_query_from_header(&mut req, "apikey").unwrap();
assert_eq!("https://example.com/api/users?page=2", req.uri.to_string());
assert_eq!(Some("example.com"), get_host(&req));
set_path_and_query(&mut req, "/users?page=2").unwrap();
assert_eq!("https://example.com/users?page=2", req.uri.to_string());
set_path_and_query(&mut req, "/").unwrap();
assert_eq!("https://example.com/", req.uri.to_string());
assert_eq!(true, set_path_and_query(&mut req, "/a b").is_err());
assert_eq!("https://example.com/", req.uri.to_string());
let mut req = RequestHeader::build("GET", b"/api?a=1", None).unwrap();
set_path_and_query(&mut req, "/v2/api?a=1").unwrap();
assert_eq!("/v2/api?a=1", req.uri.to_string());
}
#[test]
fn test_remove_query_from_header_variants() {
let mut req =
RequestHeader::build("GET", b"/path?key=val", None).unwrap();
remove_query_from_header(&mut req, "key").unwrap();
assert_eq!(req.uri.to_string(), "/path");
let mut req =
RequestHeader::build("GET", b"/path?key1=val1&key2=val2", None)
.unwrap();
remove_query_from_header(&mut req, "key1").unwrap();
assert_eq!(req.uri.to_string(), "/path?key2=val2");
let mut req =
RequestHeader::build("GET", b"/path?key1=val1&key2=val2", None)
.unwrap();
remove_query_from_header(&mut req, "key2").unwrap();
assert_eq!(req.uri.to_string(), "/path?key1=val1");
let mut req =
RequestHeader::build("GET", b"/path?key1=v1&key2=v2&key3=v3", None)
.unwrap();
remove_query_from_header(&mut req, "key2").unwrap();
assert_eq!(req.uri.to_string(), "/path?key1=v1&key3=v3");
let mut req =
RequestHeader::build("GET", b"/path?key=val", None).unwrap();
remove_query_from_header(&mut req, "nonexistent").unwrap();
assert_eq!(req.uri.to_string(), "/path?key=val");
let mut req = RequestHeader::build("GET", b"/path", None).unwrap();
remove_query_from_header(&mut req, "key").unwrap();
assert_eq!(req.uri.to_string(), "/path");
}
}