Skip to main content

sozu_command_lib/proto/
display.rs

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                // avoid displaying anything else than JSON
169                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                // exception when displaying clusters
222                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(()), // internal acknowledgement
231            ContentType::Clusters(_) | ContentType::ClusterHashes(_) => Ok(()), // not displayed directly, see print_cluster_responses
232            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                // The prefixes are boot-time only, so they are printed
247                // alongside the cap: "100" alone would not tell the
248                // operator what it is 100 connections PER.
249                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(()), // not event displayed yet!
265            // Per-worker SetMetricDetail status payload. The aggregated
266            // MetricDetailStatus is what operators read at the
267            // `sozu` CLI surface; the per-worker variant flows
268            // master-side only (collected by SetMetricDetailTask) and
269            // is never printed directly. Silent OK keeps the match
270            // exhaustive without surfacing internal IPC payloads on
271            // the operator's terminal.
272            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    // main process metrics
333    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    // workers
342    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    // clusters
350    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        // backend_id -> (metric_name -> value )
382        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    // sort the titles so they always appear in the same order
432    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    // sort the metrics so they always appear in the same order
475    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    // 0, 1, 3, 7... in the upper row
547    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            // display the count by bucket, not the incremented count
561            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
585/// Table of HTTP(S) frontends for `ListFrontends`. The header predicates are
586/// summarised by their count only: names and values may carry secrets.
587fn 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
616/// Table of the HTTP(S) frontends of `ListClusters`, one marker column per
617/// worker. Same redaction rule as [`http_frontends_table`].
618fn 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    // HTTP frontends
652    if !frontends.http_frontends.is_empty() {
653        http_frontends_table("HTTP frontends ", &frontends.http_frontends).printstd();
654    }
655
656    // HTTPS frontends
657    if !frontends.https_frontends.is_empty() {
658        http_frontends_table("HTTPS frontends", &frontends.https_frontends).printstd();
659    }
660
661    // TCP frontends
662    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    // UDP frontends
678    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        // UDP has no expect_proxy / connect_timeout (no connect handshake);
746        // it carries datagram-specific knobs instead.
747        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
774/// What a cluster keys `HRW`/`MAGLEV` selection on, for the cluster table:
775/// `header:<name>`, `cookie:<name>`, or `source_ip`, the default.
776fn 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
784/// A cluster's shuffle sharding, for the cluster table: `off`, or
785/// `<percent>% from <min> backends, <mode>`.
786fn 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            // `global` when the cluster inherits the worker's value.
894            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
987/// display all clusters in a simplified table showing their hashes
988fn 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        // Unset probes the backend's own data port.
1167        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
1188/// The custom header names of a probe with their fingerprints, in the order
1189/// they are sent. A config holds no value, so none can reach the terminal.
1190fn 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
1208/// The statuses an HTTP-mode probe accepts, in the syntax
1209/// `sozu cluster health-check set --accepted-statuses` takes back.
1210fn 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
1225/// Renders the range in the syntax `parse_http_status_range`
1226/// (`command/src/config.rs`) parses: `404`, `2xx`, `any` or `200-399`.
1227impl 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
1250/// Render an HTTPS listener's mTLS client-authentication policy.
1251///
1252/// An operator reading `sozu listeners` must be able to tell a listener that
1253/// requires a client certificate from one that does not: the two are otherwise
1254/// indistinguishable in this table, which is exactly the confusion that turns a
1255/// misapplied policy into silent unauthenticated access. The rows are emitted
1256/// whenever any client authentication key is set, an explicit `none`
1257/// included, so a listener with a policy always shows it; a listener with
1258/// none of the keys renders as it did before client authentication existed,
1259/// like the other optional rows of this table.
1260///
1261/// CA and CRL entries are PEM bodies. They are summarised as counts only —
1262/// never rendered — matching the redaction the hand-written
1263/// `Debug for HttpsListenerConfig` (`command/src/proto/mod.rs`) applies to
1264/// `certificate` / `key`.
1265fn 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    // An unknown value must not be folded to `none`: the worker rejects it at
1273    // listener build time, and displaying it as "none" here would hide why.
1274    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
1292// ISO 8601
1293fn 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
1302/// Creates an empty table of the form
1303/// ```text
1304/// ┌────────────┬─────────────┬───────────┬────────┐
1305/// │            │ header      │ header    │ header │
1306/// ├────────────┼─────────────┼───────────┼────────┤
1307/// │ cluster_id │             │           │        │
1308/// ├────────────┼─────────────┼───────────┼────────┤
1309/// │ cluster_id │             │           │        │
1310/// ├────────────┼─────────────┼───────────┼────────┤
1311/// │ cluster_id │             │           │        │
1312/// └────────────┴─────────────┴───────────┴────────┘
1313/// ```
1314fn 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
1518/// The TOML spelling of a `forwarded_headers` wire value, or the raw number
1519/// when it names no [`ForwardedHeaders`] variant.
1520pub 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/// Add H2 flood detection threshold rows to a display table.
1531/// Only shows rows for values that have been explicitly configured.
1532#[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
1594/// Add H2 connection tuning rows to a display table.
1595/// Only shows rows for values that have been explicitly configured.
1596fn 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        // The regression this guards: `ListListeners` output for a listener
1729        // with `required` client auth used to be byte-identical to one with
1730        // client auth disabled, so an operator could not verify an applied
1731        // mTLS policy from the CLI.
1732        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        // A listener with no client authentication key renders as before
1750        // client authentication existed.
1751        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        // CA and CRL entries are PEM bodies: counts are operator-useful,
1769        // contents are not, and dumping them into a terminal table would be a
1770        // regression against the redaction applied elsewhere.
1771        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        // A value the master does not recognise must stand out rather than
1801        // masquerade as the unauthenticated default.
1802        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    /// A guarded frontend must be distinguishable in both listing surfaces
1815    /// (`ListFrontends`, `ListClusters`) by its predicate count, without the
1816    /// predicate names or values being rendered.
1817    #[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}