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::fmt::Write;
28use std::net::IpAddr;
29use std::str::FromStr;
30use std::sync::Arc;
31use std::sync::atomic::{AtomicBool, Ordering};
32
33const HTTP_HEADER_X_FORWARDED_FOR: &str = "x-forwarded-for";
35const HTTP_HEADER_X_REAL_IP: &str = "x-real-ip";
36
37pub const HOST_NAME_TAG: &[u8] = b"$hostname";
40const HOST_TAG: &[u8] = b"$host";
41const SCHEME_TAG: &[u8] = b"$scheme";
42const REMOTE_ADDR_TAG: &[u8] = b"$remote_addr";
43const REMOTE_PORT_TAG: &[u8] = b"$remote_port";
44const SERVER_ADDR_TAG: &[u8] = b"$server_addr";
45const SERVER_PORT_TAG: &[u8] = b"$server_port";
46const PROXY_ADD_FORWARDED_TAG: &[u8] = b"$proxy_add_x_forwarded_for";
47const UPSTREAM_ADDR_TAG: &[u8] = b"$upstream_addr";
48const JA4_TAG: &[u8] = b"$ja4";
49
50static SCHEME_HTTPS: HeaderValue = HeaderValue::from_static("https");
52static SCHEME_HTTP: HeaderValue = HeaderValue::from_static("http");
53
54#[derive(Debug, Snafu)]
56pub enum Error {
57 #[snafu(display("invalid header value: {value} - {source}"))]
59 InvalidHeaderValue {
60 value: String,
61 source: header::InvalidHeaderValue,
62 },
63 #[snafu(display("invalid header name: {value} - {source}"))]
65 InvalidHeaderName {
66 value: String,
67 source: header::InvalidHeaderName,
68 },
69}
70type Result<T, E = Error> = std::result::Result<T, E>;
72
73pub type HttpHeader = (HeaderName, HeaderValue);
75
76pub fn get_host(header: &RequestHeader) -> Option<&str> {
81 if let Some(host) = header.uri.host() {
84 return Some(host);
85 }
86 header
88 .headers
89 .get(http::header::HOST)
90 .and_then(|value| value.to_str().ok())
92 .map(strip_port)
93}
94
95fn strip_port(host: &str) -> &str {
99 if host.starts_with('[') {
100 return match host.find(']') {
101 Some(end) => &host[..=end],
102 None => host,
103 };
104 }
105 host.split(':').next().unwrap_or(host)
106}
107
108pub fn convert_header(value: &str) -> Result<Option<HttpHeader>> {
113 value
115 .split_once(':')
116 .map(|(k, v)| {
118 let name = HeaderName::from_str(k.trim())
120 .context(InvalidHeaderNameSnafu { value: k })?;
121 let value = HeaderValue::from_str(v.trim())
123 .context(InvalidHeaderValueSnafu { value: v })?;
124 Ok(Some((name, value)))
126 })
127 .unwrap_or(Ok(None))
129}
130
131pub fn convert_headers(header_values: &[String]) -> Result<Vec<HttpHeader>> {
136 header_values
137 .iter()
138 .filter_map(|item| convert_header(item).transpose())
141 .collect()
143}
144
145pub static HTTP_HEADER_NO_STORE: HttpHeader = (
147 header::CACHE_CONTROL,
148 HeaderValue::from_static("private, no-store"),
149);
150pub static HTTP_HEADER_NO_CACHE: HttpHeader = (
151 header::CACHE_CONTROL,
152 HeaderValue::from_static("private, no-cache"),
153);
154pub static HTTP_HEADER_CONTENT_JSON: HttpHeader = (
155 header::CONTENT_TYPE,
156 HeaderValue::from_static("application/json; charset=utf-8"),
157);
158pub static HTTP_HEADER_CONTENT_HTML: HttpHeader = (
159 header::CONTENT_TYPE,
160 HeaderValue::from_static("text/html; charset=utf-8"),
161);
162pub static HTTP_HEADER_CONTENT_TEXT: HttpHeader = (
163 header::CONTENT_TYPE,
164 HeaderValue::from_static("text/plain; charset=utf-8"),
165);
166pub static HTTP_HEADER_TRANSFER_CHUNKED: HttpHeader = (
167 header::TRANSFER_ENCODING,
168 HeaderValue::from_static("chunked"),
169);
170pub static HTTP_HEADER_NAME_X_REQUEST_ID: HeaderName =
171 HeaderName::from_static("x-request-id");
172
173pub fn resolve_static_header_value(value: HeaderValue) -> HeaderValue {
181 let buf = value.as_bytes();
182 if buf.first() != Some(&b'$') {
183 return value;
184 }
185 if buf == HOST_NAME_TAG {
186 return HeaderValue::from_str(get_hostname()).unwrap_or(value);
187 }
188 let request_tags: [&[u8]; 9] = [
189 HOST_TAG,
190 SCHEME_TAG,
191 REMOTE_ADDR_TAG,
192 REMOTE_PORT_TAG,
193 SERVER_ADDR_TAG,
194 SERVER_PORT_TAG,
195 PROXY_ADD_FORWARDED_TAG,
196 UPSTREAM_ADDR_TAG,
197 JA4_TAG,
198 ];
199 if request_tags.contains(&buf) || buf.starts_with(b"$http_") {
200 return value;
201 }
202 let Ok(name) = std::str::from_utf8(&buf[1..]) else {
203 return value;
204 };
205 match std::env::var(name) {
206 Ok(env_value) => HeaderValue::from_str(&env_value).unwrap_or(value),
207 Err(_) => value,
208 }
209}
210
211#[inline]
214pub fn convert_header_value(
215 value: &HeaderValue,
216 session: &Session,
217 ctx: &Ctx,
218) -> Option<HeaderValue> {
219 let buf = value.as_bytes();
221
222 if buf.is_empty() || !(buf[0] == b'$' || buf[0] == b':') {
225 return None;
226 }
227
228 let to_header_value = |s: &str| HeaderValue::from_str(s).ok();
230
231 match buf {
233 HOST_TAG => get_host(session.req_header()).and_then(to_header_value),
234 SCHEME_TAG => Some(if ctx.conn.tls_version.is_some() {
235 SCHEME_HTTPS.clone()
236 } else {
237 SCHEME_HTTP.clone()
238 }),
239 HOST_NAME_TAG => to_header_value(get_hostname()),
240 REMOTE_ADDR_TAG => {
241 ctx.conn.remote_addr.as_deref().and_then(to_header_value)
242 },
243 REMOTE_PORT_TAG => ctx.conn.remote_port.and_then(|p| {
244 HeaderValue::from_str(itoa::Buffer::new().format(p)).ok()
247 }),
248 SERVER_ADDR_TAG => {
249 ctx.conn.server_addr.as_deref().and_then(to_header_value)
250 },
251 SERVER_PORT_TAG => ctx.conn.server_port.and_then(|p| {
252 HeaderValue::from_str(itoa::Buffer::new().format(p)).ok()
253 }),
254 UPSTREAM_ADDR_TAG => {
255 if !ctx.upstream.address.is_empty() {
256 to_header_value(&ctx.upstream.address)
257 } else {
258 None
259 }
260 },
261 JA4_TAG => ctx
262 .conn
263 .ja4
264 .as_deref()
265 .and_then(|fingerprint| to_header_value(fingerprint.ja4())),
266 PROXY_ADD_FORWARDED_TAG => {
267 ctx.conn.remote_addr.as_deref().and_then(|remote_addr| {
268 let existing = session.get_header(HTTP_HEADER_X_FORWARDED_FOR);
270 let capacity =
271 existing.map(|v| v.as_bytes().len() + 2).unwrap_or(0)
272 + remote_addr.len();
273 let mut value_buf = BytesMut::with_capacity(capacity);
274 if let Some(existing) = existing {
275 value_buf.extend_from_slice(existing.as_bytes());
276 value_buf.extend_from_slice(b", ");
277 }
278 value_buf.extend_from_slice(remote_addr.as_bytes());
279 HeaderValue::from_bytes(&value_buf).ok()
280 })
281 },
282 _ => handle_special_headers(buf, session, ctx),
284 }
285}
286
287#[inline]
290fn handle_special_headers(
291 buf: &[u8],
292 session: &Session,
293 ctx: &Ctx,
294) -> Option<HeaderValue> {
295 if let Some(name) = buf.strip_prefix(b"$http_") {
297 let key = std::str::from_utf8(name).ok()?;
298 if key.contains('_') {
301 return session.get_header(key.replace('_', "-")).cloned();
302 }
303 return session.get_header(key).cloned();
304 }
305 if buf.starts_with(b"$") {
307 let var_name = std::str::from_utf8(&buf[1..]).ok()?;
308 return std::env::var(var_name)
310 .ok()
311 .and_then(|v| HeaderValue::from_str(&v).ok());
312 }
313 if buf.starts_with(b":") {
315 let key = std::str::from_utf8(&buf[1..]).ok()?;
316 let mut value = BytesMut::with_capacity(20);
318 ctx.append_log_value(&mut value, key);
319 if !value.is_empty() {
320 return HeaderValue::from_bytes(&value).ok();
322 }
323 }
324 None
326}
327
328pub fn get_remote_addr(session: &Session) -> Option<(String, u16)> {
330 session
331 .client_addr()
332 .and_then(|addr| addr.as_inet())
334 .map(|addr| (addr.ip().to_string(), addr.port()))
336}
337
338struct TrustedProxies {
341 nets: Vec<IpNet>,
342 ips: AHashSet<IpAddr>,
343}
344
345impl TrustedProxies {
346 fn parse(values: &[String]) -> Self {
349 let mut nets = Vec::new();
350 let mut ips = AHashSet::new();
351 for item in values {
352 if let Ok(net) = IpNet::from_str(item) {
353 nets.push(net);
354 } else if let Ok(ip) = IpAddr::from_str(item) {
355 ips.insert(ip);
356 }
357 }
358 Self { nets, ips }
359 }
360
361 fn contains(&self, peer: IpAddr) -> bool {
363 self.ips.contains(&peer)
364 || self.nets.iter().any(|net| net.contains(&peer))
365 }
366}
367
368static TRUSTED_PROXIES_ENABLED: AtomicBool = AtomicBool::new(false);
376static TRUSTED_PROXIES: ArcSwapOption<TrustedProxies> =
377 ArcSwapOption::const_empty();
378
379pub fn set_trusted_proxies(proxies: &Option<Vec<String>>) {
386 let parsed = match proxies {
387 Some(list) if !list.is_empty() => Some(TrustedProxies::parse(list)),
388 _ => None,
389 };
390 let enabled = parsed.is_some();
391 TRUSTED_PROXIES.store(parsed.map(Arc::new));
392 TRUSTED_PROXIES_ENABLED.store(enabled, Ordering::Relaxed);
394}
395
396fn is_trusted_proxy(peer: IpAddr) -> bool {
398 TRUSTED_PROXIES
399 .load()
400 .as_ref()
401 .is_some_and(|trusted| trusted.contains(peer))
402}
403
404#[inline]
409pub fn ensure_client_ip<'a>(session: &Session, ctx: &'a mut Ctx) -> &'a str {
410 if ctx.conn.client_ip.is_none() {
411 ctx.conn.client_ip = Some(get_client_ip(session));
412 }
413 ctx.conn.client_ip.as_deref().unwrap_or_default()
415}
416
417pub fn get_client_ip(session: &Session) -> String {
427 if TRUSTED_PROXIES_ENABLED.load(Ordering::Relaxed) {
430 let peer = session
433 .client_addr()
434 .and_then(|addr| addr.as_inet())
435 .map(|addr| addr.ip());
436 match peer {
437 Some(ip) if is_trusted_proxy(ip) => {},
438 Some(ip) => return ip.to_string(),
439 None => return String::new(),
440 }
441 }
442 if let Some(value) = session.get_header(HTTP_HEADER_X_FORWARDED_FOR) {
444 if let Ok(s) = value.to_str()
446 && let Some(ip) = s.split(',').next()
447 {
448 let trimmed_ip = ip.trim();
449 if !trimmed_ip.is_empty() {
450 return trimmed_ip.to_string();
451 }
452 }
453 }
454 if let Some(value) = session.get_header(HTTP_HEADER_X_REAL_IP) {
456 return value.to_str().unwrap_or_default().to_string();
457 }
458 if let Some((addr, _)) = get_remote_addr(session) {
460 return addr;
461 }
462 "".to_string()
464}
465
466pub fn get_req_header_value<'a>(
468 req_header: &'a RequestHeader,
469 key: &str,
470) -> Option<&'a str> {
471 if let Some(value) = req_header.headers.get(key) {
473 if let Ok(value) = value.to_str() {
475 return Some(value);
476 }
477 }
478 None
479}
480
481pub fn get_cookie_value<'a>(
483 req_header: &'a RequestHeader,
484 cookie_name: &str,
485) -> Option<&'a str> {
486 get_req_header_value(req_header, "cookie")?
488 .split(';')
490 .find_map(|item| {
492 item.trim()
495 .strip_prefix(cookie_name)?
496 .strip_prefix('=')
497 .or_else(|| {
498 let (k, v) = item.split_once('=')?;
500 if k.trim() == cookie_name {
501 Some(v.trim())
502 } else {
503 None
504 }
505 })
506 })
507}
508
509pub fn get_query_value<'a>(
511 req_header: &'a RequestHeader,
512 name: &str,
513) -> Option<&'a str> {
514 req_header
516 .uri
517 .query()?
518 .split('&')
520 .find_map(|item| {
522 let (k, v) = item.split_once('=')?;
524 if k == name { Some(v) } else { None }
526 })
527}
528
529pub fn remove_query_from_header(
533 req_header: &mut RequestHeader,
534 name: &str,
535) -> Result<(), http::uri::InvalidUri> {
536 let Some(query_str) = req_header.uri.query() else {
538 return Ok(());
539 };
540
541 let mut new_query = String::with_capacity(query_str.len());
544
545 for item in query_str.split('&') {
547 let key = item.split('=').next().unwrap_or(item);
549
550 if key != name {
552 if !new_query.is_empty() {
554 new_query.push('&');
555 }
556 new_query.push_str(item);
558 }
559 }
560
561 let path = req_header.uri.path();
563 let new_uri_str = if new_query.is_empty() {
565 Cow::Borrowed(path)
567 } else {
568 let mut s = String::with_capacity(path.len() + 1 + new_query.len());
570 let _ = write!(&mut s, "{path}?{new_query}");
572 Cow::Owned(s)
573 };
574
575 let new_uri = http::Uri::from_str(&new_uri_str)?;
577 req_header.set_uri(new_uri);
579
580 Ok(())
581}
582
583#[cfg(test)]
584mod tests {
585 use super::*;
586 use crate::{ConnectionInfo, UpstreamInfo, new_test_session};
587 use pretty_assertions::assert_eq;
588
589 #[test]
590 fn test_convert_headers() {
591 let headers = convert_headers(&[
592 "Content-Type: application/octet-stream".to_string(),
593 "X-Server: $hostname".to_string(),
594 "X-User: $USER".to_string(),
595 ])
596 .unwrap();
597 assert_eq!(3, headers.len());
598 assert_eq!("content-type", headers[0].0.to_string());
599 assert_eq!("application/octet-stream", headers[0].1.to_str().unwrap());
600 assert_eq!("x-server", headers[1].0.to_string());
601 assert_eq!(false, headers[1].1.to_str().unwrap().is_empty());
602 assert_eq!("x-user", headers[2].0.to_string());
603 assert_eq!(false, headers[2].1.to_str().unwrap().is_empty());
604 }
605
606 #[test]
607 fn test_static_value() {
608 assert_eq!(
609 "cache-control: private, no-store",
610 format!(
611 "{}: {}",
612 HTTP_HEADER_NO_STORE.0.to_string(),
613 HTTP_HEADER_NO_STORE.1.to_str().unwrap_or_default()
614 )
615 );
616
617 assert_eq!(
618 "cache-control: private, no-cache",
619 format!(
620 "{}: {}",
621 HTTP_HEADER_NO_CACHE.0.to_string(),
622 HTTP_HEADER_NO_CACHE.1.to_str().unwrap_or_default()
623 )
624 );
625
626 assert_eq!(
627 "content-type: application/json; charset=utf-8",
628 format!(
629 "{}: {}",
630 HTTP_HEADER_CONTENT_JSON.0.to_string(),
631 HTTP_HEADER_CONTENT_JSON.1.to_str().unwrap_or_default()
632 )
633 );
634
635 assert_eq!(
636 "content-type: text/html; charset=utf-8",
637 format!(
638 "{}: {}",
639 HTTP_HEADER_CONTENT_HTML.0.to_string(),
640 HTTP_HEADER_CONTENT_HTML.1.to_str().unwrap_or_default()
641 )
642 );
643
644 assert_eq!(
645 "transfer-encoding: chunked",
646 format!(
647 "{}: {}",
648 HTTP_HEADER_TRANSFER_CHUNKED.0.to_string(),
649 HTTP_HEADER_TRANSFER_CHUNKED.1.to_str().unwrap_or_default()
650 )
651 );
652
653 assert_eq!("x-request-id", HTTP_HEADER_NAME_X_REQUEST_ID.to_string());
654
655 assert_eq!(
656 "content-type: text/plain; charset=utf-8",
657 format!(
658 "{}: {}",
659 HTTP_HEADER_CONTENT_TEXT.0.to_string(),
660 HTTP_HEADER_CONTENT_TEXT.1.to_str().unwrap_or_default()
661 )
662 );
663 }
664
665 #[tokio::test]
666 async fn test_convert_header_value() {
667 let session =
668 new_test_session(&["Host: pingap.io"], "/vicanso/pingap?size=1")
669 .await;
670 let default_state = Ctx {
671 upstream: UpstreamInfo {
672 address: "10.1.1.3:4123".to_string(),
673 ..Default::default()
674 },
675 conn: ConnectionInfo {
676 id: 102,
677 remote_addr: Some("10.1.1.1".to_string()),
678 remote_port: Some(6000),
679 server_addr: Some("10.1.1.2".to_string()),
680 server_port: Some(6001),
681 tls_version: Some("tls1.3".into()),
682 ..Default::default()
683 },
684 ..Default::default()
685 };
686
687 let value = convert_header_value(
688 &HeaderValue::from_str("$host").unwrap(),
689 &session,
690 &Ctx {
691 ..Default::default()
692 },
693 );
694 assert_eq!(true, value.is_some());
695 assert_eq!("pingap.io", value.unwrap().to_str().unwrap());
696
697 let value = convert_header_value(
698 &HeaderValue::from_str("$scheme").unwrap(),
699 &session,
700 &Ctx {
701 ..Default::default()
702 },
703 );
704 assert_eq!(true, value.is_some());
705 assert_eq!("http", value.unwrap().to_str().unwrap());
706 let value = convert_header_value(
707 &HeaderValue::from_str("$scheme").unwrap(),
708 &session,
709 &default_state,
710 );
711 assert_eq!(true, value.is_some());
712 assert_eq!("https", value.unwrap().to_str().unwrap());
713
714 let value = convert_header_value(
715 &HeaderValue::from_str("$remote_addr").unwrap(),
716 &session,
717 &default_state,
718 );
719 assert_eq!(true, value.is_some());
720 assert_eq!("10.1.1.1", value.unwrap().to_str().unwrap());
721
722 let value = convert_header_value(
723 &HeaderValue::from_str("$remote_port").unwrap(),
724 &session,
725 &default_state,
726 );
727 assert_eq!(true, value.is_some());
728 assert_eq!("6000", value.unwrap().to_str().unwrap());
729
730 let value = convert_header_value(
731 &HeaderValue::from_str("$server_addr").unwrap(),
732 &session,
733 &default_state,
734 );
735 assert_eq!(true, value.is_some());
736 assert_eq!("10.1.1.2", value.unwrap().to_str().unwrap());
737
738 let value = convert_header_value(
739 &HeaderValue::from_str("$server_port").unwrap(),
740 &session,
741 &default_state,
742 );
743 assert_eq!(true, value.is_some());
744 assert_eq!("6001", value.unwrap().to_str().unwrap());
745
746 let value = convert_header_value(
747 &HeaderValue::from_str("$upstream_addr").unwrap(),
748 &session,
749 &default_state,
750 );
751 assert_eq!(true, value.is_some());
752 assert_eq!("10.1.1.3:4123", value.unwrap().to_str().unwrap());
753
754 let value = convert_header_value(
755 &HeaderValue::from_str(":connection_id").unwrap(),
756 &session,
757 &default_state,
758 );
759 assert_eq!(true, value.is_some());
760 assert_eq!("102", value.unwrap().to_str().unwrap());
761
762 let session = new_test_session(
763 &["X-Forwarded-For: 1.1.1.1, 2.2.2.2"],
764 "/vicanso/pingap?size=1",
765 )
766 .await;
767 let value = convert_header_value(
768 &HeaderValue::from_str("$proxy_add_x_forwarded_for").unwrap(),
769 &session,
770 &Ctx {
771 conn: ConnectionInfo {
772 remote_addr: Some("10.1.1.1".to_string()),
773 ..Default::default()
774 },
775 ..Default::default()
776 },
777 );
778 assert_eq!(true, value.is_some());
779 assert_eq!(
780 "1.1.1.1, 2.2.2.2, 10.1.1.1",
781 value.unwrap().to_str().unwrap()
782 );
783
784 let session = new_test_session(&[""], "/vicanso/pingap?size=1").await;
785 let value = convert_header_value(
786 &HeaderValue::from_str("$proxy_add_x_forwarded_for").unwrap(),
787 &session,
788 &Ctx {
789 conn: ConnectionInfo {
790 remote_addr: Some("10.1.1.1".to_string()),
791 ..Default::default()
792 },
793 ..Default::default()
794 },
795 );
796 assert_eq!(true, value.is_some());
797 assert_eq!("10.1.1.1", value.unwrap().to_str().unwrap());
798
799 let session = new_test_session(&[""], "/vicanso/pingap?size=1").await;
800 let value = convert_header_value(
801 &HeaderValue::from_str("$upstream_addr").unwrap(),
802 &session,
803 &Ctx {
804 upstream: UpstreamInfo {
805 address: "10.1.1.1:8001".to_string(),
806 ..Default::default()
807 },
808 ..Default::default()
809 },
810 );
811 assert_eq!(true, value.is_some());
812 assert_eq!("10.1.1.1:8001", value.unwrap().to_str().unwrap());
813
814 let session = new_test_session(&[""], "/vicanso/pingap?size=1").await;
817 let mut ctx = Ctx::default();
818 for tag in ["$ja4", ":ja4"] {
819 let value = convert_header_value(
820 &HeaderValue::from_str(tag).unwrap(),
821 &session,
822 &ctx,
823 );
824 assert_eq!(true, value.is_none(), "{tag}");
825 }
826 ctx.conn.ja4 = Some(std::sync::Arc::new(
827 crate::Ja4Fingerprint::from_client_hello(
828 &crate::ja4::testing::spec_example_body(),
829 )
830 .unwrap(),
831 ));
832 for tag in ["$ja4", ":ja4"] {
833 let value = convert_header_value(
834 &HeaderValue::from_str(tag).unwrap(),
835 &session,
836 &ctx,
837 );
838 assert_eq!(
839 "t13d1516h2_8daaf6152771_e5627efa2ab1",
840 value.unwrap().to_str().unwrap(),
841 "{tag}"
842 );
843 }
844 assert_eq!(
846 "$ja4",
847 resolve_static_header_value(HeaderValue::from_static("$ja4"))
848 );
849
850 let session = new_test_session(
851 &["Origin: https://github.com"],
852 "/vicanso/pingap?size=1",
853 )
854 .await;
855 let value = convert_header_value(
856 &HeaderValue::from_str("$http_origin").unwrap(),
857 &session,
858 &Ctx::default(),
859 );
860 assert_eq!(true, value.is_some());
861 assert_eq!("https://github.com", value.unwrap().to_str().unwrap());
862
863 let session = new_test_session(
864 &["Origin: https://github.com"],
865 "/vicanso/pingap?size=1",
866 )
867 .await;
868 let value = convert_header_value(
869 &HeaderValue::from_str("$hostname").unwrap(),
870 &session,
871 &Ctx::default(),
872 );
873 assert_eq!(true, value.is_some());
874
875 let session = new_test_session(
876 &["Origin: https://github.com"],
877 "/vicanso/pingap?size=1",
878 )
879 .await;
880 let value = convert_header_value(
881 &HeaderValue::from_str("$HOME").unwrap(),
882 &session,
883 &Ctx::default(),
884 );
885 assert_eq!(true, value.is_some());
886
887 let session = new_test_session(
888 &["Origin: https://github.com"],
889 "/vicanso/pingap?size=1",
890 )
891 .await;
892 let value = convert_header_value(
893 &HeaderValue::from_str("UUID").unwrap(),
894 &session,
895 &Ctx::default(),
896 );
897 assert_eq!(false, value.is_some());
898 }
899
900 #[tokio::test]
901 async fn test_get_host() {
902 let session =
903 new_test_session(&["Host: pingap.io"], "/vicanso/pingap?size=1")
904 .await;
905 assert_eq!(get_host(session.req_header()), Some("pingap.io"));
906 }
907
908 #[test]
909 fn test_remove_query_from_header() {
910 let mut req =
911 RequestHeader::build("GET", b"/?apikey=123", None).unwrap();
912 remove_query_from_header(&mut req, "apikey").unwrap();
913 assert_eq!("/", req.uri.to_string());
914
915 let mut req =
916 RequestHeader::build("GET", b"/?apikey=123&name=pingap", None)
917 .unwrap();
918 remove_query_from_header(&mut req, "apikey").unwrap();
919 assert_eq!("/?name=pingap", req.uri.to_string());
920 }
921
922 #[tokio::test]
923 async fn test_get_client_ip() {
924 let session = new_test_session(
925 &["X-Forwarded-For:192.168.1.1"],
926 "/vicanso/pingap?size=1",
927 )
928 .await;
929 assert_eq!(get_client_ip(&session), "192.168.1.1");
930
931 let session = new_test_session(
932 &["X-Real-Ip:192.168.1.2"],
933 "/vicanso/pingap?size=1",
934 )
935 .await;
936 assert_eq!(get_client_ip(&session), "192.168.1.2");
937
938 set_trusted_proxies(&Some(vec!["10.0.0.0/8".to_string()]));
942 let session = new_test_session(
943 &["X-Forwarded-For:192.168.1.1"],
944 "/vicanso/pingap?size=1",
945 )
946 .await;
947 assert_ne!(get_client_ip(&session), "192.168.1.1");
948
949 set_trusted_proxies(&None);
951 let session = new_test_session(
952 &["X-Forwarded-For:192.168.1.1"],
953 "/vicanso/pingap?size=1",
954 )
955 .await;
956 assert_eq!(get_client_ip(&session), "192.168.1.1");
957 }
958
959 #[tokio::test]
960 async fn test_get_header_value() {
961 let session =
962 new_test_session(&["Host: pingap.io"], "/vicanso/pingap?size=1")
963 .await;
964 assert_eq!(
965 get_req_header_value(session.req_header(), "Host"),
966 Some("pingap.io")
967 );
968 }
969
970 #[tokio::test]
971 async fn test_get_cookie_value() {
972 let session = new_test_session(
973 &["Cookie: name=pingap"],
974 "/vicanso/pingap?size=1",
975 )
976 .await;
977 assert_eq!(
978 get_cookie_value(session.req_header(), "name"),
979 Some("pingap")
980 );
981 }
982
983 #[tokio::test]
984 async fn test_get_query_value() {
985 let session = new_test_session(
986 &["X-Forwarded-For:192.168.1.1"],
987 "/vicanso/pingap?size=1",
988 )
989 .await;
990 assert_eq!(get_query_value(session.req_header(), "size"), Some("1"));
991 }
992
993 #[test]
996 fn test_convert_header_edge_cases() {
997 assert!(convert_header("").unwrap().is_none());
999 assert!(convert_header("no-colon").unwrap().is_none());
1001 assert!(convert_header("Invalid Name: value").is_err());
1003 assert!(convert_header("Valid-Name: invalid\r\nvalue").is_err());
1005 }
1006
1007 #[test]
1009 fn test_get_host_variants() {
1010 let uri_string = "http://user:pass@authority.com/path";
1012
1013 let uri = http::Uri::from_str(uri_string).unwrap();
1015 let mut req_with_authority =
1016 RequestHeader::build("GET", b"/path", None).unwrap();
1017 req_with_authority.set_uri(uri);
1018 assert_eq!(get_host(&req_with_authority), Some("authority.com"));
1019
1020 let mut req_with_host_header =
1022 RequestHeader::build("GET", b"/path", None).unwrap();
1023 req_with_host_header
1024 .insert_header("Host", "header-host.com:8080")
1025 .unwrap();
1026 assert_eq!(get_host(&req_with_host_header), Some("header-host.com"));
1027
1028 let req_no_host = RequestHeader::build("GET", b"/path", None).unwrap();
1030 assert_eq!(get_host(&req_no_host), None);
1031
1032 for (host, expected) in [
1034 ("[::1]:8080", "[::1]"),
1035 ("[::1]", "[::1]"),
1036 ("[2001:db8::1]:443", "[2001:db8::1]"),
1037 ("example.com", "example.com"),
1038 ("example.com:8080", "example.com"),
1039 ] {
1040 let mut req = RequestHeader::build("GET", b"/path", None).unwrap();
1041 req.insert_header("Host", host).unwrap();
1042 assert_eq!(get_host(&req), Some(expected), "{host}");
1043 }
1044 }
1045
1046 #[test]
1047 fn test_resolve_static_header_value() {
1048 let resolve = |value: &str| {
1049 resolve_static_header_value(HeaderValue::from_str(value).unwrap())
1050 };
1051 assert_eq!(get_hostname(), resolve("$hostname").to_str().unwrap());
1052 let home = std::env::var("HOME").unwrap();
1053 assert_eq!(home, resolve("$HOME").to_str().unwrap());
1054 for value in [
1057 "$host",
1058 "$remote_addr",
1059 "$proxy_add_x_forwarded_for",
1060 "$http_origin",
1061 ":connection_id",
1062 "plain",
1063 "$PINGAP_TEST_UNSET_VARIABLE",
1064 ] {
1065 assert_eq!(value, resolve(value).to_str().unwrap());
1066 }
1067 }
1068
1069 #[tokio::test]
1071 async fn test_http_variable_with_underscores() {
1072 let session = new_test_session(
1073 &["User-Agent: pingap/1", "X-Env-Name: prod"],
1074 "/",
1075 )
1076 .await;
1077 for (variable, expected) in [
1078 ("$http_user_agent", Some("pingap/1")),
1079 ("$http_user-agent", Some("pingap/1")),
1080 ("$http_x_env_name", Some("prod")),
1081 ("$http_missing_header", None),
1082 ] {
1083 let value = convert_header_value(
1084 &HeaderValue::from_str(variable).unwrap(),
1085 &session,
1086 &Ctx::default(),
1087 );
1088 assert_eq!(
1089 expected,
1090 value.as_ref().and_then(|v| v.to_str().ok()),
1091 "{variable}"
1092 );
1093 }
1094 }
1095
1096 #[test]
1098 fn test_get_cookie_value_advanced() {
1099 let mut req = RequestHeader::build("GET", b"/", None).unwrap();
1100 req.insert_header("Cookie", "id=123; session=abc; theme=dark")
1101 .unwrap();
1102
1103 assert_eq!(get_cookie_value(&req, "session"), Some("abc"));
1104 assert_eq!(get_cookie_value(&req, "id"), Some("123"));
1105 assert_eq!(get_cookie_value(&req, "theme"), Some("dark"));
1106 assert_eq!(get_cookie_value(&req, "lang"), None);
1108 assert_eq!(get_cookie_value(&req, "the"), None);
1110 }
1111
1112 #[test]
1113 fn test_remove_query_from_header_variants() {
1114 let mut req =
1116 RequestHeader::build("GET", b"/path?key=val", None).unwrap();
1117 remove_query_from_header(&mut req, "key").unwrap();
1118 assert_eq!(req.uri.to_string(), "/path");
1119
1120 let mut req =
1122 RequestHeader::build("GET", b"/path?key1=val1&key2=val2", None)
1123 .unwrap();
1124 remove_query_from_header(&mut req, "key1").unwrap();
1125 assert_eq!(req.uri.to_string(), "/path?key2=val2");
1126
1127 let mut req =
1129 RequestHeader::build("GET", b"/path?key1=val1&key2=val2", None)
1130 .unwrap();
1131 remove_query_from_header(&mut req, "key2").unwrap();
1132 assert_eq!(req.uri.to_string(), "/path?key1=val1");
1133
1134 let mut req =
1136 RequestHeader::build("GET", b"/path?key1=v1&key2=v2&key3=v3", None)
1137 .unwrap();
1138 remove_query_from_header(&mut req, "key2").unwrap();
1139 assert_eq!(req.uri.to_string(), "/path?key1=v1&key3=v3");
1140
1141 let mut req =
1143 RequestHeader::build("GET", b"/path?key=val", None).unwrap();
1144 remove_query_from_header(&mut req, "nonexistent").unwrap();
1145 assert_eq!(req.uri.to_string(), "/path?key=val");
1146
1147 let mut req = RequestHeader::build("GET", b"/path", None).unwrap();
1149 remove_query_from_header(&mut req, "key").unwrap();
1150 assert_eq!(req.uri.to_string(), "/path");
1151 }
1152}