1use 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::net::{IpAddr, SocketAddr};
28use std::str::FromStr;
29use std::sync::Arc;
30use std::sync::atomic::{AtomicBool, Ordering};
31
32const HTTP_HEADER_X_FORWARDED_FOR: &str = "x-forwarded-for";
34const HTTP_HEADER_X_REAL_IP: &str = "x-real-ip";
35
36pub const HOST_NAME_TAG: &[u8] = b"$hostname";
39const HOST_TAG: &[u8] = b"$host";
40const SCHEME_TAG: &[u8] = b"$scheme";
41const REMOTE_ADDR_TAG: &[u8] = b"$remote_addr";
42const REMOTE_PORT_TAG: &[u8] = b"$remote_port";
43const SERVER_ADDR_TAG: &[u8] = b"$server_addr";
44const SERVER_PORT_TAG: &[u8] = b"$server_port";
45const PROXY_ADD_FORWARDED_TAG: &[u8] = b"$proxy_add_x_forwarded_for";
46const UPSTREAM_ADDR_TAG: &[u8] = b"$upstream_addr";
47const JA4_TAG: &[u8] = b"$ja4";
48const CLIENT_IP_TAG: &[u8] = b"$client_ip";
49const FORWARDED_PROTO_TAG: &[u8] = b"$forwarded_proto";
50const FORWARDED_HOST_TAG: &[u8] = b"$forwarded_host";
51const FORWARDED_PORT_TAG: &[u8] = b"$forwarded_port";
52const TLS_CLIENT_SUBJECT_TAG: &[u8] = b"$tls_client_subject";
53const TLS_CLIENT_FINGERPRINT_TAG: &[u8] = b"$tls_client_fingerprint";
54const TLS_CLIENT_SERIAL_TAG: &[u8] = b"$tls_client_serial";
55const TLS_CLIENT_VERIFIED_TAG: &[u8] = b"$tls_client_verified";
56
57static SCHEME_HTTPS: HeaderValue = HeaderValue::from_static("https");
59static SCHEME_HTTP: HeaderValue = HeaderValue::from_static("http");
60
61#[derive(Debug, Snafu)]
63pub enum Error {
64 #[snafu(display("invalid header value: {value} - {source}"))]
66 InvalidHeaderValue {
67 value: String,
68 source: header::InvalidHeaderValue,
69 },
70 #[snafu(display("invalid header name: {value} - {source}"))]
72 InvalidHeaderName {
73 value: String,
74 source: header::InvalidHeaderName,
75 },
76}
77type Result<T, E = Error> = std::result::Result<T, E>;
79
80pub type HttpHeader = (HeaderName, HeaderValue);
82
83pub fn get_host(header: &RequestHeader) -> Option<&str> {
88 get_request_host(header).map(strip_root_label)
89}
90
91pub fn get_request_host(header: &RequestHeader) -> Option<&str> {
100 match header.uri.host() {
103 Some(host) => Some(host),
104 None => header
106 .headers
107 .get(http::header::HOST)
108 .and_then(|value| value.to_str().ok())
110 .map(strip_port),
111 }
112}
113
114#[inline]
119pub fn strip_root_label(host: &str) -> &str {
120 match host.strip_suffix('.') {
121 Some(name) if !name.is_empty() => name,
122 _ => host,
123 }
124}
125
126pub fn merge_cookie_headers(header: &mut RequestHeader) {
135 let mut values = header.headers.get_all(http::header::COOKIE).iter();
136 let (Some(first), Some(second)) = (values.next(), values.next()) else {
137 return;
138 };
139 let mut merged = Vec::with_capacity(
140 first.len() + second.len() + 2 + values.size_hint().0 * 16,
141 );
142 merged.extend_from_slice(first.as_bytes());
143 for value in std::iter::once(second).chain(values) {
144 merged.extend_from_slice(b"; ");
145 merged.extend_from_slice(value.as_bytes());
146 }
147 if let Ok(value) = HeaderValue::from_bytes(&merged) {
148 let _ = header.insert_header(http::header::COOKIE, value);
149 }
150}
151
152fn strip_port(host: &str) -> &str {
156 if host.starts_with('[') {
157 return match host.find(']') {
158 Some(end) => &host[..=end],
159 None => host,
160 };
161 }
162 host.split(':').next().unwrap_or(host)
163}
164
165pub fn convert_header(value: &str) -> Result<Option<HttpHeader>> {
170 value
172 .split_once(':')
173 .map(|(k, v)| {
175 let name = HeaderName::from_str(k.trim())
177 .context(InvalidHeaderNameSnafu { value: k })?;
178 let value = HeaderValue::from_str(v.trim())
180 .context(InvalidHeaderValueSnafu { value: v })?;
181 Ok(Some((name, value)))
183 })
184 .unwrap_or(Ok(None))
186}
187
188pub fn convert_headers(header_values: &[String]) -> Result<Vec<HttpHeader>> {
193 header_values
194 .iter()
195 .filter_map(|item| convert_header(item).transpose())
198 .collect()
200}
201
202pub static HTTP_HEADER_NO_STORE: HttpHeader = (
204 header::CACHE_CONTROL,
205 HeaderValue::from_static("private, no-store"),
206);
207pub static HTTP_HEADER_NO_CACHE: HttpHeader = (
208 header::CACHE_CONTROL,
209 HeaderValue::from_static("private, no-cache"),
210);
211pub static HTTP_HEADER_CONTENT_JSON: HttpHeader = (
212 header::CONTENT_TYPE,
213 HeaderValue::from_static("application/json; charset=utf-8"),
214);
215pub static HTTP_HEADER_CONTENT_HTML: HttpHeader = (
216 header::CONTENT_TYPE,
217 HeaderValue::from_static("text/html; charset=utf-8"),
218);
219pub static HTTP_HEADER_CONTENT_TEXT: HttpHeader = (
220 header::CONTENT_TYPE,
221 HeaderValue::from_static("text/plain; charset=utf-8"),
222);
223pub static HTTP_HEADER_TRANSFER_CHUNKED: HttpHeader = (
224 header::TRANSFER_ENCODING,
225 HeaderValue::from_static("chunked"),
226);
227pub static HTTP_HEADER_NAME_X_REQUEST_ID: HeaderName =
228 HeaderName::from_static("x-request-id");
229
230pub fn resolve_static_header_value(value: HeaderValue) -> HeaderValue {
238 let buf = value.as_bytes();
239 if buf.first() != Some(&b'$') {
240 return value;
241 }
242 if buf == HOST_NAME_TAG {
243 return HeaderValue::from_str(get_hostname()).unwrap_or(value);
244 }
245 let request_tags: [&[u8]; 17] = [
246 HOST_TAG,
247 SCHEME_TAG,
248 REMOTE_ADDR_TAG,
249 REMOTE_PORT_TAG,
250 SERVER_ADDR_TAG,
251 SERVER_PORT_TAG,
252 PROXY_ADD_FORWARDED_TAG,
253 UPSTREAM_ADDR_TAG,
254 JA4_TAG,
255 CLIENT_IP_TAG,
256 FORWARDED_PROTO_TAG,
257 FORWARDED_HOST_TAG,
258 FORWARDED_PORT_TAG,
259 TLS_CLIENT_SUBJECT_TAG,
260 TLS_CLIENT_FINGERPRINT_TAG,
261 TLS_CLIENT_SERIAL_TAG,
262 TLS_CLIENT_VERIFIED_TAG,
263 ];
264 if request_tags.contains(&buf) || buf.starts_with(b"$http_") {
265 return value;
266 }
267 let Ok(name) = std::str::from_utf8(&buf[1..]) else {
268 return value;
269 };
270 match std::env::var(name) {
271 Ok(env_value) => HeaderValue::from_str(&env_value).unwrap_or(value),
272 Err(_) => value,
273 }
274}
275
276fn tls_client_value(
279 ctx: &Ctx,
280 field: impl Fn(&crate::TlsClientCert) -> &str,
281) -> HeaderValue {
282 ctx.conn
283 .tls_client_cert
284 .as_deref()
285 .and_then(|cert| HeaderValue::from_str(field(cert)).ok())
286 .unwrap_or_else(|| HeaderValue::from_static(""))
287}
288
289#[inline]
292pub fn convert_header_value(
293 value: &HeaderValue,
294 session: &Session,
295 ctx: &Ctx,
296) -> Option<HeaderValue> {
297 let buf = value.as_bytes();
299
300 if buf.is_empty() || !(buf[0] == b'$' || buf[0] == b':') {
303 return None;
304 }
305
306 let to_header_value = |s: &str| HeaderValue::from_str(s).ok();
308
309 match buf {
311 HOST_TAG => get_host(session.req_header()).and_then(to_header_value),
312 SCHEME_TAG => Some(if ctx.conn.tls_version.is_some() {
313 SCHEME_HTTPS.clone()
314 } else {
315 SCHEME_HTTP.clone()
316 }),
317 HOST_NAME_TAG => to_header_value(get_hostname()),
318 REMOTE_ADDR_TAG => {
319 ctx.conn.remote_addr.as_deref().and_then(to_header_value)
320 },
321 REMOTE_PORT_TAG => ctx.conn.remote_port.and_then(|p| {
322 HeaderValue::from_str(itoa::Buffer::new().format(p)).ok()
325 }),
326 SERVER_ADDR_TAG => {
327 ctx.conn.server_addr.as_deref().and_then(to_header_value)
328 },
329 SERVER_PORT_TAG => ctx.conn.server_port.and_then(|p| {
330 HeaderValue::from_str(itoa::Buffer::new().format(p)).ok()
331 }),
332 UPSTREAM_ADDR_TAG => {
333 if !ctx.upstream.address.is_empty() {
334 to_header_value(&ctx.upstream.address)
335 } else {
336 None
337 }
338 },
339 JA4_TAG => ctx
340 .conn
341 .ja4
342 .as_deref()
343 .and_then(|fingerprint| to_header_value(fingerprint.ja4())),
344 CLIENT_IP_TAG => {
349 if has_trusted_proxies() {
350 match ctx.conn.client_ip.as_deref() {
351 Some(client_ip) => to_header_value(client_ip),
352 None => to_header_value(&get_client_ip(session)),
353 }
354 } else {
355 ctx.conn.remote_addr.as_deref().and_then(to_header_value)
356 }
357 },
358 FORWARDED_PROTO_TAG => Some(if client_used_https(session, ctx) {
362 SCHEME_HTTPS.clone()
363 } else {
364 SCHEME_HTTP.clone()
365 }),
366 FORWARDED_HOST_TAG => {
369 forwarded_by_trusted_proxy(session, &HTTP_HEADER_X_FORWARDED_HOST)
370 .filter(|host| is_host_name(host))
371 .and_then(to_header_value)
372 .or_else(|| {
373 get_host(session.req_header()).and_then(to_header_value)
374 })
375 },
376 TLS_CLIENT_SUBJECT_TAG => {
382 Some(tls_client_value(ctx, |cert| cert.subject.as_str()))
383 },
384 TLS_CLIENT_FINGERPRINT_TAG => {
385 Some(tls_client_value(ctx, |cert| cert.fingerprint.as_str()))
386 },
387 TLS_CLIENT_SERIAL_TAG => {
388 Some(tls_client_value(ctx, |cert| cert.serial.as_str()))
389 },
390 TLS_CLIENT_VERIFIED_TAG => Some(HeaderValue::from_static(
391 if ctx.conn.tls_client_cert.is_some() {
392 "true"
393 } else {
394 "false"
395 },
396 )),
397 FORWARDED_PORT_TAG => {
401 let forwarded = forwarded_by_trusted_proxy(
402 session,
403 &HTTP_HEADER_X_FORWARDED_PORT,
404 )
405 .and_then(|port| port.parse::<u16>().ok())
406 .filter(|port| *port > 0)
407 .or_else(|| {
408 forwarded_scheme(session)
409 .map(|https| if https { 443 } else { 80 })
410 })
411 .or(ctx.conn.server_port);
412 forwarded.and_then(|port| {
413 HeaderValue::from_str(itoa::Buffer::new().format(port)).ok()
414 })
415 },
416 PROXY_ADD_FORWARDED_TAG => {
417 ctx.conn.remote_addr.as_deref().and_then(|remote_addr| {
418 let existing = session
424 .req_header()
425 .headers
426 .get_all(HTTP_HEADER_X_FORWARDED_FOR);
427 let capacity = existing
428 .iter()
429 .map(|v| v.as_bytes().len() + 2)
430 .sum::<usize>()
431 + remote_addr.len();
432 let mut value_buf = BytesMut::with_capacity(capacity);
433 for value in existing.iter() {
434 value_buf.extend_from_slice(value.as_bytes());
435 value_buf.extend_from_slice(b", ");
436 }
437 value_buf.extend_from_slice(remote_addr.as_bytes());
438 HeaderValue::from_bytes(&value_buf).ok()
439 })
440 },
441 _ => handle_special_headers(buf, session, ctx),
443 }
444}
445
446#[inline]
449fn handle_special_headers(
450 buf: &[u8],
451 session: &Session,
452 ctx: &Ctx,
453) -> Option<HeaderValue> {
454 if let Some(name) = buf.strip_prefix(b"$http_") {
456 let key = std::str::from_utf8(name).ok()?;
457 if key.contains('_') {
460 return session.get_header(key.replace('_', "-")).cloned();
461 }
462 return session.get_header(key).cloned();
463 }
464 if buf.starts_with(b"$") {
466 let var_name = std::str::from_utf8(&buf[1..]).ok()?;
467 return std::env::var(var_name)
469 .ok()
470 .and_then(|v| HeaderValue::from_str(&v).ok());
471 }
472 if buf.starts_with(b":") {
474 let key = std::str::from_utf8(&buf[1..]).ok()?;
475 let mut value = BytesMut::with_capacity(20);
477 ctx.append_log_value(&mut value, key);
478 if !value.is_empty() {
479 return HeaderValue::from_bytes(&value).ok();
481 }
482 }
483 None
485}
486
487pub fn get_remote_addr(session: &Session) -> Option<(String, u16)> {
494 session
495 .client_addr()
496 .and_then(|addr| addr.as_inet())
498 .map(|addr| (addr.ip().to_canonical().to_string(), addr.port()))
500}
501
502struct TrustedProxies {
505 nets: Vec<IpNet>,
506 ips: AHashSet<IpAddr>,
507}
508
509impl TrustedProxies {
510 fn parse(values: &[String]) -> Self {
513 let mut nets = Vec::new();
514 let mut ips = AHashSet::new();
515 for item in values {
516 if let Ok(net) = IpNet::from_str(item) {
517 nets.push(net);
518 } else if let Ok(ip) = IpAddr::from_str(item) {
519 ips.insert(ip.to_canonical());
520 }
521 }
522 Self { nets, ips }
523 }
524
525 fn contains(&self, peer: IpAddr) -> bool {
527 let peer = peer.to_canonical();
528 self.ips.contains(&peer)
529 || self.nets.iter().any(|net| net.contains(&peer))
530 }
531}
532
533static TRUSTED_PROXIES_ENABLED: AtomicBool = AtomicBool::new(false);
541static TRUSTED_PROXIES: ArcSwapOption<TrustedProxies> =
542 ArcSwapOption::const_empty();
543
544static HTTP_HEADER_X_FORWARDED_PROTO: HeaderName =
545 HeaderName::from_static("x-forwarded-proto");
546static HTTP_HEADER_X_FORWARDED_HOST: HeaderName =
547 HeaderName::from_static("x-forwarded-host");
548static HTTP_HEADER_X_FORWARDED_PORT: HeaderName =
549 HeaderName::from_static("x-forwarded-port");
550
551pub fn peer_is_trusted_proxy(session: &Session) -> bool {
555 if !has_trusted_proxies() {
556 return false;
557 }
558 let Some(peer) = session
559 .client_addr()
560 .and_then(|addr| addr.as_inet())
561 .map(|addr| addr.ip())
562 else {
563 return false;
564 };
565 TRUSTED_PROXIES
566 .load()
567 .as_ref()
568 .is_some_and(|trusted| trusted.contains(peer))
569}
570
571fn first_forwarded_entry<'a>(
576 values: impl Iterator<Item = &'a HeaderValue>,
577) -> Option<&'a str> {
578 values
579 .filter_map(|value| value.to_str().ok())
580 .flat_map(|value| value.split(','))
581 .map(str::trim)
582 .find(|entry| !entry.is_empty())
583}
584
585fn forwarded_by_trusted_proxy<'a>(
593 session: &'a Session,
594 name: &HeaderName,
595) -> Option<&'a str> {
596 let entry = first_forwarded_entry(
597 session.req_header().headers.get_all(name).iter(),
598 )?;
599 peer_is_trusted_proxy(session).then_some(entry)
600}
601
602fn forwarded_scheme(session: &Session) -> Option<bool> {
606 let scheme =
607 forwarded_by_trusted_proxy(session, &HTTP_HEADER_X_FORWARDED_PROTO)?;
608 if scheme.eq_ignore_ascii_case("https") {
609 Some(true)
610 } else if scheme.eq_ignore_ascii_case("http") {
611 Some(false)
612 } else {
613 None
614 }
615}
616
617pub fn client_used_https(session: &Session, ctx: &Ctx) -> bool {
621 forwarded_scheme(session).unwrap_or(ctx.conn.tls_version.is_some())
622}
623
624fn is_host_name(value: &str) -> bool {
627 value
628 .parse::<http::uri::Authority>()
629 .is_ok_and(|authority| {
630 !authority.host().is_empty() && !authority.as_str().contains('@')
631 })
632}
633
634pub fn set_trusted_proxies(proxies: &Option<Vec<String>>) {
641 let parsed = match proxies {
642 Some(list) if !list.is_empty() => Some(TrustedProxies::parse(list)),
643 _ => None,
644 };
645 let enabled = parsed.is_some();
646 TRUSTED_PROXIES.store(parsed.map(Arc::new));
647 TRUSTED_PROXIES_ENABLED.store(enabled, Ordering::Relaxed);
649}
650
651fn parse_forwarded_ip(value: &str) -> Option<IpAddr> {
654 let ip = if let Ok(ip) = IpAddr::from_str(value) {
655 ip
656 } else if let Ok(addr) = SocketAddr::from_str(value) {
657 addr.ip()
658 } else {
659 value
660 .strip_prefix('[')
661 .and_then(|value| value.strip_suffix(']'))
662 .and_then(|value| IpAddr::from_str(value).ok())?
663 };
664 Some(ip.to_canonical())
666}
667
668fn forwarded_client_ip<'a>(
681 values: impl DoubleEndedIterator<Item = &'a HeaderValue>,
682 trusted: &TrustedProxies,
683) -> Option<String> {
684 let mut first_proxy = None;
685 for value in values.rev() {
686 for item in value.as_bytes().rsplit(|b| *b == b',') {
691 let item = item.trim_ascii();
692 if item.is_empty() {
693 continue;
694 }
695 let Ok(item) = std::str::from_utf8(item) else {
697 return Some(String::from_utf8_lossy(item).into_owned());
698 };
699 match parse_forwarded_ip(item) {
700 Some(ip) if trusted.contains(ip) => first_proxy = Some(ip),
701 Some(ip) => return Some(ip.to_string()),
702 None => return Some(item.to_string()),
703 }
704 }
705 }
706 first_proxy.map(|ip| ip.to_string())
707}
708
709#[inline]
711pub fn has_trusted_proxies() -> bool {
712 TRUSTED_PROXIES_ENABLED.load(Ordering::Relaxed)
713}
714
715pub fn is_trusted_proxy(ip: IpAddr) -> bool {
719 if !has_trusted_proxies() {
720 return false;
721 }
722 TRUSTED_PROXIES
723 .load()
724 .as_ref()
725 .is_some_and(|trusted| trusted.contains(ip.to_canonical()))
726}
727
728#[inline]
736pub fn ensure_verified_client_ip<'a>(
737 session: &Session,
738 ctx: &'a mut Ctx,
739) -> &'a str {
740 if has_trusted_proxies() {
741 return ensure_client_ip(session, ctx);
742 }
743 ctx.conn.remote_addr.as_deref().unwrap_or_default()
744}
745
746#[inline]
751pub fn ensure_client_ip<'a>(session: &Session, ctx: &'a mut Ctx) -> &'a str {
752 if ctx.conn.client_ip.is_none() {
753 ctx.conn.client_ip = Some(get_client_ip(session));
754 }
755 ctx.conn.client_ip.as_deref().unwrap_or_default()
757}
758
759pub fn get_client_ip(session: &Session) -> String {
772 if TRUSTED_PROXIES_ENABLED.load(Ordering::Relaxed) {
775 let Some(peer) = session
778 .client_addr()
779 .and_then(|addr| addr.as_inet())
780 .map(|addr| addr.ip().to_canonical())
781 else {
782 return String::new();
783 };
784 let trusted = TRUSTED_PROXIES.load();
785 let Some(trusted) =
786 trusted.as_ref().filter(|trusted| trusted.contains(peer))
787 else {
788 return peer.to_string();
789 };
790 let headers = &session.req_header().headers;
791 if let Some(ip) = forwarded_client_ip(
792 headers.get_all(HTTP_HEADER_X_FORWARDED_FOR).iter(),
793 trusted,
794 ) {
795 return ip;
796 }
797 if let Some(ip) = headers
798 .get(HTTP_HEADER_X_REAL_IP)
799 .and_then(|value| value.to_str().ok())
800 .map(|value| value.trim())
801 .filter(|value| !value.is_empty())
802 {
803 return ip.to_string();
804 }
805 return peer.to_string();
806 }
807 if let Some(value) = session.get_header(HTTP_HEADER_X_FORWARDED_FOR) {
809 if let Ok(s) = value.to_str()
811 && let Some(ip) = s.split(',').next()
812 {
813 let trimmed_ip = ip.trim();
814 if !trimmed_ip.is_empty() {
815 return trimmed_ip.to_string();
816 }
817 }
818 }
819 if let Some(value) = session.get_header(HTTP_HEADER_X_REAL_IP) {
821 return value.to_str().unwrap_or_default().to_string();
822 }
823 if let Some((addr, _)) = get_remote_addr(session) {
825 return addr;
826 }
827 "".to_string()
829}
830
831pub fn get_req_header_value<'a>(
833 req_header: &'a RequestHeader,
834 key: &str,
835) -> Option<&'a str> {
836 if let Some(value) = req_header.headers.get(key) {
838 if let Ok(value) = value.to_str() {
840 return Some(value);
841 }
842 }
843 None
844}
845
846pub fn get_cookie_value<'a>(
848 req_header: &'a RequestHeader,
849 cookie_name: &str,
850) -> Option<&'a str> {
851 get_req_header_value(req_header, "cookie")?
853 .split(';')
855 .find_map(|item| {
857 item.trim()
860 .strip_prefix(cookie_name)?
861 .strip_prefix('=')
862 .or_else(|| {
863 let (k, v) = item.split_once('=')?;
865 if k.trim() == cookie_name {
866 Some(v.trim())
867 } else {
868 None
869 }
870 })
871 })
872}
873
874pub fn get_query_value<'a>(
876 req_header: &'a RequestHeader,
877 name: &str,
878) -> Option<&'a str> {
879 req_header
881 .uri
882 .query()?
883 .split('&')
885 .find_map(|item| {
887 let (k, v) = item.split_once('=')?;
889 if k == name { Some(v) } else { None }
891 })
892}
893
894pub fn decode_query_value(raw: &str) -> Cow<'_, str> {
899 if !raw.contains('%') {
900 return Cow::Borrowed(raw);
901 }
902 urlencoding::decode(raw).unwrap_or(Cow::Borrowed(raw))
903}
904
905pub fn remove_query_from_header(
909 req_header: &mut RequestHeader,
910 name: &str,
911) -> Result<(), http::Error> {
912 let Some(query_str) = req_header.uri.query() else {
914 return Ok(());
915 };
916
917 let mut new_query = String::with_capacity(query_str.len());
920
921 for item in query_str.split('&') {
923 let key = item.split('=').next().unwrap_or(item);
925
926 if key != name {
928 if !new_query.is_empty() {
930 new_query.push('&');
931 }
932 new_query.push_str(item);
934 }
935 }
936
937 let path = req_header.uri.path();
939 let mut path_and_query =
940 String::with_capacity(path.len() + 1 + new_query.len());
941 path_and_query.push_str(path);
942 if !new_query.is_empty() {
943 path_and_query.push('?');
944 path_and_query.push_str(&new_query);
945 }
946
947 set_path_and_query(req_header, &path_and_query)
948}
949
950fn path_needs_normalizing(path: &[u8]) -> bool {
954 let mut after_slash = false;
955 for (index, byte) in path.iter().enumerate() {
956 match byte {
957 b'%' | b';' | b'\\' => return true,
958 b'/' if after_slash => return true,
959 b'.' if after_slash => {
960 let rest = &path[index + 1..];
961 let rest = rest.strip_prefix(b".").unwrap_or(rest);
962 if rest.first().is_none_or(|next| *next == b'/') {
963 return true;
964 }
965 },
966 _ => {},
967 }
968 after_slash = *byte == b'/';
969 }
970 false
971}
972
973pub fn normalize_path(path: &str) -> Cow<'_, str> {
995 match normalized_path_bytes(path) {
996 None => Cow::Borrowed(path),
997 Some(normalized) => {
998 Cow::Owned(String::from_utf8_lossy(&normalized).into_owned())
999 },
1000 }
1001}
1002
1003pub fn canonical_path(path: &str) -> Cow<'_, str> {
1021 let Some(normalized) = normalized_path_bytes(path) else {
1022 return Cow::Borrowed(path);
1023 };
1024 let mut encoded = String::with_capacity(normalized.len() + 8);
1025 for byte in normalized {
1026 let plain = byte.is_ascii_alphanumeric()
1029 || matches!(
1030 byte,
1031 b'/' | b'-'
1032 | b'.'
1033 | b'_'
1034 | b'~'
1035 | b'!'
1036 | b'$'
1037 | b'&'
1038 | b'\''
1039 | b'('
1040 | b')'
1041 | b'*'
1042 | b'+'
1043 | b','
1044 | b';'
1045 | b'='
1046 | b':'
1047 | b'@'
1048 );
1049 if plain {
1050 encoded.push(byte as char);
1051 } else {
1052 encoded.push('%');
1053 encoded.push(char::from(b"0123456789ABCDEF"[(byte >> 4) as usize]));
1054 encoded.push(char::from(b"0123456789ABCDEF"[(byte & 15) as usize]));
1055 }
1056 }
1057 Cow::Owned(encoded)
1058}
1059
1060pub fn has_dot_segments(path: &str) -> bool {
1069 normalized_path(path).is_some_and(|(_, dot_segments)| dot_segments)
1070}
1071
1072fn normalized_path_bytes(path: &str) -> Option<Vec<u8>> {
1075 normalized_path(path).map(|(bytes, _)| bytes)
1076}
1077
1078fn normalized_path(path: &str) -> Option<(Vec<u8>, bool)> {
1081 let bytes = path.as_bytes();
1082 if !path.starts_with('/') || !path_needs_normalizing(bytes) {
1083 return None;
1084 }
1085 let hex = |byte: Option<&u8>| {
1086 byte.and_then(|byte| (*byte as char).to_digit(16))
1087 .map(|value| value as u8)
1088 };
1089 let mut decoded = Vec::with_capacity(bytes.len());
1090 let mut index = 0;
1091 while index < bytes.len() {
1092 if bytes[index] == b'%'
1093 && let (Some(high), Some(low)) =
1094 (hex(bytes.get(index + 1)), hex(bytes.get(index + 2)))
1095 {
1096 decoded.push(high << 4 | low);
1097 index += 3;
1098 } else {
1099 decoded.push(bytes[index]);
1100 index += 1;
1101 }
1102 }
1103
1104 let mut segments: Vec<&[u8]> = vec![];
1105 let mut dot_segments = false;
1106 let mut trailing_slash = false;
1108 for segment in decoded.split(|byte| matches!(byte, b'/' | b'\\')) {
1109 trailing_slash = true;
1110 let name = segment
1112 .split(|byte| *byte == b';')
1113 .next()
1114 .unwrap_or(segment);
1115 match name {
1116 b"" => {},
1117 b"." => dot_segments = true,
1118 b".." => {
1119 dot_segments = true;
1120 segments.pop();
1121 },
1122 _ => {
1123 segments.push(name);
1124 trailing_slash = false;
1125 },
1126 }
1127 }
1128 let mut normalized = Vec::with_capacity(decoded.len());
1129 for segment in segments.iter() {
1130 normalized.push(b'/');
1131 normalized.extend_from_slice(segment);
1132 }
1133 if trailing_slash || normalized.is_empty() {
1134 normalized.push(b'/');
1135 }
1136 Some((normalized, dot_segments))
1137}
1138
1139pub fn set_path_and_query(
1147 req_header: &mut RequestHeader,
1148 path_and_query: &str,
1149) -> Result<(), http::Error> {
1150 if req_header.uri.authority().is_none() {
1151 req_header.set_uri(http::Uri::from_str(path_and_query)?);
1152 return Ok(());
1153 }
1154 let path_and_query = http::uri::PathAndQuery::from_str(path_and_query)?;
1155 let mut parts = req_header.uri.clone().into_parts();
1156 parts.path_and_query = Some(path_and_query);
1157 req_header.set_uri(http::Uri::from_parts(parts)?);
1161
1162 Ok(())
1163}
1164
1165pub fn protect_from_connection_header(
1179 req: &mut RequestHeader,
1180 names: &[HeaderName],
1181) {
1182 if names.is_empty() || !req.headers.contains_key(header::CONNECTION) {
1183 return;
1184 }
1185 let nominated = |token: &str| {
1186 names
1187 .iter()
1188 .any(|name| name.as_str().eq_ignore_ascii_case(token))
1189 };
1190 let mut kept = vec![];
1191 let mut removed = false;
1192 for value in req.headers.get_all(header::CONNECTION).iter() {
1193 let Ok(value) = value.to_str() else {
1195 return;
1196 };
1197 for token in value.split(',').map(str::trim) {
1198 if token.is_empty() {
1199 continue;
1200 }
1201 if nominated(token) {
1202 removed = true;
1203 } else {
1204 kept.push(token);
1205 }
1206 }
1207 }
1208 if !removed {
1209 return;
1210 }
1211 let kept = kept.join(", ");
1212 req.remove_header(&header::CONNECTION);
1213 if !kept.is_empty() {
1214 let _ = req.insert_header(header::CONNECTION, kept);
1215 }
1216}
1217
1218#[cfg(test)]
1219mod tests {
1220 use super::*;
1221 use crate::{
1222 ConnectionInfo, TlsClientCert, UpstreamInfo, new_test_session,
1223 };
1224 use pretty_assertions::assert_eq;
1225
1226 #[test]
1229 fn test_protect_from_connection_header() {
1230 let protected = [
1231 HeaderName::from_static("x-real-ip"),
1232 HeaderName::from_static("x-user-id"),
1233 ];
1234 let after = |connection: &[&str]| {
1235 let mut req = RequestHeader::build("GET", b"/", None).unwrap();
1236 for value in connection {
1237 req.append_header(header::CONNECTION, *value).unwrap();
1238 }
1239 protect_from_connection_header(&mut req, &protected);
1240 req.headers
1241 .get_all(header::CONNECTION)
1242 .iter()
1243 .map(|value| value.to_str().unwrap().to_string())
1244 .collect::<Vec<_>>()
1245 };
1246 assert_eq!(Vec::<String>::new(), after(&["X-Real-IP"]));
1248 assert_eq!(vec!["keep-alive"], after(&["keep-alive, x-real-ip"]));
1249 assert_eq!(
1250 vec!["keep-alive, upgrade, X-Other"],
1251 after(&[" keep-alive ,X-USER-ID,upgrade", "x-real-ip, X-Other"])
1252 );
1253 assert_eq!(
1255 vec!["keep-alive , Upgrade"],
1256 after(&["keep-alive , Upgrade"])
1257 );
1258 assert_eq!(vec!["close", "X-Other"], after(&["close", "X-Other"]));
1259 assert_eq!(Vec::<String>::new(), after(&[]));
1260 assert_eq!(vec!["x-real-ip-2"], after(&["x-real-ip-2"]));
1262 }
1263
1264 #[test]
1265 fn test_convert_headers() {
1266 let headers = convert_headers(&[
1267 "Content-Type: application/octet-stream".to_string(),
1268 "X-Server: $hostname".to_string(),
1269 "X-User: $USER".to_string(),
1270 ])
1271 .unwrap();
1272 assert_eq!(3, headers.len());
1273 assert_eq!("content-type", headers[0].0.to_string());
1274 assert_eq!("application/octet-stream", headers[0].1.to_str().unwrap());
1275 assert_eq!("x-server", headers[1].0.to_string());
1276 assert_eq!(false, headers[1].1.to_str().unwrap().is_empty());
1277 assert_eq!("x-user", headers[2].0.to_string());
1278 assert_eq!(false, headers[2].1.to_str().unwrap().is_empty());
1279 }
1280
1281 #[test]
1282 fn test_static_value() {
1283 assert_eq!(
1284 "cache-control: private, no-store",
1285 format!(
1286 "{}: {}",
1287 HTTP_HEADER_NO_STORE.0.to_string(),
1288 HTTP_HEADER_NO_STORE.1.to_str().unwrap_or_default()
1289 )
1290 );
1291
1292 assert_eq!(
1293 "cache-control: private, no-cache",
1294 format!(
1295 "{}: {}",
1296 HTTP_HEADER_NO_CACHE.0.to_string(),
1297 HTTP_HEADER_NO_CACHE.1.to_str().unwrap_or_default()
1298 )
1299 );
1300
1301 assert_eq!(
1302 "content-type: application/json; charset=utf-8",
1303 format!(
1304 "{}: {}",
1305 HTTP_HEADER_CONTENT_JSON.0.to_string(),
1306 HTTP_HEADER_CONTENT_JSON.1.to_str().unwrap_or_default()
1307 )
1308 );
1309
1310 assert_eq!(
1311 "content-type: text/html; charset=utf-8",
1312 format!(
1313 "{}: {}",
1314 HTTP_HEADER_CONTENT_HTML.0.to_string(),
1315 HTTP_HEADER_CONTENT_HTML.1.to_str().unwrap_or_default()
1316 )
1317 );
1318
1319 assert_eq!(
1320 "transfer-encoding: chunked",
1321 format!(
1322 "{}: {}",
1323 HTTP_HEADER_TRANSFER_CHUNKED.0.to_string(),
1324 HTTP_HEADER_TRANSFER_CHUNKED.1.to_str().unwrap_or_default()
1325 )
1326 );
1327
1328 assert_eq!("x-request-id", HTTP_HEADER_NAME_X_REQUEST_ID.to_string());
1329
1330 assert_eq!(
1331 "content-type: text/plain; charset=utf-8",
1332 format!(
1333 "{}: {}",
1334 HTTP_HEADER_CONTENT_TEXT.0.to_string(),
1335 HTTP_HEADER_CONTENT_TEXT.1.to_str().unwrap_or_default()
1336 )
1337 );
1338 }
1339
1340 #[tokio::test]
1343 async fn test_tls_client_header_values() {
1344 let session = new_test_session(&["Host: pingap.io"], "/").await;
1345 let value = |ctx: &Ctx, tag: &str| {
1346 convert_header_value(
1347 &HeaderValue::from_str(tag).unwrap(),
1348 &session,
1349 ctx,
1350 )
1351 .map(|value| value.to_str().unwrap().to_string())
1352 };
1353 let mut ctx = Ctx::default();
1354 for tag in [
1355 "$tls_client_subject",
1356 "$tls_client_fingerprint",
1357 "$tls_client_serial",
1358 ] {
1359 assert_eq!(Some("".to_string()), value(&ctx, tag), "{tag}");
1360 }
1361 assert_eq!(
1362 Some("false".to_string()),
1363 value(&ctx, "$tls_client_verified")
1364 );
1365
1366 ctx.conn.tls_client_cert = Some(std::sync::Arc::new(TlsClientCert {
1367 subject: "O=Example, CN=device-42".to_string(),
1368 fingerprint: "b545db7a".to_string(),
1369 serial: "3429".to_string(),
1370 }));
1371 assert_eq!(
1372 Some("O=Example, CN=device-42".to_string()),
1373 value(&ctx, "$tls_client_subject")
1374 );
1375 assert_eq!(
1376 Some("b545db7a".to_string()),
1377 value(&ctx, "$tls_client_fingerprint")
1378 );
1379 assert_eq!(Some("3429".to_string()), value(&ctx, "$tls_client_serial"));
1380 assert_eq!(
1381 Some("true".to_string()),
1382 value(&ctx, "$tls_client_verified")
1383 );
1384 let tag = HeaderValue::from_static("$tls_client_subject");
1387 assert_eq!(tag.clone(), resolve_static_header_value(tag));
1388 }
1389
1390 #[tokio::test]
1391 async fn test_convert_header_value() {
1392 let session =
1393 new_test_session(&["Host: pingap.io"], "/vicanso/pingap?size=1")
1394 .await;
1395 let default_state = Ctx {
1396 upstream: UpstreamInfo {
1397 address: "10.1.1.3:4123".to_string(),
1398 ..Default::default()
1399 },
1400 conn: ConnectionInfo {
1401 id: 102,
1402 remote_addr: Some("10.1.1.1".to_string()),
1403 remote_port: Some(6000),
1404 server_addr: Some("10.1.1.2".to_string()),
1405 server_port: Some(6001),
1406 tls_version: Some("tls1.3".into()),
1407 ..Default::default()
1408 },
1409 ..Default::default()
1410 };
1411
1412 let value = convert_header_value(
1413 &HeaderValue::from_str("$host").unwrap(),
1414 &session,
1415 &Ctx {
1416 ..Default::default()
1417 },
1418 );
1419 assert_eq!(true, value.is_some());
1420 assert_eq!("pingap.io", value.unwrap().to_str().unwrap());
1421
1422 let value = convert_header_value(
1423 &HeaderValue::from_str("$scheme").unwrap(),
1424 &session,
1425 &Ctx {
1426 ..Default::default()
1427 },
1428 );
1429 assert_eq!(true, value.is_some());
1430 assert_eq!("http", value.unwrap().to_str().unwrap());
1431 let value = convert_header_value(
1432 &HeaderValue::from_str("$scheme").unwrap(),
1433 &session,
1434 &default_state,
1435 );
1436 assert_eq!(true, value.is_some());
1437 assert_eq!("https", value.unwrap().to_str().unwrap());
1438
1439 let value = convert_header_value(
1440 &HeaderValue::from_str("$remote_addr").unwrap(),
1441 &session,
1442 &default_state,
1443 );
1444 assert_eq!(true, value.is_some());
1445 assert_eq!("10.1.1.1", value.unwrap().to_str().unwrap());
1446
1447 let value = convert_header_value(
1448 &HeaderValue::from_str("$remote_port").unwrap(),
1449 &session,
1450 &default_state,
1451 );
1452 assert_eq!(true, value.is_some());
1453 assert_eq!("6000", value.unwrap().to_str().unwrap());
1454
1455 let value = convert_header_value(
1456 &HeaderValue::from_str("$server_addr").unwrap(),
1457 &session,
1458 &default_state,
1459 );
1460 assert_eq!(true, value.is_some());
1461 assert_eq!("10.1.1.2", value.unwrap().to_str().unwrap());
1462
1463 let value = convert_header_value(
1464 &HeaderValue::from_str("$server_port").unwrap(),
1465 &session,
1466 &default_state,
1467 );
1468 assert_eq!(true, value.is_some());
1469 assert_eq!("6001", value.unwrap().to_str().unwrap());
1470
1471 let value = convert_header_value(
1472 &HeaderValue::from_str("$upstream_addr").unwrap(),
1473 &session,
1474 &default_state,
1475 );
1476 assert_eq!(true, value.is_some());
1477 assert_eq!("10.1.1.3:4123", value.unwrap().to_str().unwrap());
1478
1479 let value = convert_header_value(
1480 &HeaderValue::from_str(":connection_id").unwrap(),
1481 &session,
1482 &default_state,
1483 );
1484 assert_eq!(true, value.is_some());
1485 assert_eq!("102", value.unwrap().to_str().unwrap());
1486
1487 let session = new_test_session(
1488 &["X-Forwarded-For: 1.1.1.1, 2.2.2.2"],
1489 "/vicanso/pingap?size=1",
1490 )
1491 .await;
1492 let value = convert_header_value(
1493 &HeaderValue::from_str("$proxy_add_x_forwarded_for").unwrap(),
1494 &session,
1495 &Ctx {
1496 conn: ConnectionInfo {
1497 remote_addr: Some("10.1.1.1".to_string()),
1498 ..Default::default()
1499 },
1500 ..Default::default()
1501 },
1502 );
1503 assert_eq!(true, value.is_some());
1504 assert_eq!(
1505 "1.1.1.1, 2.2.2.2, 10.1.1.1",
1506 value.unwrap().to_str().unwrap()
1507 );
1508
1509 let session = new_test_session(&[""], "/vicanso/pingap?size=1").await;
1510 let value = convert_header_value(
1511 &HeaderValue::from_str("$proxy_add_x_forwarded_for").unwrap(),
1512 &session,
1513 &Ctx {
1514 conn: ConnectionInfo {
1515 remote_addr: Some("10.1.1.1".to_string()),
1516 ..Default::default()
1517 },
1518 ..Default::default()
1519 },
1520 );
1521 assert_eq!(true, value.is_some());
1522 assert_eq!("10.1.1.1", value.unwrap().to_str().unwrap());
1523
1524 let session = new_test_session(&[""], "/vicanso/pingap?size=1").await;
1525 let value = convert_header_value(
1526 &HeaderValue::from_str("$upstream_addr").unwrap(),
1527 &session,
1528 &Ctx {
1529 upstream: UpstreamInfo {
1530 address: "10.1.1.1:8001".to_string(),
1531 ..Default::default()
1532 },
1533 ..Default::default()
1534 },
1535 );
1536 assert_eq!(true, value.is_some());
1537 assert_eq!("10.1.1.1:8001", value.unwrap().to_str().unwrap());
1538
1539 let session = new_test_session(&[""], "/vicanso/pingap?size=1").await;
1542 let mut ctx = Ctx::default();
1543 for tag in ["$ja4", ":ja4"] {
1544 let value = convert_header_value(
1545 &HeaderValue::from_str(tag).unwrap(),
1546 &session,
1547 &ctx,
1548 );
1549 assert_eq!(true, value.is_none(), "{tag}");
1550 }
1551 ctx.conn.ja4 = Some(std::sync::Arc::new(
1552 crate::Ja4Fingerprint::from_client_hello(
1553 &crate::ja4::testing::spec_example_body(),
1554 )
1555 .unwrap(),
1556 ));
1557 for tag in ["$ja4", ":ja4"] {
1558 let value = convert_header_value(
1559 &HeaderValue::from_str(tag).unwrap(),
1560 &session,
1561 &ctx,
1562 );
1563 assert_eq!(
1564 "t13d1516h2_8daaf6152771_e5627efa2ab1",
1565 value.unwrap().to_str().unwrap(),
1566 "{tag}"
1567 );
1568 }
1569 assert_eq!(
1571 "$ja4",
1572 resolve_static_header_value(HeaderValue::from_static("$ja4"))
1573 );
1574
1575 let session = new_test_session(
1576 &["Origin: https://github.com"],
1577 "/vicanso/pingap?size=1",
1578 )
1579 .await;
1580 let value = convert_header_value(
1581 &HeaderValue::from_str("$http_origin").unwrap(),
1582 &session,
1583 &Ctx::default(),
1584 );
1585 assert_eq!(true, value.is_some());
1586 assert_eq!("https://github.com", value.unwrap().to_str().unwrap());
1587
1588 let session = new_test_session(
1589 &["Origin: https://github.com"],
1590 "/vicanso/pingap?size=1",
1591 )
1592 .await;
1593 let value = convert_header_value(
1594 &HeaderValue::from_str("$hostname").unwrap(),
1595 &session,
1596 &Ctx::default(),
1597 );
1598 assert_eq!(true, value.is_some());
1599
1600 let session = new_test_session(
1601 &["Origin: https://github.com"],
1602 "/vicanso/pingap?size=1",
1603 )
1604 .await;
1605 let value = convert_header_value(
1606 &HeaderValue::from_str("$HOME").unwrap(),
1607 &session,
1608 &Ctx::default(),
1609 );
1610 assert_eq!(true, value.is_some());
1611
1612 let session = new_test_session(
1613 &["Origin: https://github.com"],
1614 "/vicanso/pingap?size=1",
1615 )
1616 .await;
1617 let value = convert_header_value(
1618 &HeaderValue::from_str("UUID").unwrap(),
1619 &session,
1620 &Ctx::default(),
1621 );
1622 assert_eq!(false, value.is_some());
1623 }
1624
1625 #[tokio::test]
1626 async fn test_get_host() {
1627 let session =
1628 new_test_session(&["Host: pingap.io"], "/vicanso/pingap?size=1")
1629 .await;
1630 assert_eq!(get_host(session.req_header()), Some("pingap.io"));
1631 }
1632
1633 #[test]
1634 fn test_remove_query_from_header() {
1635 let mut req =
1636 RequestHeader::build("GET", b"/?apikey=123", None).unwrap();
1637 remove_query_from_header(&mut req, "apikey").unwrap();
1638 assert_eq!("/", req.uri.to_string());
1639
1640 let mut req =
1641 RequestHeader::build("GET", b"/?apikey=123&name=pingap", None)
1642 .unwrap();
1643 remove_query_from_header(&mut req, "apikey").unwrap();
1644 assert_eq!("/?name=pingap", req.uri.to_string());
1645 }
1646
1647 #[tokio::test]
1648 async fn test_get_client_ip() {
1649 let session = new_test_session(
1650 &["X-Forwarded-For:192.168.1.1"],
1651 "/vicanso/pingap?size=1",
1652 )
1653 .await;
1654 assert_eq!(get_client_ip(&session), "192.168.1.1");
1655
1656 let session = new_test_session(
1657 &["X-Real-Ip:192.168.1.2"],
1658 "/vicanso/pingap?size=1",
1659 )
1660 .await;
1661 assert_eq!(get_client_ip(&session), "192.168.1.2");
1662
1663 let claims = [
1668 "Host: pingap.io",
1669 "X-Forwarded-For: 192.168.1.1",
1670 "X-Forwarded-Proto: https",
1671 "X-Forwarded-Host: other.example",
1672 ];
1673 let session = new_test_session(&claims, "/").await;
1674 let mut ctx = Ctx::default();
1675 ctx.conn.remote_addr = Some("10.1.1.1".to_string());
1676 let resolve = |name: &'static str, session: &Session, ctx: &Ctx| {
1677 convert_header_value(&HeaderValue::from_static(name), session, ctx)
1678 .map(|value| value.to_str().unwrap().to_string())
1679 };
1680 assert_eq!(false, peer_is_trusted_proxy(&session));
1681 ctx.conn.server_port = Some(8080);
1682 for (name, expected) in [
1683 ("$client_ip", "10.1.1.1"),
1684 ("$forwarded_proto", "http"),
1685 ("$forwarded_host", "pingap.io"),
1686 ("$forwarded_port", "8080"),
1687 ] {
1688 assert_eq!(
1689 Some(expected.to_string()),
1690 resolve(name, &session, &ctx),
1691 "{name}"
1692 );
1693 }
1694 ctx.conn.tls_version = Some(Cow::Borrowed("TLSv1.3"));
1695 assert_eq!(
1696 Some("https".to_string()),
1697 resolve("$forwarded_proto", &session, &ctx)
1698 );
1699 for name in [
1702 "$client_ip",
1703 "$forwarded_proto",
1704 "$forwarded_host",
1705 "$forwarded_port",
1706 ] {
1707 assert_eq!(
1708 name,
1709 resolve_static_header_value(HeaderValue::from_static(name))
1710 );
1711 }
1712
1713 let entry = |lines: &[&'static str]| {
1717 let values: Vec<HeaderValue> = lines
1718 .iter()
1719 .map(|line| HeaderValue::from_static(line))
1720 .collect();
1721 first_forwarded_entry(values.iter()).map(str::to_string)
1722 };
1723 assert_eq!(Some("https".to_string()), entry(&["https, http"]));
1724 assert_eq!(Some("https".to_string()), entry(&[" , https", "http"]));
1725 assert_eq!(Some("https".to_string()), entry(&["https", "http"]));
1726 assert_eq!(None, entry(&[]));
1727 assert_eq!(None, entry(&[" , "]));
1728 for host in ["shop.example.com", "shop.example.com:8443", "[::1]:80"] {
1730 assert_eq!(true, is_host_name(host), "{host}");
1731 }
1732 for host in [
1733 "",
1734 "user@shop.example.com",
1735 "shop.example.com/path",
1736 "shop example.com",
1737 "evil.example\r\nx: 1",
1738 ] {
1739 assert_eq!(false, is_host_name(host), "{host}");
1740 }
1741
1742 set_trusted_proxies(&Some(vec!["10.0.0.0/8".to_string()]));
1746 let session = new_test_session(&claims, "/").await;
1747 assert_eq!(false, peer_is_trusted_proxy(&session));
1748 ctx.conn.tls_version = None;
1749 assert_eq!(
1750 Some("http".to_string()),
1751 resolve("$forwarded_proto", &session, &ctx)
1752 );
1753 assert_eq!(
1754 Some("pingap.io".to_string()),
1755 resolve("$forwarded_host", &session, &ctx)
1756 );
1757 ctx.conn.client_ip = Some("203.0.113.9".to_string());
1759 assert_eq!(
1760 Some("203.0.113.9".to_string()),
1761 resolve("$client_ip", &session, &ctx)
1762 );
1763 let session = new_test_session(
1764 &["X-Forwarded-For:192.168.1.1"],
1765 "/vicanso/pingap?size=1",
1766 )
1767 .await;
1768 assert_ne!(get_client_ip(&session), "192.168.1.1");
1769
1770 set_trusted_proxies(&None);
1772 let session = new_test_session(
1773 &["X-Forwarded-For:192.168.1.1"],
1774 "/vicanso/pingap?size=1",
1775 )
1776 .await;
1777 assert_eq!(get_client_ip(&session), "192.168.1.1");
1778 }
1779
1780 #[test]
1781 fn test_forwarded_client_ip() {
1782 let trusted = TrustedProxies::parse(&[
1783 "10.0.0.0/8".to_string(),
1784 "192.168.1.1".to_string(),
1785 "fd00::/8".to_string(),
1786 ]);
1787 let client_ip = |lines: &[&str]| {
1788 let values: Vec<HeaderValue> = lines
1789 .iter()
1790 .map(|line| HeaderValue::from_str(line).unwrap())
1791 .collect();
1792 forwarded_client_ip(values.iter(), &trusted)
1793 };
1794 let ip = |value: &str| Some(value.to_string());
1795
1796 assert_eq!(ip("9.9.9.9"), client_ip(&["9.9.9.9"]));
1797 assert_eq!(ip("9.9.9.9"), client_ip(&["6.6.6.6, 9.9.9.9"]));
1800 assert_eq!(ip("9.9.9.9"), client_ip(&["6.6.6.6,10.1.1.1 , 9.9.9.9"]));
1801 assert_eq!(
1803 ip("9.9.9.9"),
1804 client_ip(&["6.6.6.6, 9.9.9.9, 10.0.0.2, 192.168.1.1"])
1805 );
1806 assert_eq!(ip("2001:db8::1"), client_ip(&["2001:db8::1, fd00::2"]));
1807 assert_eq!(
1809 ip("9.9.9.9"),
1810 client_ip(&["6.6.6.6", "9.9.9.9", "10.0.0.2"])
1811 );
1812 assert_eq!(ip("9.9.9.9"), client_ip(&["6.6.6.6, 9.9.9.9", "10.0.0.2"]));
1813 assert_eq!(ip("10.0.0.3"), client_ip(&["10.0.0.3, 10.0.0.2"]));
1815 assert_eq!(ip("9.9.9.9"), client_ip(&["9.9.9.9:4321, 10.0.0.2:80"]));
1817 assert_eq!(
1818 ip("2001:db8::1"),
1819 client_ip(&["[2001:db8::1]:4321, [fd00::2]"])
1820 );
1821 assert_eq!(ip("unknown"), client_ip(&["6.6.6.6, unknown, 10.0.0.2"]));
1824 assert_eq!(None, client_ip(&[]));
1825 assert_eq!(None, client_ip(&[" , "]));
1826
1827 assert_eq!(
1830 ip("9.9.9.9"),
1831 client_ip(&["6.6.6.6, ::ffff:9.9.9.9, ::ffff:10.0.0.2"])
1832 );
1833 assert_eq!(true, trusted.contains("::ffff:10.1.2.3".parse().unwrap()));
1834 assert_eq!(
1835 true,
1836 trusted.contains("::ffff:192.168.1.1".parse().unwrap())
1837 );
1838 assert_eq!(false, trusted.contains("::ffff:9.9.9.9".parse().unwrap()));
1839 let mapped = TrustedProxies::parse(&["::ffff:172.16.0.1".to_string()]);
1840 assert_eq!(true, mapped.contains("172.16.0.1".parse().unwrap()));
1841
1842 let line = HeaderValue::from_bytes(b"\xff\xfe, 9.9.9.9").unwrap();
1847 assert_eq!(
1848 ip("9.9.9.9"),
1849 forwarded_client_ip([&line].into_iter(), &trusted)
1850 );
1851 let line = HeaderValue::from_bytes(b"6.6.6.6, \xff, 9.9.9.9, 10.0.0.2")
1852 .unwrap();
1853 assert_eq!(
1854 ip("9.9.9.9"),
1855 forwarded_client_ip([&line].into_iter(), &trusted)
1856 );
1857 }
1858
1859 #[tokio::test]
1860 async fn test_get_header_value() {
1861 let session =
1862 new_test_session(&["Host: pingap.io"], "/vicanso/pingap?size=1")
1863 .await;
1864 assert_eq!(
1865 get_req_header_value(session.req_header(), "Host"),
1866 Some("pingap.io")
1867 );
1868 }
1869
1870 #[tokio::test]
1871 async fn test_get_cookie_value() {
1872 let session = new_test_session(
1873 &["Cookie: name=pingap"],
1874 "/vicanso/pingap?size=1",
1875 )
1876 .await;
1877 assert_eq!(
1878 get_cookie_value(session.req_header(), "name"),
1879 Some("pingap")
1880 );
1881 }
1882
1883 #[tokio::test]
1884 async fn test_get_query_value() {
1885 let session = new_test_session(
1886 &["X-Forwarded-For:192.168.1.1"],
1887 "/vicanso/pingap?size=1",
1888 )
1889 .await;
1890 assert_eq!(get_query_value(session.req_header(), "size"), Some("1"));
1891 }
1892
1893 #[test]
1896 fn test_convert_header_edge_cases() {
1897 assert!(convert_header("").unwrap().is_none());
1899 assert!(convert_header("no-colon").unwrap().is_none());
1901 assert!(convert_header("Invalid Name: value").is_err());
1903 assert!(convert_header("Valid-Name: invalid\r\nvalue").is_err());
1905 }
1906
1907 #[test]
1909 fn test_get_host_variants() {
1910 let uri_string = "http://user:pass@authority.com/path";
1912
1913 let uri = http::Uri::from_str(uri_string).unwrap();
1915 let mut req_with_authority =
1916 RequestHeader::build("GET", b"/path", None).unwrap();
1917 req_with_authority.set_uri(uri);
1918 assert_eq!(get_host(&req_with_authority), Some("authority.com"));
1919
1920 let mut req_with_host_header =
1922 RequestHeader::build("GET", b"/path", None).unwrap();
1923 req_with_host_header
1924 .insert_header("Host", "header-host.com:8080")
1925 .unwrap();
1926 assert_eq!(get_host(&req_with_host_header), Some("header-host.com"));
1927
1928 let req_no_host = RequestHeader::build("GET", b"/path", None).unwrap();
1930 assert_eq!(get_host(&req_no_host), None);
1931
1932 for (host, expected) in [
1934 ("[::1]:8080", "[::1]"),
1935 ("[::1]", "[::1]"),
1936 ("[2001:db8::1]:443", "[2001:db8::1]"),
1937 ("example.com", "example.com"),
1938 ("example.com:8080", "example.com"),
1939 ] {
1940 let mut req = RequestHeader::build("GET", b"/path", None).unwrap();
1941 req.insert_header("Host", host).unwrap();
1942 assert_eq!(get_host(&req), Some(expected), "{host}");
1943 }
1944 }
1945
1946 #[test]
1950 fn test_get_host_drops_the_root_label() {
1951 for (host, expected) in [
1952 ("admin.example.com.", "admin.example.com"),
1953 ("admin.example.com.:8443", "admin.example.com"),
1954 ("Admin.Example.com.", "Admin.Example.com"),
1955 ("admin.example.com", "admin.example.com"),
1956 ("admin.example.com..", "admin.example.com."),
1958 (".", "."),
1959 ] {
1960 let mut req = RequestHeader::build("GET", b"/path", None).unwrap();
1961 req.insert_header("Host", host).unwrap();
1962 assert_eq!(get_host(&req), Some(expected), "{host}");
1963 }
1964 let mut req = RequestHeader::build("GET", b"/", None).unwrap();
1966 req.set_uri(http::Uri::from_static("https://admin.example.com./a"));
1967 assert_eq!(get_host(&req), Some("admin.example.com"));
1968 assert_eq!(get_request_host(&req), Some("admin.example.com."));
1970 }
1971
1972 #[test]
1976 fn test_cache_key_keeps_the_host_as_sent() {
1977 let key = |host: &str| {
1978 let mut req = RequestHeader::build("GET", b"/a", None).unwrap();
1979 req.insert_header("Host", host).unwrap();
1980 let ctx = Ctx {
1981 cache: Some(Default::default()),
1982 ..Default::default()
1983 };
1984 format!("{:?}", crate::get_cache_key(&ctx, "GET", &req))
1985 };
1986 assert_ne!(key("example.com"), key("example.com."));
1987 assert_eq!(key("example.com"), key("EXAMPLE.com:8080"));
1988 }
1989
1990 #[test]
1993 fn test_merge_cookie_headers() {
1994 let cookies = |values: &[&str]| {
1995 let mut req = RequestHeader::build("GET", b"/", None).unwrap();
1996 for value in values {
1997 req.append_header("cookie", *value).unwrap();
1998 }
1999 merge_cookie_headers(&mut req);
2000 let merged: Vec<String> = req
2001 .headers
2002 .get_all(http::header::COOKIE)
2003 .iter()
2004 .map(|value| value.to_str().unwrap().to_string())
2005 .collect();
2006 let c = get_cookie_value(&req, "c").map(|v| v.to_string());
2007 (merged, c)
2008 };
2009 assert_eq!(
2010 (vec!["a=1; b=2; c=3".to_string()], Some("3".to_string())),
2011 cookies(&["a=1", "b=2", "c=3"])
2012 );
2013 assert_eq!(
2014 (vec!["a=1; b=2; c=3".to_string()], Some("3".to_string())),
2015 cookies(&["a=1; b=2", "c=3"])
2016 );
2017 assert_eq!(
2019 (vec!["a=1; c=3".to_string()], Some("3".to_string())),
2020 cookies(&["a=1; c=3"])
2021 );
2022 assert_eq!((vec![], None), cookies(&[]));
2023 }
2024
2025 #[tokio::test]
2030 async fn test_proxy_add_x_forwarded_for_keeps_every_line() {
2031 let session = new_test_session(
2032 &["X-Forwarded-For: 6.6.6.6", "X-Forwarded-For: 10.0.0.9"],
2033 "/",
2034 )
2035 .await;
2036 let value = convert_header_value(
2037 &HeaderValue::from_str("$proxy_add_x_forwarded_for").unwrap(),
2038 &session,
2039 &Ctx {
2040 conn: ConnectionInfo {
2041 remote_addr: Some("10.0.0.1".to_string()),
2042 ..Default::default()
2043 },
2044 ..Default::default()
2045 },
2046 );
2047 assert_eq!(
2048 "6.6.6.6, 10.0.0.9, 10.0.0.1",
2049 value.unwrap().to_str().unwrap()
2050 );
2051 }
2052
2053 #[test]
2054 fn test_resolve_static_header_value() {
2055 let resolve = |value: &str| {
2056 resolve_static_header_value(HeaderValue::from_str(value).unwrap())
2057 };
2058 assert_eq!(get_hostname(), resolve("$hostname").to_str().unwrap());
2059 let home = std::env::var("HOME").unwrap();
2060 assert_eq!(home, resolve("$HOME").to_str().unwrap());
2061 for value in [
2064 "$host",
2065 "$remote_addr",
2066 "$proxy_add_x_forwarded_for",
2067 "$http_origin",
2068 ":connection_id",
2069 "plain",
2070 "$PINGAP_TEST_UNSET_VARIABLE",
2071 ] {
2072 assert_eq!(value, resolve(value).to_str().unwrap());
2073 }
2074 }
2075
2076 #[tokio::test]
2078 async fn test_http_variable_with_underscores() {
2079 let session = new_test_session(
2080 &["User-Agent: pingap/1", "X-Env-Name: prod"],
2081 "/",
2082 )
2083 .await;
2084 for (variable, expected) in [
2085 ("$http_user_agent", Some("pingap/1")),
2086 ("$http_user-agent", Some("pingap/1")),
2087 ("$http_x_env_name", Some("prod")),
2088 ("$http_missing_header", None),
2089 ] {
2090 let value = convert_header_value(
2091 &HeaderValue::from_str(variable).unwrap(),
2092 &session,
2093 &Ctx::default(),
2094 );
2095 assert_eq!(
2096 expected,
2097 value.as_ref().and_then(|v| v.to_str().ok()),
2098 "{variable}"
2099 );
2100 }
2101 }
2102
2103 #[test]
2105 fn test_get_cookie_value_advanced() {
2106 let mut req = RequestHeader::build("GET", b"/", None).unwrap();
2107 req.insert_header("Cookie", "id=123; session=abc; theme=dark")
2108 .unwrap();
2109
2110 assert_eq!(get_cookie_value(&req, "session"), Some("abc"));
2111 assert_eq!(get_cookie_value(&req, "id"), Some("123"));
2112 assert_eq!(get_cookie_value(&req, "theme"), Some("dark"));
2113 assert_eq!(get_cookie_value(&req, "lang"), None);
2115 assert_eq!(get_cookie_value(&req, "the"), None);
2117 }
2118
2119 #[test]
2122 fn test_canonical_path() {
2123 for path in ["/", "/users/x", "/a/b.c/d", "relative"] {
2125 assert_eq!(
2126 true,
2127 matches!(canonical_path(path), Cow::Borrowed(_)),
2128 "{path}"
2129 );
2130 }
2131 for (path, expected) in [
2132 ("/%75sers/x", "/users/x"),
2133 ("/users/../other", "/other"),
2134 ("//users/./x/", "/users/x/"),
2135 ("/users;v=1/x", "/users/x"),
2136 ("/a%20b", "/a%20b"),
2137 ("/a%3fb", "/a%3Fb"),
2138 ("/a%23b", "/a%23b"),
2139 ("/a%2fb", "/a/b"),
2140 ("/100%25", "/100%25"),
2141 ("/%2561", "/%2561"),
2143 ("/caf%c3%a9", "/caf%C3%A9"), ("/%ff%00", "/%FF%00"),
2146 ] {
2147 assert_eq!(expected, canonical_path(path), "{path}");
2148 }
2149 }
2150
2151 #[test]
2152 fn test_has_dot_segments() {
2153 for path in [
2154 "/a/../b",
2155 "/a/./b",
2156 "/a/..",
2157 "/a/%2e%2e/b",
2158 "/a/%2E./b",
2159 "/a/..;x=1/b",
2160 "/a%2F..%2Fb",
2161 "/a%5c..%5cb",
2162 "/a\\..\\b",
2163 "/a/.;/b",
2164 ] {
2165 assert_eq!(true, has_dot_segments(path), "{path}");
2166 }
2167 for path in [
2168 "/",
2169 "/a/b",
2170 "/a..b/c",
2171 "/a/..b/c..",
2172 "/a/.../b",
2173 "/a%2Fb",
2174 "/a;x=../b",
2175 "/a/b;jsessionid=1",
2176 "//a//b",
2177 "/%2e%2ea/b",
2178 ] {
2179 assert_eq!(false, has_dot_segments(path), "{path}");
2180 }
2181 }
2182
2183 #[test]
2184 fn test_normalize_path() {
2185 for path in [
2187 "/",
2188 "/admin",
2189 "/admin/",
2190 "/a/b.c/d",
2191 "/.well-known/acme-challenge/token",
2192 "/.env",
2193 "/a/..b/c",
2194 "/a/b../c",
2195 "/a/...",
2196 "*",
2197 "",
2198 ] {
2199 assert_eq!(
2200 true,
2201 matches!(normalize_path(path), Cow::Borrowed(value) if value == path),
2202 "{path}"
2203 );
2204 }
2205 for (path, expected) in [
2206 ("/%61dmin", "/admin"),
2208 ("/%61dmin/users", "/admin/users"),
2209 ("//admin", "/admin"),
2210 ("/./admin", "/admin"),
2211 ("/public/../admin", "/admin"),
2212 ("/public/%2e%2e/admin", "/admin"),
2213 ("/public/%2E%2E%2Fadmin", "/admin"),
2214 ("/public/..%2fadmin/x", "/admin/x"),
2215 ("/../../admin", "/admin"),
2217 ("/..", "/"),
2218 ("/.", "/"),
2219 ("//", "/"),
2220 ("/admin//", "/admin/"),
2222 ("/admin/.", "/admin/"),
2223 ("/admin/x/..", "/admin/"),
2224 ("/admin/x/../", "/admin/"),
2225 ("/%2561dmin", "/%61dmin"),
2227 ("/100%", "/100%"),
2229 ("/a%zzb", "/a%zzb"),
2230 ("/a%4", "/a%4"),
2231 ("/%E6%96%87%E6%A1%A3/menu", "/文档/menu"),
2232 ("/a%20b", "/a b"),
2233 ("/public/..;/admin", "/admin"),
2237 ("/public/..;x=1/admin", "/admin"),
2238 ("/public/.;/admin", "/public/admin"),
2239 ("/public/%2e%2e%3b/admin", "/admin"),
2240 ("/api;v=1/admin", "/api/admin"),
2241 ("/admin;jsessionid=A1/users", "/admin/users"),
2242 ("/login;jsessionid=A1", "/login"),
2243 ("/a/;x/b", "/a/b"),
2244 ("/a/b;x/", "/a/b/"),
2245 ("/a/;x", "/a/"),
2246 ("/public\\..\\admin", "/admin"),
2248 ("/public/..%5cadmin", "/admin"),
2249 ("/a\\b", "/a/b"),
2250 ("/a\\\\b\\", "/a/b/"),
2251 ] {
2252 assert_eq!(expected, normalize_path(path), "{path}");
2253 }
2254 assert_eq!("/\u{fffd}/x", normalize_path("/%ff/x"));
2256 }
2257
2258 #[test]
2261 fn test_path_and_query_changes_keep_the_authority() {
2262 let mut req = RequestHeader::build("GET", b"/", None).unwrap();
2263 req.set_uri(http::Uri::from_static(
2264 "https://example.com/api/users?apikey=1&page=2",
2265 ));
2266 remove_query_from_header(&mut req, "apikey").unwrap();
2267 assert_eq!("https://example.com/api/users?page=2", req.uri.to_string());
2268 assert_eq!(Some("example.com"), get_host(&req));
2269
2270 set_path_and_query(&mut req, "/users?page=2").unwrap();
2271 assert_eq!("https://example.com/users?page=2", req.uri.to_string());
2272 set_path_and_query(&mut req, "/").unwrap();
2273 assert_eq!("https://example.com/", req.uri.to_string());
2274 assert_eq!(true, set_path_and_query(&mut req, "/a b").is_err());
2276 assert_eq!("https://example.com/", req.uri.to_string());
2277
2278 let mut req = RequestHeader::build("GET", b"/api?a=1", None).unwrap();
2280 set_path_and_query(&mut req, "/v2/api?a=1").unwrap();
2281 assert_eq!("/v2/api?a=1", req.uri.to_string());
2282 }
2283
2284 #[test]
2285 fn test_remove_query_from_header_variants() {
2286 let mut req =
2288 RequestHeader::build("GET", b"/path?key=val", None).unwrap();
2289 remove_query_from_header(&mut req, "key").unwrap();
2290 assert_eq!(req.uri.to_string(), "/path");
2291
2292 let mut req =
2294 RequestHeader::build("GET", b"/path?key1=val1&key2=val2", None)
2295 .unwrap();
2296 remove_query_from_header(&mut req, "key1").unwrap();
2297 assert_eq!(req.uri.to_string(), "/path?key2=val2");
2298
2299 let mut req =
2301 RequestHeader::build("GET", b"/path?key1=val1&key2=val2", None)
2302 .unwrap();
2303 remove_query_from_header(&mut req, "key2").unwrap();
2304 assert_eq!(req.uri.to_string(), "/path?key1=val1");
2305
2306 let mut req =
2308 RequestHeader::build("GET", b"/path?key1=v1&key2=v2&key3=v3", None)
2309 .unwrap();
2310 remove_query_from_header(&mut req, "key2").unwrap();
2311 assert_eq!(req.uri.to_string(), "/path?key1=v1&key3=v3");
2312
2313 let mut req =
2315 RequestHeader::build("GET", b"/path?key=val", None).unwrap();
2316 remove_query_from_header(&mut req, "nonexistent").unwrap();
2317 assert_eq!(req.uri.to_string(), "/path?key=val");
2318
2319 let mut req = RequestHeader::build("GET", b"/path", None).unwrap();
2321 remove_query_from_header(&mut req, "key").unwrap();
2322 assert_eq!(req.uri.to_string(), "/path");
2323 }
2324}