1use std::{
2 collections::{BTreeMap, HashMap, HashSet},
3 fmt::{self, Display, Formatter},
4 net::SocketAddr,
5};
6
7use prettytable::{Row, Table, cell, row};
8use time::format_description;
9use x509_parser::time::ASN1Time;
10
11use super::command::FilteredHistogram;
12use crate::{
13 AsString,
14 proto::{
15 DisplayError,
16 command::{
17 AggregatedMetrics, AvailableMetrics, CertificateAndKey, CertificateSummary,
18 CertificatesWithFingerprints, ClientAuthMode, Cluster, ClusterMetrics,
19 CustomHttpAnswers, Event, EventKind, FilteredMetrics, ForwardedHeaders,
20 HealthCheckConfig, HealthCheckMode, HealthChecksList, HttpEndpoint, HttpListenerConfig,
21 HttpStatusRange, HttpsListenerConfig, ListOfCertificatesByAddress, ListedFrontends,
22 ListenersList, MetricDetailStatus, ProtobufEndpoint, QueryCertificatesFilters,
23 RequestCounts, Response, ResponseContent, ResponseStatus, RunState, SocketAddress,
24 TlsVersion, WorkerInfos, WorkerMetrics, WorkerResponses, filtered_metrics,
25 protobuf_endpoint, request::RequestType, response_content::ContentType,
26 },
27 },
28};
29
30impl Display for CertificateAndKey {
31 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
32 let versions = self.versions.iter().fold(String::new(), |acc, tls_v| {
33 acc + " "
34 + match TlsVersion::try_from(*tls_v) {
35 Ok(v) => v.as_str_name(),
36 Err(_) => "",
37 }
38 });
39 write!(
40 f,
41 "\tcertificate: {}\n\tcertificate_chain: {:?}\n\tkey: {}\n\tTLS versions: {}\n\tnames: {:?}",
42 self.certificate,
43 self.certificate_chain,
44 self.key,
45 versions,
46 concatenate_vector(&self.names)
47 )
48 }
49}
50
51impl Display for CertificateSummary {
52 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
53 write!(f, "{}:\t{}", self.fingerprint, self.domain)
54 }
55}
56
57impl Display for QueryCertificatesFilters {
58 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
59 if let Some(d) = self.domain.clone() {
60 write!(f, "domain:{d}")
61 } else if let Some(fp) = self.fingerprint.clone() {
62 write!(f, "fingerprint:{fp}")
63 } else {
64 write!(f, "all certificates")
65 }
66 }
67}
68
69pub fn concatenate_vector(vec: &[String]) -> String {
70 vec.join(", ")
71}
72
73pub fn format_request_type(request_type: &RequestType) -> &str {
74 match request_type {
75 RequestType::SaveState(_) => "SaveState",
76 RequestType::LoadState(_) => "LoadState",
77 RequestType::CountRequests(_) => "CountRequests",
78 RequestType::ListWorkers(_) => "ListWorkers",
79 RequestType::ListFrontends(_) => "ListFrontends",
80 RequestType::ListListeners(_) => "ListListeners",
81 RequestType::LaunchWorker(_) => "LaunchWorker",
82 RequestType::UpgradeMain(_) => "UpgradeMain",
83 RequestType::UpgradeWorker(_) => "UpgradeWorker",
84 RequestType::SubscribeEvents(_) => "SubscribeEvents",
85 RequestType::ReloadConfiguration(_) => "ReloadConfiguration",
86 RequestType::Status(_) => "Status",
87 RequestType::AddCluster(_) => "AddCluster",
88 RequestType::RemoveCluster(_) => "RemoveCluster",
89 RequestType::AddHttpFrontend(_) => "AddHttpFrontend",
90 RequestType::RemoveHttpFrontend(_) => "RemoveHttpFrontend",
91 RequestType::AddHttpsFrontend(_) => "AddHttpsFrontend",
92 RequestType::RemoveHttpsFrontend(_) => "RemoveHttpsFrontend",
93 RequestType::AddCertificate(_) => "AddCertificate",
94 RequestType::ReplaceCertificate(_) => "ReplaceCertificate",
95 RequestType::RemoveCertificate(_) => "RemoveCertificate",
96 RequestType::AddTcpFrontend(_) => "AddTcpFrontend",
97 RequestType::RemoveTcpFrontend(_) => "RemoveTcpFrontend",
98 RequestType::AddBackend(_) => "AddBackend",
99 RequestType::RemoveBackend(_) => "RemoveBackend",
100 RequestType::AddHttpListener(_) => "AddHttpListener",
101 RequestType::AddHttpsListener(_) => "AddHttpsListener",
102 RequestType::AddHttpsListenerWithClientAuth(_) => "AddHttpsListenerWithClientAuth",
103 RequestType::AddTcpListener(_) => "AddTcpListener",
104 RequestType::AddUdpListener(_) => "AddUdpListener",
105 RequestType::UpdateUdpListener(_) => "UpdateUdpListener",
106 RequestType::AddUdpFrontend(_) => "AddUdpFrontend",
107 RequestType::RemoveUdpFrontend(_) => "RemoveUdpFrontend",
108 RequestType::RemoveListener(_) => "RemoveListener",
109 RequestType::ActivateListener(_) => "ActivateListener",
110 RequestType::DeactivateListener(_) => "DeactivateListener",
111 RequestType::QueryClusterById(_) => "QueryClusterById",
112 RequestType::QueryClustersByDomain(_) => "QueryClustersByDomain",
113 RequestType::QueryClustersHashes(_) => "QueryClustersHashes",
114 RequestType::QueryMetrics(_) => "QueryMetrics",
115 RequestType::SoftStop(_) => "SoftStop",
116 RequestType::HardStop(_) => "HardStop",
117 RequestType::ConfigureMetrics(_) => "ConfigureMetrics",
118 RequestType::Logging(_) => "Logging",
119 RequestType::ReturnListenSockets(_) => "ReturnListenSockets",
120 RequestType::QueryCertificatesFromTheState(_) => "QueryCertificatesFromTheState",
121 RequestType::QueryCertificatesFromWorkers(_) => "QueryCertificatesFromWorkers",
122 RequestType::UpdateHttpListener(_) => "UpdateHttpListener",
123 RequestType::UpdateHttpsListener(_) => "UpdateHttpsListener",
124 RequestType::UpdateHttpsListenerWithClientAuth(_) => "UpdateHttpsListenerWithClientAuth",
125 RequestType::UpdateTcpListener(_) => "UpdateTcpListener",
126 RequestType::SetMaxConnectionsPerIp(_) => "SetMaxConnectionsPerIp",
127 RequestType::QueryMaxConnectionsPerIp(_) => "QueryMaxConnectionsPerIp",
128 RequestType::SetMaxConnectionsPerSubnet(_) => "SetMaxConnectionsPerSubnet",
129 RequestType::SetMaxConnectionAttempts(_) => "SetMaxConnectionAttempts",
130 RequestType::QueryMaxConnectionsPerSubnet(_) => "QueryMaxConnectionsPerSubnet",
131 RequestType::SetHealthCheck(_) => "SetHealthCheck",
132 RequestType::RemoveHealthCheck(_) => "RemoveHealthCheck",
133 RequestType::QueryHealthChecks(_) => "QueryHealthChecks",
134 RequestType::SetMetricDetail(_) => "SetMetricDetail",
135 }
136}
137
138pub fn print_json_response<T: ::serde::Serialize>(input: &T) -> Result<(), DisplayError> {
139 let pretty_json = serde_json::to_string_pretty(&input).map_err(DisplayError::Json)?;
140 println!("{pretty_json}");
141 Ok(())
142}
143
144impl Response {
145 pub fn display(&self, json: bool) -> Result<(), DisplayError> {
146 match self.status() {
147 ResponseStatus::Ok => {
148 if !json {
150 println!("Success: {}", self.message)
151 }
152 }
153 ResponseStatus::Failure => println!("Failure: {}", self.message),
154 ResponseStatus::Processing => {
155 return Err(DisplayError::WrongResponseType(
156 "ResponseStatus::Processing".to_string(),
157 ));
158 }
159 }
160
161 match &self.content {
162 Some(content) => content.display(json),
163 None => {
164 if json {
165 println!("{{}}");
166 } else {
167 println!("No content");
168 }
169 Ok(())
170 }
171 }
172 }
173}
174
175impl ResponseContent {
176 fn display(&self, json: bool) -> Result<(), DisplayError> {
177 let content_type = match &self.content_type {
178 Some(content_type) => content_type,
179 None => {
180 println!("No content");
181 return Ok(());
182 }
183 };
184
185 if json {
186 return print_json_response(&content_type);
187 }
188
189 match content_type {
190 ContentType::Workers(worker_infos) => print_status(worker_infos),
191 ContentType::Metrics(aggr_metrics) => print_metrics(aggr_metrics),
192 ContentType::FrontendList(frontends) => print_frontends(frontends),
193 ContentType::ListenersList(listeners) => print_listeners(listeners),
194 ContentType::WorkerMetrics(worker_metrics) => print_worker_metrics(worker_metrics),
195 ContentType::AvailableMetrics(list) => print_available_metrics(list),
196 ContentType::RequestCounts(request_counts) => print_request_counts(request_counts),
197 ContentType::CertificatesWithFingerprints(certs) => {
198 print_certificates_with_validity(certs)
199 }
200 ContentType::WorkerResponses(worker_responses) => {
201 if worker_responses.contain_cluster_infos() {
203 print_cluster_infos(worker_responses)
204 } else if worker_responses.contain_cluster_hashes() {
205 print_cluster_hashes(worker_responses)
206 } else {
207 print_responses_by_worker(worker_responses, json)
208 }
209 }
210 ContentType::Clusters(_) | ContentType::ClusterHashes(_) => Ok(()), ContentType::CertificatesByAddress(certs) => print_certificates_by_address(certs),
212 ContentType::MetricDetailStatus(status) => print_metric_detail_status(status),
213 ContentType::MaxConnectionsPerIpLimit(limit_info) => {
214 if limit_info.limit == 0 {
215 println!("Max connections per (cluster, source-IP): unlimited (0)");
216 } else {
217 println!(
218 "Max connections per (cluster, source-IP): {}",
219 limit_info.limit
220 );
221 }
222 Ok(())
223 }
224 ContentType::MaxConnectionsPerSubnetLimit(limit_info) => {
225 if limit_info.limit == 0 {
229 println!("Max connections per (cluster, source-subnet): unlimited (0)");
230 } else {
231 println!(
232 "Max connections per (cluster, source-subnet): {}",
233 limit_info.limit
234 );
235 }
236 println!(
237 "Subnet prefixes: IPv4 /{}, IPv6 /{}",
238 limit_info.ipv4_prefix, limit_info.ipv6_prefix
239 );
240 Ok(())
241 }
242 ContentType::HealthChecksList(list) => print_health_checks(list),
243 ContentType::Event(_event) => Ok(()), ContentType::WorkerMetricDetailStatus(_) => Ok(()),
252 }
253 }
254}
255
256impl WorkerResponses {
257 fn contain_cluster_infos(&self) -> bool {
258 for response in self.map.values() {
259 if let Some(content_type) = &response.content_type
260 && matches!(content_type, ContentType::Clusters(_))
261 {
262 return true;
263 }
264 }
265 false
266 }
267
268 fn contain_cluster_hashes(&self) -> bool {
269 for response in self.map.values() {
270 if let Some(content_type) = &response.content_type
271 && matches!(content_type, ContentType::ClusterHashes(_))
272 {
273 return true;
274 }
275 }
276 false
277 }
278}
279
280pub fn print_status(worker_infos: &WorkerInfos) -> Result<(), DisplayError> {
281 let mut table = Table::new();
282 table.set_format(*prettytable::format::consts::FORMAT_BOX_CHARS);
283 table.add_row(row!["worker id", "pid", "run state"]);
284
285 let mut sorted_infos = worker_infos.vec.clone();
286 sorted_infos.sort_by_key(|worker| worker.id);
287
288 for worker_info in &sorted_infos {
289 let row = row!(
290 worker_info.id,
291 worker_info.pid,
292 RunState::try_from(worker_info.run_state)
293 .map_err(DisplayError::DecodeError)?
294 .as_str_name()
295 );
296 table.add_row(row);
297 }
298
299 table.printstd();
300 Ok(())
301}
302
303pub fn print_metrics(aggregated_metrics: &AggregatedMetrics) -> Result<(), DisplayError> {
304 println!("\nMAIN PROCESS\n============");
306 print_proxy_metrics(&aggregated_metrics.main);
307
308 if !aggregated_metrics.proxying.is_empty() {
309 println!("\nPROXYING\n============");
310 print_proxy_metrics(&aggregated_metrics.proxying);
311 }
312
313 for (worker_id, worker) in aggregated_metrics.workers.iter() {
315 if !worker.clusters.is_empty() && !worker.proxy.is_empty() {
316 println!("\nWorker {worker_id}\n=========");
317 print_worker_metrics(worker)?;
318 }
319 }
320
321 if !aggregated_metrics.clusters.is_empty() {
323 println!("\nClusters\n=======");
324 print_cluster_metrics(&aggregated_metrics.clusters);
325 }
326
327 Ok(())
328}
329
330fn print_proxy_metrics(proxy_metrics: &BTreeMap<String, FilteredMetrics>) {
331 let filtered = filter_metrics(proxy_metrics);
332 print_gauges_and_counts(&filtered);
333 print_percentiles(&filtered);
334 print_histograms(&filtered);
335}
336
337fn print_worker_metrics(worker_metrics: &WorkerMetrics) -> Result<(), DisplayError> {
338 print_proxy_metrics(&worker_metrics.proxy);
339 print_cluster_metrics(&worker_metrics.clusters);
340
341 Ok(())
342}
343
344fn print_cluster_metrics(cluster_metrics: &BTreeMap<String, ClusterMetrics>) {
345 for (cluster_id, cluster_metrics_data) in cluster_metrics.iter() {
346 println!("\nCluster {cluster_id}\n--------");
347
348 let filtered = filter_metrics(&cluster_metrics_data.cluster);
349 print_gauges_and_counts(&filtered);
350 print_percentiles(&filtered);
351 print_histograms(&filtered);
352
353 let mut backend_metric_acc: HashMap<String, BTreeMap<String, FilteredMetrics>> =
355 HashMap::new();
356
357 for backend in &cluster_metrics_data.backends {
358 for (metric_name, value) in &backend.metrics {
359 backend_metric_acc
360 .entry(backend.backend_id.clone())
361 .and_modify(|map| {
362 map.insert(metric_name.clone(), value.clone());
363 })
364 .or_insert(BTreeMap::from([(metric_name.clone(), value.clone())]));
365 }
366 }
367
368 for (backend_id, metrics) in backend_metric_acc {
369 println!("\n{cluster_id}/{backend_id}\n--------");
370 let filtered = filter_metrics(&metrics);
371 print_gauges_and_counts(&filtered);
372 print_percentiles(&filtered);
373 print_histograms(&filtered);
374 }
375 }
376}
377
378fn filter_metrics(
379 metrics: &BTreeMap<String, FilteredMetrics>,
380) -> BTreeMap<String, FilteredMetrics> {
381 let mut filtered_metrics = BTreeMap::new();
382
383 for (metric_key, filtered_value) in metrics.iter() {
384 filtered_metrics.insert(
385 metric_key.replace('\t', ".").to_string(),
386 filtered_value.clone(),
387 );
388 }
389 filtered_metrics
390}
391
392fn print_gauges_and_counts(filtered_metrics: &BTreeMap<String, FilteredMetrics>) {
393 let mut titles: Vec<String> = filtered_metrics
394 .iter()
395 .filter_map(|(title, filtered_data)| match filtered_data.inner {
396 Some(filtered_metrics::Inner::Count(_)) | Some(filtered_metrics::Inner::Gauge(_)) => {
397 Some(title.to_owned())
398 }
399 _ => None,
400 })
401 .collect();
402
403 titles.sort();
405
406 if titles.is_empty() {
407 return;
408 }
409
410 let mut table = Table::new();
411 table.set_format(*prettytable::format::consts::FORMAT_BOX_CHARS);
412
413 table.set_titles(Row::new(vec![cell!(""), cell!("gauge"), cell!("count")]));
414
415 for title in titles {
416 let mut row = vec![cell!(title)];
417 match filtered_metrics.get(&title) {
418 Some(filtered_metrics) => match filtered_metrics.inner {
419 Some(filtered_metrics::Inner::Count(c)) => {
420 row.push(cell!(""));
421 row.push(cell!(c))
422 }
423 Some(filtered_metrics::Inner::Gauge(c)) => {
424 row.push(cell!(c));
425 row.push(cell!(""))
426 }
427 _ => {}
428 },
429 _ => row.push(cell!("")),
430 }
431 table.add_row(Row::new(row));
432 }
433
434 table.printstd();
435}
436
437fn print_percentiles(filtered_metrics: &BTreeMap<String, FilteredMetrics>) {
438 let mut percentile_titles: Vec<String> = filtered_metrics
439 .iter()
440 .filter_map(|(title, filtered_data)| match filtered_data.inner.clone() {
441 Some(filtered_metrics::Inner::Percentiles(_)) => Some(title.to_owned()),
442 _ => None,
443 })
444 .collect();
445
446 percentile_titles.sort();
448
449 if percentile_titles.is_empty() {
450 return;
451 }
452
453 let mut percentile_table = Table::new();
454 percentile_table.set_format(*prettytable::format::consts::FORMAT_BOX_CHARS);
455
456 percentile_table.set_titles(Row::new(vec![
457 cell!("Percentiles"),
458 cell!("samples"),
459 cell!("p50"),
460 cell!("p90"),
461 cell!("p99"),
462 cell!("p99.9"),
463 cell!("p99.99"),
464 cell!("p99.999"),
465 cell!("p100"),
466 ]));
467
468 for title in percentile_titles {
469 if let Some(FilteredMetrics {
470 inner: Some(filtered_metrics::Inner::Percentiles(percentiles)),
471 }) = filtered_metrics.get(&title)
472 {
473 percentile_table.add_row(Row::new(vec![
474 cell!(title),
475 cell!(percentiles.samples),
476 cell!(percentiles.p_50),
477 cell!(percentiles.p_90),
478 cell!(percentiles.p_99),
479 cell!(percentiles.p_99_9),
480 cell!(percentiles.p_99_99),
481 cell!(percentiles.p_99_999),
482 cell!(percentiles.p_100),
483 ]));
484 } else {
485 println!("Something went VERY wrong here");
486 }
487 }
488
489 percentile_table.printstd();
490}
491
492fn print_histograms(filtered_metrics: &BTreeMap<String, FilteredMetrics>) {
493 let histograms: BTreeMap<String, FilteredHistogram> = filtered_metrics
494 .iter()
495 .filter_map(|(name, metric)| match metric.inner.clone() {
496 Some(filtered_metrics::Inner::Histogram(hist)) => Some((name.to_owned(), hist)),
497 _ => None,
498 })
499 .collect();
500
501 let mut histogram_titles: Vec<String> = histograms.keys().map(ToOwned::to_owned).collect();
502 histogram_titles.sort();
503 if histogram_titles.is_empty() {
504 return;
505 }
506
507 let mut histogram_table = Table::new();
508 histogram_table.set_format(*prettytable::format::consts::FORMAT_BOX_CHARS);
509
510 let mut first_row = Row::new(vec![cell!("Histograms (ms)"), cell!("sum"), cell!("count")]);
511
512 let biggest_hist_length = histograms
513 .values()
514 .map(|hist| hist.buckets.len())
515 .max()
516 .unwrap_or(0);
517
518 for exponent in 0..biggest_hist_length {
520 first_row.add_cell(cell!(format!("{}", (1 << exponent) - 1)));
521 }
522 histogram_table.set_titles(first_row);
523
524 for title in histogram_titles {
525 if let Some(hist) = histograms.get(&title) {
526 let trimmed_name = title.strip_suffix("_histogram").unwrap_or_default();
527 let mut row = Row::new(vec![
528 cell!(trimmed_name),
529 cell!(hist.sum),
530 cell!(hist.count),
531 ]);
532 let mut last_bucket_count = 0;
534 for bucket in &hist.buckets {
535 row.add_cell(cell!(bucket.count - last_bucket_count));
536 last_bucket_count = bucket.count;
537 }
538 histogram_table.add_row(row);
539 }
540 }
541
542 histogram_table.printstd();
543}
544
545fn print_available_metrics(available_metrics: &AvailableMetrics) -> Result<(), DisplayError> {
546 println!("Available metrics on the proxy level:");
547 for metric_name in &available_metrics.proxy_metrics {
548 println!("\t{metric_name}");
549 }
550 println!("Available metrics on the cluster level:");
551 for metric_name in &available_metrics.cluster_metrics {
552 println!("\t{metric_name}");
553 }
554 Ok(())
555}
556
557fn print_frontends(frontends: &ListedFrontends) -> Result<(), DisplayError> {
558 trace!(" We received this frontends to display {:#?}", frontends);
559 if !frontends.http_frontends.is_empty() {
561 let mut table = Table::new();
562 table.set_format(*prettytable::format::consts::FORMAT_BOX_CHARS);
563 table.add_row(row!["HTTP frontends "]);
564 table.add_row(row![
565 "cluster_id",
566 "address",
567 "hostname",
568 "path",
569 "method",
570 "position",
571 "tags"
572 ]);
573 for http_frontend in frontends.http_frontends.iter() {
574 table.add_row(row!(
575 http_frontend
576 .cluster_id
577 .clone()
578 .unwrap_or("Deny".to_owned()),
579 http_frontend.address.to_string(),
580 http_frontend.hostname.to_string(),
581 format!("{:?}", http_frontend.path),
582 format!("{:?}", http_frontend.method),
583 format!("{:?}", http_frontend.position),
584 format_tags_to_string(&http_frontend.tags)
585 ));
586 }
587 table.printstd();
588 }
589
590 if !frontends.https_frontends.is_empty() {
592 let mut table = Table::new();
593 table.set_format(*prettytable::format::consts::FORMAT_BOX_CHARS);
594 table.add_row(row!["HTTPS frontends"]);
595 table.add_row(row![
596 "cluster_id",
597 "address",
598 "hostname",
599 "path",
600 "method",
601 "position",
602 "tags"
603 ]);
604 for https_frontend in frontends.https_frontends.iter() {
605 table.add_row(row!(
606 https_frontend
607 .cluster_id
608 .clone()
609 .unwrap_or("Deny".to_owned()),
610 https_frontend.address.to_string(),
611 https_frontend.hostname.to_string(),
612 format!("{:?}", https_frontend.path),
613 format!("{:?}", https_frontend.method),
614 format!("{:?}", https_frontend.position),
615 format_tags_to_string(&https_frontend.tags)
616 ));
617 }
618 table.printstd();
619 }
620
621 if !frontends.tcp_frontends.is_empty() {
623 let mut table = Table::new();
624 table.set_format(*prettytable::format::consts::FORMAT_BOX_CHARS);
625 table.add_row(row!["TCP frontends "]);
626 table.add_row(row!["Cluster ID", "address", "tags"]);
627 for tcp_frontend in frontends.tcp_frontends.iter() {
628 table.add_row(row!(
629 tcp_frontend.cluster_id,
630 tcp_frontend.address,
631 format_tags_to_string(&tcp_frontend.tags)
632 ));
633 }
634 table.printstd();
635 }
636
637 if !frontends.udp_frontends.is_empty() {
639 let mut table = Table::new();
640 table.set_format(*prettytable::format::consts::FORMAT_BOX_CHARS);
641 table.add_row(row!["UDP frontends "]);
642 table.add_row(row!["Cluster ID", "address", "tags"]);
643 for udp_frontend in frontends.udp_frontends.iter() {
644 table.add_row(row!(
645 udp_frontend.cluster_id,
646 udp_frontend.address,
647 format_tags_to_string(&udp_frontend.tags)
648 ));
649 }
650 table.printstd();
651 }
652 Ok(())
653}
654
655pub fn print_listeners(listeners_list: &ListenersList) -> Result<(), DisplayError> {
656 println!("\nHTTP LISTENERS\n================");
657
658 for http_listener in listeners_list.http_listeners.values() {
659 println!("{http_listener}");
660 }
661
662 println!("\nHTTPS LISTENERS\n================");
663
664 for https_listener in listeners_list.https_listeners.values() {
665 println!("{https_listener}");
666 }
667
668 println!("\nTCP LISTENERS\n================");
669
670 if !listeners_list.tcp_listeners.is_empty() {
671 let mut table = Table::new();
672 table.set_format(*prettytable::format::consts::FORMAT_BOX_CHARS);
673 table.add_row(row!["TCP frontends"]);
674 table.add_row(row![
675 "socket address",
676 "interface",
677 "public address",
678 "expect proxy",
679 "front timeout",
680 "back timeout",
681 "connect timeout",
682 "activated"
683 ]);
684 for tcp_listener in listeners_list.tcp_listeners.values() {
685 table.add_row(row![
686 format!("{:?}", tcp_listener.address),
687 tcp_listener.interface.as_deref().unwrap_or("-"),
688 format!("{:?}", tcp_listener.public_address),
689 tcp_listener.expect_proxy,
690 tcp_listener.front_timeout,
691 tcp_listener.back_timeout,
692 tcp_listener.connect_timeout,
693 tcp_listener.active,
694 ]);
695 }
696 table.printstd();
697 }
698
699 println!("\nUDP LISTENERS\n================");
700
701 if !listeners_list.udp_listeners.is_empty() {
702 let mut table = Table::new();
703 table.set_format(*prettytable::format::consts::FORMAT_BOX_CHARS);
704 table.add_row(row!["UDP listeners"]);
705 table.add_row(row![
708 "socket address",
709 "interface",
710 "public address",
711 "front timeout",
712 "back timeout",
713 "max rx datagram size",
714 "max flows",
715 "activated"
716 ]);
717 for udp_listener in listeners_list.udp_listeners.values() {
718 table.add_row(row![
719 format!("{:?}", udp_listener.address),
720 udp_listener.interface.as_deref().unwrap_or("-"),
721 format!("{:?}", udp_listener.public_address),
722 udp_listener.front_timeout,
723 udp_listener.back_timeout,
724 udp_listener.max_rx_datagram_size,
725 udp_listener.max_flows,
726 udp_listener.active,
727 ]);
728 }
729 table.printstd();
730 }
731 Ok(())
732}
733
734fn affinity_key_label(cluster: &Cluster) -> String {
737 match (&cluster.affinity_header, &cluster.affinity_cookie) {
738 (Some(header), _) => format!("header:{header}"),
739 (None, Some(cookie)) => format!("cookie:{cookie}"),
740 (None, None) => String::from("source_ip"),
741 }
742}
743
744fn shuffle_sharding_label(cluster: &Cluster) -> String {
747 match cluster.shard_percent {
748 None => String::from("off"),
749 Some(percent) => format!(
750 "{percent}% from {} backends, {}",
751 cluster
752 .shard_min_backends
753 .unwrap_or(crate::config::DEFAULT_SHARD_MIN_BACKENDS),
754 cluster.shard_mode().as_str_name()
755 ),
756 }
757}
758
759fn print_cluster_infos(worker_responses: &WorkerResponses) -> Result<(), DisplayError> {
760 let mut cluster_table = create_cluster_table(
761 vec![
762 "id",
763 "sticky_session",
764 "https_redirect",
765 "affinity_key",
766 "shuffle_sharding",
767 "max_connection_attempts",
768 ],
769 &worker_responses.map,
770 );
771
772 let mut frontend_table =
773 create_cluster_table(vec!["id", "hostname", "path"], &worker_responses.map);
774
775 let mut https_frontend_table =
776 create_cluster_table(vec!["id", "hostname", "path"], &worker_responses.map);
777
778 let mut tcp_frontend_table = create_cluster_table(vec!["id", "address"], &worker_responses.map);
779
780 let mut udp_frontend_table = create_cluster_table(vec!["id", "address"], &worker_responses.map);
781
782 let mut backend_table = create_cluster_table(
783 vec!["backend id", "IP address", "Backup"],
784 &worker_responses.map,
785 );
786
787 let worker_ids: HashSet<&String> = worker_responses.map.keys().collect();
788
789 let mut cluster_infos = BTreeMap::new();
790 let mut http_frontends = BTreeMap::new();
791 let mut https_frontends = BTreeMap::new();
792 let mut tcp_frontends = BTreeMap::new();
793 let mut udp_frontends = BTreeMap::new();
794 let mut backends = BTreeMap::new();
795
796 for (worker_id, response_content) in worker_responses.map.iter() {
797 if let Some(ContentType::Clusters(clusters)) = &response_content.content_type {
798 for cluster in clusters.vec.iter() {
799 if cluster.configuration.is_some() {
800 let entry = cluster_infos.entry(cluster).or_insert(Vec::new());
801 entry.push(worker_id.to_owned());
802 }
803
804 for frontend in cluster.http_frontends.iter() {
805 let entry = http_frontends.entry(frontend).or_insert(Vec::new());
806 entry.push(worker_id.to_owned());
807 }
808
809 for frontend in cluster.https_frontends.iter() {
810 let entry = https_frontends.entry(frontend).or_insert(Vec::new());
811 entry.push(worker_id.to_owned());
812 }
813
814 for frontend in cluster.tcp_frontends.iter() {
815 let entry = tcp_frontends.entry(frontend).or_insert(Vec::new());
816 entry.push(worker_id.to_owned());
817 }
818
819 for frontend in cluster.udp_frontends.iter() {
820 let entry = udp_frontends.entry(frontend).or_insert(Vec::new());
821 entry.push(worker_id.to_owned());
822 }
823
824 for backend in cluster.backends.iter() {
825 let entry = backends.entry(backend).or_insert(Vec::new());
826 entry.push(worker_id.to_owned());
827 }
828 }
829 }
830 }
831
832 if cluster_infos.is_empty() {
833 println!("no cluster found");
834 return Ok(());
835 }
836
837 println!("Cluster level configuration:\n");
838
839 for (cluster_info, workers_the_cluster_is_present_on) in cluster_infos.iter() {
840 let configuration = cluster_info.configuration.as_ref();
841 let mut row = vec![
842 cell!(
843 configuration
844 .map(|conf| conf.cluster_id.to_owned())
845 .unwrap_or_else(|| String::from("None"))
846 ),
847 cell!(configuration.is_some_and(|conf| conf.sticky_session)),
848 cell!(configuration.is_some_and(|conf| conf.https_redirect)),
849 cell!(
850 configuration
851 .map(affinity_key_label)
852 .unwrap_or_else(|| String::from("-"))
853 ),
854 cell!(
855 configuration
856 .map(shuffle_sharding_label)
857 .unwrap_or_else(|| String::from("-"))
858 ),
859 cell!(
861 configuration
862 .and_then(|conf| conf.max_connection_attempts)
863 .map_or_else(|| String::from("global"), |attempts| attempts.to_string())
864 ),
865 ];
866
867 for worker in workers_the_cluster_is_present_on {
868 if worker_ids.contains(worker) {
869 row.push(cell!("X"));
870 } else {
871 row.push(cell!(""));
872 }
873 }
874
875 cluster_table.add_row(Row::new(row));
876 }
877
878 cluster_table.printstd();
879
880 println!("\nHTTP frontends configuration for:\n");
881
882 for (key, values) in http_frontends.iter() {
883 let mut row = Vec::new();
884 match &key.cluster_id {
885 Some(cluster_id) => row.push(cell!(cluster_id)),
886 None => row.push(cell!("-")),
887 }
888 row.push(cell!(key.hostname));
889 row.push(cell!(key.path));
890
891 for val in values.iter() {
892 if worker_ids.contains(val) {
893 row.push(cell!("X"));
894 } else {
895 row.push(cell!(""));
896 }
897 }
898
899 frontend_table.add_row(Row::new(row));
900 }
901
902 frontend_table.printstd();
903
904 println!("\nHTTPS frontends configuration for:\n");
905
906 for (key, values) in https_frontends.iter() {
907 let mut row = Vec::new();
908 match &key.cluster_id {
909 Some(cluster_id) => row.push(cell!(cluster_id)),
910 None => row.push(cell!("-")),
911 }
912 row.push(cell!(key.hostname));
913 row.push(cell!(key.path));
914
915 for val in values.iter() {
916 if worker_ids.contains(val) {
917 row.push(cell!("X"));
918 } else {
919 row.push(cell!(""));
920 }
921 }
922
923 https_frontend_table.add_row(Row::new(row));
924 }
925
926 https_frontend_table.printstd();
927
928 println!("\nTCP frontends configuration:\n");
929
930 for (key, values) in tcp_frontends.iter() {
931 let mut row = vec![cell!(key.cluster_id), cell!(format!("{}", key.address))];
932
933 for val in values.iter() {
934 if worker_ids.contains(val) {
935 row.push(cell!(String::from("X")));
936 } else {
937 row.push(cell!(String::from("")));
938 }
939 }
940
941 tcp_frontend_table.add_row(Row::new(row));
942 }
943
944 tcp_frontend_table.printstd();
945
946 println!("\nUDP frontends configuration:\n");
947
948 for (key, values) in udp_frontends.iter() {
949 let mut row = vec![cell!(key.cluster_id), cell!(format!("{}", key.address))];
950
951 for val in values.iter() {
952 if worker_ids.contains(val) {
953 row.push(cell!(String::from("X")));
954 } else {
955 row.push(cell!(String::from("")));
956 }
957 }
958
959 udp_frontend_table.add_row(Row::new(row));
960 }
961
962 udp_frontend_table.printstd();
963
964 println!("\nbackends configuration:\n");
965
966 for (key, values) in backends.iter() {
967 let mut row = vec![
968 cell!(key.backend_id),
969 cell!(format!("{}", key.address)),
970 cell!(
971 key.backup
972 .map(|b| if b { "X" } else { "" })
973 .unwrap_or_else(|| "")
974 ),
975 ];
976
977 for val in values {
978 if worker_ids.contains(&val) {
979 row.push(cell!("X"));
980 } else {
981 row.push(cell!(""));
982 }
983 }
984
985 backend_table.add_row(Row::new(row));
986 }
987
988 backend_table.printstd();
989
990 Ok(())
991}
992
993fn print_cluster_hashes(worker_responses: &WorkerResponses) -> Result<(), DisplayError> {
995 let mut clusters_table = Table::new();
996 clusters_table.set_format(*prettytable::format::consts::FORMAT_BOX_CHARS);
997 let mut header = vec![cell!("cluster id")];
998 for worker_id in worker_responses.map.keys() {
999 header.push(cell!(format!("worker {}", worker_id)));
1000 }
1001 header.push(cell!("desynchronized"));
1002 clusters_table.add_row(Row::new(header));
1003
1004 let mut cluster_hashes = HashMap::new();
1005
1006 for response_content in worker_responses.map.values() {
1007 if let Some(ContentType::ClusterHashes(hashes)) = &response_content.content_type {
1008 for (cluster_id, hash) in hashes.map.iter() {
1009 cluster_hashes
1010 .entry(cluster_id)
1011 .or_insert(Vec::new())
1012 .push(hash);
1013 }
1014 }
1015 }
1016
1017 for (cluster_id, hashes) in cluster_hashes.iter() {
1018 let mut row = vec![cell!(cluster_id)];
1019 for val in hashes.iter() {
1020 row.push(cell!(format!("{val}")));
1021 }
1022
1023 let hs: HashSet<&u64> = hashes.iter().cloned().collect();
1024 if hs.len() > 1 {
1025 row.push(cell!("X"));
1026 } else {
1027 row.push(cell!(""));
1028 }
1029
1030 clusters_table.add_row(Row::new(row));
1031 }
1032
1033 clusters_table.printstd();
1034 Ok(())
1035}
1036
1037fn print_responses_by_worker(
1038 worker_responses: &WorkerResponses,
1039 json: bool,
1040) -> Result<(), DisplayError> {
1041 for (worker_id, content) in worker_responses.map.iter() {
1042 println!("Worker {worker_id}");
1043 content.display(json)?;
1044 }
1045
1046 Ok(())
1047}
1048
1049pub fn print_certificates_with_validity(
1050 certs: &CertificatesWithFingerprints,
1051) -> Result<(), DisplayError> {
1052 if certs.certs.is_empty() {
1053 println!("No certificates match your request.");
1054 return Ok(());
1055 }
1056
1057 let mut table = Table::new();
1058 table.set_format(*prettytable::format::consts::FORMAT_CLEAN);
1059 table.add_row(row![
1060 "fingeprint",
1061 "valid not before",
1062 "valide not after",
1063 "domain names",
1064 ]);
1065
1066 for (fingerprint, cert) in &certs.certs {
1067 let (_unparsed, pem_certificate) =
1068 x509_parser::pem::parse_x509_pem(cert.certificate.as_bytes())
1069 .expect("Could not parse pem certificate");
1070
1071 let x509_certificate = pem_certificate
1072 .parse_x509()
1073 .expect("Could not parse x509 certificate");
1074
1075 let validity = x509_certificate.validity();
1076
1077 table.add_row(row!(
1078 fingerprint,
1079 format_datetime(validity.not_before)?,
1080 format_datetime(validity.not_after)?,
1081 concatenate_vector(&cert.names),
1082 ));
1083 }
1084 table.printstd();
1085
1086 Ok(())
1087}
1088
1089fn print_certificates_by_address(list: &ListOfCertificatesByAddress) -> Result<(), DisplayError> {
1090 for certs in list.certificates.iter() {
1091 println!("\t{}:", certs.address);
1092
1093 for summary in certs.certificate_summaries.iter() {
1094 println!("\t\t{summary}");
1095 }
1096 }
1097 Ok(())
1098}
1099
1100fn print_metric_detail_status(status: &MetricDetailStatus) -> Result<(), DisplayError> {
1101 let mut table = Table::new();
1102 table.set_format(*prettytable::format::consts::FORMAT_BOX_CHARS);
1103 table.add_row(row![
1104 "scope",
1105 "configured",
1106 "previous effective",
1107 "effective",
1108 "active leases"
1109 ]);
1110 table.add_row(row!(
1111 "main",
1112 status.configured().as_str_name(),
1113 status.previous_effective().as_str_name(),
1114 status.effective().as_str_name(),
1115 "—",
1116 ));
1117 for (worker_id, w) in &status.workers {
1118 table.add_row(row!(
1119 format!("worker:{worker_id}"),
1120 w.configured().as_str_name(),
1121 w.previous_effective().as_str_name(),
1122 w.effective().as_str_name(),
1123 w.active_lease_count,
1124 ));
1125 }
1126 table.printstd();
1127 Ok(())
1128}
1129
1130fn print_request_counts(request_counts: &RequestCounts) -> Result<(), DisplayError> {
1131 let mut table = Table::new();
1132 table.set_format(*prettytable::format::consts::FORMAT_BOX_CHARS);
1133 table.add_row(row!["request type", "count"]);
1134
1135 for (request_type, count) in &request_counts.map {
1136 table.add_row(row!(request_type, count));
1137 }
1138 table.printstd();
1139 Ok(())
1140}
1141
1142fn print_health_checks(list: &HealthChecksList) -> Result<(), DisplayError> {
1143 if list.map.is_empty() {
1144 println!("No health checks configured.");
1145 return Ok(());
1146 }
1147
1148 let mut table = Table::new();
1149 table.set_format(*prettytable::format::consts::FORMAT_BOX_CHARS);
1150 table.add_row(row![
1151 "cluster",
1152 "mode",
1153 "uri",
1154 "interval",
1155 "timeout",
1156 "healthy threshold",
1157 "unhealthy threshold",
1158 "accepted statuses"
1159 ]);
1160
1161 let mut entries: Vec<_> = list.map.iter().collect();
1162 entries.sort_by_key(|(id, _)| id.as_str());
1163
1164 for (cluster_id, config) in entries {
1165 let (mode, uri, accepted) = match HealthCheckMode::try_from(config.mode) {
1166 Ok(HealthCheckMode::Http) => ("http", config.uri.as_str(), accepted_statuses(config)),
1167 Ok(HealthCheckMode::Tcp) => ("tcp", "-", "-".to_owned()),
1168 Err(_) => ("unknown", config.uri.as_str(), "-".to_owned()),
1169 };
1170
1171 table.add_row(row![
1172 cluster_id,
1173 mode,
1174 uri,
1175 format!("{}s", config.interval),
1176 format!("{}s", config.timeout),
1177 config.healthy_threshold,
1178 config.unhealthy_threshold,
1179 accepted
1180 ]);
1181 }
1182 table.printstd();
1183 Ok(())
1184}
1185
1186fn accepted_statuses(config: &HealthCheckConfig) -> String {
1189 if !config.accepted_statuses.is_empty() {
1190 return config
1191 .accepted_statuses
1192 .iter()
1193 .map(HttpStatusRange::to_string)
1194 .collect::<Vec<_>>()
1195 .join(",");
1196 }
1197 match config.expected_status {
1198 0 => "2xx".to_owned(),
1199 status => status.to_string(),
1200 }
1201}
1202
1203impl Display for HttpStatusRange {
1206 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
1207 if self.start == self.end {
1208 write!(f, "{}", self.start)
1209 } else if self.start == crate::config::HTTP_STATUS_MIN
1210 && self.end == crate::config::HTTP_STATUS_MAX
1211 {
1212 write!(f, "any")
1213 } else if self.start.is_multiple_of(100) && self.end == self.start + 99 {
1214 write!(f, "{}xx", self.start / 100)
1215 } else {
1216 write!(f, "{}-{}", self.start, self.end)
1217 }
1218 }
1219}
1220
1221fn format_tags_to_string(tags: &BTreeMap<String, String>) -> String {
1222 tags.iter()
1223 .map(|(k, v)| format!("{k}={v}"))
1224 .collect::<Vec<_>>()
1225 .join(", ")
1226}
1227
1228fn add_client_auth_rows(
1244 table: &mut Table,
1245 client_auth: &Option<i32>,
1246 client_ca_certificates: &[String],
1247 client_ca_crls: &[String],
1248) {
1249 let raw = client_auth.unwrap_or(ClientAuthMode::ClientAuthNone as i32);
1250 let mode = match ClientAuthMode::try_from(raw) {
1253 Ok(mode) => mode.as_str_name().to_owned(),
1254 Err(_) => format!("UNKNOWN ({raw})"),
1255 };
1256 table.add_row(row!["client auth", mode]);
1257 table.add_row(row!["client CA certificates", client_ca_certificates.len()]);
1258 table.add_row(row!["client CA CRLs", client_ca_crls.len()]);
1259}
1260
1261fn list_string_vec(vec: &[String]) -> String {
1262 let mut output = String::new();
1263 for item in vec.iter() {
1264 output.push_str(item);
1265 output.push('\n');
1266 }
1267 output
1268}
1269
1270fn format_datetime(asn1_time: ASN1Time) -> Result<String, DisplayError> {
1272 let datetime = asn1_time.to_datetime();
1273
1274 let formatted = datetime
1275 .format(&format_description::well_known::Iso8601::DEFAULT)
1276 .map_err(|_| DisplayError::DateTime)?;
1277 Ok(formatted)
1278}
1279
1280fn create_cluster_table(headers: Vec<&str>, data: &BTreeMap<String, ResponseContent>) -> Table {
1293 let mut table = Table::new();
1294 table.set_format(*prettytable::format::consts::FORMAT_BOX_CHARS);
1295 let mut row_header: Vec<_> = headers.iter().map(|h| cell!(h)).collect();
1296 for ref key in data.keys() {
1297 row_header.push(cell!(&key));
1298 }
1299 table.add_row(Row::new(row_header));
1300 table
1301}
1302
1303impl Display for SocketAddress {
1304 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
1305 write!(f, "{}", SocketAddr::from(*self))
1306 }
1307}
1308
1309impl Display for ProtobufEndpoint {
1310 fn fmt(&self, f: &mut Formatter) -> fmt::Result {
1311 match &self.inner {
1312 Some(protobuf_endpoint::Inner::Http(HttpEndpoint {
1313 method,
1314 authority,
1315 path,
1316 status,
1317 ..
1318 })) => write!(
1319 f,
1320 "{} {} {} -> {}",
1321 authority.as_string_or("-"),
1322 method.as_string_or("-"),
1323 path.as_string_or("-"),
1324 status.as_string_or("-"),
1325 ),
1326 Some(protobuf_endpoint::Inner::Tcp(_)) => {
1327 write!(f, "-")
1328 }
1329 None => Ok(()),
1330 }
1331 }
1332}
1333
1334impl Display for HttpListenerConfig {
1335 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
1336 let mut table = Table::new();
1337 table.set_format(*prettytable::format::consts::FORMAT_BOX_CHARS);
1338 table.add_row(row!["socket address", format!("{:?}", self.address)]);
1339 table.add_row(row!["interface", self.interface.as_deref().unwrap_or("-")]);
1340 table.add_row(row!["public address", format!("{:?}", self.public_address),]);
1341 for http_answer_row in CustomHttpAnswers::to_rows(&self.http_answers) {
1342 table.add_row(http_answer_row);
1343 }
1344 table.add_row(row!["expect proxy", self.expect_proxy]);
1345 table.add_row(row!["sticky name", self.sticky_name]);
1346 table.add_row(row!["front timeout", self.front_timeout]);
1347 table.add_row(row!["back timeout", self.back_timeout]);
1348 table.add_row(row!["connect timeout", self.connect_timeout]);
1349 table.add_row(row!["request timeout", self.request_timeout]);
1350 table.add_row(row!["activated", self.active]);
1351 add_h2_flood_rows(
1352 &mut table,
1353 &self.h2_max_rst_stream_per_window,
1354 &self.h2_max_ping_per_window,
1355 &self.h2_max_settings_per_window,
1356 &self.h2_max_empty_data_per_window,
1357 &self.h2_max_window_update_stream0_per_window,
1358 &self.h2_max_continuation_frames,
1359 &self.h2_max_glitch_count,
1360 &self.h2_max_rst_stream_lifetime,
1361 &self.h2_max_rst_stream_abusive_lifetime,
1362 &self.h2_max_rst_stream_emitted_lifetime,
1363 &self.h2_max_header_list_size,
1364 &self.h2_max_header_table_size,
1365 &self.h2_max_header_fields,
1366 &self.h2_stream_refusal_percent,
1367 );
1368 add_h2_connection_rows(
1369 &mut table,
1370 &self.h2_initial_connection_window,
1371 &self.h2_max_concurrent_streams,
1372 &self.h2_stream_shrink_ratio,
1373 );
1374 if let Some(v) = &self.h2_stream_idle_timeout_seconds {
1375 table.add_row(row!["h2 stream idle timeout (seconds)", v]);
1376 }
1377 if let Some(v) = &self.h2_graceful_shutdown_deadline_seconds {
1378 table.add_row(row!["h2 graceful shutdown deadline (seconds)", v]);
1379 }
1380 if let Some(v) = &self.sozu_id_header {
1381 table.add_row(row!["Sozu-Id correlation header name", v]);
1382 }
1383 if let Some(v) = self.elide_x_real_ip {
1384 table.add_row(row!["elide X-Real-IP (anti-spoof)", v]);
1385 }
1386 if let Some(v) = self.send_x_real_ip {
1387 table.add_row(row!["send X-Real-IP (peer IP)", v]);
1388 }
1389 if let Some(v) = self.forwarded_headers {
1390 table.add_row(row!["forwarded headers", forwarded_headers_label(v)]);
1391 }
1392 write!(f, "{table}")
1393 }
1394}
1395
1396impl Display for HttpsListenerConfig {
1397 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
1398 let mut table = Table::new();
1399 table.set_format(*prettytable::format::consts::FORMAT_BOX_CHARS);
1400 let mut tls_versions = String::new();
1401 for tls_version in self.versions.iter() {
1402 tls_versions.push_str(&format!("{tls_version:?}\n"));
1403 }
1404
1405 table.add_row(row!["socket address", format!("{:?}", self.address)]);
1406 table.add_row(row!["interface", self.interface.as_deref().unwrap_or("-")]);
1407 table.add_row(row!["public address", format!("{:?}", self.public_address)]);
1408 for http_answer_row in CustomHttpAnswers::to_rows(&self.http_answers) {
1409 table.add_row(http_answer_row);
1410 }
1411 table.add_row(row!["versions", tls_versions]);
1412 table.add_row(row!["cipher list", list_string_vec(&self.cipher_list),]);
1413 table.add_row(row!["cipher suites", list_string_vec(&self.cipher_suites),]);
1414 table.add_row(row![
1415 "signature algorithms",
1416 list_string_vec(&self.signature_algorithms),
1417 ]);
1418 table.add_row(row!["groups list", list_string_vec(&self.groups_list),]);
1419 table.add_row(row![
1420 "alpn protocols",
1421 list_string_vec(&self.alpn_protocols),
1422 ]);
1423 table.add_row(row!["key", format!("{:?}", self.key),]);
1424 table.add_row(row!["expect proxy", self.expect_proxy]);
1425 table.add_row(row!["sticky name", self.sticky_name]);
1426 table.add_row(row!["front timeout", self.front_timeout]);
1427 table.add_row(row!["back timeout", self.back_timeout]);
1428 table.add_row(row!["connect timeout", self.connect_timeout]);
1429 table.add_row(row!["request timeout", self.request_timeout]);
1430 table.add_row(row!["activated", self.active]);
1431 add_h2_flood_rows(
1432 &mut table,
1433 &self.h2_max_rst_stream_per_window,
1434 &self.h2_max_ping_per_window,
1435 &self.h2_max_settings_per_window,
1436 &self.h2_max_empty_data_per_window,
1437 &self.h2_max_window_update_stream0_per_window,
1438 &self.h2_max_continuation_frames,
1439 &self.h2_max_glitch_count,
1440 &self.h2_max_rst_stream_lifetime,
1441 &self.h2_max_rst_stream_abusive_lifetime,
1442 &self.h2_max_rst_stream_emitted_lifetime,
1443 &self.h2_max_header_list_size,
1444 &self.h2_max_header_table_size,
1445 &self.h2_max_header_fields,
1446 &self.h2_stream_refusal_percent,
1447 );
1448 add_h2_connection_rows(
1449 &mut table,
1450 &self.h2_initial_connection_window,
1451 &self.h2_max_concurrent_streams,
1452 &self.h2_stream_shrink_ratio,
1453 );
1454 if let Some(v) = self.strict_sni_binding {
1455 table.add_row(row!["strict sni binding", v]);
1456 }
1457 if let Some(v) = self.disable_http11 {
1458 table.add_row(row!["disable http/1.1", v]);
1459 }
1460 if let Some(v) = &self.h2_stream_idle_timeout_seconds {
1461 table.add_row(row!["h2 stream idle timeout (seconds)", v]);
1462 }
1463 if let Some(v) = &self.h2_graceful_shutdown_deadline_seconds {
1464 table.add_row(row!["h2 graceful shutdown deadline (seconds)", v]);
1465 }
1466 if let Some(v) = &self.sozu_id_header {
1467 table.add_row(row!["Sozu-Id correlation header name", v]);
1468 }
1469 if let Some(v) = self.elide_x_real_ip {
1470 table.add_row(row!["elide X-Real-IP (anti-spoof)", v]);
1471 }
1472 if let Some(v) = self.send_x_real_ip {
1473 table.add_row(row!["send X-Real-IP (peer IP)", v]);
1474 }
1475 if let Some(v) = self.forwarded_headers {
1476 table.add_row(row!["forwarded headers", forwarded_headers_label(v)]);
1477 }
1478 if self.client_auth.is_some()
1479 || !self.client_ca_certificates.is_empty()
1480 || !self.client_ca_crls.is_empty()
1481 {
1482 add_client_auth_rows(
1483 &mut table,
1484 &self.client_auth,
1485 &self.client_ca_certificates,
1486 &self.client_ca_crls,
1487 );
1488 }
1489 write!(f, "{table}")
1490 }
1491}
1492
1493pub fn forwarded_headers_label(value: i32) -> String {
1496 match ForwardedHeaders::try_from(value) {
1497 Ok(ForwardedHeaders::Both) => "both".to_owned(),
1498 Ok(ForwardedHeaders::XForwarded) => "x_forwarded".to_owned(),
1499 Ok(ForwardedHeaders::Rfc7239) => "rfc7239".to_owned(),
1500 Ok(ForwardedHeaders::None) => "none".to_owned(),
1501 Err(_) => format!("unknown ({value})"),
1502 }
1503}
1504
1505#[allow(clippy::too_many_arguments)]
1508fn add_h2_flood_rows(
1509 table: &mut Table,
1510 max_rst_stream: &Option<u32>,
1511 max_ping: &Option<u32>,
1512 max_settings: &Option<u32>,
1513 max_empty_data: &Option<u32>,
1514 max_window_update_stream0: &Option<u32>,
1515 max_continuation: &Option<u32>,
1516 max_glitch: &Option<u32>,
1517 max_rst_stream_lifetime: &Option<u64>,
1518 max_rst_stream_abusive_lifetime: &Option<u64>,
1519 max_rst_stream_emitted_lifetime: &Option<u64>,
1520 max_header_list_size: &Option<u32>,
1521 max_header_table_size: &Option<u32>,
1522 max_header_fields: &Option<u32>,
1523 stream_refusal_percent: &Option<u32>,
1524) {
1525 if let Some(v) = max_rst_stream {
1526 table.add_row(row!["h2 max rst_stream/window", v]);
1527 }
1528 if let Some(v) = max_ping {
1529 table.add_row(row!["h2 max ping/window", v]);
1530 }
1531 if let Some(v) = max_settings {
1532 table.add_row(row!["h2 max settings/window", v]);
1533 }
1534 if let Some(v) = max_empty_data {
1535 table.add_row(row!["h2 max empty_data/window", v]);
1536 }
1537 if let Some(v) = max_window_update_stream0 {
1538 table.add_row(row!["h2 max window_update stream0/window", v]);
1539 }
1540 if let Some(v) = max_continuation {
1541 table.add_row(row!["h2 max continuation frames", v]);
1542 }
1543 if let Some(v) = max_glitch {
1544 table.add_row(row!["h2 max glitch count", v]);
1545 }
1546 if let Some(v) = max_rst_stream_lifetime {
1547 table.add_row(row!["h2 max rst_stream lifetime", v]);
1548 }
1549 if let Some(v) = max_rst_stream_abusive_lifetime {
1550 table.add_row(row!["h2 max rst_stream abusive lifetime", v]);
1551 }
1552 if let Some(v) = max_rst_stream_emitted_lifetime {
1553 table.add_row(row!["h2 max rst_stream emitted lifetime", v]);
1554 }
1555 if let Some(v) = max_header_list_size {
1556 table.add_row(row!["h2 max header list size", v]);
1557 }
1558 if let Some(v) = max_header_table_size {
1559 table.add_row(row!["h2 max header table size", v]);
1560 }
1561 if let Some(v) = max_header_fields {
1562 table.add_row(row!["h2 max header fields", v]);
1563 }
1564 if let Some(v) = stream_refusal_percent {
1565 table.add_row(row!["h2 stream refusal percent", v]);
1566 }
1567}
1568
1569fn add_h2_connection_rows(
1572 table: &mut Table,
1573 window: &Option<u32>,
1574 max_streams: &Option<u32>,
1575 shrink_ratio: &Option<u32>,
1576) {
1577 if let Some(v) = window {
1578 table.add_row(row!["h2 initial connection window", v]);
1579 }
1580 if let Some(v) = max_streams {
1581 table.add_row(row!["h2 max concurrent streams", v]);
1582 }
1583 if let Some(v) = shrink_ratio {
1584 table.add_row(row!["h2 stream shrink ratio", v]);
1585 }
1586}
1587
1588impl CustomHttpAnswers {
1589 fn to_rows(option: &Option<Self>) -> Vec<Row> {
1590 let mut rows = Vec::new();
1591 if let Some(answers) = option {
1592 if let Some(a) = &answers.answer_301 {
1593 rows.push(row!("301", a));
1594 }
1595 if let Some(a) = &answers.answer_400 {
1596 rows.push(row!("400", a));
1597 }
1598 if let Some(a) = &answers.answer_404 {
1599 rows.push(row!("404", a));
1600 }
1601 if let Some(a) = &answers.answer_408 {
1602 rows.push(row!("408", a));
1603 }
1604 if let Some(a) = &answers.answer_413 {
1605 rows.push(row!("413", a));
1606 }
1607 if let Some(a) = &answers.answer_421 {
1608 rows.push(row!("421", a));
1609 }
1610 if let Some(a) = &answers.answer_502 {
1611 rows.push(row!("502", a));
1612 }
1613 if let Some(a) = &answers.answer_503 {
1614 rows.push(row!("503", a));
1615 }
1616 if let Some(a) = &answers.answer_504 {
1617 rows.push(row!("504", a));
1618 }
1619 if let Some(a) = &answers.answer_507 {
1620 rows.push(row!("507", a));
1621 }
1622 }
1623 rows
1624 }
1625}
1626
1627impl Display for Event {
1628 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
1629 let kind = match self.kind() {
1630 EventKind::BackendDown => "backend down",
1631 EventKind::BackendUp => "backend up",
1632 EventKind::NoAvailableBackends => "no available backends",
1633 EventKind::RemovedBackendHasNoConnections => "removed backend has no connections",
1634 EventKind::ClusterAdded => "cluster added",
1635 EventKind::ClusterRemoved => "cluster removed",
1636 EventKind::FrontendAdded => "frontend added",
1637 EventKind::FrontendRemoved => "frontend removed",
1638 EventKind::CertificateAdded => "certificate added",
1639 EventKind::CertificateRemoved => "certificate removed",
1640 EventKind::CertificateReplaced => "certificate replaced",
1641 EventKind::ListenerActivated => "listener activated",
1642 EventKind::ListenerDeactivated => "listener deactivated",
1643 EventKind::ConfigurationReloaded => "configuration reloaded",
1644 EventKind::WorkerKilled => "worker killed",
1645 EventKind::WorkerRelaunched => "worker relaunched",
1646 EventKind::LoggingLevelChanged => "logging level changed",
1647 EventKind::MetricsConfigured => "metrics configured",
1648 EventKind::ListenerUpdated => "listener updated",
1649 EventKind::StateLoaded => "state loaded",
1650 EventKind::StateSaved => "state saved",
1651 EventKind::ListenerAdded => "listener added",
1652 EventKind::ListenerRemoved => "listener removed",
1653 EventKind::SozuStopRequested => "stop requested",
1654 EventKind::MainUpgraded => "main upgraded",
1655 EventKind::WorkerUpgraded => "worker upgraded",
1656 EventKind::EventsSubscribed => "events subscribed",
1657 EventKind::HealthCheckHealthy => "health check: backend healthy",
1658 EventKind::HealthCheckUnhealthy => "health check: backend unhealthy",
1659 EventKind::ClusterRecovered => "cluster recovered",
1660 EventKind::MetricDetailChanged => "metric detail changed",
1661 };
1662 let address = match &self.address {
1663 Some(a) => a.to_string(),
1664 None => String::new(),
1665 };
1666 write!(
1667 f,
1668 "{}, backend={}, cluster={}, address={}",
1669 kind,
1670 self.backend_id(),
1671 self.cluster_id(),
1672 address,
1673 )
1674 }
1675}
1676
1677#[cfg(test)]
1678mod tests {
1679 use crate::proto::command::{ClientAuthMode, HttpsListenerConfig, SocketAddress};
1680
1681 const CA_PEM_SENTINEL: &str = "CLIENT_CA_PEM_BODY_SENTINEL";
1682 const CRL_PEM_SENTINEL: &str = "CLIENT_CRL_PEM_BODY_SENTINEL";
1683
1684 fn https_config(client_auth: Option<i32>, cas: &[&str], crls: &[&str]) -> HttpsListenerConfig {
1685 HttpsListenerConfig {
1686 address: SocketAddress::new_v4(127, 0, 0, 1, 8443),
1687 client_auth,
1688 client_ca_certificates: cas.iter().map(|s| s.to_string()).collect(),
1689 client_ca_crls: crls.iter().map(|s| s.to_string()).collect(),
1690 ..Default::default()
1691 }
1692 }
1693
1694 #[test]
1695 fn listener_display_distinguishes_client_auth_modes() {
1696 let none = format!(
1701 "{}",
1702 https_config(Some(ClientAuthMode::ClientAuthNone as i32), &[], &[])
1703 );
1704 let required = format!(
1705 "{}",
1706 https_config(
1707 Some(ClientAuthMode::ClientAuthRequired as i32),
1708 &[CA_PEM_SENTINEL],
1709 &[],
1710 )
1711 );
1712
1713 assert!(
1714 none.contains(ClientAuthMode::ClientAuthNone.as_str_name()),
1715 "an explicit none must be visible: {none}"
1716 );
1717 let unset = format!("{}", https_config(None, &[], &[]));
1720 assert!(
1721 !unset.contains("client auth"),
1722 "a listener without client auth keys must show no client auth row: {unset}"
1723 );
1724 assert!(
1725 required.contains(ClientAuthMode::ClientAuthRequired.as_str_name()),
1726 "required client auth must be visible: {required}"
1727 );
1728 assert_ne!(
1729 none, required,
1730 "listeners with and without client auth must not render identically"
1731 );
1732 }
1733
1734 #[test]
1735 fn listener_display_summarises_ca_and_crl_without_rendering_pem() {
1736 let output = format!(
1740 "{}",
1741 https_config(
1742 Some(ClientAuthMode::ClientAuthOptional as i32),
1743 &[CA_PEM_SENTINEL, CA_PEM_SENTINEL],
1744 &[CRL_PEM_SENTINEL],
1745 )
1746 );
1747
1748 assert!(
1749 !output.contains(CA_PEM_SENTINEL),
1750 "listener display leaked a client CA PEM body: {output}"
1751 );
1752 assert!(
1753 !output.contains(CRL_PEM_SENTINEL),
1754 "listener display leaked a client CRL PEM body: {output}"
1755 );
1756 assert!(
1757 output.contains("client CA certificates"),
1758 "missing client CA count row: {output}"
1759 );
1760 assert!(
1761 output.contains("client CA CRLs"),
1762 "missing client CRL count row: {output}"
1763 );
1764 }
1765
1766 #[test]
1767 fn listener_display_does_not_fold_an_unknown_mode_to_none() {
1768 let output = format!("{}", https_config(Some(999), &[], &[]));
1771
1772 assert!(
1773 output.contains("UNKNOWN"),
1774 "an unknown client_auth value must render as unknown: {output}"
1775 );
1776 assert!(
1777 !output.contains(ClientAuthMode::ClientAuthNone.as_str_name()),
1778 "an unknown client_auth value must not render as none: {output}"
1779 );
1780 }
1781}