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