1use http::HeaderMap;
4use regex::Regex;
5use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7use std::sync::{LazyLock, RwLock};
8use uuid::Uuid;
9
10#[derive(Clone, Debug, Serialize, Deserialize, Default)]
12#[non_exhaustive]
13pub struct ProxyConfig {
14 #[serde(default)]
16 pub server: ServerConfig,
17
18 #[serde(default)]
20 pub tls: TlsConfig,
21
22 #[serde(default)]
24 pub rate_limit: RateLimitDefaults,
25
26 #[serde(default)]
28 pub routes: Vec<Route>,
29
30 #[serde(default)]
32 pub upstreams: Vec<Upstream>,
33}
34
35#[derive(Clone, Debug, Serialize, Deserialize)]
37#[non_exhaustive]
38pub struct ServerConfig {
39 #[serde(default = "default_http_host")]
41 pub http_host: String,
42
43 #[serde(default = "default_http_port")]
45 pub http_port: u16,
46
47 #[serde(default)]
55 pub http2_port: Option<u16>,
56
57 #[serde(default = "default_https_host")]
59 pub https_host: String,
60
61 #[serde(default = "default_https_port")]
63 pub https_port: u16,
64
65 #[serde(default)]
67 pub https_enabled: bool,
68
69 #[serde(default = "default_true")]
71 pub database_config: bool,
72
73 pub config_file: Option<String>,
75
76 #[serde(default = "default_max_connections")]
85 pub max_connections: usize,
86
87 #[serde(default = "default_header_read_timeout_secs")]
94 pub header_read_timeout_secs: u64,
95
96 #[serde(default = "default_idle_timeout_secs")]
104 pub idle_timeout_secs: u64,
105
106 #[serde(default = "default_http2_keepalive_interval_secs")]
112 pub http2_keepalive_interval_secs: u64,
113
114 #[serde(default = "default_http2_keepalive_timeout_secs")]
117 pub http2_keepalive_timeout_secs: u64,
118
119 #[serde(default = "default_tunnel_idle_timeout_secs")]
126 pub tunnel_idle_timeout_secs: u64,
127
128 #[serde(default = "default_allowed_upgrade_protocols")]
139 pub allowed_upgrade_protocols: Vec<String>,
140
141 #[serde(default)]
150 pub trust_forwarded_headers: bool,
151}
152
153impl Default for ServerConfig {
154 fn default() -> Self {
155 Self {
156 http_host: default_http_host(),
157 http_port: default_http_port(),
158 http2_port: None,
159 https_host: default_https_host(),
160 https_port: default_https_port(),
161 https_enabled: false,
162 database_config: true,
163 config_file: None,
164 max_connections: default_max_connections(),
165 header_read_timeout_secs: default_header_read_timeout_secs(),
166 idle_timeout_secs: default_idle_timeout_secs(),
167 http2_keepalive_interval_secs: default_http2_keepalive_interval_secs(),
168 http2_keepalive_timeout_secs: default_http2_keepalive_timeout_secs(),
169 tunnel_idle_timeout_secs: default_tunnel_idle_timeout_secs(),
170 allowed_upgrade_protocols: default_allowed_upgrade_protocols(),
171 trust_forwarded_headers: false,
172 }
173 }
174}
175
176#[derive(Clone, Debug, Serialize, Deserialize)]
178#[non_exhaustive]
179pub struct TlsConfig {
180 #[serde(default)]
182 pub acme_enabled: bool,
183
184 #[serde(default = "default_acme_directory")]
186 pub acme_directory: String,
187
188 pub acme_email: Option<String>,
190
191 #[serde(default = "default_cert_dir")]
193 pub cert_dir: String,
194
195 #[serde(default)]
197 pub acme_staging: bool,
198
199 #[serde(default)]
205 pub cert_file: Option<String>,
206
207 #[serde(default)]
209 pub key_file: Option<String>,
210
211 #[serde(default)]
218 pub acme_directory_url: Option<String>,
219
220 #[serde(default)]
225 pub acme_root_pem: Option<String>,
226}
227
228impl Default for TlsConfig {
229 fn default() -> Self {
230 Self {
231 acme_enabled: false,
232 acme_directory: default_acme_directory(),
233 acme_email: None,
234 cert_dir: default_cert_dir(),
235 acme_staging: false,
236 cert_file: None,
237 key_file: None,
238 acme_directory_url: None,
239 acme_root_pem: None,
240 }
241 }
242}
243
244#[derive(Clone, Debug, Serialize, Deserialize)]
246#[non_exhaustive]
247pub struct RateLimitDefaults {
248 #[serde(default = "default_rate_limit_requests")]
250 pub requests: u32,
251
252 #[serde(default = "default_rate_limit_window")]
254 pub window_secs: u32,
255
256 #[serde(default = "default_burst")]
258 pub burst: u32,
259
260 #[serde(default = "default_max_tracked_keys")]
268 pub max_tracked_keys: usize,
269}
270
271impl Default for RateLimitDefaults {
272 fn default() -> Self {
273 Self {
274 requests: default_rate_limit_requests(),
275 window_secs: default_rate_limit_window(),
276 burst: default_burst(),
277 max_tracked_keys: default_max_tracked_keys(),
278 }
279 }
280}
281
282#[derive(Clone, Debug, Serialize, Deserialize)]
284pub struct Route {
285 pub id: Option<Uuid>,
287
288 pub name: String,
290
291 pub description: Option<String>,
293
294 #[serde(default, rename = "match")]
296 pub match_: RouteMatch,
297
298 #[serde(default = "default_priority")]
300 pub priority: i32,
301
302 pub upstream: String,
304
305 #[serde(default)]
307 pub strip_path: bool,
308
309 #[serde(default)]
311 pub add_headers: HashMap<String, String>,
312
313 #[serde(default)]
315 pub remove_headers: Vec<String>,
316
317 pub rate_limit: Option<RouteRateLimit>,
319
320 #[serde(default = "default_timeout")]
322 pub timeout_secs: u32,
323
324 #[serde(default)]
326 pub retry_count: u32,
327
328 #[serde(default = "default_true")]
330 pub enabled: bool,
331}
332
333#[derive(Clone, Debug, Default, Serialize, Deserialize)]
335pub struct RouteMatch {
336 pub host: Option<String>,
338
339 pub path: Option<String>,
341
342 #[serde(default)]
344 pub path_type: PathMatchType,
345
346 #[serde(default)]
348 pub headers: HashMap<String, String>,
349
350 pub methods: Option<Vec<String>>,
352}
353
354static REGEX_CACHE: LazyLock<RwLock<HashMap<String, Option<Regex>>>> =
369 LazyLock::new(|| RwLock::new(HashMap::new()));
370
371fn compiled_regex(pattern: &str) -> Option<Regex> {
375 if let Ok(cache) = REGEX_CACHE.read() {
376 if let Some(entry) = cache.get(pattern) {
377 return entry.clone();
378 }
379 }
380
381 let compiled = match Regex::new(pattern) {
382 Ok(re) => Some(re),
383 Err(error) => {
384 tracing::warn!(
385 %pattern, %error,
386 "route path regex does not compile; the route matches nothing"
387 );
388 None
389 }
390 };
391
392 if let Ok(mut cache) = REGEX_CACHE.write() {
393 cache.insert(pattern.to_owned(), compiled.clone());
394 }
395 compiled
396}
397
398impl RouteMatch {
399 pub fn matches(&self, host: &str, path: &str, method: &str, headers: &HeaderMap) -> bool {
412 self.host_matches(host)
413 && self.path_matches(path)
414 && self.method_matches(method)
415 && self.headers_match(headers)
416 }
417
418 fn host_matches(&self, host: &str) -> bool {
422 let Some(pattern) = self.host.as_deref() else {
423 return true;
424 };
425 let pattern = pattern.trim().to_ascii_lowercase();
426 let host = host.trim().to_ascii_lowercase();
427
428 if pattern == "*" || pattern.is_empty() {
429 return true;
430 }
431 if pattern == host {
432 return true;
433 }
434 match pattern.strip_prefix("*.") {
437 Some(suffix) if !suffix.is_empty() => host
438 .strip_suffix(&format!(".{suffix}"))
439 .is_some_and(|label| !label.is_empty() && !label.contains('.')),
440 _ => false,
441 }
442 }
443
444 fn path_matches(&self, path: &str) -> bool {
450 let Some(pattern) = self.path.as_deref() else {
451 return true;
452 };
453 match self.path_type {
454 PathMatchType::Prefix => path_has_prefix(path, pattern),
455 PathMatchType::Exact => path == pattern,
456 PathMatchType::Regex => match compiled_regex(pattern) {
457 Some(re) => re.is_match(path),
458 None => false,
459 },
460 }
461 }
462
463 fn method_matches(&self, method: &str) -> bool {
465 match self.methods.as_deref() {
466 None | Some([]) => true,
467 Some(allowed) => allowed.iter().any(|m| m.eq_ignore_ascii_case(method)),
468 }
469 }
470
471 fn headers_match(&self, headers: &HeaderMap) -> bool {
474 self.headers.iter().all(|(name, expected)| {
475 headers
476 .get(name.as_str())
477 .and_then(|value| value.to_str().ok())
478 .is_some_and(|value| value == expected)
479 })
480 }
481}
482
483pub fn path_has_prefix(path: &str, prefix: &str) -> bool {
491 if prefix.is_empty() || prefix.ends_with('/') {
492 return path.starts_with(prefix);
493 }
494 match path.strip_prefix(prefix) {
495 Some(rest) => rest.is_empty() || rest.starts_with('/'),
496 None => false,
497 }
498}
499
500#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
502#[serde(rename_all = "lowercase")]
503pub enum PathMatchType {
504 #[default]
506 Prefix,
507 Exact,
509 Regex,
511}
512
513#[derive(Clone, Debug, Serialize, Deserialize)]
515pub struct RouteRateLimit {
516 pub requests: u32,
518 pub window_secs: u32,
520 #[serde(default)]
522 pub key: RateLimitKey,
523}
524
525#[derive(Clone, Debug, Default, Serialize, Deserialize)]
527#[serde(rename_all = "snake_case")]
528pub enum RateLimitKey {
529 #[default]
531 ClientIp,
532 Header(String),
534 Route,
536}
537
538#[derive(Clone, Debug, Serialize, Deserialize)]
540pub struct Upstream {
541 pub id: Option<Uuid>,
543
544 pub name: String,
546
547 pub description: Option<String>,
549
550 #[serde(default)]
552 pub lb_strategy: LoadBalanceStrategy,
553
554 #[serde(default)]
556 pub backends: Vec<Backend>,
557
558 #[serde(default)]
560 pub health_check: HealthCheckConfig,
561
562 #[serde(default = "default_true")]
564 pub enabled: bool,
565}
566
567impl Upstream {
568 pub fn resolved_id(&self) -> Uuid {
577 self.id
578 .unwrap_or_else(|| Uuid::new_v5(&Uuid::NAMESPACE_OID, self.name.as_bytes()))
579 }
580}
581
582#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
584#[serde(rename_all = "snake_case")]
585pub enum LoadBalanceStrategy {
586 #[default]
588 RoundRobin,
589 LeastConnections,
591 Weighted,
593 Random,
595 Sticky,
597}
598
599#[derive(Clone, Debug, Serialize, Deserialize)]
601pub struct Backend {
602 pub id: Option<Uuid>,
604
605 pub address: String,
607
608 #[serde(default = "default_scheme")]
610 pub scheme: String,
611
612 #[serde(default = "default_weight")]
614 pub weight: u32,
615
616 #[serde(default = "default_true")]
618 pub enabled: bool,
619
620 #[serde(default)]
627 pub http_version: UpstreamHttpVersion,
628}
629
630#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
632#[serde(rename_all = "lowercase")]
633pub enum UpstreamHttpVersion {
634 #[default]
636 #[serde(alias = "http1", alias = "http/1.1")]
637 Http11,
638 #[serde(alias = "http2", alias = "h2")]
640 H2c,
641}
642
643#[derive(Clone, Debug, Serialize, Deserialize)]
645#[non_exhaustive]
646pub struct HealthCheckConfig {
647 #[serde(default = "default_true")]
649 pub enabled: bool,
650
651 #[serde(default = "default_health_path")]
653 pub path: String,
654
655 #[serde(default = "default_health_interval")]
657 pub interval_secs: u32,
658
659 #[serde(default = "default_health_timeout")]
661 pub timeout_secs: u32,
662
663 #[serde(default = "default_healthy_threshold")]
665 pub healthy_threshold: u32,
666
667 #[serde(default = "default_unhealthy_threshold")]
669 pub unhealthy_threshold: u32,
670}
671
672impl Default for HealthCheckConfig {
673 fn default() -> Self {
674 Self {
675 enabled: true,
676 path: default_health_path(),
677 interval_secs: default_health_interval(),
678 timeout_secs: default_health_timeout(),
679 healthy_threshold: default_healthy_threshold(),
680 unhealthy_threshold: default_unhealthy_threshold(),
681 }
682 }
683}
684
685fn default_http_host() -> String {
687 "0.0.0.0".into()
688}
689fn default_http_port() -> u16 {
690 8080
691}
692fn default_https_host() -> String {
693 "0.0.0.0".into()
694}
695fn default_https_port() -> u16 {
696 8443
697}
698fn default_true() -> bool {
699 true
700}
701fn default_acme_directory() -> String {
702 "https://acme-v02.api.letsencrypt.org/directory".into()
703}
704fn default_cert_dir() -> String {
705 "./certs".into()
706}
707fn default_rate_limit_requests() -> u32 {
708 1000
709}
710fn default_rate_limit_window() -> u32 {
711 60
712}
713fn default_burst() -> u32 {
714 50
715}
716fn default_max_tracked_keys() -> usize {
717 100_000
718}
719fn default_max_connections() -> usize {
720 10_000
721}
722fn default_header_read_timeout_secs() -> u64 {
723 30
724}
725fn default_idle_timeout_secs() -> u64 {
726 60
727}
728fn default_http2_keepalive_interval_secs() -> u64 {
729 20
730}
731fn default_http2_keepalive_timeout_secs() -> u64 {
732 20
733}
734fn default_tunnel_idle_timeout_secs() -> u64 {
735 300
736}
737fn default_allowed_upgrade_protocols() -> Vec<String> {
738 vec!["websocket".into()]
739}
740fn default_priority() -> i32 {
741 100
742}
743fn default_timeout() -> u32 {
744 30
745}
746fn default_scheme() -> String {
747 "http".into()
748}
749fn default_weight() -> u32 {
750 100
751}
752fn default_health_path() -> String {
753 "/health".into()
754}
755fn default_health_interval() -> u32 {
756 10
757}
758fn default_health_timeout() -> u32 {
759 5
760}
761fn default_healthy_threshold() -> u32 {
762 2
763}
764fn default_unhealthy_threshold() -> u32 {
765 3
766}
767
768#[derive(Clone, Debug, Serialize, Deserialize, Default)]
770pub struct AcmeConfig {
771 #[serde(default)]
773 pub enabled: bool,
774
775 pub email: Option<String>,
777
778 #[serde(default)]
780 pub staging: bool,
781
782 #[serde(default)]
784 pub domains: Vec<String>,
785}
786
787#[cfg(test)]
788mod upstream_version_tests {
789 use super::*;
790
791 fn header_map(pairs: &[(&str, &str)]) -> HeaderMap {
792 let mut map = HeaderMap::new();
793 for (name, value) in pairs {
794 map.insert(
795 http::header::HeaderName::from_bytes(name.as_bytes()).unwrap(),
796 http::HeaderValue::from_str(value).unwrap(),
797 );
798 }
799 map
800 }
801
802 fn any() -> RouteMatch {
804 RouteMatch::default()
805 }
806
807 #[test]
808 fn an_empty_match_matches_anything() {
809 let m = any();
810 assert!(m.matches("example.com", "/anything", "GET", &HeaderMap::new()));
811 assert!(m.matches("", "/", "DELETE", &HeaderMap::new()));
812 }
813
814 #[test]
815 fn a_prefix_path_matches_by_prefix() {
816 let m = RouteMatch {
817 path: Some("/v1".into()),
818 ..any()
819 };
820 assert!(m.matches("h", "/v1", "GET", &HeaderMap::new()));
821 assert!(m.matches("h", "/v1/users", "GET", &HeaderMap::new()));
822 assert!(!m.matches("h", "/v2", "GET", &HeaderMap::new()));
823 }
824
825 #[test]
826 fn a_prefix_matches_on_segment_boundaries_only() {
827 let m = RouteMatch {
830 path: Some("/public".into()),
831 ..any()
832 };
833 assert!(m.matches("h", "/public", "GET", &HeaderMap::new()));
834 assert!(m.matches("h", "/public/", "GET", &HeaderMap::new()));
835 assert!(m.matches("h", "/public/a/b", "GET", &HeaderMap::new()));
836 assert!(!m.matches("h", "/publicity", "GET", &HeaderMap::new()));
837 assert!(!m.matches("h", "/public-admin", "GET", &HeaderMap::new()));
838 assert!(!m.matches("h", "/publi", "GET", &HeaderMap::new()));
839 }
840
841 #[test]
842 fn a_prefix_with_a_trailing_slash_keeps_its_meaning() {
843 let m = RouteMatch {
844 path: Some("/public/".into()),
845 ..any()
846 };
847 assert!(m.matches("h", "/public/", "GET", &HeaderMap::new()));
848 assert!(m.matches("h", "/public/x", "GET", &HeaderMap::new()));
849 assert!(!m.matches("h", "/public", "GET", &HeaderMap::new()));
850
851 let root = RouteMatch {
852 path: Some("/".into()),
853 ..any()
854 };
855 assert!(root.matches("h", "/", "GET", &HeaderMap::new()));
856 assert!(root.matches("h", "/anything/at/all", "GET", &HeaderMap::new()));
857 }
858
859 #[test]
860 fn the_new_server_settings_have_defaults() {
861 let server: ServerConfig = toml::from_str("http_port = 8080").unwrap();
863 assert_eq!(server.max_connections, 10_000);
864 assert_eq!(server.header_read_timeout_secs, 30);
865 assert_eq!(server.idle_timeout_secs, 60);
866 assert_eq!(server.tunnel_idle_timeout_secs, 300);
867 assert_eq!(server.allowed_upgrade_protocols, vec!["websocket"]);
868 assert!(!server.trust_forwarded_headers);
869
870 let limits: RateLimitDefaults = toml::from_str("requests = 10").unwrap();
871 assert_eq!(limits.max_tracked_keys, 100_000);
872 }
873
874 #[test]
875 fn an_exact_path_does_not_match_a_longer_one() {
876 let m = RouteMatch {
879 path: Some("/health".into()),
880 path_type: PathMatchType::Exact,
881 ..any()
882 };
883 assert!(m.matches("h", "/health", "GET", &HeaderMap::new()));
884 assert!(!m.matches("h", "/health-internal", "GET", &HeaderMap::new()));
885 assert!(!m.matches("h", "/health/deep", "GET", &HeaderMap::new()));
886 }
887
888 #[test]
889 fn a_regex_path_matches_the_pattern() {
890 let m = RouteMatch {
891 path: Some(r"^/v[0-9]+/users$".into()),
892 path_type: PathMatchType::Regex,
893 ..any()
894 };
895 assert!(m.matches("h", "/v1/users", "GET", &HeaderMap::new()));
896 assert!(m.matches("h", "/v22/users", "GET", &HeaderMap::new()));
897 assert!(!m.matches("h", "/v1/users/1", "GET", &HeaderMap::new()));
898 assert!(!m.matches("h", "/users", "GET", &HeaderMap::new()));
899 }
900
901 #[test]
902 fn a_regex_is_compiled_once_and_reused() {
903 let pattern = format!("^/cache-probe-{}/[0-9]+$", uuid::Uuid::new_v4());
907 let m = RouteMatch {
908 path: Some(pattern.clone()),
909 path_type: PathMatchType::Regex,
910 ..any()
911 };
912
913 assert!(!REGEX_CACHE.read().unwrap().contains_key(&pattern));
914 assert!(m.matches("h", "/nope", "GET", &HeaderMap::new()) || true);
915 assert!(
916 REGEX_CACHE.read().unwrap().contains_key(&pattern),
917 "the compiled regex was not cached"
918 );
919
920 let path = pattern
922 .trim_start_matches('^')
923 .trim_end_matches("/[0-9]+$")
924 .to_string();
925 assert!(m.matches("h", &format!("{path}/42"), "GET", &HeaderMap::new()));
926 }
927
928 #[test]
929 fn a_broken_regex_is_cached_as_a_failure() {
930 let pattern = format!("/broken-{}/(unclosed", uuid::Uuid::new_v4());
932 let m = RouteMatch {
933 path: Some(pattern.clone()),
934 path_type: PathMatchType::Regex,
935 ..any()
936 };
937 assert!(!m.matches("h", "/anything", "GET", &HeaderMap::new()));
938 let cache = REGEX_CACHE.read().unwrap();
939 let entry = cache
940 .get(&pattern)
941 .expect("a pattern that does not compile should still be cached");
942 assert!(
943 entry.is_none(),
944 "it should be cached as a failure, not as a compiled regex"
945 );
946 }
947
948 #[test]
949 fn a_regex_that_does_not_compile_matches_nothing() {
950 let m = RouteMatch {
953 path: Some("/v1/(unclosed".into()),
954 path_type: PathMatchType::Regex,
955 ..any()
956 };
957 assert!(!m.matches("h", "/v1/anything", "GET", &HeaderMap::new()));
958 assert!(!m.matches("h", "/v1/(unclosed", "GET", &HeaderMap::new()));
959 }
960
961 #[test]
962 fn methods_are_honoured_case_insensitively() {
963 let m = RouteMatch {
964 methods: Some(vec!["GET".into(), "head".into()]),
965 ..any()
966 };
967 assert!(m.matches("h", "/", "GET", &HeaderMap::new()));
968 assert!(m.matches("h", "/", "get", &HeaderMap::new()));
969 assert!(m.matches("h", "/", "HEAD", &HeaderMap::new()));
970 assert!(!m.matches("h", "/", "DELETE", &HeaderMap::new()));
972 assert!(!m.matches("h", "/", "POST", &HeaderMap::new()));
973 }
974
975 #[test]
976 fn an_empty_method_list_means_any() {
977 let m = RouteMatch {
978 methods: Some(vec![]),
979 ..any()
980 };
981 assert!(m.matches("h", "/", "PATCH", &HeaderMap::new()));
982 }
983
984 #[test]
985 fn every_named_header_must_be_present_with_that_value() {
986 let m = RouteMatch {
987 headers: HashMap::from([
988 ("x-tenant".to_string(), "acme".to_string()),
989 ("x-env".to_string(), "prod".to_string()),
990 ]),
991 ..any()
992 };
993
994 assert!(m.matches(
995 "h",
996 "/",
997 "GET",
998 &header_map(&[("x-tenant", "acme"), ("x-env", "prod")])
999 ));
1000 assert!(!m.matches("h", "/", "GET", &header_map(&[("x-tenant", "acme")])));
1002 assert!(!m.matches(
1004 "h",
1005 "/",
1006 "GET",
1007 &header_map(&[("x-tenant", "other"), ("x-env", "prod")])
1008 ));
1009 assert!(!m.matches("h", "/", "GET", &HeaderMap::new()));
1010 }
1011
1012 #[test]
1013 fn header_names_are_case_insensitive() {
1014 let m = RouteMatch {
1015 headers: HashMap::from([("X-Tenant".to_string(), "acme".to_string())]),
1016 ..any()
1017 };
1018 assert!(m.matches("h", "/", "GET", &header_map(&[("x-tenant", "acme")])));
1019 }
1020
1021 #[test]
1022 fn a_host_matches_exactly_or_by_a_single_label_wildcard() {
1023 let exact = RouteMatch {
1024 host: Some("api.example.com".into()),
1025 ..any()
1026 };
1027 assert!(exact.matches("api.example.com", "/", "GET", &HeaderMap::new()));
1028 assert!(!exact.matches("www.example.com", "/", "GET", &HeaderMap::new()));
1029
1030 let wild = RouteMatch {
1031 host: Some("*.example.com".into()),
1032 ..any()
1033 };
1034 assert!(wild.matches("api.example.com", "/", "GET", &HeaderMap::new()));
1035 assert!(!wild.matches("example.com", "/", "GET", &HeaderMap::new()));
1037 assert!(!wild.matches("a.b.example.com", "/", "GET", &HeaderMap::new()));
1038 assert!(!wild.matches("api.example.com.evil.test", "/", "GET", &HeaderMap::new()));
1040 }
1041
1042 #[test]
1043 fn a_star_host_matches_anything() {
1044 let m = RouteMatch {
1045 host: Some("*".into()),
1046 ..any()
1047 };
1048 assert!(m.matches("anything.test", "/", "GET", &HeaderMap::new()));
1049 assert!(m.matches("", "/", "GET", &HeaderMap::new()));
1050 }
1051
1052 #[test]
1053 fn host_matching_is_case_insensitive() {
1054 let m = RouteMatch {
1055 host: Some("API.Example.COM".into()),
1056 ..any()
1057 };
1058 assert!(m.matches("api.example.com", "/", "GET", &HeaderMap::new()));
1059 }
1060
1061 #[test]
1062 fn every_set_criterion_has_to_hold() {
1063 let m = RouteMatch {
1065 host: Some("api.example.com".into()),
1066 path: Some("/v1".into()),
1067 path_type: PathMatchType::Prefix,
1068 methods: Some(vec!["POST".into()]),
1069 headers: HashMap::from([("x-key".to_string(), "k".to_string())]),
1070 };
1071 let headers = header_map(&[("x-key", "k")]);
1072
1073 assert!(m.matches("api.example.com", "/v1/x", "POST", &headers));
1074 assert!(!m.matches("other.example.com", "/v1/x", "POST", &headers));
1075 assert!(!m.matches("api.example.com", "/v2/x", "POST", &headers));
1076 assert!(!m.matches("api.example.com", "/v1/x", "GET", &headers));
1077 assert!(!m.matches("api.example.com", "/v1/x", "POST", &HeaderMap::new()));
1078 }
1079
1080 #[test]
1081 fn test_backend_defaults_to_http11() {
1082 let backend: Backend = toml::from_str(r#"address = "127.0.0.1:8080""#).unwrap();
1084 assert_eq!(backend.http_version, UpstreamHttpVersion::Http11);
1085 }
1086
1087 #[test]
1088 fn test_backend_h2c_aliases() {
1089 for spelling in ["h2c", "h2", "http2"] {
1092 let toml_src = format!("address = \"127.0.0.1:8080\"\nhttp_version = \"{spelling}\"");
1093 let backend: Backend = toml::from_str(&toml_src).unwrap();
1094 assert_eq!(
1095 backend.http_version,
1096 UpstreamHttpVersion::H2c,
1097 "spelling {spelling} should select h2c"
1098 );
1099 }
1100 }
1101
1102 #[test]
1103 fn test_backend_http11_aliases() {
1104 for spelling in ["http11", "http1"] {
1105 let toml_src = format!("address = \"127.0.0.1:8080\"\nhttp_version = \"{spelling}\"");
1106 let backend: Backend = toml::from_str(&toml_src).unwrap();
1107 assert_eq!(backend.http_version, UpstreamHttpVersion::Http11);
1108 }
1109 }
1110
1111 #[test]
1112 fn test_http2_port_is_optional_and_off_by_default() {
1113 let server: ServerConfig = toml::from_str("").unwrap();
1114 assert_eq!(server.http2_port, None);
1115
1116 let server: ServerConfig = toml::from_str("http2_port = 19081").unwrap();
1117 assert_eq!(server.http2_port, Some(19081));
1118 }
1119}