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 Err(e) => format!("Wrong variant of PathRuleKind: {e}"),
485 };
486
487 match &self.method {
488 Some(method) => write!(f, "{s};{method}")?,
489 None => write!(f, "{s}")?,
490 }
491
492 if !self.header_predicates.is_empty() {
502 let mut predicates: Vec<(String, i32, Option<&str>)> = self
503 .header_predicates
504 .iter()
505 .map(|predicate| {
506 (
507 predicate.name.to_ascii_lowercase(),
508 predicate.kind,
509 predicate.value.as_deref(),
510 )
511 })
512 .collect();
513 predicates.sort();
514 predicates.dedup();
515 for (name, kind, value) in predicates {
516 write!(f, ";H{kind}:{name:?}")?;
517 if let Some(value) = value {
518 write!(f, "={value:?}")?;
519 }
520 }
521 }
522 Ok(())
523 }
524}
525
526#[derive(Debug)]
527pub struct ParseErrorLoadBalancing;
528
529impl fmt::Display for ParseErrorLoadBalancing {
530 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
531 write!(f, "Cannot find the load balancing policy asked")
532 }
533}
534
535impl error::Error for ParseErrorLoadBalancing {
536 fn description(&self) -> &str {
537 "Cannot find the load balancing policy asked"
538 }
539
540 fn cause(&self) -> Option<&dyn error::Error> {
541 None
542 }
543}
544
545impl FromStr for LoadBalancingAlgorithms {
546 type Err = ParseErrorLoadBalancing;
547
548 fn from_str(s: &str) -> Result<Self, Self::Err> {
549 match s.to_lowercase().as_str() {
550 "round_robin" => Ok(LoadBalancingAlgorithms::RoundRobin),
551 "random" => Ok(LoadBalancingAlgorithms::Random),
552 "power_of_two" => Ok(LoadBalancingAlgorithms::PowerOfTwo),
553 "least_loaded" => Ok(LoadBalancingAlgorithms::LeastLoaded),
554 "hrw" => Ok(LoadBalancingAlgorithms::Hrw),
555 "maglev" => Ok(LoadBalancingAlgorithms::Maglev),
556 _ => Err(ParseErrorLoadBalancing {}),
557 }
558 }
559}
560
561impl SocketAddress {
562 pub fn new_v4(a: u8, b: u8, c: u8, d: u8, port: u16) -> Self {
563 SocketAddr::new(IpAddr::V4(Ipv4Addr::new(a, b, c, d)), port).into()
564 }
565}
566
567impl From<SocketAddr> for SocketAddress {
568 fn from(socket_addr: SocketAddr) -> SocketAddress {
569 let ip_inner = match socket_addr {
570 SocketAddr::V4(ip_v4_addr) => ip_address::Inner::V4(u32::from(*ip_v4_addr.ip())),
571 SocketAddr::V6(ip_v6_addr) => {
572 ip_address::Inner::V6(Uint128::from(u128::from(*ip_v6_addr.ip())))
573 }
574 };
575
576 let encoded = SocketAddress {
577 port: socket_addr.port() as u32,
578 ip: IpAddress {
579 inner: Some(ip_inner),
580 },
581 };
582
583 debug_assert_eq!(
588 encoded.port,
589 socket_addr.port() as u32,
590 "port must round-trip losslessly into the proto"
591 );
592 debug_assert_eq!(
593 matches!(encoded.ip.inner, Some(ip_address::Inner::V4(_))),
594 socket_addr.is_ipv4(),
595 "proto IP family must match the source SocketAddr family"
596 );
597 encoded
598 }
599}
600
601impl From<SocketAddress> for SocketAddr {
602 fn from(socket_address: SocketAddress) -> Self {
603 debug_assert!(
608 socket_address.port <= u16::MAX as u32,
609 "self-encoded proto port must fit in a u16"
610 );
611 let had_inner = socket_address.ip.inner.is_some();
612 let port = socket_address.port as u16;
613
614 let ip = match socket_address.ip.inner {
615 Some(inner) => match inner {
616 ip_address::Inner::V4(v4_value) => IpAddr::V4(Ipv4Addr::from(v4_value)),
617 ip_address::Inner::V6(v6_value) => IpAddr::V6(Ipv6Addr::from(u128::from(v6_value))),
618 },
619 None => IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), };
621
622 let decoded = SocketAddr::new(ip, port);
623 debug_assert!(
626 had_inner || decoded.ip() == IpAddr::V4(Ipv4Addr::UNSPECIFIED),
627 "missing inner IP must decode to the unspecified-V4 sentinel"
628 );
629 decoded
630 }
631}
632
633impl From<Uint128> for u128 {
634 fn from(value: Uint128) -> Self {
635 let combined = value.low as u128 | ((value.high as u128) << 64);
636 debug_assert_eq!(
640 combined as u64, value.low,
641 "low half must be the bottom 64 bits"
642 );
643 debug_assert_eq!(
644 (combined >> 64) as u64,
645 value.high,
646 "high half must be the top 64 bits"
647 );
648 combined
649 }
650}
651
652impl From<u128> for Uint128 {
653 fn from(value: u128) -> Self {
654 let low = value as u64;
655 let high = (value >> 64) as u64;
656 let packed = Uint128 { low, high };
657 debug_assert_eq!(
661 u128::from(Uint128 {
662 low: packed.low,
663 high: packed.high
664 }),
665 value,
666 "u128 → Uint128 → u128 must round-trip"
667 );
668 packed
669 }
670}
671
672impl From<i128> for Uint128 {
673 fn from(value: i128) -> Self {
674 Uint128::from(value as u128)
675 }
676}
677
678impl From<Ulid> for Uint128 {
679 fn from(value: Ulid) -> Self {
680 let (low, high) = value.into();
681 let packed = Uint128 { low, high };
682 debug_assert_eq!(
685 Ulid::from((packed.low, packed.high)),
686 value,
687 "Ulid → Uint128 must preserve all 128 bits"
688 );
689 packed
690 }
691}
692
693impl From<Uint128> for Ulid {
694 fn from(value: Uint128) -> Self {
695 let Uint128 { low, high } = value;
696 let ulid = Ulid::from((low, high));
697 debug_assert_eq!(
700 Uint128::from(ulid),
701 value,
702 "Uint128 → Ulid must preserve all 128 bits"
703 );
704 ulid
705 }
706}
707
708#[cfg(test)]
709mod tests {
710 use prost::Message;
711
712 use super::*;
713 use crate::proto::command::{HeaderMatchKind, HeaderPredicate};
714
715 fn https_listener(client_auth: Option<i32>) -> HttpsListenerConfig {
716 HttpsListenerConfig {
717 address: SocketAddress::new_v4(127, 0, 0, 1, 8443),
718 client_auth,
719 client_ca_certificates: vec!["CLIENT CA PEM".to_owned()],
720 ..Default::default()
721 }
722 }
723
724 const NONE: i32 = ClientAuthMode::ClientAuthNone as i32;
725 const OPTIONAL: i32 = ClientAuthMode::ClientAuthOptional as i32;
726 const REQUIRED: i32 = ClientAuthMode::ClientAuthRequired as i32;
727
728 #[test]
729 fn add_https_listener_picks_the_verb_from_the_client_auth_mode() {
730 for client_auth in [None, Some(NONE)] {
731 assert!(
732 matches!(
733 RequestType::add_https_listener(https_listener(client_auth)),
734 RequestType::AddHttpsListener(_)
735 ),
736 "a listener without client auth ({client_auth:?}) keeps the verb every worker decodes"
737 );
738 }
739 for client_auth in [Some(OPTIONAL), Some(REQUIRED), Some(999)] {
742 assert!(
743 matches!(
744 RequestType::add_https_listener(https_listener(client_auth)),
745 RequestType::AddHttpsListenerWithClientAuth(_)
746 ),
747 "a listener asking for a client certificate ({client_auth:?}) needs the mTLS verb"
748 );
749 }
750 }
751
752 #[test]
756 fn silent_close_frontends_travel_on_their_own_verb() {
757 let front = |redirect: Option<RedirectPolicy>| RequestHttpFrontend {
758 hostname: "example.com".to_owned(),
759 redirect: redirect.map(|policy| policy as i32),
760 ..Default::default()
761 };
762 let silent = front(Some(RedirectPolicy::SilentClose));
763 assert_eq!(
764 RequestType::AddHttpFrontend(silent.clone()).into_canonical(),
765 RequestType::AddHttpFrontendWithSilentClose(silent.clone()),
766 "silent close sent on the historical verb must be moved to its own verb"
767 );
768 assert_eq!(
769 RequestType::AddHttpsFrontend(silent.clone()).into_canonical(),
770 RequestType::AddHttpsFrontendWithSilentClose(silent.clone()),
771 );
772 for policy in [
773 None,
774 Some(RedirectPolicy::Forward),
775 Some(RedirectPolicy::Unauthorized),
776 ] {
777 let plain = front(policy);
778 assert_eq!(
779 RequestType::AddHttpFrontendWithSilentClose(plain.clone()).into_canonical(),
780 RequestType::AddHttpFrontend(plain.clone()),
781 "a frontend that is not silent close keeps the verb every worker decodes"
782 );
783 assert_eq!(
784 RequestType::AddHttpsFrontendWithSilentClose(plain.clone()).into_canonical(),
785 RequestType::AddHttpsFrontend(plain),
786 );
787 }
788 }
789
790 #[test]
795 fn silent_close_verbs_use_their_allocated_wire_tags() {
796 let silent = RequestHttpFrontend {
799 hostname: "example.com".to_owned(),
800 redirect: Some(RedirectPolicy::SilentClose as i32),
801 ..Default::default()
802 };
803 assert_eq!(
804 decode_on_a_legacy_worker(RequestType::add_http_frontend(silent.clone())),
805 None
806 );
807 assert_eq!(
808 decode_on_a_legacy_worker(RequestType::add_https_frontend(silent.clone())),
809 None
810 );
811 let plain = RequestHttpFrontend {
812 redirect: Some(RedirectPolicy::Forward as i32),
813 ..silent
814 };
815 assert_eq!(
816 decode_on_a_legacy_worker(RequestType::add_http_frontend(plain.clone())),
817 Some(LegacyRequestType::AddHttpFrontend(plain.clone()))
818 );
819 assert_eq!(
820 decode_on_a_legacy_worker(RequestType::add_https_frontend(plain.clone())),
821 Some(LegacyRequestType::AddHttpsFrontend(plain))
822 );
823
824 let front = RequestHttpFrontend {
825 hostname: "example.com".to_owned(),
826 redirect: Some(RedirectPolicy::SilentClose as i32),
827 ..Default::default()
828 };
829 let encode = |request_type| {
830 Request {
831 request_type: Some(request_type),
832 }
833 .encode_to_vec()
834 };
835 assert_eq!(
837 &encode(RequestType::AddHttpFrontendWithSilentClose(front.clone()))[..2],
838 &[0xB2, 0x04],
839 "tag 70"
840 );
841 assert_eq!(
842 &encode(RequestType::AddHttpsFrontendWithSilentClose(front.clone()))[..2],
843 &[0xBA, 0x04],
844 "tag 71"
845 );
846 let legacy = encode(RequestType::AddHttpFrontend(front));
848 assert_ne!(&legacy[..2], &[0xB2, 0x04]);
849 }
850
851 #[test]
852 fn into_canonical_moves_https_listeners_to_their_verb_both_ways() {
853 let required = https_listener(Some(REQUIRED));
854 assert_eq!(
855 RequestType::AddHttpsListener(required.clone()).into_canonical(),
856 RequestType::AddHttpsListenerWithClientAuth(required.clone()),
857 "client auth sent on the historical verb must be moved to the mTLS verb"
858 );
859 assert_eq!(
860 RequestType::AddHttpsListenerWithClientAuth(required.clone()).into_canonical(),
861 RequestType::AddHttpsListenerWithClientAuth(required),
862 );
863
864 let none = https_listener(None);
865 assert_eq!(
866 RequestType::AddHttpsListenerWithClientAuth(none.clone()).into_canonical(),
867 RequestType::AddHttpsListener(none.clone()),
868 "a listener without client auth goes back to the verb every worker decodes"
869 );
870 assert_eq!(
871 Request::from(RequestType::AddHttpsListener(none.clone())).into_canonical(),
872 Request::from(RequestType::AddHttpsListener(none)),
873 );
874
875 let other = RequestType::SoftStop(Default::default());
876 assert_eq!(other.clone().into_canonical(), other);
877 assert_eq!(
878 Request { request_type: None }.into_canonical(),
879 Request { request_type: None }
880 );
881 }
882
883 #[derive(Clone, PartialEq, Message)]
888 struct LegacyWorkerRequest {
889 #[prost(string, tag = "1")]
890 id: String,
891 #[prost(message, optional, tag = "2")]
892 content: Option<LegacyRequest>,
893 }
894
895 #[derive(Clone, PartialEq, Message)]
896 struct LegacyRequest {
897 #[prost(oneof = "LegacyRequestType", tags = "15, 17, 27, 48")]
898 request_type: Option<LegacyRequestType>,
899 }
900
901 #[derive(Clone, PartialEq, prost::Oneof)]
902 enum LegacyRequestType {
903 #[prost(message, tag = "15")]
905 AddHttpFrontend(RequestHttpFrontend),
906 #[prost(message, tag = "17")]
908 AddHttpsFrontend(RequestHttpFrontend),
909 #[prost(message, tag = "27")]
912 AddHttpsListener(LegacyHttpsListenerConfig),
913 #[prost(message, tag = "48")]
916 UpdateHttpsListener(LegacyHttpsListenerConfig),
917 }
918
919 #[derive(Clone, PartialEq, Message)]
920 struct LegacyHttpsListenerConfig {
921 #[prost(message, optional, tag = "1")]
922 address: Option<SocketAddress>,
923 }
924
925 fn decode_on_a_legacy_worker(request: RequestType) -> Option<LegacyRequestType> {
926 let bytes = WorkerRequest::new("ID-1".to_owned(), request.into()).encode_to_vec();
927 let legacy = LegacyWorkerRequest::decode(bytes.as_slice())
928 .expect("a legacy worker decodes the envelope of every request");
929 assert_eq!(legacy.id, "ID-1");
930 legacy
931 .content
932 .expect("the envelope always carries a request")
933 .request_type
934 }
935
936 #[test]
940 fn a_worker_that_predates_mtls_cannot_decode_a_listener_that_requires_it() {
941 let required = https_listener(Some(REQUIRED));
942
943 assert!(
946 matches!(
947 decode_on_a_legacy_worker(RequestType::AddHttpsListener(required.clone())),
948 Some(LegacyRequestType::AddHttpsListener(_))
949 ),
950 "precondition: the historical verb decodes on an old worker without its client auth"
951 );
952
953 assert_eq!(
956 decode_on_a_legacy_worker(
957 RequestType::AddHttpsListener(required.clone()).into_canonical()
958 ),
959 None,
960 "a listener that requires a client certificate must be undecodable by an old worker"
961 );
962 assert_eq!(
963 decode_on_a_legacy_worker(RequestType::add_https_listener(required)),
964 None
965 );
966
967 assert_eq!(
970 decode_on_a_legacy_worker(RequestType::add_https_listener(https_listener(None))),
971 Some(LegacyRequestType::AddHttpsListener(
972 LegacyHttpsListenerConfig {
973 address: Some(SocketAddress::new_v4(127, 0, 0, 1, 8443)),
974 }
975 ))
976 );
977 }
978
979 fn policy_patch(policy: Option<ClientAuthPolicy>) -> UpdateHttpsListenerConfig {
980 UpdateHttpsListenerConfig {
981 address: SocketAddress::new_v4(127, 0, 0, 1, 8443),
982 client_auth_policy: policy,
983 ..Default::default()
984 }
985 }
986
987 #[test]
988 fn update_https_listener_picks_the_verb_from_the_policy_field() {
989 assert!(matches!(
990 RequestType::update_https_listener(policy_patch(None)),
991 RequestType::UpdateHttpsListener(_)
992 ));
993 for client_auth in [None, Some(NONE), Some(REQUIRED)] {
996 let policy = ClientAuthPolicy {
997 client_auth,
998 ..Default::default()
999 };
1000 assert!(
1001 matches!(
1002 RequestType::update_https_listener(policy_patch(Some(policy))),
1003 RequestType::UpdateHttpsListenerWithClientAuth(_)
1004 ),
1005 "a patch carrying a policy ({client_auth:?}) needs the mTLS verb"
1006 );
1007 }
1008 let with_policy = policy_patch(Some(ClientAuthPolicy::default()));
1009 assert_eq!(
1010 RequestType::UpdateHttpsListener(with_policy.clone()).into_canonical(),
1011 RequestType::UpdateHttpsListenerWithClientAuth(with_policy)
1012 );
1013 assert_eq!(
1014 RequestType::UpdateHttpsListenerWithClientAuth(policy_patch(None)).into_canonical(),
1015 RequestType::UpdateHttpsListener(policy_patch(None))
1016 );
1017 }
1018
1019 #[test]
1020 fn a_worker_that_predates_mtls_cannot_decode_a_client_auth_policy_patch() {
1021 let refresh = policy_patch(Some(https_listener(Some(REQUIRED)).client_auth_policy()));
1022 assert!(
1023 matches!(
1024 decode_on_a_legacy_worker(RequestType::UpdateHttpsListener(refresh.clone())),
1025 Some(LegacyRequestType::UpdateHttpsListener(_))
1026 ),
1027 "precondition: the historical verb decodes on an old worker without its policy"
1028 );
1029 assert_eq!(
1030 decode_on_a_legacy_worker(RequestType::update_https_listener(refresh)),
1031 None,
1032 "a patch replacing the client auth policy must be undecodable by an old worker"
1033 );
1034 assert!(matches!(
1035 decode_on_a_legacy_worker(RequestType::update_https_listener(policy_patch(None))),
1036 Some(LegacyRequestType::UpdateHttpsListener(_))
1037 ));
1038 }
1039
1040 #[test]
1041 fn applying_a_client_auth_policy_replaces_all_of_it() {
1042 let mut listener = https_listener(Some(REQUIRED));
1043 listener.client_ca_crls = vec!["OLD CRL".to_owned()];
1044 let policy = ClientAuthPolicy {
1045 client_auth: Some(OPTIONAL),
1046 client_ca_certificates: vec!["NEW CA".to_owned()],
1047 client_ca_crls: vec![],
1048 };
1049 listener.apply_client_auth_policy(&policy);
1050 assert_eq!(listener.client_auth_policy(), policy);
1051 assert!(
1052 listener.client_ca_crls.is_empty(),
1053 "a CRL the policy leaves out is dropped, not kept"
1054 );
1055 }
1056
1057 #[derive(Clone, PartialEq, Message)]
1061 struct LegacyFrontendWorkerRequest {
1062 #[prost(string, tag = "1")]
1063 id: String,
1064 #[prost(message, optional, tag = "2")]
1065 content: Option<LegacyFrontendRequest>,
1066 }
1067
1068 #[derive(Clone, PartialEq, Message)]
1069 struct LegacyFrontendRequest {
1070 #[prost(oneof = "LegacyFrontendRequestType", tags = "15, 16, 17, 18")]
1071 request_type: Option<LegacyFrontendRequestType>,
1072 }
1073
1074 #[derive(Clone, PartialEq, prost::Oneof)]
1075 enum LegacyFrontendRequestType {
1076 #[prost(message, tag = "15")]
1077 AddHttp(LegacyHttpFrontend),
1078 #[prost(message, tag = "16")]
1079 RemoveHttp(LegacyHttpFrontend),
1080 #[prost(message, tag = "17")]
1081 AddHttps(LegacyHttpFrontend),
1082 #[prost(message, tag = "18")]
1083 RemoveHttps(LegacyHttpFrontend),
1084 }
1085
1086 #[derive(Clone, PartialEq, Message)]
1087 struct LegacyHttpFrontend {
1088 #[prost(string, tag = "3")]
1089 hostname: String,
1090 }
1091
1092 fn decode_frontend_on_a_legacy_worker(
1093 request: RequestType,
1094 ) -> Option<LegacyFrontendRequestType> {
1095 let bytes = WorkerRequest::new("ID-1".to_owned(), request.into()).encode_to_vec();
1096 LegacyFrontendWorkerRequest::decode(bytes.as_slice())
1097 .expect("a legacy worker decodes the envelope of every request")
1098 .content
1099 .expect("the envelope always carries a request")
1100 .request_type
1101 }
1102
1103 fn frontend(with_predicate: bool) -> RequestHttpFrontend {
1104 RequestHttpFrontend {
1105 cluster_id: Some("canary".to_owned()),
1106 address: SocketAddress::new_v4(127, 0, 0, 1, 8080),
1107 hostname: "example.com".to_owned(),
1108 header_predicates: if with_predicate {
1109 vec![HeaderPredicate {
1110 name: "X-Canary".to_owned(),
1111 kind: HeaderMatchKind::Present as i32,
1112 value: None,
1113 }]
1114 } else {
1115 Vec::new()
1116 },
1117 ..Default::default()
1118 }
1119 }
1120
1121 #[test]
1122 fn frontend_verbs_follow_the_header_predicates() {
1123 let guarded = frontend(true);
1124 let plain = frontend(false);
1125 assert!(matches!(
1126 RequestType::add_http_frontend(guarded.clone()),
1127 RequestType::AddHttpFrontendWithHeaderPredicates(_)
1128 ));
1129 assert!(matches!(
1130 RequestType::remove_http_frontend(guarded.clone()),
1131 RequestType::RemoveHttpFrontendWithHeaderPredicates(_)
1132 ));
1133 assert!(matches!(
1134 RequestType::add_https_frontend(guarded.clone()),
1135 RequestType::AddHttpsFrontendWithHeaderPredicates(_)
1136 ));
1137 assert!(matches!(
1138 RequestType::remove_https_frontend(guarded.clone()),
1139 RequestType::RemoveHttpsFrontendWithHeaderPredicates(_)
1140 ));
1141 assert_eq!(
1143 RequestType::add_http_frontend(plain.clone()),
1144 RequestType::AddHttpFrontend(plain.clone())
1145 );
1146 assert_eq!(
1147 RequestType::remove_https_frontend(plain.clone()),
1148 RequestType::RemoveHttpsFrontend(plain.clone())
1149 );
1150 assert_eq!(
1152 RequestType::AddHttpFrontend(guarded.clone()).into_canonical(),
1153 RequestType::AddHttpFrontendWithHeaderPredicates(guarded.clone())
1154 );
1155 assert_eq!(
1156 RequestType::RemoveHttpsFrontendWithHeaderPredicates(plain.clone()).into_canonical(),
1157 RequestType::RemoveHttpsFrontend(plain)
1158 );
1159 }
1160
1161 #[test]
1162 fn a_worker_that_predates_header_routing_cannot_decode_a_guarded_frontend() {
1163 let guarded = frontend(true);
1164 assert!(
1167 matches!(
1168 decode_frontend_on_a_legacy_worker(RequestType::AddHttpFrontend(guarded.clone())),
1169 Some(LegacyFrontendRequestType::AddHttp(_))
1170 ),
1171 "precondition: the historical verb decodes on an old worker without its guard"
1172 );
1173 for request in [
1174 RequestType::add_http_frontend(guarded.clone()),
1175 RequestType::remove_http_frontend(guarded.clone()),
1176 RequestType::add_https_frontend(guarded.clone()),
1177 RequestType::remove_https_frontend(guarded.clone()),
1178 RequestType::AddHttpsFrontend(guarded.clone()).into_canonical(),
1179 RequestType::RemoveHttpFrontend(guarded.clone()).into_canonical(),
1180 ] {
1181 assert_eq!(
1182 decode_frontend_on_a_legacy_worker(request),
1183 None,
1184 "a guarded frontend must be undecodable by an old worker"
1185 );
1186 }
1187 assert_eq!(
1189 decode_frontend_on_a_legacy_worker(RequestType::add_http_frontend(frontend(false))),
1190 Some(LegacyFrontendRequestType::AddHttp(LegacyHttpFrontend {
1191 hostname: "example.com".to_owned()
1192 }))
1193 );
1194 }
1195
1196 #[test]
1199 fn header_predicate_verbs_use_the_allocated_wire_tags() {
1200 let guarded = frontend(true);
1201 for (request, tag) in [
1202 (RequestType::add_http_frontend(guarded.clone()), 66u32),
1203 (RequestType::remove_http_frontend(guarded.clone()), 67),
1204 (RequestType::add_https_frontend(guarded.clone()), 68),
1205 (RequestType::remove_https_frontend(guarded.clone()), 69),
1206 ] {
1207 let bytes = Request::from(request).encode_to_vec();
1208 let (key, _) = prost::encoding::decode_varint(&mut bytes.as_slice())
1210 .map(|k| (k, ()))
1211 .expect("a request starts with a field key");
1212 assert_eq!(key >> 3, u64::from(tag));
1213 assert_eq!(key & 7, 2);
1214 }
1215 }
1216}