1use std::{cmp::Ordering, collections::BTreeMap, fmt, net::SocketAddr};
2
3use crate::{
4 proto::command::{
5 AddBackend, FilteredTimeSerie, Header, HeaderPredicate, HstsConfig, LoadBalancingParams,
6 PathRule, PathRuleKind, RequestHttpFrontend, RequestTcpFrontend, RequestUdpFrontend,
7 Response, ResponseContent, ResponseStatus, RulePosition, RunState, WorkerResponse,
8 },
9 state::ClusterId,
10};
11
12impl Response {
13 pub fn new(
14 status: ResponseStatus,
15 message: String,
16 content: Option<ResponseContent>,
17 ) -> Response {
18 Response {
19 status: status as i32,
20 message,
21 content,
22 }
23 }
24}
25
26#[derive(Clone, PartialOrd, Ord, PartialEq, Eq, Hash, Serialize, Deserialize)]
28pub struct HttpFrontend {
29 pub cluster_id: Option<ClusterId>,
31 pub address: SocketAddr,
32 pub hostname: String,
33 #[serde(default)]
34 #[serde(skip_serializing_if = "is_default_path_rule")]
35 pub path: PathRule,
36 #[serde(default)]
37 #[serde(skip_serializing_if = "Option::is_none")]
38 pub method: Option<String>,
39 #[serde(default)]
40 pub position: RulePosition,
41 pub tags: Option<BTreeMap<String, String>>,
42 #[serde(default)]
48 #[serde(skip_serializing_if = "Option::is_none")]
49 pub redirect: Option<i32>,
50 #[serde(default)]
51 #[serde(skip_serializing_if = "Option::is_none")]
52 pub redirect_scheme: Option<i32>,
53 #[serde(default)]
54 #[serde(skip_serializing_if = "Option::is_none")]
55 pub redirect_template: Option<String>,
56 #[serde(default)]
57 #[serde(skip_serializing_if = "Option::is_none")]
58 pub rewrite_host: Option<String>,
59 #[serde(default)]
60 #[serde(skip_serializing_if = "Option::is_none")]
61 pub rewrite_path: Option<String>,
62 #[serde(default)]
63 #[serde(skip_serializing_if = "Option::is_none")]
64 pub rewrite_port: Option<u32>,
65 #[serde(default)]
66 #[serde(skip_serializing_if = "Option::is_none")]
67 pub required_auth: Option<bool>,
68 #[serde(default)]
69 #[serde(skip_serializing_if = "Vec::is_empty")]
70 pub headers: Vec<Header>,
71 #[serde(default)]
74 #[serde(skip_serializing_if = "Option::is_none")]
75 pub hsts: Option<HstsConfig>,
76 #[serde(default)]
79 #[serde(skip_serializing_if = "Vec::is_empty")]
80 pub header_predicates: Vec<HeaderPredicate>,
81}
82
83impl fmt::Debug for HttpFrontend {
84 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
85 let tags_count = self.tags.as_ref().map(BTreeMap::len);
86 let tags_len = self.tags.as_ref().map(|tags| {
87 tags.iter().fold(0usize, |total, (key, value)| {
88 total.saturating_add(key.len()).saturating_add(value.len())
89 })
90 });
91 let headers_len = self.headers.iter().fold(0usize, |total, header| {
92 total
93 .saturating_add(header.key.len())
94 .saturating_add(header.val.len())
95 });
96
97 f.debug_struct("HttpFrontend")
98 .field("cluster_id_len", &self.cluster_id.as_deref().map(str::len))
99 .field("address", &self.address)
100 .field("hostname_len", &self.hostname.len())
101 .field("path_kind", &self.path.kind)
102 .field("path_len", &self.path.value.len())
103 .field("method_len", &self.method.as_ref().map(String::len))
104 .field("position", &self.position)
105 .field("tags_count", &tags_count)
106 .field("tags_len", &tags_len)
107 .field("redirect", &self.redirect)
108 .field("required_auth", &self.required_auth)
109 .field("redirect_scheme", &self.redirect_scheme)
110 .field(
111 "redirect_template_len",
112 &self.redirect_template.as_ref().map(String::len),
113 )
114 .field(
115 "rewrite_host_len",
116 &self.rewrite_host.as_ref().map(String::len),
117 )
118 .field(
119 "rewrite_path_len",
120 &self.rewrite_path.as_ref().map(String::len),
121 )
122 .field("rewrite_port", &self.rewrite_port)
123 .field("headers_count", &self.headers.len())
124 .field("headers_len", &headers_len)
125 .field("hsts", &self.hsts)
126 .field("header_predicates_count", &self.header_predicates.len())
127 .finish_non_exhaustive()
128 }
129}
130
131impl From<HttpFrontend> for RequestHttpFrontend {
132 fn from(val: HttpFrontend) -> Self {
133 let source_address = val.address;
134 let source_hostname = val.hostname.clone();
135 let request_frontend = RequestHttpFrontend {
136 cluster_id: val.cluster_id.as_deref().map(str::to_owned),
137 address: val.address.into(),
138 hostname: val.hostname,
139 path: val.path,
140 method: val.method,
141 position: val.position.into(),
142 tags: val.tags.unwrap_or_default(),
143 redirect: val.redirect,
144 redirect_scheme: val.redirect_scheme,
145 redirect_template: val.redirect_template,
146 rewrite_host: val.rewrite_host,
147 rewrite_path: val.rewrite_path,
148 rewrite_port: val.rewrite_port,
149 required_auth: val.required_auth,
150 headers: val.headers,
151 hsts: val.hsts,
152 header_predicates: val.header_predicates,
153 };
154
155 debug_assert_eq!(
160 SocketAddr::from(request_frontend.address),
161 source_address,
162 "frontend address must round-trip through the proto encoding"
163 );
164 debug_assert_eq!(
165 request_frontend.hostname, source_hostname,
166 "frontend hostname must survive the proto conversion"
167 );
168 request_frontend
169 }
170}
171
172impl From<Backend> for AddBackend {
173 fn from(val: Backend) -> Self {
174 let source_address = val.address;
175 let source_cluster_id = val.cluster_id.clone();
176 let source_backend_id = val.backend_id.clone();
177 let add_backend = AddBackend {
178 cluster_id: val.cluster_id.to_string(),
179 backend_id: val.backend_id,
180 address: val.address.into(),
181 sticky_id: val.sticky_id,
182 load_balancing_parameters: val.load_balancing_parameters,
183 backup: val.backup,
184 unix_path: val.unix_path,
185 };
186
187 debug_assert_eq!(
192 *add_backend.cluster_id, *source_cluster_id,
193 "backend cluster_id must survive the proto conversion"
194 );
195 debug_assert_eq!(
196 add_backend.backend_id, source_backend_id,
197 "backend_id must survive the proto conversion"
198 );
199 debug_assert_eq!(
200 SocketAddr::from(add_backend.address),
201 source_address,
202 "backend address must round-trip through the proto encoding"
203 );
204 add_backend
205 }
206}
207
208impl PathRule {
209 pub fn prefix<S>(value: S) -> Self
210 where
211 S: ToString,
212 {
213 let rule = Self {
214 kind: PathRuleKind::Prefix.into(),
215 value: value.to_string(),
216 };
217 debug_assert_eq!(
220 PathRuleKind::try_from(rule.kind),
221 Ok(PathRuleKind::Prefix),
222 "prefix() must encode a Prefix-kind rule"
223 );
224 rule
225 }
226
227 pub fn regex<S>(value: S) -> Self
228 where
229 S: ToString,
230 {
231 let rule = Self {
232 kind: PathRuleKind::Regex.into(),
233 value: value.to_string(),
234 };
235 debug_assert_eq!(
236 PathRuleKind::try_from(rule.kind),
237 Ok(PathRuleKind::Regex),
238 "regex() must encode a Regex-kind rule"
239 );
240 rule
241 }
242
243 pub fn regex_path<S>(value: S) -> Self
247 where
248 S: ToString,
249 {
250 let rule = Self {
251 kind: PathRuleKind::RegexPath.into(),
252 value: value.to_string(),
253 };
254 debug_assert_eq!(
255 PathRuleKind::try_from(rule.kind),
256 Ok(PathRuleKind::RegexPath),
257 "regex_path() must encode a RegexPath-kind rule"
258 );
259 rule
260 }
261
262 pub fn equals<S>(value: S) -> Self
263 where
264 S: ToString,
265 {
266 let rule = Self {
267 kind: PathRuleKind::Equals.into(),
268 value: value.to_string(),
269 };
270 debug_assert_eq!(
271 PathRuleKind::try_from(rule.kind),
272 Ok(PathRuleKind::Equals),
273 "equals() must encode an Equals-kind rule"
274 );
275 rule
276 }
277
278 pub fn from_cli_options(
279 path_prefix: Option<String>,
280 path_regex: Option<String>,
281 path_equals: Option<String>,
282 path_regex_path: Option<String>,
283 ) -> Self {
284 let had_prefix = path_prefix.is_some();
289 let had_regex = path_regex.is_some();
290 let had_equals = path_equals.is_some();
291 let had_regex_path = path_regex_path.is_some();
292 let rule = match (path_prefix, path_regex, path_equals, path_regex_path) {
293 (Some(prefix), _, _, _) => PathRule {
294 kind: PathRuleKind::Prefix as i32,
295 value: prefix,
296 },
297 (None, Some(regex), _, _) => PathRule {
298 kind: PathRuleKind::Regex as i32,
299 value: regex,
300 },
301 (None, None, Some(equals), _) => PathRule {
302 kind: PathRuleKind::Equals as i32,
303 value: equals,
304 },
305 (None, None, None, Some(regex_path)) => PathRule {
306 kind: PathRuleKind::RegexPath as i32,
307 value: regex_path,
308 },
309 _ => PathRule::default(),
310 };
311
312 debug_assert!(
316 !had_prefix || rule.kind == PathRuleKind::Prefix as i32,
317 "a path prefix must produce a Prefix rule regardless of other flags"
318 );
319 debug_assert!(
320 had_prefix || !had_regex || rule.kind == PathRuleKind::Regex as i32,
321 "absent a prefix, a regex must produce a Regex rule"
322 );
323 debug_assert!(
324 had_prefix
325 || had_regex
326 || had_equals
327 || !had_regex_path
328 || rule.kind == PathRuleKind::RegexPath as i32,
329 "alone, a path-only regex must produce a RegexPath rule"
330 );
331 debug_assert!(
332 had_regex_path || rule.kind != PathRuleKind::RegexPath as i32,
333 "a RegexPath rule can only come from a path-only regex flag"
334 );
335 rule
336 }
337}
338
339pub fn is_default_path_rule(p: &PathRule) -> bool {
340 PathRuleKind::try_from(p.kind) == Ok(PathRuleKind::Prefix) && p.value.is_empty()
341}
342
343impl fmt::Display for PathRule {
344 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
345 match PathRuleKind::try_from(self.kind) {
346 Ok(PathRuleKind::Prefix) => write!(f, "prefix '{}'", self.value),
347 Ok(PathRuleKind::Regex) => write!(f, "regexp '{}'", self.value),
348 Ok(PathRuleKind::Equals) => write!(f, "equals '{}'", self.value),
349 Ok(PathRuleKind::RegexPath) => write!(f, "path-only regexp '{}'", self.value),
350 Err(_) => write!(f, ""),
351 }
352 }
353}
354
355#[derive(Debug, Clone, PartialOrd, Ord, PartialEq, Eq, Hash, Serialize, Deserialize)]
357pub struct TcpFrontend {
358 pub cluster_id: ClusterId,
359 pub address: SocketAddr,
360 pub tags: BTreeMap<String, String>,
362 #[serde(default)]
365 pub sni: Option<String>,
366 #[serde(default)]
369 pub alpn: Vec<String>,
370}
371
372impl From<TcpFrontend> for RequestTcpFrontend {
373 fn from(val: TcpFrontend) -> Self {
374 let source_address = val.address;
375 let source_cluster_id = val.cluster_id.clone();
376 let source_sni = val.sni.clone();
377 let source_alpn = val.alpn.clone();
378 let request_frontend = RequestTcpFrontend {
379 cluster_id: val.cluster_id.to_string(),
380 address: val.address.into(),
381 tags: val.tags,
382 sni: val.sni,
383 alpn: val.alpn,
384 };
385
386 debug_assert_eq!(
389 *request_frontend.cluster_id, *source_cluster_id,
390 "TCP frontend cluster_id must survive the proto conversion"
391 );
392 debug_assert_eq!(
393 SocketAddr::from(request_frontend.address),
394 source_address,
395 "TCP frontend address must round-trip through the proto encoding"
396 );
397 debug_assert_eq!(
401 request_frontend.sni, source_sni,
402 "TCP frontend sni must survive the proto conversion"
403 );
404 debug_assert_eq!(
405 request_frontend.alpn, source_alpn,
406 "TCP frontend alpn must survive the proto conversion"
407 );
408 request_frontend
409 }
410}
411
412#[derive(Debug, Clone, PartialOrd, Ord, PartialEq, Eq, Hash, Serialize, Deserialize)]
414pub struct UdpFrontend {
415 pub cluster_id: ClusterId,
416 pub address: SocketAddr,
417 pub tags: BTreeMap<String, String>,
419}
420
421impl From<UdpFrontend> for RequestUdpFrontend {
422 fn from(val: UdpFrontend) -> Self {
423 RequestUdpFrontend {
424 cluster_id: val.cluster_id.to_string(),
425 address: val.address.into(),
426 tags: val.tags,
427 }
428 }
429}
430
431#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
433pub struct Backend {
434 pub cluster_id: ClusterId,
435 pub backend_id: String,
436 pub address: SocketAddr,
437 #[serde(default)]
438 #[serde(skip_serializing_if = "Option::is_none")]
439 pub sticky_id: Option<String>,
440 #[serde(default)]
441 #[serde(skip_serializing_if = "Option::is_none")]
442 pub load_balancing_parameters: Option<LoadBalancingParams>,
443 #[serde(default)]
444 #[serde(skip_serializing_if = "Option::is_none")]
445 pub backup: Option<bool>,
446 #[serde(default)]
449 #[serde(skip_serializing_if = "Option::is_none")]
450 pub unix_path: Option<String>,
451}
452
453impl Ord for Backend {
454 fn cmp(&self, o: &Backend) -> Ordering {
455 let fields_all_equal = self.cluster_id == o.cluster_id
461 && self.backend_id == o.backend_id
462 && self.sticky_id == o.sticky_id
463 && self.load_balancing_parameters == o.load_balancing_parameters
464 && self.backup == o.backup
465 && self.unix_path == o.unix_path
466 && self.address == o.address;
467
468 let ordering = self
469 .cluster_id
470 .cmp(&o.cluster_id)
471 .then(self.backend_id.cmp(&o.backend_id))
472 .then(self.sticky_id.cmp(&o.sticky_id))
473 .then(
474 self.load_balancing_parameters
475 .cmp(&o.load_balancing_parameters),
476 )
477 .then(self.backup.cmp(&o.backup))
478 .then(self.unix_path.cmp(&o.unix_path))
479 .then(socketaddr_cmp(&self.address, &o.address));
480
481 debug_assert_eq!(
482 ordering == Ordering::Equal,
483 fields_all_equal,
484 "Backend::cmp returns Equal iff every keyed field is equal"
485 );
486 ordering
487 }
488}
489
490impl PartialOrd for Backend {
491 fn partial_cmp(&self, other: &Backend) -> Option<Ordering> {
492 Some(self.cmp(other))
493 }
494}
495
496impl Backend {
497 pub fn to_add_backend(self) -> AddBackend {
498 let source_address = self.address;
499 let source_backend_id = self.backend_id.clone();
500 let add_backend = AddBackend {
501 cluster_id: self.cluster_id.to_string(),
502 address: self.address.into(),
503 sticky_id: self.sticky_id,
504 backend_id: self.backend_id,
505 load_balancing_parameters: self.load_balancing_parameters,
506 backup: self.backup,
507 unix_path: self.unix_path,
508 };
509
510 debug_assert_eq!(
513 add_backend.backend_id, source_backend_id,
514 "backend_id must survive to_add_backend"
515 );
516 debug_assert_eq!(
517 SocketAddr::from(add_backend.address),
518 source_address,
519 "backend address must round-trip through to_add_backend"
520 );
521 add_backend
522 }
523}
524
525impl fmt::Display for RunState {
526 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
527 write!(f, "{self:?}")
528 }
529}
530
531pub type MessageId = String;
532
533impl WorkerResponse {
534 pub fn ok<T>(id: T) -> Self
535 where
536 T: ToString,
537 {
538 Self {
539 id: id.to_string(),
540 message: String::new(),
541 status: ResponseStatus::Ok.into(),
542 content: None,
543 }
544 }
545
546 pub fn ok_with_content<T>(id: T, content: ResponseContent) -> Self
547 where
548 T: ToString,
549 {
550 Self {
551 id: id.to_string(),
552 status: ResponseStatus::Ok.into(),
553 message: String::new(),
554 content: Some(content),
555 }
556 }
557
558 pub fn error<T, U>(id: T, error: U) -> Self
559 where
560 T: ToString,
561 U: ToString,
562 {
563 Self {
564 id: id.to_string(),
565 message: error.to_string(),
566 status: ResponseStatus::Failure.into(),
567 content: None,
568 }
569 }
570
571 pub fn processing<T>(id: T) -> Self
572 where
573 T: ToString,
574 {
575 Self {
576 id: id.to_string(),
577 message: String::new(),
578 status: ResponseStatus::Processing.into(),
579 content: None,
580 }
581 }
582
583 pub fn with_status<T>(id: T, status: ResponseStatus) -> Self
584 where
585 T: ToString,
586 {
587 Self {
588 id: id.to_string(),
589 message: String::new(),
590 status: status.into(),
591 content: None,
592 }
593 }
594
595 pub fn is_failure(&self) -> bool {
596 self.status == ResponseStatus::Failure as i32
597 }
598}
599
600impl fmt::Display for WorkerResponse {
601 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
602 write!(f, "{}-{:?}", self.id, self.status)
603 }
604}
605
606impl fmt::Display for FilteredTimeSerie {
607 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
608 writeln!(f, "FilteredTimeSerie {{")?;
614 writeln!(f, "last_second: {},", self.last_second)?;
615 writeln!(f, "last_minute:")?;
616 for window in self.last_minute.chunks(10) {
617 writeln!(f, "{window:?}")?;
618 }
619 writeln!(f, "last_hour:")?;
620 for window in self.last_hour.chunks(10) {
621 writeln!(f, "{window:?}")?;
622 }
623 write!(f, "}}")
624 }
625}
626
627fn socketaddr_cmp(a: &SocketAddr, b: &SocketAddr) -> Ordering {
628 let ordering = a.ip().cmp(&b.ip()).then(a.port().cmp(&b.port()));
629 debug_assert_eq!(
633 ordering == Ordering::Equal,
634 a.ip() == b.ip() && a.port() == b.port(),
635 "socketaddr_cmp is Equal iff ip and port both match"
636 );
637 ordering
638}
639
640#[cfg(test)]
641mod tests {
642 use crate::proto::command::{FilteredTimeSerie, PathRule, PathRuleKind};
653
654 #[test]
657 fn the_path_only_regex_cli_flag_builds_a_regex_path_rule_and_ranks_last() {
658 let some = |v: &str| Some(v.to_owned());
659 assert_eq!(
660 PathRule::from_cli_options(None, None, None, some("^/a$")),
661 PathRule::regex_path("^/a$"),
662 );
663 assert_eq!(
664 PathRule::from_cli_options(None, None, some("/e"), some("^/a$")),
665 PathRule::equals("/e"),
666 );
667 assert_eq!(
668 PathRule::from_cli_options(None, some("/r"), None, some("^/a$")),
669 PathRule::regex("/r"),
670 );
671 assert_eq!(
672 PathRule::from_cli_options(some("/p"), None, None, some("^/a$")),
673 PathRule::prefix("/p"),
674 );
675 assert_eq!(
676 PathRule::regex_path("/x").to_string(),
677 "path-only regexp '/x'",
678 "the frontend listing names the kind",
679 );
680 assert_eq!(
681 PathRuleKind::try_from(PathRule::regex_path("/x").kind),
682 Ok(PathRuleKind::RegexPath)
683 );
684 }
685
686 #[test]
687 fn a_series_shorter_than_sixty_samples_renders_instead_of_panicking() {
688 let serie = FilteredTimeSerie {
689 last_second: 42,
690 last_minute: vec![1, 2, 3],
691 last_hour: vec![],
692 };
693
694 let rendered = serie.to_string();
695
696 assert!(
697 rendered.contains("last_second: 42"),
698 "the rendering must keep its header, got {rendered:?}"
699 );
700 assert!(
701 rendered.contains("[1, 2, 3]"),
702 "a partial minute window must render the samples it has, got {rendered:?}"
703 );
704 }
705
706 #[test]
707 fn a_full_series_still_renders_six_windows_per_serie() {
708 let serie = FilteredTimeSerie {
712 last_second: 7,
713 last_minute: (0..60).collect(),
714 last_hour: (0..60).collect(),
715 };
716
717 let rendered = serie.to_string();
718 let lines: Vec<&str> = rendered.lines().collect();
719
720 assert_eq!(
721 lines.len(),
722 17,
723 "1 header + 1 label + 6 windows + 1 label + 6 windows + 1 brace, got {rendered:?}"
724 );
725 assert_eq!(lines[2], "last_minute:");
726 assert_eq!(lines[3], "[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]");
727 assert_eq!(lines[8], "[50, 51, 52, 53, 54, 55, 56, 57, 58, 59]");
728 assert_eq!(lines[9], "last_hour:");
729 assert_eq!(lines[16], "}");
730 }
731}