Skip to main content

sozu_command_lib/proto/
mod.rs

1use std::collections::BTreeMap;
2
3use command::{
4    AggregatedMetrics, BackendMetrics, Bucket, FilteredHistogram, FilteredMetrics, Percentiles,
5    filtered_metrics::Inner,
6};
7use prost::UnknownEnumValue;
8
9/// Contains all types received by and sent from Sōzu
10pub mod command {
11    include!(concat!(env!("OUT_DIR"), "/command.rs"));
12
13    /// Hand-written in `crate::secret` so that decoding wipes a secret before
14    /// its buffer can be released; `command/build.rs` maps the message to it.
15    pub use crate::secret::HealthCheckHeaderValue;
16}
17
18/// Implementation of fmt::Display for the protobuf types, used in the CLI
19pub mod display;
20
21#[derive(thiserror::Error, Debug)]
22pub enum DisplayError {
23    #[error("Could not display content")]
24    DisplayContent(String),
25    #[error("Error while parsing response to JSON")]
26    Json(serde_json::Error),
27    #[error("got the wrong response content type: {0}")]
28    WrongResponseType(String),
29    #[error("Could not format the datetime to ISO 8601")]
30    DateTime,
31    #[error("unrecognized protobuf variant: {0}")]
32    DecodeError(UnknownEnumValue),
33}
34
35// Simple helper to build ResponseContent from ContentType
36impl From<command::response_content::ContentType> for command::ResponseContent {
37    fn from(value: command::response_content::ContentType) -> Self {
38        Self {
39            content_type: Some(value),
40        }
41    }
42}
43
44// Simple helper to build Request from RequestType
45impl From<command::request::RequestType> for command::Request {
46    fn from(value: command::request::RequestType) -> Self {
47        Self {
48            request_type: Some(value),
49        }
50    }
51}
52
53impl std::fmt::Debug for command::Request {
54    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
55        let mut request = f.debug_struct("Request");
56        match self.request_type.as_ref() {
57            Some(request_type) => request.field("request_type", request_type),
58            None => request.field("request_type", &"Unallowed"),
59        };
60        request.finish()
61    }
62}
63
64impl std::fmt::Debug for command::request::RequestType {
65    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
66        use command::request::RequestType;
67
68        match self {
69            RequestType::AddCluster(value) => f.debug_tuple("AddCluster").field(value).finish(),
70            RequestType::AddHttpFrontend(value) => {
71                f.debug_tuple("AddHttpFrontend").field(value).finish()
72            }
73            RequestType::AddHttpFrontendWithSilentClose(value) => f
74                .debug_tuple("AddHttpFrontendWithSilentClose")
75                .field(value)
76                .finish(),
77            RequestType::AddHttpsFrontendWithSilentClose(value) => f
78                .debug_tuple("AddHttpsFrontendWithSilentClose")
79                .field(value)
80                .finish(),
81            RequestType::RemoveHttpFrontend(value) => {
82                f.debug_tuple("RemoveHttpFrontend").field(value).finish()
83            }
84            RequestType::AddHttpsFrontend(value) => {
85                f.debug_tuple("AddHttpsFrontend").field(value).finish()
86            }
87            RequestType::RemoveHttpsFrontend(value) => {
88                f.debug_tuple("RemoveHttpsFrontend").field(value).finish()
89            }
90            RequestType::AddHttpFrontendWithHeaderPredicates(value) => f
91                .debug_tuple("AddHttpFrontendWithHeaderPredicates")
92                .field(value)
93                .finish(),
94            RequestType::RemoveHttpFrontendWithHeaderPredicates(value) => f
95                .debug_tuple("RemoveHttpFrontendWithHeaderPredicates")
96                .field(value)
97                .finish(),
98            RequestType::AddHttpsFrontendWithHeaderPredicates(value) => f
99                .debug_tuple("AddHttpsFrontendWithHeaderPredicates")
100                .field(value)
101                .finish(),
102            RequestType::RemoveHttpsFrontendWithHeaderPredicates(value) => f
103                .debug_tuple("RemoveHttpsFrontendWithHeaderPredicates")
104                .field(value)
105                .finish(),
106            RequestType::AddCertificate(value) => {
107                f.debug_tuple("AddCertificate").field(value).finish()
108            }
109            RequestType::ReplaceCertificate(value) => {
110                f.debug_tuple("ReplaceCertificate").field(value).finish()
111            }
112            RequestType::RemoveCertificate(value) => {
113                f.debug_tuple("RemoveCertificate").field(value).finish()
114            }
115            RequestType::AddTcpFrontend(value) => {
116                f.debug_tuple("AddTcpFrontend").field(value).finish()
117            }
118            RequestType::RemoveTcpFrontend(value) => {
119                f.debug_tuple("RemoveTcpFrontend").field(value).finish()
120            }
121            RequestType::AddHttpListener(value) => {
122                f.debug_tuple("AddHttpListener").field(value).finish()
123            }
124            RequestType::AddHttpsListener(value) => {
125                f.debug_tuple("AddHttpsListener").field(value).finish()
126            }
127            RequestType::AddHttpsListenerWithClientAuth(value) => f
128                .debug_tuple("AddHttpsListenerWithClientAuth")
129                .field(value)
130                .finish(),
131            RequestType::UpdateHttpListener(value) => {
132                f.debug_tuple("UpdateHttpListener").field(value).finish()
133            }
134            RequestType::UpdateHttpsListener(value) => {
135                f.debug_tuple("UpdateHttpsListener").field(value).finish()
136            }
137            RequestType::UpdateHttpsListenerWithClientAuth(value) => f
138                .debug_tuple("UpdateHttpsListenerWithClientAuth")
139                .field(value)
140                .finish(),
141            RequestType::QueryCertificatesFromTheState(value) => f
142                .debug_tuple("QueryCertificatesFromTheState")
143                .field(value)
144                .finish(),
145            RequestType::QueryCertificatesFromWorkers(value) => f
146                .debug_tuple("QueryCertificatesFromWorkers")
147                .field(value)
148                .finish(),
149            other => f.write_str(display::format_request_type(other)),
150        }
151    }
152}
153
154impl std::fmt::Debug for command::CertificateAndKey {
155    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
156        let certificate_chain_len = self
157            .certificate_chain
158            .iter()
159            .map(String::len)
160            .fold(0usize, usize::saturating_add);
161
162        f.debug_struct("CertificateAndKey")
163            .field("certificate", &"[redacted]")
164            .field("certificate_len", &self.certificate.len())
165            .field("certificate_chain", &"[redacted]")
166            .field("certificate_chain_count", &self.certificate_chain.len())
167            .field("certificate_chain_len", &certificate_chain_len)
168            .field("key", &"[redacted]")
169            .field("key_len", &self.key.len())
170            .field("versions_count", &self.versions.len())
171            .field("names_count", &self.names.len())
172            .field("names_len", &total_string_len(&self.names))
173            .finish()
174    }
175}
176
177impl std::fmt::Debug for command::RemoveCertificate {
178    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
179        f.debug_struct("RemoveCertificate")
180            .field("address", &self.address)
181            .field("fingerprint_len", &self.fingerprint.len())
182            .finish_non_exhaustive()
183    }
184}
185
186impl std::fmt::Debug for command::ReplaceCertificate {
187    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
188        f.debug_struct("ReplaceCertificate")
189            .field("address", &self.address)
190            .field("new_certificate", &self.new_certificate)
191            .field("fingerprint_len", &self.old_fingerprint.len())
192            .field("new_expired_at", &self.new_expired_at)
193            .finish_non_exhaustive()
194    }
195}
196
197impl std::fmt::Debug for command::Cluster {
198    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
199        let (answers_count, answers_key_len, answers_value_len) =
200            summarize_string_map(&self.answers);
201
202        f.debug_struct("Cluster")
203            .field("cluster_id_len", &self.cluster_id.len())
204            .field("sticky_session", &self.sticky_session)
205            .field("https_redirect", &self.https_redirect)
206            .field("proxy_protocol", &self.proxy_protocol)
207            .field("load_balancing", &self.load_balancing)
208            .field("answer_503_len", &self.answer_503.as_ref().map(String::len))
209            .field("load_metric", &self.load_metric)
210            .field("http2", &self.http2)
211            .field("answers_count", &answers_count)
212            .field("answers_key_len", &answers_key_len)
213            .field("answers_value_len", &answers_value_len)
214            .field("https_redirect_port", &self.https_redirect_port)
215            .field("authorized_hashes_count", &self.authorized_hashes.len())
216            .field(
217                "authorized_hashes_len",
218                &total_string_len(&self.authorized_hashes),
219            )
220            .field(
221                "www_authenticate_len",
222                &self.www_authenticate.as_ref().map(String::len),
223            )
224            .field("max_connections_per_ip", &self.max_connections_per_ip)
225            .field("retry_after", &self.retry_after)
226            .field("health_check_present", &self.health_check.is_some())
227            .field("udp_present", &self.udp.is_some())
228            .field(
229                "affinity_header_len",
230                &self.affinity_header.as_ref().map(String::len),
231            )
232            .field(
233                "affinity_cookie_len",
234                &self.affinity_cookie.as_ref().map(String::len),
235            )
236            .field("shard_percent", &self.shard_percent)
237            .field("shard_min_backends", &self.shard_min_backends)
238            .field("shard_mode", &self.shard_mode)
239            .finish_non_exhaustive()
240    }
241}
242
243impl std::fmt::Debug for command::Header {
244    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
245        f.debug_struct("Header")
246            .field("position", &self.position)
247            .field("key_len", &self.key.len())
248            .field("val_len", &self.val.len())
249            .finish_non_exhaustive()
250    }
251}
252
253/// Longest header name, in bytes, [`command::HealthCheckHeader`]'s `Debug` prints.
254const HEALTH_CHECK_HEADER_DEBUG_NAME_MAX: usize = 64;
255
256/// `name` cut to [`HEALTH_CHECK_HEADER_DEBUG_NAME_MAX`] bytes on a char
257/// boundary, and whether it was cut.
258fn bounded_health_check_header_name(name: &str) -> (&str, bool) {
259    let mut end = name.len().min(HEALTH_CHECK_HEADER_DEBUG_NAME_MAX);
260    while !name.is_char_boundary(end) {
261        end -= 1;
262    }
263    (&name[..end], end < name.len())
264}
265
266/// A health-check header is public metadata: `Debug` prints a bounded name and
267/// the keyed fingerprint of the value, never the value, which this type does
268/// not carry.
269impl std::fmt::Debug for command::HealthCheckHeader {
270    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
271        let (name, truncated) = bounded_health_check_header_name(&self.name);
272        f.debug_struct("HealthCheckHeader")
273            .field("name", &name)
274            .field("name_truncated", &truncated)
275            .field("fingerprint", &self.fingerprint)
276            .finish()
277    }
278}
279
280/// The ingress value is a secret: `Debug` prints a bounded name and a fixed
281/// placeholder, never the bytes and never their length.
282impl std::fmt::Debug for command::HealthCheckHeaderValue {
283    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
284        let (name, truncated) = bounded_health_check_header_name(&self.name);
285        f.debug_struct("HealthCheckHeaderValue")
286            .field("name", &name)
287            .field("name_truncated", &truncated)
288            .field("value", &format_args!("<protected>"))
289            .finish()
290    }
291}
292
293impl std::fmt::Debug for command::HeaderPredicate {
294    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
295        f.debug_struct("HeaderPredicate")
296            .field("kind", &self.kind)
297            .field("name_len", &self.name.len())
298            .field("value_len", &self.value.as_ref().map(String::len))
299            .finish_non_exhaustive()
300    }
301}
302
303impl std::fmt::Debug for command::RequestHttpFrontend {
304    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
305        let tags_len = self.tags.iter().fold(0usize, |total, (key, value)| {
306            total.saturating_add(key.len()).saturating_add(value.len())
307        });
308        let headers_len = self.headers.iter().fold(0usize, |total, header| {
309            total
310                .saturating_add(header.key.len())
311                .saturating_add(header.val.len())
312        });
313
314        f.debug_struct("RequestHttpFrontend")
315            .field("cluster_id_len", &self.cluster_id.as_ref().map(String::len))
316            .field("address", &self.address)
317            .field("hostname_len", &self.hostname.len())
318            .field("path_kind", &self.path.kind)
319            .field("path_len", &self.path.value.len())
320            .field("method_len", &self.method.as_ref().map(String::len))
321            .field("position", &self.position)
322            .field("tags_count", &self.tags.len())
323            .field("tags_len", &tags_len)
324            .field("redirect", &self.redirect)
325            .field("required_auth", &self.required_auth)
326            .field("redirect_scheme", &self.redirect_scheme)
327            .field(
328                "redirect_template_len",
329                &self.redirect_template.as_ref().map(String::len),
330            )
331            .field(
332                "rewrite_host_len",
333                &self.rewrite_host.as_ref().map(String::len),
334            )
335            .field(
336                "rewrite_path_len",
337                &self.rewrite_path.as_ref().map(String::len),
338            )
339            .field("rewrite_port", &self.rewrite_port)
340            .field("headers_count", &self.headers.len())
341            .field("headers_len", &headers_len)
342            .field("hsts", &self.hsts)
343            .field("header_predicates_count", &self.header_predicates.len())
344            .finish_non_exhaustive()
345    }
346}
347
348impl std::fmt::Debug for command::RequestTcpFrontend {
349    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
350        let (tags_count, tags_key_len, tags_value_len) = summarize_string_map(&self.tags);
351
352        f.debug_struct("RequestTcpFrontend")
353            .field("cluster_id_len", &self.cluster_id.len())
354            .field("address", &self.address)
355            .field("tags_count", &tags_count)
356            .field("tags_key_len", &tags_key_len)
357            .field("tags_value_len", &tags_value_len)
358            .field("sni_len", &self.sni.as_ref().map(String::len))
359            .field("alpn_count", &self.alpn.len())
360            .field("alpn_len", &total_string_len(&self.alpn))
361            .finish_non_exhaustive()
362    }
363}
364
365fn total_string_len<'a>(values: impl IntoIterator<Item = &'a String>) -> usize {
366    values
367        .into_iter()
368        .map(String::len)
369        .fold(0usize, usize::saturating_add)
370}
371
372fn summarize_http_answers(
373    answers: Option<&command::CustomHttpAnswers>,
374) -> (Option<&'static str>, Option<usize>, Option<usize>) {
375    let Some(answers) = answers else {
376        return (None, None, None);
377    };
378    let values = [
379        answers.answer_301.as_ref(),
380        answers.answer_400.as_ref(),
381        answers.answer_401.as_ref(),
382        answers.answer_404.as_ref(),
383        answers.answer_408.as_ref(),
384        answers.answer_413.as_ref(),
385        answers.answer_421.as_ref(),
386        answers.answer_429.as_ref(),
387        answers.answer_502.as_ref(),
388        answers.answer_503.as_ref(),
389        answers.answer_504.as_ref(),
390        answers.answer_507.as_ref(),
391    ];
392
393    (
394        Some("[redacted]"),
395        Some(values.iter().flatten().count()),
396        Some(total_string_len(values.iter().flatten().copied())),
397    )
398}
399
400fn summarize_string_map(
401    values: &std::collections::BTreeMap<String, String>,
402) -> (usize, usize, usize) {
403    (
404        values.len(),
405        total_string_len(values.keys()),
406        total_string_len(values.values()),
407    )
408}
409
410impl std::fmt::Debug for command::HttpListenerConfig {
411    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
412        let (http_answers, http_answers_count, http_answers_len) =
413            summarize_http_answers(self.http_answers.as_ref());
414        let (answers_count, answers_key_len, answers_value_len) =
415            summarize_string_map(&self.answers);
416
417        f.debug_struct("HttpListenerConfig")
418            .field("address", &self.address)
419            .field("interface", &self.interface)
420            .field("public_address", &self.public_address)
421            .field("expect_proxy", &self.expect_proxy)
422            .field("sticky_name_len", &self.sticky_name.len())
423            .field("front_timeout", &self.front_timeout)
424            .field("back_timeout", &self.back_timeout)
425            .field("connect_timeout", &self.connect_timeout)
426            .field("request_timeout", &self.request_timeout)
427            .field("active", &self.active)
428            .field("http_answers", &http_answers)
429            .field("http_answers_count", &http_answers_count)
430            .field("http_answers_len", &http_answers_len)
431            .field(
432                "sozu_id_header_len",
433                &self.sozu_id_header.as_ref().map(String::len),
434            )
435            .field("answers_count", &answers_count)
436            .field("answers_key_len", &answers_key_len)
437            .field("answers_value_len", &answers_value_len)
438            .field("elide_x_real_ip", &self.elide_x_real_ip)
439            .field("send_x_real_ip", &self.send_x_real_ip)
440            .field("forwarded_headers", &self.forwarded_headers)
441            .finish_non_exhaustive()
442    }
443}
444
445impl std::fmt::Debug for command::UpdateHttpListenerConfig {
446    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
447        let (http_answers, http_answers_count, http_answers_len) =
448            summarize_http_answers(self.http_answers.as_ref());
449        let (answers_count, answers_key_len, answers_value_len) =
450            summarize_string_map(&self.answers);
451
452        f.debug_struct("UpdateHttpListenerConfig")
453            .field("address", &self.address)
454            .field("public_address", &self.public_address)
455            .field(
456                "sticky_name_len",
457                &self.sticky_name.as_ref().map(String::len),
458            )
459            .field("http_answers", &http_answers)
460            .field("http_answers_count", &http_answers_count)
461            .field("http_answers_len", &http_answers_len)
462            .field(
463                "sozu_id_header_len",
464                &self.sozu_id_header.as_ref().map(String::len),
465            )
466            .field("answers_count", &answers_count)
467            .field("answers_key_len", &answers_key_len)
468            .field("answers_value_len", &answers_value_len)
469            .field("forwarded_headers", &self.forwarded_headers)
470            .finish_non_exhaustive()
471    }
472}
473
474impl std::fmt::Debug for command::UpdateHttpsListenerConfig {
475    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
476        let (http_answers, http_answers_count, http_answers_len) =
477            summarize_http_answers(self.http_answers.as_ref());
478        let alpn_protocols_count = self
479            .alpn_protocols
480            .as_ref()
481            .map(|protocols| protocols.values.len());
482        let alpn_protocols_len = self
483            .alpn_protocols
484            .as_ref()
485            .map(|protocols| total_string_len(&protocols.values));
486        let (answers_count, answers_key_len, answers_value_len) =
487            summarize_string_map(&self.answers);
488
489        f.debug_struct("UpdateHttpsListenerConfig")
490            .field("address", &self.address)
491            .field("public_address", &self.public_address)
492            .field(
493                "sticky_name_len",
494                &self.sticky_name.as_ref().map(String::len),
495            )
496            .field("http_answers", &http_answers)
497            .field("http_answers_count", &http_answers_count)
498            .field("http_answers_len", &http_answers_len)
499            .field("alpn_protocols_count", &alpn_protocols_count)
500            .field("alpn_protocols_len", &alpn_protocols_len)
501            .field(
502                "sozu_id_header_len",
503                &self.sozu_id_header.as_ref().map(String::len),
504            )
505            .field("answers_count", &answers_count)
506            .field("answers_key_len", &answers_key_len)
507            .field("answers_value_len", &answers_value_len)
508            .field("forwarded_headers", &self.forwarded_headers)
509            .field("client_auth_policy", &self.client_auth_policy)
510            .finish_non_exhaustive()
511    }
512}
513
514/// CA and CRL entries are PEM bodies: counted and measured, never printed.
515impl std::fmt::Debug for command::ClientAuthPolicy {
516    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
517        f.debug_struct("ClientAuthPolicy")
518            .field("client_auth", &self.client_auth)
519            .field(
520                "client_ca_certificates_count",
521                &self.client_ca_certificates.len(),
522            )
523            .field(
524                "client_ca_certificates_len",
525                &total_string_len(&self.client_ca_certificates),
526            )
527            .field("client_ca_crls_count", &self.client_ca_crls.len())
528            .field(
529                "client_ca_crls_len",
530                &total_string_len(&self.client_ca_crls),
531            )
532            .finish()
533    }
534}
535
536impl std::fmt::Debug for command::HttpsListenerConfig {
537    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
538        let certificate = self.certificate.as_ref().map(|_| "[redacted]");
539        let certificate_len = self.certificate.as_ref().map(String::len);
540        let certificate_chain_len = total_string_len(&self.certificate_chain);
541        let key = self.key.as_ref().map(|_| "[redacted]");
542        let key_len = self.key.as_ref().map(String::len);
543        let (http_answers, http_answers_count, http_answers_len) =
544            summarize_http_answers(self.http_answers.as_ref());
545        let sozu_id_header_len = self.sozu_id_header.as_ref().map(String::len);
546        let (answers_count, answers_key_len, answers_value_len) =
547            summarize_string_map(&self.answers);
548
549        f.debug_struct("HttpsListenerConfig")
550            .field("address", &self.address)
551            .field("interface", &self.interface)
552            .field("public_address", &self.public_address)
553            .field("expect_proxy", &self.expect_proxy)
554            .field("sticky_name_len", &self.sticky_name.len())
555            .field("front_timeout", &self.front_timeout)
556            .field("back_timeout", &self.back_timeout)
557            .field("connect_timeout", &self.connect_timeout)
558            .field("request_timeout", &self.request_timeout)
559            .field("active", &self.active)
560            .field("versions_count", &self.versions.len())
561            .field("cipher_list_count", &self.cipher_list.len())
562            .field("cipher_list_len", &total_string_len(&self.cipher_list))
563            .field("cipher_suites_count", &self.cipher_suites.len())
564            .field("cipher_suites_len", &total_string_len(&self.cipher_suites))
565            .field(
566                "signature_algorithms_count",
567                &self.signature_algorithms.len(),
568            )
569            .field(
570                "signature_algorithms_len",
571                &total_string_len(&self.signature_algorithms),
572            )
573            .field("groups_list_count", &self.groups_list.len())
574            .field("groups_list_len", &total_string_len(&self.groups_list))
575            .field("certificate", &certificate)
576            .field("certificate_len", &certificate_len)
577            .field("certificate_chain", &"[redacted]")
578            .field("certificate_chain_count", &self.certificate_chain.len())
579            .field("certificate_chain_len", &certificate_chain_len)
580            .field("key", &key)
581            .field("key_len", &key_len)
582            .field("send_tls13_tickets", &self.send_tls13_tickets)
583            .field("http_answers", &http_answers)
584            .field("http_answers_count", &http_answers_count)
585            .field("http_answers_len", &http_answers_len)
586            .field("alpn_protocols_count", &self.alpn_protocols.len())
587            .field(
588                "alpn_protocols_len",
589                &total_string_len(&self.alpn_protocols),
590            )
591            .field(
592                "h2_max_rst_stream_per_window",
593                &self.h2_max_rst_stream_per_window,
594            )
595            .field("h2_max_ping_per_window", &self.h2_max_ping_per_window)
596            .field(
597                "h2_max_settings_per_window",
598                &self.h2_max_settings_per_window,
599            )
600            .field(
601                "h2_max_empty_data_per_window",
602                &self.h2_max_empty_data_per_window,
603            )
604            .field(
605                "h2_max_continuation_frames",
606                &self.h2_max_continuation_frames,
607            )
608            .field("h2_max_glitch_count", &self.h2_max_glitch_count)
609            .field(
610                "h2_initial_connection_window",
611                &self.h2_initial_connection_window,
612            )
613            .field("h2_max_concurrent_streams", &self.h2_max_concurrent_streams)
614            .field("h2_stream_shrink_ratio", &self.h2_stream_shrink_ratio)
615            .field(
616                "h2_max_rst_stream_lifetime",
617                &self.h2_max_rst_stream_lifetime,
618            )
619            .field(
620                "h2_max_rst_stream_abusive_lifetime",
621                &self.h2_max_rst_stream_abusive_lifetime,
622            )
623            .field(
624                "h2_max_rst_stream_emitted_lifetime",
625                &self.h2_max_rst_stream_emitted_lifetime,
626            )
627            .field("h2_max_header_list_size", &self.h2_max_header_list_size)
628            .field("strict_sni_binding", &self.strict_sni_binding)
629            .field("disable_http11", &self.disable_http11)
630            .field(
631                "h2_stream_idle_timeout_seconds",
632                &self.h2_stream_idle_timeout_seconds,
633            )
634            .field("h2_max_header_table_size", &self.h2_max_header_table_size)
635            .field(
636                "h2_graceful_shutdown_deadline_seconds",
637                &self.h2_graceful_shutdown_deadline_seconds,
638            )
639            .field(
640                "h2_max_window_update_stream0_per_window",
641                &self.h2_max_window_update_stream0_per_window,
642            )
643            .field("sozu_id_header_len", &sozu_id_header_len)
644            .field("answers_count", &answers_count)
645            .field("answers_key_len", &answers_key_len)
646            .field("answers_value_len", &answers_value_len)
647            .field("elide_x_real_ip", &self.elide_x_real_ip)
648            .field("send_x_real_ip", &self.send_x_real_ip)
649            .field("forwarded_headers", &self.forwarded_headers)
650            .field("hsts", &self.hsts)
651            .field("h2_max_header_fields", &self.h2_max_header_fields)
652            .field("h2_stream_refusal_percent", &self.h2_stream_refusal_percent)
653            .field("client_auth", &self.client_auth)
654            .field(
655                "client_ca_certificates_count",
656                &self.client_ca_certificates.len(),
657            )
658            .field(
659                "client_ca_certificates_len",
660                &total_string_len(&self.client_ca_certificates),
661            )
662            .field("client_ca_crls_count", &self.client_ca_crls.len())
663            .field(
664                "client_ca_crls_len",
665                &total_string_len(&self.client_ca_crls),
666            )
667            .finish()
668    }
669}
670
671impl std::fmt::Debug for command::QueryCertificatesFilters {
672    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
673        f.debug_struct("QueryCertificatesFilters")
674            .field("domain_len", &self.domain.as_ref().map(String::len))
675            .field(
676                "fingerprint_len",
677                &self.fingerprint.as_ref().map(String::len),
678            )
679            .finish()
680    }
681}
682
683impl std::fmt::Debug for command::CertificateSummary {
684    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
685        f.debug_struct("CertificateSummary")
686            .field("domain_len", &self.domain.len())
687            .field("fingerprint_len", &self.fingerprint.len())
688            .finish()
689    }
690}
691
692impl std::fmt::Debug for command::CertificatesByAddress {
693    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
694        let domain_len = self
695            .certificate_summaries
696            .iter()
697            .map(|summary| summary.domain.len())
698            .fold(0usize, usize::saturating_add);
699        let fingerprint_len = self
700            .certificate_summaries
701            .iter()
702            .map(|summary| summary.fingerprint.len())
703            .fold(0usize, usize::saturating_add);
704
705        f.debug_struct("CertificatesByAddress")
706            .field("address", &self.address)
707            .field("certificates_count", &self.certificate_summaries.len())
708            .field("domain_len", &domain_len)
709            .field("fingerprint_len", &fingerprint_len)
710            .finish()
711    }
712}
713
714impl std::fmt::Debug for command::ListOfCertificatesByAddress {
715    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
716        let certificates_count = self
717            .certificates
718            .iter()
719            .map(|entry| entry.certificate_summaries.len())
720            .fold(0usize, usize::saturating_add);
721        let domain_len = self
722            .certificates
723            .iter()
724            .flat_map(|entry| &entry.certificate_summaries)
725            .map(|summary| summary.domain.len())
726            .fold(0usize, usize::saturating_add);
727        let fingerprint_len = self
728            .certificates
729            .iter()
730            .flat_map(|entry| &entry.certificate_summaries)
731            .map(|summary| summary.fingerprint.len())
732            .fold(0usize, usize::saturating_add);
733
734        f.debug_struct("ListOfCertificatesByAddress")
735            .field("listeners_count", &self.certificates.len())
736            .field("certificates_count", &certificates_count)
737            .field("domain_len", &domain_len)
738            .field("fingerprint_len", &fingerprint_len)
739            .finish()
740    }
741}
742
743impl std::fmt::Debug for command::CertificatesWithFingerprints {
744    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
745        let fingerprint_len = total_string_len(self.certs.keys());
746        let names_count = self
747            .certs
748            .values()
749            .map(|certificate| certificate.names.len())
750            .fold(0usize, usize::saturating_add);
751        let names_len = self
752            .certs
753            .values()
754            .flat_map(|certificate| &certificate.names)
755            .map(String::len)
756            .fold(0usize, usize::saturating_add);
757
758        f.debug_struct("CertificatesWithFingerprints")
759            .field("certificates_count", &self.certs.len())
760            .field("fingerprint_len", &fingerprint_len)
761            .field("names_count", &names_count)
762            .field("names_len", &names_len)
763            .finish()
764    }
765}
766
767fn response_content_kind(content: Option<&command::ResponseContent>) -> Option<&'static str> {
768    use command::response_content::ContentType;
769
770    content
771        .and_then(|response| response.content_type.as_ref())
772        .map(|content_type| match content_type {
773            ContentType::Workers(_) => "workers",
774            ContentType::Metrics(_) => "metrics",
775            ContentType::WorkerResponses(_) => "worker_responses",
776            ContentType::Event(_) => "event",
777            ContentType::FrontendList(_) => "frontend_list",
778            ContentType::ListenersList(_) => "listeners_list",
779            ContentType::WorkerMetrics(_) => "worker_metrics",
780            ContentType::AvailableMetrics(_) => "available_metrics",
781            ContentType::Clusters(_) => "clusters",
782            ContentType::ClusterHashes(_) => "cluster_hashes",
783            ContentType::CertificatesByAddress(_) => "certificates_by_address",
784            ContentType::CertificatesWithFingerprints(_) => "certificates_with_fingerprints",
785            ContentType::RequestCounts(_) => "request_counts",
786            ContentType::MaxConnectionsPerIpLimit(_) => "max_connections_per_ip_limit",
787            ContentType::MaxConnectionsPerSubnetLimit(_) => "max_connections_per_subnet_limit",
788            ContentType::HealthChecksList(_) => "health_checks_list",
789            ContentType::MetricDetailStatus(_) => "metric_detail_status",
790            ContentType::WorkerMetricDetailStatus(_) => "worker_metric_detail_status",
791            ContentType::ClosedSessions(_) => "closed_sessions",
792            ContentType::ProtectedHealthCheckReply(_) => "protected_health_check_reply",
793        })
794}
795
796impl std::fmt::Debug for command::ResponseContent {
797    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
798        f.debug_struct("ResponseContent")
799            .field("content_type", &response_content_kind(Some(self)))
800            .finish_non_exhaustive()
801    }
802}
803
804impl std::fmt::Debug for command::WorkerResponses {
805    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
806        f.debug_struct("WorkerResponses")
807            .field("workers_count", &self.map.len())
808            .field("worker_ids_len", &total_string_len(self.map.keys()))
809            .finish_non_exhaustive()
810    }
811}
812
813impl std::fmt::Debug for command::WorkerRequest {
814    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
815        f.debug_struct("WorkerRequest")
816            .field("id_len", &self.id.len())
817            .field("content", &self.content)
818            .finish()
819    }
820}
821
822impl std::fmt::Debug for command::WorkerResponse {
823    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
824        f.debug_struct("WorkerResponse")
825            .field("id_len", &self.id.len())
826            .field("status", &self.status)
827            .field("message_len", &self.message.len())
828            .field(
829                "content_type",
830                &response_content_kind(self.content.as_ref()),
831            )
832            .finish()
833    }
834}
835
836impl AggregatedMetrics {
837    /// Merge metrics that were received from several workers
838    ///
839    /// Each worker gather the same kind of metrics,
840    /// for its own proxying logic, and for the same clusters with their backends.
841    /// This means we have to reduce each metric from N instances to 1.
842    pub fn merge_metrics(&mut self) {
843        // avoid copying the worker metrics, by taking them
844        let workers = std::mem::take(&mut self.workers);
845
846        for (_worker_id, worker) in workers {
847            for (metric_name, new_value) in worker.proxy {
848                if new_value.is_mergeable() {
849                    self.proxying
850                        .entry(metric_name)
851                        .and_modify(|old_value| old_value.merge(&new_value))
852                        .or_insert(new_value);
853                }
854            }
855
856            for (cluster_id, mut cluster_metrics) in worker.clusters {
857                for (metric_name, new_value) in cluster_metrics.cluster {
858                    if new_value.is_mergeable() {
859                        let cluster = self.clusters.entry(cluster_id.to_owned()).or_default();
860
861                        cluster
862                            .cluster
863                            .entry(metric_name)
864                            .and_modify(|old_value| old_value.merge(&new_value))
865                            .or_insert(new_value);
866                    }
867                }
868
869                for backend in cluster_metrics.backends.drain(..) {
870                    for (metric_name, new_value) in backend.metrics {
871                        if new_value.is_mergeable() {
872                            let cluster = self.clusters.entry(cluster_id.to_owned()).or_default();
873
874                            let found_backend = cluster
875                                .backends
876                                .iter_mut()
877                                .find(|present| present.backend_id == backend.backend_id);
878
879                            if let Some(existing_backend) = found_backend {
880                                let _ = existing_backend
881                                    .metrics
882                                    .entry(metric_name)
883                                    .and_modify(|old_value| old_value.merge(&new_value))
884                                    .or_insert(new_value);
885                            } else {
886                                cluster.backends.push(BackendMetrics {
887                                    backend_id: backend.backend_id.clone(),
888                                    metrics: BTreeMap::from([(metric_name, new_value)]),
889                                });
890                            };
891                        }
892                    }
893                }
894            }
895        }
896    }
897}
898
899impl FilteredMetrics {
900    pub fn merge(&mut self, right: &Self) {
901        match (&self.inner, &right.inner) {
902            (Some(Inner::Gauge(a)), Some(Inner::Gauge(b))) => {
903                *self = Self {
904                    inner: Some(Inner::Gauge(a + b)),
905                };
906            }
907            (Some(Inner::Count(a)), Some(Inner::Count(b))) => {
908                *self = Self {
909                    inner: Some(Inner::Count(a + b)),
910                };
911            }
912            (Some(Inner::Histogram(a)), Some(Inner::Histogram(b))) => {
913                let longest_len = a.buckets.len().max(b.buckets.len());
914
915                let mut a_count = 0;
916                let mut b_count = 0;
917                let buckets = (0..longest_len)
918                    .map(|i| {
919                        if let Some(a_bucket) = a.buckets.get(i) {
920                            a_count = a_bucket.count;
921                        }
922                        if let Some(b_bucket) = b.buckets.get(i) {
923                            b_count = b_bucket.count;
924                        }
925                        Bucket {
926                            le: (1 << i) - 1, // the bucket less-or-equal limits are normalized: 0, 1, 3, 7, 15, ...
927                            count: a_count + b_count,
928                        }
929                    })
930                    .collect();
931
932                *self = Self {
933                    inner: Some(Inner::Histogram(FilteredHistogram {
934                        count: a.count + b.count,
935                        sum: a.sum + b.sum,
936                        buckets,
937                    })),
938                };
939            }
940            (Some(Inner::Percentiles(a)), Some(Inner::Percentiles(b))) => {
941                // You cannot statistically merge two percentile summaries
942                // without the underlying samples. The companion
943                // `<name>_histogram` Inner::Histogram value is the source
944                // of truth for accurate aggregation and merges correctly
945                // above. We still propagate the percentile shape so legacy
946                // consumers reading it observe at least the worst-case
947                // upper bound across workers — element-wise max preserves
948                // the "is anyone slow?" intent. `samples` and `sum` add so
949                // the totals reflect cross-worker volume.
950                *self = Self {
951                    inner: Some(Inner::Percentiles(Percentiles {
952                        samples: a.samples + b.samples,
953                        p_50: a.p_50.max(b.p_50),
954                        p_90: a.p_90.max(b.p_90),
955                        p_99: a.p_99.max(b.p_99),
956                        p_99_9: a.p_99_9.max(b.p_99_9),
957                        p_99_99: a.p_99_99.max(b.p_99_99),
958                        p_99_999: a.p_99_999.max(b.p_99_999),
959                        p_100: a.p_100.max(b.p_100),
960                        sum: a.sum + b.sum,
961                    })),
962                };
963            }
964            _ => {}
965        }
966    }
967
968    fn is_mergeable(&self) -> bool {
969        match &self.inner {
970            Some(Inner::Gauge(_))
971            | Some(Inner::Count(_))
972            | Some(Inner::Histogram(_))
973            | Some(Inner::Percentiles(_)) => true,
974            // Inner::Time and Inner::Timeserie are never used in Sōzu
975            Some(Inner::Time(_)) | Some(Inner::TimeSerie(_)) | None => false,
976        }
977    }
978}
979
980#[cfg(test)]
981mod tests {
982    use std::{
983        collections::{BTreeMap, HashMap},
984        net::SocketAddr,
985    };
986
987    use crate::{certificate::Fingerprint, state::ConfigState};
988
989    use super::AggregatedMetrics;
990    use super::command::{
991        AddCertificate, AlpnProtocols, Bucket, CertificateAndKey, CertificateSummary,
992        CertificatesByAddress, CertificatesWithFingerprints, Cluster, ClusterMetrics,
993        CustomHttpAnswers, FilteredHistogram, FilteredMetrics, Header, HeaderMatchKind,
994        HeaderPosition, HeaderPredicate, HttpListenerConfig, HttpsListenerConfig,
995        ListOfCertificatesByAddress, PathRule, PathRuleKind, Percentiles, QueryCertificatesFilters,
996        RedirectPolicy, RedirectScheme, RemoveCertificate, ReplaceCertificate, Request,
997        RequestHttpFrontend, RequestTcpFrontend, ResponseContent, ResponseStatus, RulePosition,
998        TlsVersion, UpdateHttpListenerConfig, UpdateHttpsListenerConfig, WorkerMetrics,
999        WorkerRequest, WorkerResponse, WorkerResponses, filtered_metrics::Inner,
1000        request::RequestType, response_content::ContentType,
1001    };
1002
1003    #[test]
1004    fn request_debug_redacts_string_variant_payloads_directly_and_when_nested() {
1005        const PATH_SECRET: &str = "REQUEST_SAVE_STATE_PATH_SECRET_SENTINEL";
1006
1007        let request_type = RequestType::SaveState(format!("{PATH_SECRET}{}", "x".repeat(4096)));
1008        let direct_output = format!("{request_type:?}");
1009        let request = Request::from(request_type);
1010        let worker_request = WorkerRequest {
1011            id: "safe-request-id".to_owned(),
1012            content: request.clone(),
1013        };
1014        let outputs = [
1015            direct_output,
1016            format!("{request:?}"),
1017            format!("{worker_request:?}"),
1018        ];
1019
1020        for output in outputs {
1021            assert!(
1022                !output.contains(PATH_SECRET),
1023                "Request Debug leaked its string variant payload: {output}"
1024            );
1025            assert!(
1026                output.contains("SaveState"),
1027                "Request Debug omitted the bounded request kind: {output}"
1028            );
1029            assert!(
1030                output.len() <= 256,
1031                "Request Debug output is not bounded: {} bytes",
1032                output.len()
1033            );
1034        }
1035    }
1036
1037    #[test]
1038    fn listener_and_patch_debug_redacts_user_controlled_material() {
1039        const HTTP_STICKY_SECRET: &str = "HTTP_LISTENER_STICKY_SECRET_SENTINEL";
1040        const HTTP_LEGACY_ANSWER_SECRET: &str = "HTTP_LISTENER_LEGACY_ANSWER_SECRET_SENTINEL";
1041        const HTTP_HEADER_SECRET: &str = "HTTP_LISTENER_HEADER_SECRET_SENTINEL";
1042        const HTTP_ANSWER_KEY_SECRET: &str = "HTTP_LISTENER_ANSWER_KEY_SECRET_SENTINEL";
1043        const HTTP_ANSWER_BODY_SECRET: &str = "HTTP_LISTENER_ANSWER_BODY_SECRET_SENTINEL";
1044        const HTTP_PATCH_STICKY_SECRET: &str = "HTTP_PATCH_STICKY_SECRET_SENTINEL";
1045        const HTTP_PATCH_LEGACY_ANSWER_SECRET: &str = "HTTP_PATCH_LEGACY_ANSWER_SECRET_SENTINEL";
1046        const HTTP_PATCH_HEADER_SECRET: &str = "HTTP_PATCH_HEADER_SECRET_SENTINEL";
1047        const HTTP_PATCH_ANSWER_KEY_SECRET: &str = "HTTP_PATCH_ANSWER_KEY_SECRET_SENTINEL";
1048        const HTTP_PATCH_ANSWER_BODY_SECRET: &str = "HTTP_PATCH_ANSWER_BODY_SECRET_SENTINEL";
1049        const HTTPS_PATCH_STICKY_SECRET: &str = "HTTPS_PATCH_STICKY_SECRET_SENTINEL";
1050        const HTTPS_PATCH_LEGACY_ANSWER_SECRET: &str = "HTTPS_PATCH_LEGACY_ANSWER_SECRET_SENTINEL";
1051        const HTTPS_PATCH_ALPN_SECRET: &str = "HTTPS_PATCH_ALPN_SECRET_SENTINEL";
1052        const HTTPS_PATCH_HEADER_SECRET: &str = "HTTPS_PATCH_HEADER_SECRET_SENTINEL";
1053        const HTTPS_PATCH_ANSWER_KEY_SECRET: &str = "HTTPS_PATCH_ANSWER_KEY_SECRET_SENTINEL";
1054        const HTTPS_PATCH_ANSWER_BODY_SECRET: &str = "HTTPS_PATCH_ANSWER_BODY_SECRET_SENTINEL";
1055
1056        let long_value = |marker: &str| format!("{marker}{}", "x".repeat(4096));
1057        let http_address: SocketAddr = "127.0.0.1:8080"
1058            .parse()
1059            .expect("test HTTP listener address must parse");
1060        let http_listener = HttpListenerConfig {
1061            address: http_address.into(),
1062            sticky_name: long_value(HTTP_STICKY_SECRET),
1063            http_answers: Some(CustomHttpAnswers {
1064                answer_503: Some(long_value(HTTP_LEGACY_ANSWER_SECRET)),
1065                ..Default::default()
1066            }),
1067            sozu_id_header: Some(long_value(HTTP_HEADER_SECRET)),
1068            answers: BTreeMap::from([(
1069                long_value(HTTP_ANSWER_KEY_SECRET),
1070                long_value(HTTP_ANSWER_BODY_SECRET),
1071            )]),
1072            ..Default::default()
1073        };
1074        let http_request = Request::from(RequestType::AddHttpListener(http_listener.clone()));
1075        let http_worker_request = WorkerRequest {
1076            id: "safe-add-http-listener-id".to_owned(),
1077            content: http_request.clone(),
1078        };
1079
1080        let http_patch = UpdateHttpListenerConfig {
1081            address: http_address.into(),
1082            sticky_name: Some(long_value(HTTP_PATCH_STICKY_SECRET)),
1083            http_answers: Some(CustomHttpAnswers {
1084                answer_503: Some(long_value(HTTP_PATCH_LEGACY_ANSWER_SECRET)),
1085                ..Default::default()
1086            }),
1087            sozu_id_header: Some(long_value(HTTP_PATCH_HEADER_SECRET)),
1088            answers: BTreeMap::from([(
1089                long_value(HTTP_PATCH_ANSWER_KEY_SECRET),
1090                long_value(HTTP_PATCH_ANSWER_BODY_SECRET),
1091            )]),
1092            ..Default::default()
1093        };
1094        let http_patch_request = Request::from(RequestType::UpdateHttpListener(http_patch.clone()));
1095        let http_patch_worker_request = WorkerRequest {
1096            id: "safe-update-http-listener-id".to_owned(),
1097            content: http_patch_request.clone(),
1098        };
1099
1100        let https_address: SocketAddr = "127.0.0.1:8443"
1101            .parse()
1102            .expect("test HTTPS listener address must parse");
1103        let https_patch = UpdateHttpsListenerConfig {
1104            address: https_address.into(),
1105            sticky_name: Some(long_value(HTTPS_PATCH_STICKY_SECRET)),
1106            http_answers: Some(CustomHttpAnswers {
1107                answer_503: Some(long_value(HTTPS_PATCH_LEGACY_ANSWER_SECRET)),
1108                ..Default::default()
1109            }),
1110            alpn_protocols: Some(AlpnProtocols {
1111                values: vec![long_value(HTTPS_PATCH_ALPN_SECRET)],
1112            }),
1113            sozu_id_header: Some(long_value(HTTPS_PATCH_HEADER_SECRET)),
1114            answers: BTreeMap::from([(
1115                long_value(HTTPS_PATCH_ANSWER_KEY_SECRET),
1116                long_value(HTTPS_PATCH_ANSWER_BODY_SECRET),
1117            )]),
1118            ..Default::default()
1119        };
1120        let https_patch_request =
1121            Request::from(RequestType::UpdateHttpsListener(https_patch.clone()));
1122        let https_patch_worker_request = WorkerRequest {
1123            id: "safe-update-https-listener-id".to_owned(),
1124            content: https_patch_request.clone(),
1125        };
1126
1127        let debug_outputs = [
1128            ("HttpListenerConfig", format!("{http_listener:?}")),
1129            ("AddHttpListener Request", format!("{http_request:?}")),
1130            (
1131                "AddHttpListener WorkerRequest",
1132                format!("{http_worker_request:?}"),
1133            ),
1134            ("UpdateHttpListenerConfig", format!("{http_patch:?}")),
1135            (
1136                "UpdateHttpListener Request",
1137                format!("{http_patch_request:?}"),
1138            ),
1139            (
1140                "UpdateHttpListener WorkerRequest",
1141                format!("{http_patch_worker_request:?}"),
1142            ),
1143            ("UpdateHttpsListenerConfig", format!("{https_patch:?}")),
1144            (
1145                "UpdateHttpsListener Request",
1146                format!("{https_patch_request:?}"),
1147            ),
1148            (
1149                "UpdateHttpsListener WorkerRequest",
1150                format!("{https_patch_worker_request:?}"),
1151            ),
1152        ];
1153        let secrets = [
1154            HTTP_STICKY_SECRET,
1155            HTTP_LEGACY_ANSWER_SECRET,
1156            HTTP_HEADER_SECRET,
1157            HTTP_ANSWER_KEY_SECRET,
1158            HTTP_ANSWER_BODY_SECRET,
1159            HTTP_PATCH_STICKY_SECRET,
1160            HTTP_PATCH_LEGACY_ANSWER_SECRET,
1161            HTTP_PATCH_HEADER_SECRET,
1162            HTTP_PATCH_ANSWER_KEY_SECRET,
1163            HTTP_PATCH_ANSWER_BODY_SECRET,
1164            HTTPS_PATCH_STICKY_SECRET,
1165            HTTPS_PATCH_LEGACY_ANSWER_SECRET,
1166            HTTPS_PATCH_ALPN_SECRET,
1167            HTTPS_PATCH_HEADER_SECRET,
1168            HTTPS_PATCH_ANSWER_KEY_SECRET,
1169            HTTPS_PATCH_ANSWER_BODY_SECRET,
1170        ];
1171        let mut leaks = Vec::new();
1172        for (label, output) in &debug_outputs {
1173            for secret in secrets {
1174                if output.contains(secret) {
1175                    leaks.push(format!("{label}:{secret}"));
1176                }
1177            }
1178        }
1179        assert!(
1180            leaks.is_empty(),
1181            "listener Debug leaked secret markers through {}",
1182            leaks.join(", ")
1183        );
1184
1185        let expected_metadata = [
1186            (0, "sticky_name_len: 4132"),
1187            (0, "http_answers_count: Some(1)"),
1188            (0, "answers_count: 1"),
1189            (3, "sticky_name_len: Some(4129)"),
1190            (3, "http_answers_count: Some(1)"),
1191            (3, "answers_count: 1"),
1192            (6, "sticky_name_len: Some(4130)"),
1193            (6, "http_answers_count: Some(1)"),
1194            (6, "alpn_protocols_count: Some(1)"),
1195            (6, "answers_count: 1"),
1196        ];
1197        for (output_index, safe_metadata) in expected_metadata {
1198            assert!(
1199                debug_outputs[output_index].1.contains(safe_metadata),
1200                "{} Debug omitted safe metadata {safe_metadata}: {}",
1201                debug_outputs[output_index].0,
1202                debug_outputs[output_index].1,
1203            );
1204        }
1205        for (label, output) in debug_outputs {
1206            assert!(
1207                output.len() <= 4096,
1208                "{label} Debug output is not bounded: {} bytes",
1209                output.len()
1210            );
1211        }
1212    }
1213
1214    #[test]
1215    fn https_listener_debug_redacts_user_controlled_material() {
1216        const STICKY_NAME_SECRET: &str = "HTTPS_LISTENER_STICKY_NAME_SECRET_SENTINEL";
1217        const CIPHER_LIST_SECRET: &str = "HTTPS_LISTENER_CIPHER_LIST_SECRET_SENTINEL";
1218        const CIPHER_SUITES_SECRET: &str = "HTTPS_LISTENER_CIPHER_SUITES_SECRET_SENTINEL";
1219        const SIGNATURE_ALGORITHMS_SECRET: &str =
1220            "HTTPS_LISTENER_SIGNATURE_ALGORITHMS_SECRET_SENTINEL";
1221        const GROUPS_LIST_SECRET: &str = "HTTPS_LISTENER_GROUPS_LIST_SECRET_SENTINEL";
1222        const CERTIFICATE_SECRET: &str = "HTTPS_LISTENER_CERTIFICATE_SECRET_SENTINEL";
1223        const CHAIN_SECRET: &str = "HTTPS_LISTENER_CHAIN_SECRET_SENTINEL";
1224        const KEY_SECRET: &str = "HTTPS_LISTENER_KEY_SECRET_SENTINEL";
1225        const HTTP_ANSWER_SECRET: &str = "HTTPS_LISTENER_HTTP_ANSWER_SECRET_SENTINEL";
1226        const ALPN_SECRET: &str = "HTTPS_LISTENER_ALPN_SECRET_SENTINEL";
1227        const SOZU_ID_HEADER_SECRET: &str = "HTTPS_LISTENER_SOZU_ID_HEADER_SECRET_SENTINEL";
1228        const ANSWER_KEY_SECRET: &str = "HTTPS_LISTENER_ANSWER_KEY_SECRET_SENTINEL";
1229        const ANSWER_VALUE_SECRET: &str = "HTTPS_LISTENER_ANSWER_VALUE_SECRET_SENTINEL";
1230
1231        let long_value = |marker: &str| format!("{marker}{}", "x".repeat(4096));
1232        let address: SocketAddr = "127.0.0.1:8443"
1233            .parse()
1234            .expect("test HTTPS listener address must parse");
1235        let public_address: SocketAddr = "203.0.113.1:443"
1236            .parse()
1237            .expect("test public HTTPS listener address must parse");
1238        let listener = HttpsListenerConfig {
1239            address: address.into(),
1240            public_address: Some(public_address.into()),
1241            sticky_name: long_value(STICKY_NAME_SECRET),
1242            versions: vec![TlsVersion::TlsV13 as i32],
1243            cipher_list: vec![long_value(CIPHER_LIST_SECRET)],
1244            cipher_suites: vec![long_value(CIPHER_SUITES_SECRET)],
1245            signature_algorithms: vec![long_value(SIGNATURE_ALGORITHMS_SECRET)],
1246            groups_list: vec![long_value(GROUPS_LIST_SECRET)],
1247            certificate: Some(long_value(CERTIFICATE_SECRET)),
1248            certificate_chain: vec![long_value(CHAIN_SECRET)],
1249            key: Some(long_value(KEY_SECRET)),
1250            http_answers: Some(CustomHttpAnswers {
1251                answer_404: Some(long_value(HTTP_ANSWER_SECRET)),
1252                ..Default::default()
1253            }),
1254            alpn_protocols: vec![long_value(ALPN_SECRET)],
1255            sozu_id_header: Some(long_value(SOZU_ID_HEADER_SECRET)),
1256            answers: BTreeMap::from([(
1257                long_value(ANSWER_KEY_SECRET),
1258                long_value(ANSWER_VALUE_SECRET),
1259            )]),
1260            ..Default::default()
1261        };
1262        let add_https_listener = RequestType::AddHttpsListener(listener.clone());
1263        let request = Request::from(add_https_listener.clone());
1264        let worker_request = WorkerRequest {
1265            id: "safe-add-https-listener-id".to_owned(),
1266            content: request.clone(),
1267        };
1268        let state = ConfigState {
1269            https_listeners: BTreeMap::from([(address.into(), listener.clone())]),
1270            ..Default::default()
1271        };
1272        let debug_outputs = [
1273            ("HttpsListenerConfig", format!("{listener:?}")),
1274            (
1275                "RequestType::AddHttpsListener",
1276                format!("{add_https_listener:?}"),
1277            ),
1278            ("Request", format!("{request:?}")),
1279            ("WorkerRequest", format!("{worker_request:?}")),
1280            ("ConfigState", format!("{state:?}")),
1281        ];
1282        let secrets = [
1283            STICKY_NAME_SECRET,
1284            CIPHER_LIST_SECRET,
1285            CIPHER_SUITES_SECRET,
1286            SIGNATURE_ALGORITHMS_SECRET,
1287            GROUPS_LIST_SECRET,
1288            CERTIFICATE_SECRET,
1289            CHAIN_SECRET,
1290            KEY_SECRET,
1291            HTTP_ANSWER_SECRET,
1292            ALPN_SECRET,
1293            SOZU_ID_HEADER_SECRET,
1294            ANSWER_KEY_SECRET,
1295            ANSWER_VALUE_SECRET,
1296        ];
1297        let mut leaks = Vec::new();
1298        for (label, output) in &debug_outputs {
1299            for secret in secrets {
1300                if output.contains(secret) {
1301                    leaks.push(format!("{label}:{secret}"));
1302                }
1303            }
1304        }
1305        assert!(
1306            leaks.is_empty(),
1307            "HTTPS listener Debug leaked secret markers through {}",
1308            leaks.join(", ")
1309        );
1310
1311        let direct_output = &debug_outputs[0].1;
1312        let expected_metadata = [
1313            format!("sticky_name_len: {}", long_value(STICKY_NAME_SECRET).len()),
1314            "versions_count: 1".to_owned(),
1315            "cipher_list_count: 1".to_owned(),
1316            format!("cipher_list_len: {}", long_value(CIPHER_LIST_SECRET).len()),
1317            "cipher_suites_count: 1".to_owned(),
1318            "signature_algorithms_count: 1".to_owned(),
1319            "groups_list_count: 1".to_owned(),
1320            "certificate: Some(\"[redacted]\")".to_owned(),
1321            format!(
1322                "certificate_len: Some({})",
1323                long_value(CERTIFICATE_SECRET).len()
1324            ),
1325            "certificate_chain: \"[redacted]\"".to_owned(),
1326            "certificate_chain_count: 1".to_owned(),
1327            "key: Some(\"[redacted]\")".to_owned(),
1328            "http_answers_count: Some(1)".to_owned(),
1329            "alpn_protocols_count: 1".to_owned(),
1330            format!(
1331                "sozu_id_header_len: Some({})",
1332                long_value(SOZU_ID_HEADER_SECRET).len()
1333            ),
1334            "answers_count: 1".to_owned(),
1335        ];
1336        for safe_metadata in expected_metadata {
1337            assert!(
1338                direct_output.contains(&safe_metadata),
1339                "HttpsListenerConfig Debug omitted safe metadata {safe_metadata}: {direct_output}"
1340            );
1341        }
1342        for (label, output) in debug_outputs {
1343            assert!(
1344                output.len() <= 4096,
1345                "{label} Debug output is not bounded: {} bytes",
1346                output.len()
1347            );
1348        }
1349    }
1350
1351    #[test]
1352    fn header_debug_redacts_key_and_value() {
1353        const KEY_SECRET: &str = "HEADER_KEY_SECRET_SENTINEL";
1354        const VALUE_SECRET: &str = "HEADER_VALUE_SECRET_SENTINEL";
1355
1356        let long_value = |marker: &str| format!("{marker}{}", "x".repeat(4096));
1357        let header = Header {
1358            position: HeaderPosition::Unspecified as i32,
1359            key: long_value(KEY_SECRET),
1360            val: long_value(VALUE_SECRET),
1361        };
1362        let output = format!("{header:?}");
1363
1364        for secret in [KEY_SECRET, VALUE_SECRET] {
1365            assert!(
1366                !output.contains(secret),
1367                "Header Debug leaked user-controlled marker {secret}"
1368            );
1369        }
1370        for metadata in [
1371            "position: 0".to_owned(),
1372            format!("key_len: {}", long_value(KEY_SECRET).len()),
1373            format!("val_len: {}", long_value(VALUE_SECRET).len()),
1374        ] {
1375            assert!(
1376                output.contains(&metadata),
1377                "Header Debug omitted safe metadata {metadata}: {output}"
1378            );
1379        }
1380        assert!(
1381            output.len() <= 256,
1382            "Header Debug output is not bounded: {} bytes",
1383            output.len()
1384        );
1385    }
1386
1387    #[test]
1388    fn http_frontend_request_debug_redacts_user_controlled_material() {
1389        const CLUSTER_SECRET: &str = "HTTP_FRONTEND_CLUSTER_SECRET_SENTINEL";
1390        const HOSTNAME_SECRET: &str = "HTTP_FRONTEND_HOSTNAME_SECRET_SENTINEL";
1391        const PATH_SECRET: &str = "HTTP_FRONTEND_PATH_SECRET_SENTINEL";
1392        const METHOD_SECRET: &str = "HTTP_FRONTEND_METHOD_SECRET_SENTINEL";
1393        const TAG_KEY_SECRET: &str = "HTTP_FRONTEND_TAG_KEY_SECRET_SENTINEL";
1394        const TAG_VALUE_SECRET: &str = "HTTP_FRONTEND_TAG_VALUE_SECRET_SENTINEL";
1395        const REDIRECT_SECRET: &str = "HTTP_FRONTEND_REDIRECT_SECRET_SENTINEL";
1396        const REWRITE_HOST_SECRET: &str = "HTTP_FRONTEND_REWRITE_HOST_SECRET_SENTINEL";
1397        const REWRITE_PATH_SECRET: &str = "HTTP_FRONTEND_REWRITE_PATH_SECRET_SENTINEL";
1398        const HEADER_KEY_SECRET: &str = "HTTP_FRONTEND_HEADER_KEY_SECRET_SENTINEL";
1399        const HEADER_VALUE_SECRET: &str = "HTTP_FRONTEND_HEADER_VALUE_SECRET_SENTINEL";
1400        const PREDICATE_NAME_SECRET: &str = "HTTP_FRONTEND_PREDICATE_NAME_SECRET_SENTINEL";
1401        const PREDICATE_VALUE_SECRET: &str = "HTTP_FRONTEND_PREDICATE_VALUE_SECRET_SENTINEL";
1402
1403        let long_value = |marker: &str| format!("{marker}{}", "x".repeat(4096));
1404        let frontend = RequestHttpFrontend {
1405            cluster_id: Some(long_value(CLUSTER_SECRET)),
1406            address: Default::default(),
1407            hostname: long_value(HOSTNAME_SECRET),
1408            path: PathRule {
1409                value: long_value(PATH_SECRET),
1410                kind: PathRuleKind::Prefix as i32,
1411            },
1412            method: Some(long_value(METHOD_SECRET)),
1413            position: RulePosition::Tree as i32,
1414            tags: BTreeMap::from([(long_value(TAG_KEY_SECRET), long_value(TAG_VALUE_SECRET))]),
1415            redirect: Some(RedirectPolicy::Forward as i32),
1416            required_auth: Some(true),
1417            redirect_scheme: Some(RedirectScheme::UseSame as i32),
1418            redirect_template: Some(long_value(REDIRECT_SECRET)),
1419            rewrite_host: Some(long_value(REWRITE_HOST_SECRET)),
1420            rewrite_path: Some(long_value(REWRITE_PATH_SECRET)),
1421            rewrite_port: Some(8443),
1422            headers: vec![Header {
1423                position: HeaderPosition::Request as i32,
1424                key: long_value(HEADER_KEY_SECRET),
1425                val: long_value(HEADER_VALUE_SECRET),
1426            }],
1427            hsts: None,
1428            header_predicates: vec![HeaderPredicate {
1429                name: long_value(PREDICATE_NAME_SECRET),
1430                kind: HeaderMatchKind::Exact as i32,
1431                value: Some(long_value(PREDICATE_VALUE_SECRET)),
1432            }],
1433        };
1434        let request_type = RequestType::AddHttpsFrontend(frontend.clone());
1435        let request = Request::from(request_type.clone());
1436        let worker_request = WorkerRequest {
1437            id: "safe-http-frontend-request-id".to_owned(),
1438            content: request.clone(),
1439        };
1440        let debug_outputs = [
1441            ("RequestHttpFrontend", format!("{frontend:?}")),
1442            ("RequestType", format!("{request_type:?}")),
1443            ("Request", format!("{request:?}")),
1444            ("WorkerRequest", format!("{worker_request:?}")),
1445        ];
1446        let secrets = [
1447            CLUSTER_SECRET,
1448            HOSTNAME_SECRET,
1449            PATH_SECRET,
1450            METHOD_SECRET,
1451            TAG_KEY_SECRET,
1452            TAG_VALUE_SECRET,
1453            REDIRECT_SECRET,
1454            REWRITE_HOST_SECRET,
1455            REWRITE_PATH_SECRET,
1456            HEADER_KEY_SECRET,
1457            HEADER_VALUE_SECRET,
1458            PREDICATE_NAME_SECRET,
1459            PREDICATE_VALUE_SECRET,
1460        ];
1461
1462        for (label, output) in &debug_outputs {
1463            for secret in secrets {
1464                assert!(
1465                    !output.contains(secret),
1466                    "{label} Debug leaked user-controlled marker {secret}: {output}"
1467                );
1468            }
1469            assert!(
1470                output.len() <= 2048,
1471                "{label} Debug output is not bounded: {} bytes",
1472                output.len()
1473            );
1474        }
1475
1476        let direct_output = &debug_outputs[0].1;
1477        let expected_metadata = [
1478            format!("cluster_id_len: Some({})", long_value(CLUSTER_SECRET).len()),
1479            format!("hostname_len: {}", long_value(HOSTNAME_SECRET).len()),
1480            format!("path_len: {}", long_value(PATH_SECRET).len()),
1481            format!("method_len: Some({})", long_value(METHOD_SECRET).len()),
1482            "tags_count: 1".to_owned(),
1483            "headers_count: 1".to_owned(),
1484            "header_predicates_count: 1".to_owned(),
1485        ];
1486        for safe_metadata in expected_metadata {
1487            assert!(
1488                direct_output.contains(&safe_metadata),
1489                "RequestHttpFrontend Debug omitted safe metadata {safe_metadata}: {direct_output}"
1490            );
1491        }
1492    }
1493
1494    #[test]
1495    fn certificate_query_request_debug_redacts_filters_across_command_wrappers() {
1496        const DOMAIN_SECRET: &str = "QUERY_REQUEST_DOMAIN_SECRET_SENTINEL";
1497        const FINGERPRINT_SECRET: &str = "QUERY_REQUEST_FINGERPRINT_SECRET_SENTINEL";
1498        const REQUEST_ID_SECRET: &str = "QUERY_REQUEST_ID_SECRET_SENTINEL";
1499
1500        let long_value = |marker: &str| format!("{marker}{}", "x".repeat(4096));
1501        let filters = QueryCertificatesFilters {
1502            domain: Some(long_value(DOMAIN_SECRET)),
1503            fingerprint: Some(long_value(FINGERPRINT_SECRET)),
1504        };
1505        let request_type = RequestType::QueryCertificatesFromWorkers(filters.clone());
1506        let request = Request::from(request_type.clone());
1507        let worker_request = WorkerRequest {
1508            id: long_value(REQUEST_ID_SECRET),
1509            content: request.clone(),
1510        };
1511        let outputs = [
1512            ("QueryCertificatesFilters", format!("{filters:?}")),
1513            ("RequestType", format!("{request_type:?}")),
1514            ("Request", format!("{request:?}")),
1515            ("WorkerRequest", format!("{worker_request:?}")),
1516        ];
1517
1518        for (label, output) in &outputs {
1519            for secret in [DOMAIN_SECRET, FINGERPRINT_SECRET, REQUEST_ID_SECRET] {
1520                assert!(
1521                    !output.contains(secret),
1522                    "{label} Debug leaked certificate-query marker {secret}: {output}"
1523                );
1524            }
1525            assert!(
1526                output.len() <= 1024,
1527                "{label} Debug output is not bounded: {} bytes",
1528                output.len()
1529            );
1530        }
1531
1532        let filter_output = &outputs[0].1;
1533        for metadata in [
1534            format!("domain_len: Some({})", long_value(DOMAIN_SECRET).len()),
1535            format!(
1536                "fingerprint_len: Some({})",
1537                long_value(FINGERPRINT_SECRET).len()
1538            ),
1539        ] {
1540            assert!(
1541                filter_output.contains(&metadata),
1542                "QueryCertificatesFilters Debug omitted bounded metadata {metadata}: {filter_output}"
1543            );
1544        }
1545        assert!(
1546            outputs[3]
1547                .1
1548                .contains(&format!("id_len: {}", long_value(REQUEST_ID_SECRET).len())),
1549            "WorkerRequest Debug omitted the bounded request-id length: {}",
1550            outputs[3].1
1551        );
1552    }
1553
1554    #[test]
1555    fn silent_close_frontend_request_debug_keeps_redacted_payload() {
1556        const HOSTNAME_SECRET: &str = "SILENT_CLOSE_HOSTNAME_SECRET_SENTINEL";
1557
1558        let frontend = RequestHttpFrontend {
1559            hostname: HOSTNAME_SECRET.to_owned(),
1560            path: PathRule {
1561                value: "/".to_owned(),
1562                kind: PathRuleKind::Prefix as i32,
1563            },
1564            position: RulePosition::Tree as i32,
1565            redirect: Some(RedirectPolicy::SilentClose as i32),
1566            ..Default::default()
1567        };
1568
1569        for (name, request_type) in [
1570            (
1571                "AddHttpFrontendWithSilentClose",
1572                RequestType::AddHttpFrontendWithSilentClose(frontend.clone()),
1573            ),
1574            (
1575                "AddHttpsFrontendWithSilentClose",
1576                RequestType::AddHttpsFrontendWithSilentClose(frontend.clone()),
1577            ),
1578        ] {
1579            let output = format!("{request_type:?}");
1580            assert!(
1581                output.contains(&format!("hostname_len: {}", HOSTNAME_SECRET.len())),
1582                "{name} Debug dropped the frontend payload: {output}"
1583            );
1584            assert!(
1585                !output.contains(HOSTNAME_SECRET),
1586                "{name} Debug leaked the hostname: {output}"
1587            );
1588        }
1589    }
1590
1591    #[test]
1592    fn certificate_query_response_debug_redacts_results_across_worker_wrappers() {
1593        const DOMAIN_SECRET: &str = "QUERY_RESPONSE_DOMAIN_SECRET_SENTINEL";
1594        const FINGERPRINT_SECRET: &str = "QUERY_RESPONSE_FINGERPRINT_SECRET_SENTINEL";
1595        const MAP_KEY_SECRET: &str = "QUERY_RESPONSE_MAP_KEY_SECRET_SENTINEL";
1596        const CERTIFICATE_SECRET: &str = "QUERY_RESPONSE_CERTIFICATE_SECRET_SENTINEL";
1597        const KEY_SECRET: &str = "QUERY_RESPONSE_KEY_SECRET_SENTINEL";
1598        const NAME_SECRET: &str = "QUERY_RESPONSE_NAME_SECRET_SENTINEL";
1599        const WORKER_ID_SECRET: &str = "QUERY_RESPONSE_WORKER_ID_SECRET_SENTINEL";
1600        const RESPONSE_ID_SECRET: &str = "QUERY_RESPONSE_ID_SECRET_SENTINEL";
1601        const MESSAGE_SECRET: &str = "QUERY_RESPONSE_MESSAGE_SECRET_SENTINEL";
1602
1603        let long_value = |marker: &str| format!("{marker}{}", "x".repeat(4096));
1604        let summary = CertificateSummary {
1605            domain: long_value(DOMAIN_SECRET),
1606            fingerprint: long_value(FINGERPRINT_SECRET),
1607        };
1608        let by_address = CertificatesByAddress {
1609            address: Default::default(),
1610            certificate_summaries: vec![summary.clone()],
1611        };
1612        let list = ListOfCertificatesByAddress {
1613            certificates: vec![by_address.clone()],
1614        };
1615        let worker_content =
1616            ResponseContent::from(ContentType::CertificatesByAddress(list.clone()));
1617        let worker_response = WorkerResponse {
1618            id: long_value(RESPONSE_ID_SECRET),
1619            status: ResponseStatus::Ok as i32,
1620            message: long_value(MESSAGE_SECRET),
1621            content: Some(worker_content.clone()),
1622        };
1623        let worker_responses = WorkerResponses {
1624            map: BTreeMap::from([(long_value(WORKER_ID_SECRET), worker_content.clone())]),
1625        };
1626        let gathered_content =
1627            ResponseContent::from(ContentType::WorkerResponses(worker_responses.clone()));
1628        let main_certificates = CertificatesWithFingerprints {
1629            certs: BTreeMap::from([(
1630                long_value(MAP_KEY_SECRET),
1631                CertificateAndKey {
1632                    certificate: long_value(CERTIFICATE_SECRET),
1633                    certificate_chain: Vec::new(),
1634                    key: long_value(KEY_SECRET),
1635                    versions: Vec::new(),
1636                    names: vec![long_value(NAME_SECRET)],
1637                },
1638            )]),
1639        };
1640        let main_content = ResponseContent::from(ContentType::CertificatesWithFingerprints(
1641            main_certificates.clone(),
1642        ));
1643        let outputs = [
1644            ("CertificateSummary", format!("{summary:?}")),
1645            ("CertificatesByAddress", format!("{by_address:?}")),
1646            ("ListOfCertificatesByAddress", format!("{list:?}")),
1647            ("worker ResponseContent", format!("{worker_content:?}")),
1648            ("WorkerResponse", format!("{worker_response:?}")),
1649            ("WorkerResponses", format!("{worker_responses:?}")),
1650            ("gathered ResponseContent", format!("{gathered_content:?}")),
1651            (
1652                "CertificatesWithFingerprints",
1653                format!("{main_certificates:?}"),
1654            ),
1655            ("main ResponseContent", format!("{main_content:?}")),
1656        ];
1657        let secrets = [
1658            DOMAIN_SECRET,
1659            FINGERPRINT_SECRET,
1660            MAP_KEY_SECRET,
1661            CERTIFICATE_SECRET,
1662            KEY_SECRET,
1663            NAME_SECRET,
1664            WORKER_ID_SECRET,
1665            RESPONSE_ID_SECRET,
1666            MESSAGE_SECRET,
1667        ];
1668
1669        for (label, output) in &outputs {
1670            for secret in secrets {
1671                assert!(
1672                    !output.contains(secret),
1673                    "{label} Debug leaked certificate-query marker {secret}: {output}"
1674                );
1675            }
1676            assert!(
1677                output.len() <= 1024,
1678                "{label} Debug output is not bounded: {} bytes",
1679                output.len()
1680            );
1681        }
1682
1683        for metadata in ["listeners_count: 1", "certificates_count: 1"] {
1684            assert!(
1685                outputs[2].1.contains(metadata),
1686                "ListOfCertificatesByAddress Debug omitted bounded metadata {metadata}: {}",
1687                outputs[2].1
1688            );
1689        }
1690        for metadata in ["id_len:", "message_len:", "content_type:"] {
1691            assert!(
1692                outputs[4].1.contains(metadata),
1693                "WorkerResponse Debug omitted bounded metadata {metadata}: {}",
1694                outputs[4].1
1695            );
1696        }
1697    }
1698
1699    #[test]
1700    fn certificate_debug_redacts_pem_material() {
1701        const CERTIFICATE_SECRET: &str = "CERTIFICATE_SECRET_SENTINEL";
1702        const CHAIN_SECRET: &str = "CHAIN_SECRET_SENTINEL";
1703        const KEY_SECRET: &str = "KEY_SECRET_SENTINEL";
1704        const NAME_SECRET: &str = "CERTIFICATE_NAME_SECRET_SENTINEL";
1705
1706        let certificate = CertificateAndKey {
1707            certificate: CERTIFICATE_SECRET.to_owned(),
1708            certificate_chain: vec![CHAIN_SECRET.to_owned()],
1709            key: KEY_SECRET.to_owned(),
1710            versions: vec![i32::MIN; 1_024],
1711            names: vec![format!("{NAME_SECRET}{}", "x".repeat(4096))],
1712        };
1713        let add_certificate = AddCertificate {
1714            address: Default::default(),
1715            certificate: certificate.clone(),
1716            expired_at: None,
1717        };
1718        let replace_certificate = ReplaceCertificate {
1719            address: Default::default(),
1720            new_certificate: certificate.clone(),
1721            old_fingerprint: "safe-old-fingerprint".to_owned(),
1722            new_expired_at: None,
1723        };
1724        let add_request = Request::from(RequestType::AddCertificate(add_certificate.clone()));
1725        let replace_request =
1726            Request::from(RequestType::ReplaceCertificate(replace_certificate.clone()));
1727        let add_worker_request = WorkerRequest {
1728            id: "safe-add-request-id".to_owned(),
1729            content: add_request.clone(),
1730        };
1731        let replace_worker_request = WorkerRequest {
1732            id: "safe-replace-request-id".to_owned(),
1733            content: replace_request.clone(),
1734        };
1735        let state_address: SocketAddr = "127.0.0.1:443"
1736            .parse()
1737            .expect("test certificate address must parse");
1738        let state = ConfigState {
1739            certificates: HashMap::from([(
1740                state_address,
1741                HashMap::from([(Fingerprint(vec![0; 32]), certificate.clone())]),
1742            )]),
1743            ..Default::default()
1744        };
1745
1746        let debug_outputs = [
1747            format!("{certificate:?}"),
1748            format!("{add_certificate:?}"),
1749            format!("{replace_certificate:?}"),
1750            format!("{add_request:?}"),
1751            format!("{replace_request:?}"),
1752            format!("{add_worker_request:?}"),
1753            format!("{replace_worker_request:?}"),
1754        ];
1755        let state_debug = format!("{state:?}");
1756
1757        for output in debug_outputs {
1758            for secret in [CERTIFICATE_SECRET, CHAIN_SECRET, KEY_SECRET, NAME_SECRET] {
1759                assert!(
1760                    !output.contains(secret),
1761                    "debug output leaked secret marker {secret}: {output}"
1762                );
1763            }
1764            assert!(
1765                output.contains("[redacted]"),
1766                "debug output must make redaction explicit: {output}"
1767            );
1768            assert!(
1769                output.contains("names_count: 1") && output.contains("names_len: 4128"),
1770                "debug output must retain bounded certificate-name metadata: {output}"
1771            );
1772            assert!(
1773                output.contains("versions_count: 1024"),
1774                "debug output must retain bounded TLS-version metadata: {output}"
1775            );
1776            assert!(
1777                output.len() <= 4096,
1778                "certificate Debug output is not bounded: {} bytes",
1779                output.len()
1780            );
1781        }
1782
1783        for secret in [CERTIFICATE_SECRET, CHAIN_SECRET, KEY_SECRET, NAME_SECRET] {
1784            assert!(
1785                !state_debug.contains(secret),
1786                "ConfigState Debug leaked secret marker {secret}: {state_debug}"
1787            );
1788        }
1789        assert!(
1790            state_debug.contains("certificate_addresses_count: 1")
1791                && state_debug.contains("certificates_count: 1"),
1792            "ConfigState Debug must retain count-only certificate metadata: {state_debug}"
1793        );
1794        assert!(
1795            state_debug.len() <= 4096,
1796            "ConfigState Debug output is not bounded: {} bytes",
1797            state_debug.len()
1798        );
1799    }
1800
1801    #[test]
1802    fn certificate_fingerprint_control_debug_is_redacted_across_wrappers() {
1803        const FINGERPRINT_SECRET: &str = "CERTIFICATE_CONTROL_FINGERPRINT_SECRET_SENTINEL";
1804
1805        let fingerprint = format!("{FINGERPRINT_SECRET}{}", "f".repeat(4096));
1806        let remove = RemoveCertificate {
1807            address: Default::default(),
1808            fingerprint: fingerprint.clone(),
1809        };
1810        let replace = ReplaceCertificate {
1811            address: Default::default(),
1812            new_certificate: CertificateAndKey::default(),
1813            old_fingerprint: fingerprint.clone(),
1814            new_expired_at: None,
1815        };
1816        let remove_request = Request::from(RequestType::RemoveCertificate(remove.clone()));
1817        let replace_request = Request::from(RequestType::ReplaceCertificate(replace.clone()));
1818        let outputs = [
1819            ("RemoveCertificate", format!("{remove:?}")),
1820            ("ReplaceCertificate", format!("{replace:?}")),
1821            ("remove Request", format!("{remove_request:?}")),
1822            ("replace Request", format!("{replace_request:?}")),
1823            (
1824                "remove WorkerRequest",
1825                format!(
1826                    "{:?}",
1827                    WorkerRequest {
1828                        id: "remove-certificate-test".to_owned(),
1829                        content: remove_request,
1830                    }
1831                ),
1832            ),
1833            (
1834                "replace WorkerRequest",
1835                format!(
1836                    "{:?}",
1837                    WorkerRequest {
1838                        id: "replace-certificate-test".to_owned(),
1839                        content: replace_request,
1840                    }
1841                ),
1842            ),
1843        ];
1844
1845        for (label, output) in outputs {
1846            assert!(
1847                !output.contains(FINGERPRINT_SECRET),
1848                "{label} Debug leaked the certificate fingerprint: {output}"
1849            );
1850            assert!(
1851                output.contains(&format!("fingerprint_len: {}", fingerprint.len())),
1852                "{label} Debug omitted bounded fingerprint metadata: {output}"
1853            );
1854            assert!(
1855                output.len() <= 1024,
1856                "{label} Debug output is not bounded: {} bytes",
1857                output.len()
1858            );
1859        }
1860    }
1861
1862    #[test]
1863    fn cluster_debug_redacts_answers_and_authorization_data_across_wrappers() {
1864        const CLUSTER_SECRET: &str = "CLUSTER_ID_SECRET_SENTINEL";
1865        const LEGACY_ANSWER_SECRET: &str = "CLUSTER_LEGACY_ANSWER_SECRET_SENTINEL";
1866        const ANSWER_KEY_SECRET: &str = "CLUSTER_ANSWER_KEY_SECRET_SENTINEL";
1867        const ANSWER_BODY_SECRET: &str = "CLUSTER_ANSWER_BODY_SECRET_SENTINEL";
1868        const AUTH_HASH_SECRET: &str = "CLUSTER_AUTH_HASH_SECRET_SENTINEL";
1869        const REALM_SECRET: &str = "CLUSTER_AUTH_REALM_SECRET_SENTINEL";
1870
1871        let long_value = |marker: &str| format!("{marker}{}", "x".repeat(4096));
1872        let cluster = Cluster {
1873            cluster_id: long_value(CLUSTER_SECRET),
1874            answer_503: Some(long_value(LEGACY_ANSWER_SECRET)),
1875            answers: BTreeMap::from([(
1876                long_value(ANSWER_KEY_SECRET),
1877                long_value(ANSWER_BODY_SECRET),
1878            )]),
1879            authorized_hashes: vec![long_value(AUTH_HASH_SECRET)],
1880            www_authenticate: Some(long_value(REALM_SECRET)),
1881            ..Default::default()
1882        };
1883        let request = Request::from(RequestType::AddCluster(cluster.clone()));
1884        let outputs = [
1885            ("Cluster", format!("{cluster:?}")),
1886            ("Request", format!("{request:?}")),
1887            (
1888                "WorkerRequest",
1889                format!(
1890                    "{:?}",
1891                    WorkerRequest {
1892                        id: "cluster-redaction-test".to_owned(),
1893                        content: request,
1894                    }
1895                ),
1896            ),
1897        ];
1898
1899        for (label, output) in outputs {
1900            for secret in [
1901                CLUSTER_SECRET,
1902                LEGACY_ANSWER_SECRET,
1903                ANSWER_KEY_SECRET,
1904                ANSWER_BODY_SECRET,
1905                AUTH_HASH_SECRET,
1906                REALM_SECRET,
1907            ] {
1908                assert!(
1909                    !output.contains(secret),
1910                    "{label} Debug leaked cluster marker {secret}: {output}"
1911                );
1912            }
1913            for metadata in [
1914                "cluster_id_len:",
1915                "answers_count: 1",
1916                "authorized_hashes_count: 1",
1917            ] {
1918                assert!(
1919                    output.contains(metadata),
1920                    "{label} Debug omitted bounded cluster metadata {metadata}: {output}"
1921                );
1922            }
1923            assert!(
1924                output.len() <= 2048,
1925                "{label} Debug output is not bounded: {} bytes",
1926                output.len()
1927            );
1928        }
1929    }
1930
1931    #[test]
1932    fn tcp_frontend_debug_redacts_stored_sni_routing_data_across_wrappers() {
1933        const CLUSTER_SECRET: &str = "TCP_FRONTEND_CLUSTER_SECRET_SENTINEL";
1934        const TAG_KEY_SECRET: &str = "TCP_FRONTEND_TAG_KEY_SECRET_SENTINEL";
1935        const TAG_VALUE_SECRET: &str = "TCP_FRONTEND_TAG_VALUE_SECRET_SENTINEL";
1936        const SNI_SECRET: &str = "TCP_FRONTEND_SNI_SECRET_SENTINEL";
1937        const ALPN_SECRET: &str = "TCP_FRONTEND_ALPN_SECRET_SENTINEL";
1938
1939        let long_value = |marker: &str| format!("{marker}{}", "x".repeat(4096));
1940        let frontend = RequestTcpFrontend {
1941            cluster_id: long_value(CLUSTER_SECRET),
1942            address: Default::default(),
1943            tags: BTreeMap::from([(long_value(TAG_KEY_SECRET), long_value(TAG_VALUE_SECRET))]),
1944            sni: Some(long_value(SNI_SECRET)),
1945            alpn: vec![long_value(ALPN_SECRET)],
1946        };
1947        let request = Request::from(RequestType::AddTcpFrontend(frontend.clone()));
1948        let outputs = [
1949            ("RequestTcpFrontend", format!("{frontend:?}")),
1950            ("Request", format!("{request:?}")),
1951            (
1952                "WorkerRequest",
1953                format!(
1954                    "{:?}",
1955                    WorkerRequest {
1956                        id: "tcp-frontend-redaction-test".to_owned(),
1957                        content: request,
1958                    }
1959                ),
1960            ),
1961        ];
1962
1963        for (label, output) in outputs {
1964            for secret in [
1965                CLUSTER_SECRET,
1966                TAG_KEY_SECRET,
1967                TAG_VALUE_SECRET,
1968                SNI_SECRET,
1969                ALPN_SECRET,
1970            ] {
1971                assert!(
1972                    !output.contains(secret),
1973                    "{label} Debug leaked TCP frontend marker {secret}: {output}"
1974                );
1975            }
1976            for metadata in [
1977                "cluster_id_len:",
1978                "tags_count: 1",
1979                "sni_len: Some(",
1980                "alpn_count: 1",
1981            ] {
1982                assert!(
1983                    output.contains(metadata),
1984                    "{label} Debug omitted bounded TCP frontend metadata {metadata}: {output}"
1985                );
1986            }
1987            assert!(
1988                output.len() <= 1024,
1989                "{label} Debug output is not bounded: {} bytes",
1990                output.len()
1991            );
1992        }
1993    }
1994
1995    #[test]
1996    fn merge_relocates_single_worker_to_top_level() {
1997        // Regression: a one-worker fleet must populate `clusters` and
1998        // `proxying` so CLI/TUI consumers reading those maps see the
1999        // worker's data. `std::mem::take(&mut self.workers)` empties the
2000        // per-worker map after relocation, which is the documented
2001        // contract when the caller asked for the merged shape.
2002        let mut worker = WorkerMetrics {
2003            proxy: BTreeMap::new(),
2004            clusters: BTreeMap::new(),
2005        };
2006        worker.proxy.insert(
2007            "requests".to_owned(),
2008            FilteredMetrics {
2009                inner: Some(Inner::Count(42)),
2010            },
2011        );
2012        let mut cluster = ClusterMetrics {
2013            cluster: BTreeMap::new(),
2014            backends: Vec::new(),
2015        };
2016        cluster.cluster.insert(
2017            "requests".to_owned(),
2018            FilteredMetrics {
2019                inner: Some(Inner::Count(7)),
2020            },
2021        );
2022        worker.clusters.insert("cluster-a".to_owned(), cluster);
2023
2024        let mut agg = AggregatedMetrics {
2025            main: BTreeMap::new(),
2026            workers: BTreeMap::from([("0".to_owned(), worker)]),
2027            clusters: BTreeMap::new(),
2028            proxying: BTreeMap::new(),
2029        };
2030
2031        agg.merge_metrics();
2032
2033        assert!(
2034            agg.workers.is_empty(),
2035            "merge takes ownership of the per-worker map"
2036        );
2037        assert_eq!(
2038            agg.proxying.get("requests"),
2039            Some(&FilteredMetrics {
2040                inner: Some(Inner::Count(42)),
2041            }),
2042            "single worker's proxy counter must surface in proxying"
2043        );
2044        let cluster_a = agg
2045            .clusters
2046            .get("cluster-a")
2047            .expect("cluster row must surface in top-level clusters");
2048        assert_eq!(
2049            cluster_a.cluster.get("requests"),
2050            Some(&FilteredMetrics {
2051                inner: Some(Inner::Count(7)),
2052            })
2053        );
2054    }
2055
2056    #[test]
2057    fn merge_counts_and_gauges() {
2058        let mut gauge_a = FilteredMetrics {
2059            inner: Some(Inner::Gauge(4)),
2060        };
2061        let gauge_b = FilteredMetrics {
2062            inner: Some(Inner::Gauge(4)),
2063        };
2064
2065        gauge_a.merge(&gauge_b);
2066
2067        assert_eq!(
2068            gauge_a,
2069            FilteredMetrics {
2070                inner: Some(Inner::Gauge(8)),
2071            }
2072        );
2073
2074        let mut count_a = FilteredMetrics {
2075            inner: Some(Inner::Count(3)),
2076        };
2077        let count_b = FilteredMetrics {
2078            inner: Some(Inner::Count(3)),
2079        };
2080
2081        count_a.merge(&count_b);
2082
2083        assert_eq!(
2084            count_a,
2085            FilteredMetrics {
2086                inner: Some(Inner::Count(6)),
2087            }
2088        );
2089    }
2090
2091    #[test]
2092    fn merge_percentiles_takes_max_per_quantile() {
2093        // Multi-worker percentile aggregation propagates the worst-case
2094        // quantile across workers and accumulates samples + sum so the
2095        // surfaced summary remains the "is anyone slow?" upper bound.
2096        let mut left = FilteredMetrics {
2097            inner: Some(Inner::Percentiles(Percentiles {
2098                samples: 100,
2099                p_50: 5,
2100                p_90: 20,
2101                p_99: 100,
2102                p_99_9: 200,
2103                p_99_99: 250,
2104                p_99_999: 300,
2105                p_100: 400,
2106                sum: 12_000,
2107            })),
2108        };
2109        let right = FilteredMetrics {
2110            inner: Some(Inner::Percentiles(Percentiles {
2111                samples: 50,
2112                p_50: 7,
2113                p_90: 15,
2114                p_99: 80,
2115                p_99_9: 240,
2116                p_99_99: 245,
2117                p_99_999: 290,
2118                p_100: 380,
2119                sum: 6_000,
2120            })),
2121        };
2122        left.merge(&right);
2123        assert_eq!(
2124            left,
2125            FilteredMetrics {
2126                inner: Some(Inner::Percentiles(Percentiles {
2127                    samples: 150,
2128                    p_50: 7,
2129                    p_90: 20,
2130                    p_99: 100,
2131                    p_99_9: 240,
2132                    p_99_99: 250,
2133                    p_99_999: 300,
2134                    p_100: 400,
2135                    sum: 18_000,
2136                })),
2137            }
2138        );
2139    }
2140
2141    #[test]
2142    fn merge_histograms() {
2143        let mut histogram_a = FilteredMetrics {
2144            inner: Some(Inner::Histogram(FilteredHistogram {
2145                sum: 95,
2146                count: 30,
2147                buckets: vec![
2148                    Bucket { le: 0, count: 1 },
2149                    Bucket { le: 1, count: 2 },
2150                    Bucket { le: 3, count: 10 },
2151                    Bucket { le: 7, count: 25 },
2152                    Bucket { le: 15, count: 27 },
2153                    Bucket { le: 31, count: 30 },
2154                ],
2155            })),
2156        };
2157
2158        let histogram_b = FilteredMetrics {
2159            inner: Some(Inner::Histogram(FilteredHistogram {
2160                sum: 82,
2161                count: 40,
2162                buckets: vec![
2163                    Bucket { le: 0, count: 0 },
2164                    Bucket { le: 1, count: 0 },
2165                    Bucket { le: 3, count: 12 },
2166                    Bucket { le: 7, count: 30 },
2167                    Bucket { le: 15, count: 40 },
2168                    // note: there is no bucket for "le: 31"
2169                ],
2170            })),
2171        };
2172
2173        histogram_a.merge(&histogram_b);
2174
2175        let merged_histogram = FilteredMetrics {
2176            inner: Some(Inner::Histogram(FilteredHistogram {
2177                sum: 177,
2178                count: 70,
2179                buckets: vec![
2180                    Bucket { le: 0, count: 1 },
2181                    Bucket { le: 1, count: 2 },
2182                    Bucket { le: 3, count: 22 },
2183                    Bucket { le: 7, count: 55 },
2184                    Bucket { le: 15, count: 67 },
2185                    Bucket { le: 31, count: 70 }, // note: the total count of histogram b is added, even though histogram b has no bucket
2186                ],
2187            })),
2188        };
2189
2190        assert_eq!(histogram_a, merged_histogram);
2191    }
2192
2193    const HEALTH_CHECK_SECRET: &str = "SYNTHETIC-DEBUG-HC-SECRET-a91d";
2194    const HEALTH_CHECK_FINGERPRINT: &str = "sozu-hc-hmac-sha256:v1:00112233445566778899aabbccddeeff:0000000000000000000000000000000000000000000000000000000000000000";
2195
2196    fn fingerprinted_health_check() -> super::command::HealthCheckConfig {
2197        use super::command::{HealthCheckConfig, HealthCheckHeader};
2198        HealthCheckConfig {
2199            uri: "/".to_owned(),
2200            interval: 10,
2201            timeout: 5,
2202            healthy_threshold: 3,
2203            unhealthy_threshold: 3,
2204            headers: vec![HealthCheckHeader {
2205                name: "Authorization".to_owned(),
2206                fingerprint: HEALTH_CHECK_FINGERPRINT.to_owned(),
2207            }],
2208            ..Default::default()
2209        }
2210    }
2211
2212    /// `Debug` of a header prints a bounded name and its fingerprint, and the
2213    /// ingress carrier prints a placeholder instead of the bytes or their
2214    /// length, alone or inside a request.
2215    #[test]
2216    fn health_check_header_debug_shows_metadata_and_never_the_ingress_value() {
2217        use super::command::{
2218            HealthCheckHeader, HealthCheckHeaderValue, SetHealthCheck, SetProtectedHealthCheck,
2219        };
2220
2221        let long_name = format!("X-{}", "n".repeat(200));
2222        let alone = format!(
2223            "{:?}",
2224            HealthCheckHeader {
2225                name: long_name.clone(),
2226                fingerprint: HEALTH_CHECK_FINGERPRINT.to_owned(),
2227            }
2228        );
2229        assert!(!alone.contains(&long_name), "the name is bounded: {alone}");
2230        assert!(alone.contains("name_truncated: true"), "{alone}");
2231        assert!(alone.contains(HEALTH_CHECK_FINGERPRINT), "{alone}");
2232        // A multi-byte name is cut on a char boundary, never mid-character.
2233        let wide = format!(
2234            "{:?}",
2235            HealthCheckHeader {
2236                name: "\u{e9}".repeat(100),
2237                fingerprint: String::new(),
2238            }
2239        );
2240        assert!(wide.contains("name_truncated: true"), "{wide}");
2241
2242        let config = fingerprinted_health_check();
2243        let carrier = HealthCheckHeaderValue {
2244            name: "Authorization".to_owned(),
2245            value: HEALTH_CHECK_SECRET.as_bytes().to_vec(),
2246        };
2247        let protected = Request::from(RequestType::SetProtectedHealthCheck(
2248            SetProtectedHealthCheck {
2249                cluster_id: "app".to_owned(),
2250                config: config.to_owned(),
2251                values: vec![carrier.to_owned()],
2252            },
2253        ));
2254        let worker_request = WorkerRequest::new("id".to_owned(), protected.to_owned());
2255        let legacy = Request::from(RequestType::SetHealthCheck(SetHealthCheck {
2256            cluster_id: "app".to_owned(),
2257            config,
2258        }));
2259        let numeric = format!("{:?}", HEALTH_CHECK_SECRET.as_bytes());
2260        for (what, rendered) in [
2261            ("carrier", format!("{carrier:?}")),
2262            ("carrier pretty", format!("{carrier:#?}")),
2263            ("protected request", format!("{protected:?}")),
2264            ("protected request pretty", format!("{protected:#?}")),
2265            ("worker request", format!("{worker_request:?}")),
2266            ("legacy request", format!("{legacy:?}")),
2267        ] {
2268            assert!(
2269                !rendered.contains(HEALTH_CHECK_SECRET) && !rendered.contains(&numeric),
2270                "{what} Debug leaked a header value: {rendered}"
2271            );
2272            if what.starts_with("carrier") {
2273                assert!(rendered.contains("<protected>"), "{what}: {rendered}");
2274            }
2275        }
2276    }
2277}