Skip to main content

sozu_command_lib/
response.rs

1use std::{cmp::Ordering, collections::BTreeMap, fmt, net::SocketAddr};
2
3use crate::{
4    proto::command::{
5        AddBackend, FilteredTimeSerie, Header, HstsConfig, LoadBalancingParams, PathRule,
6        PathRuleKind, RequestHttpFrontend, RequestTcpFrontend, RequestUdpFrontend, Response,
7        ResponseContent, ResponseStatus, RulePosition, RunState, WorkerResponse,
8    },
9    state::ClusterId,
10};
11
12impl Response {
13    pub fn new(
14        status: ResponseStatus,
15        message: String,
16        content: Option<ResponseContent>,
17    ) -> Response {
18        Response {
19            status: status as i32,
20            message,
21            content,
22        }
23    }
24}
25
26/// An HTTP or HTTPS frontend, as used *within* Sōzu
27#[derive(Clone, PartialOrd, Ord, PartialEq, Eq, Hash, Serialize, Deserialize)]
28pub struct HttpFrontend {
29    /// Send a 401, DENY, if cluster_id is None
30    pub cluster_id: Option<ClusterId>,
31    pub address: SocketAddr,
32    pub hostname: String,
33    #[serde(default)]
34    #[serde(skip_serializing_if = "is_default_path_rule")]
35    pub path: PathRule,
36    #[serde(default)]
37    #[serde(skip_serializing_if = "Option::is_none")]
38    pub method: Option<String>,
39    #[serde(default)]
40    pub position: RulePosition,
41    pub tags: Option<BTreeMap<String, String>>,
42    /// Resolved frontend-level policy carried over from
43    /// [`RequestHttpFrontend`]. The router consults these to build a
44    /// [`Route::Frontend(Rc<Frontend>)`] when any are non-default,
45    /// otherwise falls back to the legacy `Route::ClusterId` /
46    /// `Route::Deny` shapes.
47    #[serde(default)]
48    #[serde(skip_serializing_if = "Option::is_none")]
49    pub redirect: Option<i32>,
50    #[serde(default)]
51    #[serde(skip_serializing_if = "Option::is_none")]
52    pub redirect_scheme: Option<i32>,
53    #[serde(default)]
54    #[serde(skip_serializing_if = "Option::is_none")]
55    pub redirect_template: Option<String>,
56    #[serde(default)]
57    #[serde(skip_serializing_if = "Option::is_none")]
58    pub rewrite_host: Option<String>,
59    #[serde(default)]
60    #[serde(skip_serializing_if = "Option::is_none")]
61    pub rewrite_path: Option<String>,
62    #[serde(default)]
63    #[serde(skip_serializing_if = "Option::is_none")]
64    pub rewrite_port: Option<u32>,
65    #[serde(default)]
66    #[serde(skip_serializing_if = "Option::is_none")]
67    pub required_auth: Option<bool>,
68    #[serde(default)]
69    #[serde(skip_serializing_if = "Vec::is_empty")]
70    pub headers: Vec<Header>,
71    /// Resolved per-frontend HSTS (RFC 6797) policy. `None` means inherit
72    /// the listener default at frontend-add time in the worker.
73    #[serde(default)]
74    #[serde(skip_serializing_if = "Option::is_none")]
75    pub hsts: Option<HstsConfig>,
76}
77
78impl fmt::Debug for HttpFrontend {
79    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
80        let tags_count = self.tags.as_ref().map(BTreeMap::len);
81        let tags_len = self.tags.as_ref().map(|tags| {
82            tags.iter().fold(0usize, |total, (key, value)| {
83                total.saturating_add(key.len()).saturating_add(value.len())
84            })
85        });
86        let headers_len = self.headers.iter().fold(0usize, |total, header| {
87            total
88                .saturating_add(header.key.len())
89                .saturating_add(header.val.len())
90        });
91
92        f.debug_struct("HttpFrontend")
93            .field("cluster_id_len", &self.cluster_id.as_ref().map(String::len))
94            .field("address", &self.address)
95            .field("hostname_len", &self.hostname.len())
96            .field("path_kind", &self.path.kind)
97            .field("path_len", &self.path.value.len())
98            .field("method_len", &self.method.as_ref().map(String::len))
99            .field("position", &self.position)
100            .field("tags_count", &tags_count)
101            .field("tags_len", &tags_len)
102            .field("redirect", &self.redirect)
103            .field("required_auth", &self.required_auth)
104            .field("redirect_scheme", &self.redirect_scheme)
105            .field(
106                "redirect_template_len",
107                &self.redirect_template.as_ref().map(String::len),
108            )
109            .field(
110                "rewrite_host_len",
111                &self.rewrite_host.as_ref().map(String::len),
112            )
113            .field(
114                "rewrite_path_len",
115                &self.rewrite_path.as_ref().map(String::len),
116            )
117            .field("rewrite_port", &self.rewrite_port)
118            .field("headers_count", &self.headers.len())
119            .field("headers_len", &headers_len)
120            .field("hsts", &self.hsts)
121            .finish_non_exhaustive()
122    }
123}
124
125impl From<HttpFrontend> for RequestHttpFrontend {
126    fn from(val: HttpFrontend) -> Self {
127        let source_address = val.address;
128        let source_hostname = val.hostname.clone();
129        let request_frontend = RequestHttpFrontend {
130            cluster_id: val.cluster_id,
131            address: val.address.into(),
132            hostname: val.hostname,
133            path: val.path,
134            method: val.method,
135            position: val.position.into(),
136            tags: val.tags.unwrap_or_default(),
137            redirect: val.redirect,
138            redirect_scheme: val.redirect_scheme,
139            redirect_template: val.redirect_template,
140            rewrite_host: val.rewrite_host,
141            rewrite_path: val.rewrite_path,
142            rewrite_port: val.rewrite_port,
143            required_auth: val.required_auth,
144            headers: val.headers,
145            hsts: val.hsts,
146        };
147
148        // POST: the proto-encoded address decodes back to the source SocketAddr
149        // and the hostname is unchanged — the in-Sōzu → wire conversion must be
150        // routing-preserving (the SocketAddress ⇔ SocketAddr round-trip is
151        // exercised in request.rs; here we tie the frontend identity to it).
152        debug_assert_eq!(
153            SocketAddr::from(request_frontend.address),
154            source_address,
155            "frontend address must round-trip through the proto encoding"
156        );
157        debug_assert_eq!(
158            request_frontend.hostname, source_hostname,
159            "frontend hostname must survive the proto conversion"
160        );
161        request_frontend
162    }
163}
164
165impl From<Backend> for AddBackend {
166    fn from(val: Backend) -> Self {
167        let source_address = val.address;
168        let source_cluster_id = val.cluster_id.clone();
169        let source_backend_id = val.backend_id.clone();
170        let add_backend = AddBackend {
171            cluster_id: val.cluster_id,
172            backend_id: val.backend_id,
173            address: val.address.into(),
174            sticky_id: val.sticky_id,
175            load_balancing_parameters: val.load_balancing_parameters,
176            backup: val.backup,
177        };
178
179        // POST: backend identity (cluster + backend id) and the wire address
180        // are preserved — a backend that changed cluster/id/address here would
181        // be registered under the wrong key and never receive (or steal)
182        // traffic.
183        debug_assert_eq!(
184            add_backend.cluster_id, source_cluster_id,
185            "backend cluster_id must survive the proto conversion"
186        );
187        debug_assert_eq!(
188            add_backend.backend_id, source_backend_id,
189            "backend_id must survive the proto conversion"
190        );
191        debug_assert_eq!(
192            SocketAddr::from(add_backend.address),
193            source_address,
194            "backend address must round-trip through the proto encoding"
195        );
196        add_backend
197    }
198}
199
200impl PathRule {
201    pub fn prefix<S>(value: S) -> Self
202    where
203        S: ToString,
204    {
205        let rule = Self {
206            kind: PathRuleKind::Prefix.into(),
207            value: value.to_string(),
208        };
209        // POST: the encoded kind decodes back to the Prefix variant — the proto
210        // i32 must round-trip or the router would misclassify the match type.
211        debug_assert_eq!(
212            PathRuleKind::try_from(rule.kind),
213            Ok(PathRuleKind::Prefix),
214            "prefix() must encode a Prefix-kind rule"
215        );
216        rule
217    }
218
219    pub fn regex<S>(value: S) -> Self
220    where
221        S: ToString,
222    {
223        let rule = Self {
224            kind: PathRuleKind::Regex.into(),
225            value: value.to_string(),
226        };
227        debug_assert_eq!(
228            PathRuleKind::try_from(rule.kind),
229            Ok(PathRuleKind::Regex),
230            "regex() must encode a Regex-kind rule"
231        );
232        rule
233    }
234
235    pub fn equals<S>(value: S) -> Self
236    where
237        S: ToString,
238    {
239        let rule = Self {
240            kind: PathRuleKind::Equals.into(),
241            value: value.to_string(),
242        };
243        debug_assert_eq!(
244            PathRuleKind::try_from(rule.kind),
245            Ok(PathRuleKind::Equals),
246            "equals() must encode an Equals-kind rule"
247        );
248        rule
249    }
250
251    pub fn from_cli_options(
252        path_prefix: Option<String>,
253        path_regex: Option<String>,
254        path_equals: Option<String>,
255    ) -> Self {
256        // PRE: prefix takes precedence over regex, which takes precedence over
257        // equals. Capture which arms are populated so the post-condition can
258        // assert the precedence actually fired.
259        let had_prefix = path_prefix.is_some();
260        let had_regex = path_regex.is_some();
261        let rule = match (path_prefix, path_regex, path_equals) {
262            (Some(prefix), _, _) => PathRule {
263                kind: PathRuleKind::Prefix as i32,
264                value: prefix,
265            },
266            (None, Some(regex), _) => PathRule {
267                kind: PathRuleKind::Regex as i32,
268                value: regex,
269            },
270            (None, None, Some(equals)) => PathRule {
271                kind: PathRuleKind::Equals as i32,
272                value: equals,
273            },
274            _ => PathRule::default(),
275        };
276
277        // POST: a present prefix wins outright; absent a prefix, a present
278        // regex wins. The resolved kind must reflect that precedence so two
279        // simultaneously-set CLI flags can never silently pick the wrong rule.
280        debug_assert!(
281            !had_prefix || rule.kind == PathRuleKind::Prefix as i32,
282            "a path prefix must produce a Prefix rule regardless of other flags"
283        );
284        debug_assert!(
285            had_prefix || !had_regex || rule.kind == PathRuleKind::Regex as i32,
286            "absent a prefix, a regex must produce a Regex rule"
287        );
288        rule
289    }
290}
291
292pub fn is_default_path_rule(p: &PathRule) -> bool {
293    PathRuleKind::try_from(p.kind) == Ok(PathRuleKind::Prefix) && p.value.is_empty()
294}
295
296impl fmt::Display for PathRule {
297    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
298        match PathRuleKind::try_from(self.kind) {
299            Ok(PathRuleKind::Prefix) => write!(f, "prefix '{}'", self.value),
300            Ok(PathRuleKind::Regex) => write!(f, "regexp '{}'", self.value),
301            Ok(PathRuleKind::Equals) => write!(f, "equals '{}'", self.value),
302            Err(_) => write!(f, ""),
303        }
304    }
305}
306
307/// A TCP frontend, as used *within* Sōzu
308#[derive(Debug, Clone, PartialOrd, Ord, PartialEq, Eq, Hash, Serialize, Deserialize)]
309pub struct TcpFrontend {
310    pub cluster_id: String,
311    pub address: SocketAddr,
312    /// custom tags to identify the frontend in the access logs
313    pub tags: BTreeMap<String, String>,
314    /// SNI hostname to match against the TLS ClientHello (exact hostname or a
315    /// single leading `*.` wildcard label). `None` matches regardless of SNI.
316    #[serde(default)]
317    pub sni: Option<String>,
318    /// ALPN protocol names this frontend matches; empty is the catch-all for
319    /// its `sni` on this listener.
320    #[serde(default)]
321    pub alpn: Vec<String>,
322}
323
324impl From<TcpFrontend> for RequestTcpFrontend {
325    fn from(val: TcpFrontend) -> Self {
326        let source_address = val.address;
327        let source_cluster_id = val.cluster_id.clone();
328        let source_sni = val.sni.clone();
329        let source_alpn = val.alpn.clone();
330        let request_frontend = RequestTcpFrontend {
331            cluster_id: val.cluster_id,
332            address: val.address.into(),
333            tags: val.tags,
334            sni: val.sni,
335            alpn: val.alpn,
336        };
337
338        // POST: cluster identity and the wire address are preserved across the
339        // proto conversion (same routing guarantee as the HTTP frontend path).
340        debug_assert_eq!(
341            request_frontend.cluster_id, source_cluster_id,
342            "TCP frontend cluster_id must survive the proto conversion"
343        );
344        debug_assert_eq!(
345            SocketAddr::from(request_frontend.address),
346            source_address,
347            "TCP frontend address must round-trip through the proto encoding"
348        );
349        // POST: SNI/ALPN routing identity is preserved across the proto
350        // conversion — losing either here would silently misroute or
351        // over-match traffic on this frontend's listener.
352        debug_assert_eq!(
353            request_frontend.sni, source_sni,
354            "TCP frontend sni must survive the proto conversion"
355        );
356        debug_assert_eq!(
357            request_frontend.alpn, source_alpn,
358            "TCP frontend alpn must survive the proto conversion"
359        );
360        request_frontend
361    }
362}
363
364/// A UDP frontend, as used *within* Sōzu
365#[derive(Debug, Clone, PartialOrd, Ord, PartialEq, Eq, Hash, Serialize, Deserialize)]
366pub struct UdpFrontend {
367    pub cluster_id: String,
368    pub address: SocketAddr,
369    /// custom tags to identify the frontend in the access logs
370    pub tags: BTreeMap<String, String>,
371}
372
373impl From<UdpFrontend> for RequestUdpFrontend {
374    fn from(val: UdpFrontend) -> Self {
375        RequestUdpFrontend {
376            cluster_id: val.cluster_id,
377            address: val.address.into(),
378            tags: val.tags,
379        }
380    }
381}
382
383/// A backend, as used *within* Sōzu
384#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
385pub struct Backend {
386    pub cluster_id: String,
387    pub backend_id: String,
388    pub address: SocketAddr,
389    #[serde(default)]
390    #[serde(skip_serializing_if = "Option::is_none")]
391    pub sticky_id: Option<String>,
392    #[serde(default)]
393    #[serde(skip_serializing_if = "Option::is_none")]
394    pub load_balancing_parameters: Option<LoadBalancingParams>,
395    #[serde(default)]
396    #[serde(skip_serializing_if = "Option::is_none")]
397    pub backup: Option<bool>,
398}
399
400impl Ord for Backend {
401    fn cmp(&self, o: &Backend) -> Ordering {
402        // INV: Equal can only be returned when every keyed field compares
403        // Equal — the tuple of field orderings is the source of truth, and the
404        // `.then(...)` fold must not collapse two distinct backends to Equal
405        // (which would let one silently evict the other from a BTree key set).
406        // Computed inline (no recursion into `cmp`) so it stays cheap.
407        let fields_all_equal = self.cluster_id == o.cluster_id
408            && self.backend_id == o.backend_id
409            && self.sticky_id == o.sticky_id
410            && self.load_balancing_parameters == o.load_balancing_parameters
411            && self.backup == o.backup
412            && self.address == o.address;
413
414        let ordering = self
415            .cluster_id
416            .cmp(&o.cluster_id)
417            .then(self.backend_id.cmp(&o.backend_id))
418            .then(self.sticky_id.cmp(&o.sticky_id))
419            .then(
420                self.load_balancing_parameters
421                    .cmp(&o.load_balancing_parameters),
422            )
423            .then(self.backup.cmp(&o.backup))
424            .then(socketaddr_cmp(&self.address, &o.address));
425
426        debug_assert_eq!(
427            ordering == Ordering::Equal,
428            fields_all_equal,
429            "Backend::cmp returns Equal iff every keyed field is equal"
430        );
431        ordering
432    }
433}
434
435impl PartialOrd for Backend {
436    fn partial_cmp(&self, other: &Backend) -> Option<Ordering> {
437        Some(self.cmp(other))
438    }
439}
440
441impl Backend {
442    pub fn to_add_backend(self) -> AddBackend {
443        let source_address = self.address;
444        let source_backend_id = self.backend_id.clone();
445        let add_backend = AddBackend {
446            cluster_id: self.cluster_id,
447            address: self.address.into(),
448            sticky_id: self.sticky_id,
449            backend_id: self.backend_id,
450            load_balancing_parameters: self.load_balancing_parameters,
451            backup: self.backup,
452        };
453
454        // POST: identity (backend id) and wire address are preserved — same
455        // routing guarantee as the `From<Backend>` path, kept in lockstep.
456        debug_assert_eq!(
457            add_backend.backend_id, source_backend_id,
458            "backend_id must survive to_add_backend"
459        );
460        debug_assert_eq!(
461            SocketAddr::from(add_backend.address),
462            source_address,
463            "backend address must round-trip through to_add_backend"
464        );
465        add_backend
466    }
467}
468
469impl fmt::Display for RunState {
470    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
471        write!(f, "{self:?}")
472    }
473}
474
475pub type MessageId = String;
476
477impl WorkerResponse {
478    pub fn ok<T>(id: T) -> Self
479    where
480        T: ToString,
481    {
482        Self {
483            id: id.to_string(),
484            message: String::new(),
485            status: ResponseStatus::Ok.into(),
486            content: None,
487        }
488    }
489
490    pub fn ok_with_content<T>(id: T, content: ResponseContent) -> Self
491    where
492        T: ToString,
493    {
494        Self {
495            id: id.to_string(),
496            status: ResponseStatus::Ok.into(),
497            message: String::new(),
498            content: Some(content),
499        }
500    }
501
502    pub fn error<T, U>(id: T, error: U) -> Self
503    where
504        T: ToString,
505        U: ToString,
506    {
507        Self {
508            id: id.to_string(),
509            message: error.to_string(),
510            status: ResponseStatus::Failure.into(),
511            content: None,
512        }
513    }
514
515    pub fn processing<T>(id: T) -> Self
516    where
517        T: ToString,
518    {
519        Self {
520            id: id.to_string(),
521            message: String::new(),
522            status: ResponseStatus::Processing.into(),
523            content: None,
524        }
525    }
526
527    pub fn with_status<T>(id: T, status: ResponseStatus) -> Self
528    where
529        T: ToString,
530    {
531        Self {
532            id: id.to_string(),
533            message: String::new(),
534            status: status.into(),
535            content: None,
536        }
537    }
538
539    pub fn is_failure(&self) -> bool {
540        self.status == ResponseStatus::Failure as i32
541    }
542}
543
544impl fmt::Display for WorkerResponse {
545    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
546        write!(f, "{}-{:?}", self.id, self.status)
547    }
548}
549
550impl fmt::Display for FilteredTimeSerie {
551    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
552        write!(
553            f,
554            "FilteredTimeSerie {{\nlast_second: {},\nlast_minute:\n{:?}\n{:?}\n{:?}\n{:?}\n{:?}\n{:?}\nlast_hour:\n{:?}\n{:?}\n{:?}\n{:?}\n{:?}\n{:?}\n}}",
555            self.last_second,
556            &self.last_minute[0..10],
557            &self.last_minute[10..20],
558            &self.last_minute[20..30],
559            &self.last_minute[30..40],
560            &self.last_minute[40..50],
561            &self.last_minute[50..60],
562            &self.last_hour[0..10],
563            &self.last_hour[10..20],
564            &self.last_hour[20..30],
565            &self.last_hour[30..40],
566            &self.last_hour[40..50],
567            &self.last_hour[50..60]
568        )
569    }
570}
571
572fn socketaddr_cmp(a: &SocketAddr, b: &SocketAddr) -> Ordering {
573    let ordering = a.ip().cmp(&b.ip()).then(a.port().cmp(&b.port()));
574    // INV: two socket addresses compare Equal iff both IP and port match —
575    // the `.then` fold must not declare distinct (ip, port) pairs equal, which
576    // would make two different backends indistinguishable to the BTree key.
577    debug_assert_eq!(
578        ordering == Ordering::Equal,
579        a.ip() == b.ip() && a.port() == b.port(),
580        "socketaddr_cmp is Equal iff ip and port both match"
581    );
582    ordering
583}