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