1use std::{
2 error,
3 fmt::{self, Display},
4 fs::File,
5 io::{BufReader, Read},
6 net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr},
7 str::FromStr,
8};
9
10use prost::{DecodeError, Message};
11use rusty_ulid::Ulid;
12
13use crate::{
14 proto::{
15 command::{
16 AddBackend, ClientAuthMode, ClientAuthPolicy, HttpsListenerConfig, InitialState,
17 IpAddress, LoadBalancingAlgorithms, PathRuleKind, RedirectPolicy, Request,
18 RequestHttpFrontend, RulePosition, SocketAddress, Uint128, UpdateHttpsListenerConfig,
19 WorkerRequest, ip_address, request::RequestType,
20 },
21 display::format_request_type,
22 },
23 response::HttpFrontend,
24 state::ClusterId,
25};
26
27#[derive(thiserror::Error, Debug)]
28pub enum RequestError {
29 #[error("invalid value {value} for field '{name}'")]
30 InvalidValue { name: String, value: i32 },
31 #[error("Could not read requests from file: {0}")]
32 ReadFile(std::io::Error),
33 #[error("Could not decode requests: {0}")]
34 Decode(DecodeError),
35}
36
37impl Request {
38 pub fn get_destinations(&self) -> ProxyDestinations {
40 let mut proxy_destination = ProxyDestinations {
41 to_http_proxy: false,
42 to_https_proxy: false,
43 to_tcp_proxy: false,
44 to_udp_proxy: false,
45 };
46 let request_type = match &self.request_type {
47 Some(t) => t,
48 None => return proxy_destination,
49 };
50
51 match request_type {
52 RequestType::AddHttpFrontend(_)
53 | RequestType::AddHttpFrontendWithSilentClose(_)
54 | RequestType::RemoveHttpFrontend(_)
55 | RequestType::AddHttpFrontendWithHeaderPredicates(_)
56 | RequestType::RemoveHttpFrontendWithHeaderPredicates(_) => {
57 proxy_destination.to_http_proxy = true
58 }
59
60 RequestType::AddHttpsFrontend(_)
61 | RequestType::AddHttpsFrontendWithSilentClose(_)
62 | RequestType::RemoveHttpsFrontend(_)
63 | RequestType::AddHttpsFrontendWithHeaderPredicates(_)
64 | RequestType::RemoveHttpsFrontendWithHeaderPredicates(_)
65 | RequestType::AddCertificate(_)
66 | RequestType::QueryCertificatesFromWorkers(_)
67 | RequestType::ReplaceCertificate(_)
68 | RequestType::RemoveCertificate(_) => proxy_destination.to_https_proxy = true,
69
70 RequestType::AddTcpFrontend(_) | RequestType::RemoveTcpFrontend(_) => {
71 proxy_destination.to_tcp_proxy = true
72 }
73
74 RequestType::AddUdpFrontend(_) | RequestType::RemoveUdpFrontend(_) => {
75 proxy_destination.to_udp_proxy = true
76 }
77
78 RequestType::AddCluster(_)
79 | RequestType::AddBackend(_)
80 | RequestType::AddUnixBackend(_)
81 | RequestType::RemoveCluster(_)
82 | RequestType::RemoveBackend(_)
83 | RequestType::SetHealthCheck(_)
84 | RequestType::SetProtectedHealthCheck(_)
85 | RequestType::RemoveHealthCheck(_)
86 | RequestType::SoftStop(_)
87 | RequestType::HardStop(_)
88 | RequestType::Status(_) => {
89 proxy_destination.to_http_proxy = true;
90 proxy_destination.to_https_proxy = true;
91 proxy_destination.to_tcp_proxy = true;
92 proxy_destination.to_udp_proxy = true;
93 }
94
95 RequestType::ConfigureMetrics(_)
97 | RequestType::SetMetricDetail(_)
98 | RequestType::QueryMetrics(_)
99 | RequestType::Logging(_)
100 | RequestType::QueryClustersHashes(_)
101 | RequestType::QueryClusterById(_)
102 | RequestType::QueryClustersByDomain(_)
103 | RequestType::SetMaxConnectionsPerIp(_)
104 | RequestType::QueryMaxConnectionsPerIp(_)
105 | RequestType::SetMaxConnectionsPerSubnet(_)
106 | RequestType::QueryMaxConnectionsPerSubnet(_)
107 | RequestType::SetMaxConnectionAttempts(_)
108 | RequestType::CloseSessions(_)
109 | RequestType::QueryProtectedHealthCheckSupport(_)
112 | RequestType::ProtectedHealthCheckOrder(_) => {}
113
114 RequestType::AddHttpsListener(_)
118 | RequestType::AddHttpsListenerWithClientAuth(_)
119 | RequestType::AddHttpListener(_)
120 | RequestType::AddTcpListener(_)
121 | RequestType::AddUdpListener(_)
122 | RequestType::UpdateHttpListener(_)
123 | RequestType::UpdateHttpsListener(_)
124 | RequestType::UpdateHttpsListenerWithClientAuth(_)
125 | RequestType::UpdateTcpListener(_)
126 | RequestType::UpdateUdpListener(_)
127 | RequestType::RemoveListener(_)
128 | RequestType::ActivateListener(_)
129 | RequestType::DeactivateListener(_)
130 | RequestType::ReturnListenSockets(_) => {}
131
132 RequestType::SaveState(_)
134 | RequestType::CountRequests(_)
135 | RequestType::QueryCertificatesFromTheState(_)
136 | RequestType::QueryHealthChecks(_)
137 | RequestType::LoadState(_)
138 | RequestType::ListWorkers(_)
139 | RequestType::ListFrontends(_)
140 | RequestType::ListListeners(_)
141 | RequestType::LaunchWorker(_)
142 | RequestType::UpgradeMain(_)
143 | RequestType::UpgradeWorker(_)
144 | RequestType::SubscribeEvents(_)
145 | RequestType::ReloadConfiguration(_) => {}
146 }
147
148 debug_assert!(
155 !(proxy_destination.to_http_proxy
156 && proxy_destination.to_https_proxy
157 && proxy_destination.to_tcp_proxy)
158 || matches!(
159 self.request_type,
160 Some(
161 RequestType::AddCluster(_)
162 | RequestType::AddBackend(_)
163 | RequestType::AddUnixBackend(_)
164 | RequestType::RemoveCluster(_)
165 | RequestType::RemoveBackend(_)
166 | RequestType::SetHealthCheck(_)
167 | RequestType::SetProtectedHealthCheck(_)
168 | RequestType::RemoveHealthCheck(_)
169 | RequestType::SoftStop(_)
170 | RequestType::HardStop(_)
171 | RequestType::Status(_)
172 )
173 ),
174 "only cluster-wide / broadcast orders may target all three proxy planes"
175 );
176 debug_assert!(
178 self.request_type.is_some()
179 || (!proxy_destination.to_http_proxy
180 && !proxy_destination.to_https_proxy
181 && !proxy_destination.to_tcp_proxy),
182 "a request without a request_type must have no proxy destination"
183 );
184 proxy_destination
185 }
186
187 pub fn is_a_stop(&self) -> bool {
189 matches!(
190 self.request_type,
191 Some(RequestType::SoftStop(_)) | Some(RequestType::HardStop(_))
192 )
193 }
194
195 pub fn short_name(&self) -> &str {
196 match &self.request_type {
197 Some(request_type) => format_request_type(request_type),
198 None => "Unallowed",
199 }
200 }
201}
202
203impl WorkerRequest {
204 pub fn new(id: String, content: Request) -> Self {
205 Self { id, content }
206 }
207}
208
209impl HttpsListenerConfig {
210 pub fn requests_client_auth(&self) -> bool {
217 self.client_auth
218 .is_some_and(|mode| mode != ClientAuthMode::ClientAuthNone as i32)
219 }
220
221 pub fn client_auth_policy(&self) -> ClientAuthPolicy {
223 ClientAuthPolicy {
224 client_auth: self.client_auth,
225 client_ca_certificates: self.client_ca_certificates.clone(),
226 client_ca_crls: self.client_ca_crls.clone(),
227 }
228 }
229
230 pub fn apply_client_auth_policy(&mut self, policy: &ClientAuthPolicy) {
235 self.client_auth = policy.client_auth;
236 self.client_ca_certificates = policy.client_ca_certificates.clone();
237 self.client_ca_crls = policy.client_ca_crls.clone();
238 }
239}
240
241impl RequestType {
242 pub fn add_https_listener(listener: HttpsListenerConfig) -> Self {
251 if listener.requests_client_auth() {
252 RequestType::AddHttpsListenerWithClientAuth(listener)
253 } else {
254 RequestType::AddHttpsListener(listener)
255 }
256 }
257
258 pub fn update_https_listener(patch: UpdateHttpsListenerConfig) -> Self {
264 if patch.client_auth_policy.is_some() {
265 RequestType::UpdateHttpsListenerWithClientAuth(patch)
266 } else {
267 RequestType::UpdateHttpsListener(patch)
268 }
269 }
270
271 pub fn add_backend(backend: AddBackend) -> Self {
279 if backend.unix_path.is_some() {
280 RequestType::AddUnixBackend(backend)
281 } else {
282 RequestType::AddBackend(backend)
283 }
284 }
285
286 pub fn into_canonical(self) -> Self {
301 match self {
302 RequestType::AddBackend(backend) | RequestType::AddUnixBackend(backend) => {
303 RequestType::add_backend(backend)
304 }
305 RequestType::AddHttpsListener(listener)
306 | RequestType::AddHttpsListenerWithClientAuth(listener) => {
307 RequestType::add_https_listener(listener)
308 }
309 RequestType::UpdateHttpsListener(patch)
310 | RequestType::UpdateHttpsListenerWithClientAuth(patch) => {
311 RequestType::update_https_listener(patch)
312 }
313 RequestType::AddHttpFrontend(front)
314 | RequestType::AddHttpFrontendWithSilentClose(front)
315 | RequestType::AddHttpFrontendWithHeaderPredicates(front) => {
316 RequestType::add_http_frontend(front)
317 }
318 RequestType::RemoveHttpFrontend(front)
319 | RequestType::RemoveHttpFrontendWithHeaderPredicates(front) => {
320 RequestType::remove_http_frontend(front)
321 }
322 RequestType::AddHttpsFrontend(front)
323 | RequestType::AddHttpsFrontendWithSilentClose(front)
324 | RequestType::AddHttpsFrontendWithHeaderPredicates(front) => {
325 RequestType::add_https_frontend(front)
326 }
327 RequestType::RemoveHttpsFrontend(front)
328 | RequestType::RemoveHttpsFrontendWithHeaderPredicates(front) => {
329 RequestType::remove_https_frontend(front)
330 }
331 other => other,
332 }
333 }
334
335 pub fn add_http_frontend(front: RequestHttpFrontend) -> Self {
347 if !front.header_predicates.is_empty() {
348 RequestType::AddHttpFrontendWithHeaderPredicates(front)
349 } else if front.redirect == Some(RedirectPolicy::SilentClose as i32) {
350 RequestType::AddHttpFrontendWithSilentClose(front)
351 } else {
352 RequestType::AddHttpFrontend(front)
353 }
354 }
355
356 pub fn remove_http_frontend(front: RequestHttpFrontend) -> Self {
359 if front.header_predicates.is_empty() {
360 RequestType::RemoveHttpFrontend(front)
361 } else {
362 RequestType::RemoveHttpFrontendWithHeaderPredicates(front)
363 }
364 }
365
366 pub fn add_https_frontend(front: RequestHttpFrontend) -> Self {
368 if !front.header_predicates.is_empty() {
369 RequestType::AddHttpsFrontendWithHeaderPredicates(front)
370 } else if front.redirect == Some(RedirectPolicy::SilentClose as i32) {
371 RequestType::AddHttpsFrontendWithSilentClose(front)
372 } else {
373 RequestType::AddHttpsFrontend(front)
374 }
375 }
376
377 pub fn remove_https_frontend(front: RequestHttpFrontend) -> Self {
379 if front.header_predicates.is_empty() {
380 RequestType::RemoveHttpsFrontend(front)
381 } else {
382 RequestType::RemoveHttpsFrontendWithHeaderPredicates(front)
383 }
384 }
385}
386
387impl Request {
388 pub fn into_canonical(self) -> Self {
390 Request {
391 request_type: self.request_type.map(RequestType::into_canonical),
392 }
393 }
394}
395
396impl fmt::Display for WorkerRequest {
397 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
398 write!(f, "{}-{:?}", self.id, self.content)
399 }
400}
401
402pub fn read_initial_state_from_file(file: &mut File) -> Result<InitialState, RequestError> {
403 let mut buf_reader = BufReader::new(file);
404 read_initial_state(&mut buf_reader)
405}
406
407pub fn read_initial_state<R: Read>(reader: &mut R) -> Result<InitialState, RequestError> {
408 let mut buffer = Vec::new();
409 reader
410 .read_to_end(&mut buffer)
411 .map_err(RequestError::ReadFile)?;
412
413 InitialState::decode(&buffer[..]).map_err(RequestError::Decode)
414}
415
416#[derive(Debug, Clone, PartialEq, Eq, Hash)]
417pub struct ProxyDestinations {
418 pub to_http_proxy: bool,
419 pub to_https_proxy: bool,
420 pub to_tcp_proxy: bool,
421 pub to_udp_proxy: bool,
422}
423
424impl RequestHttpFrontend {
425 pub fn to_frontend(self) -> Result<HttpFrontend, RequestError> {
427 let requested_hostname = self.hostname.clone();
428 let requested_cluster_id = self.cluster_id.clone();
429 let frontend = HttpFrontend {
430 address: self.address.into(),
431 cluster_id: self.cluster_id.map(ClusterId::from),
432 hostname: self.hostname,
433 path: self.path,
434 method: self.method,
435 position: RulePosition::try_from(self.position).map_err(|_| {
436 RequestError::InvalidValue {
437 name: "position".to_string(),
438 value: self.position,
439 }
440 })?,
441 tags: Some(self.tags),
442 redirect: self.redirect,
443 redirect_scheme: self.redirect_scheme,
444 redirect_template: self.redirect_template,
445 rewrite_host: self.rewrite_host,
446 rewrite_path: self.rewrite_path,
447 rewrite_port: self.rewrite_port,
448 required_auth: self.required_auth,
449 headers: self.headers,
450 hsts: self.hsts,
451 header_predicates: self.header_predicates,
452 };
453
454 debug_assert_eq!(
459 frontend.hostname, requested_hostname,
460 "hostname must survive the frontend conversion"
461 );
462 debug_assert_eq!(
463 frontend.cluster_id.as_deref(),
464 requested_cluster_id.as_deref(),
465 "cluster_id must survive the frontend conversion"
466 );
467 Ok(frontend)
468 }
469}
470
471impl Display for RequestHttpFrontend {
472 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
474 let s = match &PathRuleKind::try_from(self.path.kind) {
475 Ok(PathRuleKind::Prefix) => {
476 format!("{};{};P{}", self.address, self.hostname, self.path.value)
477 }
478 Ok(PathRuleKind::Regex) => {
479 format!("{};{};R{}", self.address, self.hostname, self.path.value)
480 }
481 Ok(PathRuleKind::Equals) => {
482 format!("{};{};={}", self.address, self.hostname, self.path.value)
483 }
484 Ok(PathRuleKind::RegexPath) => {
488 format!("{};{};Q{}", self.address, self.hostname, self.path.value)
489 }
490 Err(e) => format!("Wrong variant of PathRuleKind: {e}"),
491 };
492
493 match &self.method {
494 Some(method) => write!(f, "{s};{method}")?,
495 None => write!(f, "{s}")?,
496 }
497
498 if !self.header_predicates.is_empty() {
508 let mut predicates: Vec<(String, i32, Option<&str>)> = self
509 .header_predicates
510 .iter()
511 .map(|predicate| {
512 (
513 predicate.name.to_ascii_lowercase(),
514 predicate.kind,
515 predicate.value.as_deref(),
516 )
517 })
518 .collect();
519 predicates.sort();
520 predicates.dedup();
521 for (name, kind, value) in predicates {
522 write!(f, ";H{kind}:{name:?}")?;
523 if let Some(value) = value {
524 write!(f, "={value:?}")?;
525 }
526 }
527 }
528 Ok(())
529 }
530}
531
532#[derive(Debug)]
533pub struct ParseErrorLoadBalancing;
534
535impl fmt::Display for ParseErrorLoadBalancing {
536 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
537 write!(f, "Cannot find the load balancing policy asked")
538 }
539}
540
541impl error::Error for ParseErrorLoadBalancing {
542 fn description(&self) -> &str {
543 "Cannot find the load balancing policy asked"
544 }
545
546 fn cause(&self) -> Option<&dyn error::Error> {
547 None
548 }
549}
550
551impl FromStr for LoadBalancingAlgorithms {
552 type Err = ParseErrorLoadBalancing;
553
554 fn from_str(s: &str) -> Result<Self, Self::Err> {
555 match s.to_lowercase().as_str() {
556 "round_robin" => Ok(LoadBalancingAlgorithms::RoundRobin),
557 "random" => Ok(LoadBalancingAlgorithms::Random),
558 "power_of_two" => Ok(LoadBalancingAlgorithms::PowerOfTwo),
559 "least_loaded" => Ok(LoadBalancingAlgorithms::LeastLoaded),
560 "hrw" => Ok(LoadBalancingAlgorithms::Hrw),
561 "maglev" => Ok(LoadBalancingAlgorithms::Maglev),
562 _ => Err(ParseErrorLoadBalancing {}),
563 }
564 }
565}
566
567impl SocketAddress {
568 pub fn new_v4(a: u8, b: u8, c: u8, d: u8, port: u16) -> Self {
569 SocketAddr::new(IpAddr::V4(Ipv4Addr::new(a, b, c, d)), port).into()
570 }
571}
572
573impl From<SocketAddr> for SocketAddress {
574 fn from(socket_addr: SocketAddr) -> SocketAddress {
575 let ip_inner = match socket_addr {
576 SocketAddr::V4(ip_v4_addr) => ip_address::Inner::V4(u32::from(*ip_v4_addr.ip())),
577 SocketAddr::V6(ip_v6_addr) => {
578 ip_address::Inner::V6(Uint128::from(u128::from(*ip_v6_addr.ip())))
579 }
580 };
581
582 let encoded = SocketAddress {
583 port: socket_addr.port() as u32,
584 ip: IpAddress {
585 inner: Some(ip_inner),
586 },
587 };
588
589 debug_assert_eq!(
594 encoded.port,
595 socket_addr.port() as u32,
596 "port must round-trip losslessly into the proto"
597 );
598 debug_assert_eq!(
599 matches!(encoded.ip.inner, Some(ip_address::Inner::V4(_))),
600 socket_addr.is_ipv4(),
601 "proto IP family must match the source SocketAddr family"
602 );
603 encoded
604 }
605}
606
607impl From<SocketAddress> for SocketAddr {
608 fn from(socket_address: SocketAddress) -> Self {
609 debug_assert!(
614 socket_address.port <= u16::MAX as u32,
615 "self-encoded proto port must fit in a u16"
616 );
617 let had_inner = socket_address.ip.inner.is_some();
618 let port = socket_address.port as u16;
619
620 let ip = match socket_address.ip.inner {
621 Some(inner) => match inner {
622 ip_address::Inner::V4(v4_value) => IpAddr::V4(Ipv4Addr::from(v4_value)),
623 ip_address::Inner::V6(v6_value) => IpAddr::V6(Ipv6Addr::from(u128::from(v6_value))),
624 },
625 None => IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), };
627
628 let decoded = SocketAddr::new(ip, port);
629 debug_assert!(
632 had_inner || decoded.ip() == IpAddr::V4(Ipv4Addr::UNSPECIFIED),
633 "missing inner IP must decode to the unspecified-V4 sentinel"
634 );
635 decoded
636 }
637}
638
639impl From<Uint128> for u128 {
640 fn from(value: Uint128) -> Self {
641 let combined = value.low as u128 | ((value.high as u128) << 64);
642 debug_assert_eq!(
646 combined as u64, value.low,
647 "low half must be the bottom 64 bits"
648 );
649 debug_assert_eq!(
650 (combined >> 64) as u64,
651 value.high,
652 "high half must be the top 64 bits"
653 );
654 combined
655 }
656}
657
658impl From<u128> for Uint128 {
659 fn from(value: u128) -> Self {
660 let low = value as u64;
661 let high = (value >> 64) as u64;
662 let packed = Uint128 { low, high };
663 debug_assert_eq!(
667 u128::from(Uint128 {
668 low: packed.low,
669 high: packed.high
670 }),
671 value,
672 "u128 → Uint128 → u128 must round-trip"
673 );
674 packed
675 }
676}
677
678impl From<i128> for Uint128 {
679 fn from(value: i128) -> Self {
680 Uint128::from(value as u128)
681 }
682}
683
684impl From<Ulid> for Uint128 {
685 fn from(value: Ulid) -> Self {
686 let (low, high) = value.into();
687 let packed = Uint128 { low, high };
688 debug_assert_eq!(
691 Ulid::from((packed.low, packed.high)),
692 value,
693 "Ulid → Uint128 must preserve all 128 bits"
694 );
695 packed
696 }
697}
698
699impl From<Uint128> for Ulid {
700 fn from(value: Uint128) -> Self {
701 let Uint128 { low, high } = value;
702 let ulid = Ulid::from((low, high));
703 debug_assert_eq!(
706 Uint128::from(ulid),
707 value,
708 "Uint128 → Ulid must preserve all 128 bits"
709 );
710 ulid
711 }
712}
713
714#[cfg(test)]
715mod tests {
716 use prost::Message;
717
718 use super::*;
719 use crate::proto::command::{HeaderMatchKind, HeaderPredicate, PathRule};
720
721 fn https_listener(client_auth: Option<i32>) -> HttpsListenerConfig {
722 HttpsListenerConfig {
723 address: SocketAddress::new_v4(127, 0, 0, 1, 8443),
724 client_auth,
725 client_ca_certificates: vec!["CLIENT CA PEM".to_owned()],
726 ..Default::default()
727 }
728 }
729
730 const NONE: i32 = ClientAuthMode::ClientAuthNone as i32;
731 const OPTIONAL: i32 = ClientAuthMode::ClientAuthOptional as i32;
732 const REQUIRED: i32 = ClientAuthMode::ClientAuthRequired as i32;
733
734 #[test]
735 fn add_https_listener_picks_the_verb_from_the_client_auth_mode() {
736 for client_auth in [None, Some(NONE)] {
737 assert!(
738 matches!(
739 RequestType::add_https_listener(https_listener(client_auth)),
740 RequestType::AddHttpsListener(_)
741 ),
742 "a listener without client auth ({client_auth:?}) keeps the verb every worker decodes"
743 );
744 }
745 for client_auth in [Some(OPTIONAL), Some(REQUIRED), Some(999)] {
748 assert!(
749 matches!(
750 RequestType::add_https_listener(https_listener(client_auth)),
751 RequestType::AddHttpsListenerWithClientAuth(_)
752 ),
753 "a listener asking for a client certificate ({client_auth:?}) needs the mTLS verb"
754 );
755 }
756 }
757
758 #[test]
762 fn silent_close_frontends_travel_on_their_own_verb() {
763 let front = |redirect: Option<RedirectPolicy>| RequestHttpFrontend {
764 hostname: "example.com".to_owned(),
765 redirect: redirect.map(|policy| policy as i32),
766 ..Default::default()
767 };
768 let silent = front(Some(RedirectPolicy::SilentClose));
769 assert_eq!(
770 RequestType::AddHttpFrontend(silent.clone()).into_canonical(),
771 RequestType::AddHttpFrontendWithSilentClose(silent.clone()),
772 "silent close sent on the historical verb must be moved to its own verb"
773 );
774 assert_eq!(
775 RequestType::AddHttpsFrontend(silent.clone()).into_canonical(),
776 RequestType::AddHttpsFrontendWithSilentClose(silent.clone()),
777 );
778 for policy in [
779 None,
780 Some(RedirectPolicy::Forward),
781 Some(RedirectPolicy::Unauthorized),
782 ] {
783 let plain = front(policy);
784 assert_eq!(
785 RequestType::AddHttpFrontendWithSilentClose(plain.clone()).into_canonical(),
786 RequestType::AddHttpFrontend(plain.clone()),
787 "a frontend that is not silent close keeps the verb every worker decodes"
788 );
789 assert_eq!(
790 RequestType::AddHttpsFrontendWithSilentClose(plain.clone()).into_canonical(),
791 RequestType::AddHttpsFrontend(plain),
792 );
793 }
794 }
795
796 #[test]
801 fn silent_close_verbs_use_their_allocated_wire_tags() {
802 let silent = RequestHttpFrontend {
805 hostname: "example.com".to_owned(),
806 redirect: Some(RedirectPolicy::SilentClose as i32),
807 ..Default::default()
808 };
809 assert_eq!(
810 decode_on_a_legacy_worker(RequestType::add_http_frontend(silent.clone())),
811 None
812 );
813 assert_eq!(
814 decode_on_a_legacy_worker(RequestType::add_https_frontend(silent.clone())),
815 None
816 );
817 let plain = RequestHttpFrontend {
818 redirect: Some(RedirectPolicy::Forward as i32),
819 ..silent
820 };
821 assert_eq!(
822 decode_on_a_legacy_worker(RequestType::add_http_frontend(plain.clone())),
823 Some(LegacyRequestType::AddHttpFrontend(plain.clone()))
824 );
825 assert_eq!(
826 decode_on_a_legacy_worker(RequestType::add_https_frontend(plain.clone())),
827 Some(LegacyRequestType::AddHttpsFrontend(plain))
828 );
829
830 let front = RequestHttpFrontend {
831 hostname: "example.com".to_owned(),
832 redirect: Some(RedirectPolicy::SilentClose as i32),
833 ..Default::default()
834 };
835 let encode = |request_type| {
836 Request {
837 request_type: Some(request_type),
838 }
839 .encode_to_vec()
840 };
841 assert_eq!(
843 &encode(RequestType::AddHttpFrontendWithSilentClose(front.clone()))[..2],
844 &[0xB2, 0x04],
845 "tag 70"
846 );
847 assert_eq!(
848 &encode(RequestType::AddHttpsFrontendWithSilentClose(front.clone()))[..2],
849 &[0xBA, 0x04],
850 "tag 71"
851 );
852 let legacy = encode(RequestType::AddHttpFrontend(front));
854 assert_ne!(&legacy[..2], &[0xB2, 0x04]);
855 }
856
857 #[test]
858 fn into_canonical_moves_https_listeners_to_their_verb_both_ways() {
859 let required = https_listener(Some(REQUIRED));
860 assert_eq!(
861 RequestType::AddHttpsListener(required.clone()).into_canonical(),
862 RequestType::AddHttpsListenerWithClientAuth(required.clone()),
863 "client auth sent on the historical verb must be moved to the mTLS verb"
864 );
865 assert_eq!(
866 RequestType::AddHttpsListenerWithClientAuth(required.clone()).into_canonical(),
867 RequestType::AddHttpsListenerWithClientAuth(required),
868 );
869
870 let none = https_listener(None);
871 assert_eq!(
872 RequestType::AddHttpsListenerWithClientAuth(none.clone()).into_canonical(),
873 RequestType::AddHttpsListener(none.clone()),
874 "a listener without client auth goes back to the verb every worker decodes"
875 );
876 assert_eq!(
877 Request::from(RequestType::AddHttpsListener(none.clone())).into_canonical(),
878 Request::from(RequestType::AddHttpsListener(none)),
879 );
880
881 let other = RequestType::SoftStop(Default::default());
882 assert_eq!(other.clone().into_canonical(), other);
883 assert_eq!(
884 Request { request_type: None }.into_canonical(),
885 Request { request_type: None }
886 );
887 }
888
889 #[derive(Clone, PartialEq, Message)]
894 struct LegacyWorkerRequest {
895 #[prost(string, tag = "1")]
896 id: String,
897 #[prost(message, optional, tag = "2")]
898 content: Option<LegacyRequest>,
899 }
900
901 #[derive(Clone, PartialEq, Message)]
902 struct LegacyRequest {
903 #[prost(oneof = "LegacyRequestType", tags = "15, 17, 27, 48")]
904 request_type: Option<LegacyRequestType>,
905 }
906
907 #[derive(Clone, PartialEq, prost::Oneof)]
908 enum LegacyRequestType {
909 #[prost(message, tag = "15")]
911 AddHttpFrontend(RequestHttpFrontend),
912 #[prost(message, tag = "17")]
914 AddHttpsFrontend(RequestHttpFrontend),
915 #[prost(message, tag = "27")]
918 AddHttpsListener(LegacyHttpsListenerConfig),
919 #[prost(message, tag = "48")]
922 UpdateHttpsListener(LegacyHttpsListenerConfig),
923 }
924
925 #[derive(Clone, PartialEq, Message)]
926 struct LegacyHttpsListenerConfig {
927 #[prost(message, optional, tag = "1")]
928 address: Option<SocketAddress>,
929 }
930
931 fn decode_on_a_legacy_worker(request: RequestType) -> Option<LegacyRequestType> {
932 let bytes = WorkerRequest::new("ID-1".to_owned(), request.into()).encode_to_vec();
933 let legacy = LegacyWorkerRequest::decode(bytes.as_slice())
934 .expect("a legacy worker decodes the envelope of every request");
935 assert_eq!(legacy.id, "ID-1");
936 legacy
937 .content
938 .expect("the envelope always carries a request")
939 .request_type
940 }
941
942 #[test]
946 fn a_worker_that_predates_mtls_cannot_decode_a_listener_that_requires_it() {
947 let required = https_listener(Some(REQUIRED));
948
949 assert!(
952 matches!(
953 decode_on_a_legacy_worker(RequestType::AddHttpsListener(required.clone())),
954 Some(LegacyRequestType::AddHttpsListener(_))
955 ),
956 "precondition: the historical verb decodes on an old worker without its client auth"
957 );
958
959 assert_eq!(
962 decode_on_a_legacy_worker(
963 RequestType::AddHttpsListener(required.clone()).into_canonical()
964 ),
965 None,
966 "a listener that requires a client certificate must be undecodable by an old worker"
967 );
968 assert_eq!(
969 decode_on_a_legacy_worker(RequestType::add_https_listener(required)),
970 None
971 );
972
973 assert_eq!(
976 decode_on_a_legacy_worker(RequestType::add_https_listener(https_listener(None))),
977 Some(LegacyRequestType::AddHttpsListener(
978 LegacyHttpsListenerConfig {
979 address: Some(SocketAddress::new_v4(127, 0, 0, 1, 8443)),
980 }
981 ))
982 );
983 }
984
985 fn policy_patch(policy: Option<ClientAuthPolicy>) -> UpdateHttpsListenerConfig {
986 UpdateHttpsListenerConfig {
987 address: SocketAddress::new_v4(127, 0, 0, 1, 8443),
988 client_auth_policy: policy,
989 ..Default::default()
990 }
991 }
992
993 #[test]
994 fn update_https_listener_picks_the_verb_from_the_policy_field() {
995 assert!(matches!(
996 RequestType::update_https_listener(policy_patch(None)),
997 RequestType::UpdateHttpsListener(_)
998 ));
999 for client_auth in [None, Some(NONE), Some(REQUIRED)] {
1002 let policy = ClientAuthPolicy {
1003 client_auth,
1004 ..Default::default()
1005 };
1006 assert!(
1007 matches!(
1008 RequestType::update_https_listener(policy_patch(Some(policy))),
1009 RequestType::UpdateHttpsListenerWithClientAuth(_)
1010 ),
1011 "a patch carrying a policy ({client_auth:?}) needs the mTLS verb"
1012 );
1013 }
1014 let with_policy = policy_patch(Some(ClientAuthPolicy::default()));
1015 assert_eq!(
1016 RequestType::UpdateHttpsListener(with_policy.clone()).into_canonical(),
1017 RequestType::UpdateHttpsListenerWithClientAuth(with_policy)
1018 );
1019 assert_eq!(
1020 RequestType::UpdateHttpsListenerWithClientAuth(policy_patch(None)).into_canonical(),
1021 RequestType::UpdateHttpsListener(policy_patch(None))
1022 );
1023 }
1024
1025 #[test]
1026 fn a_worker_that_predates_mtls_cannot_decode_a_client_auth_policy_patch() {
1027 let refresh = policy_patch(Some(https_listener(Some(REQUIRED)).client_auth_policy()));
1028 assert!(
1029 matches!(
1030 decode_on_a_legacy_worker(RequestType::UpdateHttpsListener(refresh.clone())),
1031 Some(LegacyRequestType::UpdateHttpsListener(_))
1032 ),
1033 "precondition: the historical verb decodes on an old worker without its policy"
1034 );
1035 assert_eq!(
1036 decode_on_a_legacy_worker(RequestType::update_https_listener(refresh)),
1037 None,
1038 "a patch replacing the client auth policy must be undecodable by an old worker"
1039 );
1040 assert!(matches!(
1041 decode_on_a_legacy_worker(RequestType::update_https_listener(policy_patch(None))),
1042 Some(LegacyRequestType::UpdateHttpsListener(_))
1043 ));
1044 }
1045
1046 #[test]
1047 fn applying_a_client_auth_policy_replaces_all_of_it() {
1048 let mut listener = https_listener(Some(REQUIRED));
1049 listener.client_ca_crls = vec!["OLD CRL".to_owned()];
1050 let policy = ClientAuthPolicy {
1051 client_auth: Some(OPTIONAL),
1052 client_ca_certificates: vec!["NEW CA".to_owned()],
1053 client_ca_crls: vec![],
1054 };
1055 listener.apply_client_auth_policy(&policy);
1056 assert_eq!(listener.client_auth_policy(), policy);
1057 assert!(
1058 listener.client_ca_crls.is_empty(),
1059 "a CRL the policy leaves out is dropped, not kept"
1060 );
1061 }
1062
1063 #[derive(Clone, PartialEq, Message)]
1067 struct LegacyFrontendWorkerRequest {
1068 #[prost(string, tag = "1")]
1069 id: String,
1070 #[prost(message, optional, tag = "2")]
1071 content: Option<LegacyFrontendRequest>,
1072 }
1073
1074 #[derive(Clone, PartialEq, Message)]
1075 struct LegacyFrontendRequest {
1076 #[prost(oneof = "LegacyFrontendRequestType", tags = "15, 16, 17, 18")]
1077 request_type: Option<LegacyFrontendRequestType>,
1078 }
1079
1080 #[derive(Clone, PartialEq, prost::Oneof)]
1081 enum LegacyFrontendRequestType {
1082 #[prost(message, tag = "15")]
1083 AddHttp(LegacyHttpFrontend),
1084 #[prost(message, tag = "16")]
1085 RemoveHttp(LegacyHttpFrontend),
1086 #[prost(message, tag = "17")]
1087 AddHttps(LegacyHttpFrontend),
1088 #[prost(message, tag = "18")]
1089 RemoveHttps(LegacyHttpFrontend),
1090 }
1091
1092 #[derive(Clone, PartialEq, Message)]
1093 struct LegacyHttpFrontend {
1094 #[prost(string, tag = "3")]
1095 hostname: String,
1096 }
1097
1098 fn decode_frontend_on_a_legacy_worker(
1099 request: RequestType,
1100 ) -> Option<LegacyFrontendRequestType> {
1101 let bytes = WorkerRequest::new("ID-1".to_owned(), request.into()).encode_to_vec();
1102 LegacyFrontendWorkerRequest::decode(bytes.as_slice())
1103 .expect("a legacy worker decodes the envelope of every request")
1104 .content
1105 .expect("the envelope always carries a request")
1106 .request_type
1107 }
1108
1109 #[test]
1112 fn a_path_only_regex_frontend_keys_apart_from_the_same_regex() {
1113 let with_path = |path: PathRule| RequestHttpFrontend {
1114 path,
1115 ..frontend(false)
1116 };
1117 let regex = with_path(PathRule::regex("/a.*")).to_string();
1118 let regex_path = with_path(PathRule::regex_path("/a.*")).to_string();
1119 assert_ne!(regex, regex_path);
1120 assert_eq!(regex_path, "127.0.0.1:8080;example.com;Q/a.*");
1121 }
1122
1123 fn frontend(with_predicate: bool) -> RequestHttpFrontend {
1124 RequestHttpFrontend {
1125 cluster_id: Some("canary".to_owned()),
1126 address: SocketAddress::new_v4(127, 0, 0, 1, 8080),
1127 hostname: "example.com".to_owned(),
1128 header_predicates: if with_predicate {
1129 vec![HeaderPredicate {
1130 name: "X-Canary".to_owned(),
1131 kind: HeaderMatchKind::Present as i32,
1132 value: None,
1133 }]
1134 } else {
1135 Vec::new()
1136 },
1137 ..Default::default()
1138 }
1139 }
1140
1141 #[test]
1142 fn frontend_verbs_follow_the_header_predicates() {
1143 let guarded = frontend(true);
1144 let plain = frontend(false);
1145 assert!(matches!(
1146 RequestType::add_http_frontend(guarded.clone()),
1147 RequestType::AddHttpFrontendWithHeaderPredicates(_)
1148 ));
1149 assert!(matches!(
1150 RequestType::remove_http_frontend(guarded.clone()),
1151 RequestType::RemoveHttpFrontendWithHeaderPredicates(_)
1152 ));
1153 assert!(matches!(
1154 RequestType::add_https_frontend(guarded.clone()),
1155 RequestType::AddHttpsFrontendWithHeaderPredicates(_)
1156 ));
1157 assert!(matches!(
1158 RequestType::remove_https_frontend(guarded.clone()),
1159 RequestType::RemoveHttpsFrontendWithHeaderPredicates(_)
1160 ));
1161 assert_eq!(
1163 RequestType::add_http_frontend(plain.clone()),
1164 RequestType::AddHttpFrontend(plain.clone())
1165 );
1166 assert_eq!(
1167 RequestType::remove_https_frontend(plain.clone()),
1168 RequestType::RemoveHttpsFrontend(plain.clone())
1169 );
1170 assert_eq!(
1172 RequestType::AddHttpFrontend(guarded.clone()).into_canonical(),
1173 RequestType::AddHttpFrontendWithHeaderPredicates(guarded.clone())
1174 );
1175 assert_eq!(
1176 RequestType::RemoveHttpsFrontendWithHeaderPredicates(plain.clone()).into_canonical(),
1177 RequestType::RemoveHttpsFrontend(plain)
1178 );
1179 }
1180
1181 #[test]
1182 fn a_worker_that_predates_header_routing_cannot_decode_a_guarded_frontend() {
1183 let guarded = frontend(true);
1184 assert!(
1187 matches!(
1188 decode_frontend_on_a_legacy_worker(RequestType::AddHttpFrontend(guarded.clone())),
1189 Some(LegacyFrontendRequestType::AddHttp(_))
1190 ),
1191 "precondition: the historical verb decodes on an old worker without its guard"
1192 );
1193 for request in [
1194 RequestType::add_http_frontend(guarded.clone()),
1195 RequestType::remove_http_frontend(guarded.clone()),
1196 RequestType::add_https_frontend(guarded.clone()),
1197 RequestType::remove_https_frontend(guarded.clone()),
1198 RequestType::AddHttpsFrontend(guarded.clone()).into_canonical(),
1199 RequestType::RemoveHttpFrontend(guarded.clone()).into_canonical(),
1200 ] {
1201 assert_eq!(
1202 decode_frontend_on_a_legacy_worker(request),
1203 None,
1204 "a guarded frontend must be undecodable by an old worker"
1205 );
1206 }
1207 assert_eq!(
1209 decode_frontend_on_a_legacy_worker(RequestType::add_http_frontend(frontend(false))),
1210 Some(LegacyFrontendRequestType::AddHttp(LegacyHttpFrontend {
1211 hostname: "example.com".to_owned()
1212 }))
1213 );
1214 }
1215
1216 #[test]
1219 fn header_predicate_verbs_use_the_allocated_wire_tags() {
1220 let guarded = frontend(true);
1221 for (request, tag) in [
1222 (RequestType::add_http_frontend(guarded.clone()), 66u32),
1223 (RequestType::remove_http_frontend(guarded.clone()), 67),
1224 (RequestType::add_https_frontend(guarded.clone()), 68),
1225 (RequestType::remove_https_frontend(guarded.clone()), 69),
1226 ] {
1227 let bytes = Request::from(request).encode_to_vec();
1228 let (key, _) = prost::encoding::decode_varint(&mut bytes.as_slice())
1230 .map(|k| (k, ()))
1231 .expect("a request starts with a field key");
1232 assert_eq!(key >> 3, u64::from(tag));
1233 assert_eq!(key & 7, 2);
1234 }
1235 }
1236}