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