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(Debug, 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 From<HttpFrontend> for RequestHttpFrontend {
79    fn from(val: HttpFrontend) -> Self {
80        let source_address = val.address;
81        let source_hostname = val.hostname.clone();
82        let request_frontend = RequestHttpFrontend {
83            cluster_id: val.cluster_id,
84            address: val.address.into(),
85            hostname: val.hostname,
86            path: val.path,
87            method: val.method,
88            position: val.position.into(),
89            tags: val.tags.unwrap_or_default(),
90            redirect: val.redirect,
91            redirect_scheme: val.redirect_scheme,
92            redirect_template: val.redirect_template,
93            rewrite_host: val.rewrite_host,
94            rewrite_path: val.rewrite_path,
95            rewrite_port: val.rewrite_port,
96            required_auth: val.required_auth,
97            headers: val.headers,
98            hsts: val.hsts,
99        };
100
101        // POST: the proto-encoded address decodes back to the source SocketAddr
102        // and the hostname is unchanged — the in-Sōzu → wire conversion must be
103        // routing-preserving (the SocketAddress ⇔ SocketAddr round-trip is
104        // exercised in request.rs; here we tie the frontend identity to it).
105        debug_assert_eq!(
106            SocketAddr::from(request_frontend.address),
107            source_address,
108            "frontend address must round-trip through the proto encoding"
109        );
110        debug_assert_eq!(
111            request_frontend.hostname, source_hostname,
112            "frontend hostname must survive the proto conversion"
113        );
114        request_frontend
115    }
116}
117
118impl From<Backend> for AddBackend {
119    fn from(val: Backend) -> Self {
120        let source_address = val.address;
121        let source_cluster_id = val.cluster_id.clone();
122        let source_backend_id = val.backend_id.clone();
123        let add_backend = AddBackend {
124            cluster_id: val.cluster_id,
125            backend_id: val.backend_id,
126            address: val.address.into(),
127            sticky_id: val.sticky_id,
128            load_balancing_parameters: val.load_balancing_parameters,
129            backup: val.backup,
130        };
131
132        // POST: backend identity (cluster + backend id) and the wire address
133        // are preserved — a backend that changed cluster/id/address here would
134        // be registered under the wrong key and never receive (or steal)
135        // traffic.
136        debug_assert_eq!(
137            add_backend.cluster_id, source_cluster_id,
138            "backend cluster_id must survive the proto conversion"
139        );
140        debug_assert_eq!(
141            add_backend.backend_id, source_backend_id,
142            "backend_id must survive the proto conversion"
143        );
144        debug_assert_eq!(
145            SocketAddr::from(add_backend.address),
146            source_address,
147            "backend address must round-trip through the proto encoding"
148        );
149        add_backend
150    }
151}
152
153impl PathRule {
154    pub fn prefix<S>(value: S) -> Self
155    where
156        S: ToString,
157    {
158        let rule = Self {
159            kind: PathRuleKind::Prefix.into(),
160            value: value.to_string(),
161        };
162        // POST: the encoded kind decodes back to the Prefix variant — the proto
163        // i32 must round-trip or the router would misclassify the match type.
164        debug_assert_eq!(
165            PathRuleKind::try_from(rule.kind),
166            Ok(PathRuleKind::Prefix),
167            "prefix() must encode a Prefix-kind rule"
168        );
169        rule
170    }
171
172    pub fn regex<S>(value: S) -> Self
173    where
174        S: ToString,
175    {
176        let rule = Self {
177            kind: PathRuleKind::Regex.into(),
178            value: value.to_string(),
179        };
180        debug_assert_eq!(
181            PathRuleKind::try_from(rule.kind),
182            Ok(PathRuleKind::Regex),
183            "regex() must encode a Regex-kind rule"
184        );
185        rule
186    }
187
188    pub fn equals<S>(value: S) -> Self
189    where
190        S: ToString,
191    {
192        let rule = Self {
193            kind: PathRuleKind::Equals.into(),
194            value: value.to_string(),
195        };
196        debug_assert_eq!(
197            PathRuleKind::try_from(rule.kind),
198            Ok(PathRuleKind::Equals),
199            "equals() must encode an Equals-kind rule"
200        );
201        rule
202    }
203
204    pub fn from_cli_options(
205        path_prefix: Option<String>,
206        path_regex: Option<String>,
207        path_equals: Option<String>,
208    ) -> Self {
209        // PRE: prefix takes precedence over regex, which takes precedence over
210        // equals. Capture which arms are populated so the post-condition can
211        // assert the precedence actually fired.
212        let had_prefix = path_prefix.is_some();
213        let had_regex = path_regex.is_some();
214        let rule = match (path_prefix, path_regex, path_equals) {
215            (Some(prefix), _, _) => PathRule {
216                kind: PathRuleKind::Prefix as i32,
217                value: prefix,
218            },
219            (None, Some(regex), _) => PathRule {
220                kind: PathRuleKind::Regex as i32,
221                value: regex,
222            },
223            (None, None, Some(equals)) => PathRule {
224                kind: PathRuleKind::Equals as i32,
225                value: equals,
226            },
227            _ => PathRule::default(),
228        };
229
230        // POST: a present prefix wins outright; absent a prefix, a present
231        // regex wins. The resolved kind must reflect that precedence so two
232        // simultaneously-set CLI flags can never silently pick the wrong rule.
233        debug_assert!(
234            !had_prefix || rule.kind == PathRuleKind::Prefix as i32,
235            "a path prefix must produce a Prefix rule regardless of other flags"
236        );
237        debug_assert!(
238            had_prefix || !had_regex || rule.kind == PathRuleKind::Regex as i32,
239            "absent a prefix, a regex must produce a Regex rule"
240        );
241        rule
242    }
243}
244
245pub fn is_default_path_rule(p: &PathRule) -> bool {
246    PathRuleKind::try_from(p.kind) == Ok(PathRuleKind::Prefix) && p.value.is_empty()
247}
248
249impl fmt::Display for PathRule {
250    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
251        match PathRuleKind::try_from(self.kind) {
252            Ok(PathRuleKind::Prefix) => write!(f, "prefix '{}'", self.value),
253            Ok(PathRuleKind::Regex) => write!(f, "regexp '{}'", self.value),
254            Ok(PathRuleKind::Equals) => write!(f, "equals '{}'", self.value),
255            Err(_) => write!(f, ""),
256        }
257    }
258}
259
260/// A TCP frontend, as used *within* Sōzu
261#[derive(Debug, Clone, PartialOrd, Ord, PartialEq, Eq, Hash, Serialize, Deserialize)]
262pub struct TcpFrontend {
263    pub cluster_id: String,
264    pub address: SocketAddr,
265    /// custom tags to identify the frontend in the access logs
266    pub tags: BTreeMap<String, String>,
267}
268
269impl From<TcpFrontend> for RequestTcpFrontend {
270    fn from(val: TcpFrontend) -> Self {
271        let source_address = val.address;
272        let source_cluster_id = val.cluster_id.clone();
273        let request_frontend = RequestTcpFrontend {
274            cluster_id: val.cluster_id,
275            address: val.address.into(),
276            tags: val.tags,
277        };
278
279        // POST: cluster identity and the wire address are preserved across the
280        // proto conversion (same routing guarantee as the HTTP frontend path).
281        debug_assert_eq!(
282            request_frontend.cluster_id, source_cluster_id,
283            "TCP frontend cluster_id must survive the proto conversion"
284        );
285        debug_assert_eq!(
286            SocketAddr::from(request_frontend.address),
287            source_address,
288            "TCP frontend address must round-trip through the proto encoding"
289        );
290        request_frontend
291    }
292}
293
294/// A UDP frontend, as used *within* Sōzu
295#[derive(Debug, Clone, PartialOrd, Ord, PartialEq, Eq, Hash, Serialize, Deserialize)]
296pub struct UdpFrontend {
297    pub cluster_id: String,
298    pub address: SocketAddr,
299    /// custom tags to identify the frontend in the access logs
300    pub tags: BTreeMap<String, String>,
301}
302
303impl From<UdpFrontend> for RequestUdpFrontend {
304    fn from(val: UdpFrontend) -> Self {
305        RequestUdpFrontend {
306            cluster_id: val.cluster_id,
307            address: val.address.into(),
308            tags: val.tags,
309        }
310    }
311}
312
313/// A backend, as used *within* Sōzu
314#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
315pub struct Backend {
316    pub cluster_id: String,
317    pub backend_id: String,
318    pub address: SocketAddr,
319    #[serde(default)]
320    #[serde(skip_serializing_if = "Option::is_none")]
321    pub sticky_id: Option<String>,
322    #[serde(default)]
323    #[serde(skip_serializing_if = "Option::is_none")]
324    pub load_balancing_parameters: Option<LoadBalancingParams>,
325    #[serde(default)]
326    #[serde(skip_serializing_if = "Option::is_none")]
327    pub backup: Option<bool>,
328}
329
330impl Ord for Backend {
331    fn cmp(&self, o: &Backend) -> Ordering {
332        // INV: Equal can only be returned when every keyed field compares
333        // Equal — the tuple of field orderings is the source of truth, and the
334        // `.then(...)` fold must not collapse two distinct backends to Equal
335        // (which would let one silently evict the other from a BTree key set).
336        // Computed inline (no recursion into `cmp`) so it stays cheap.
337        let fields_all_equal = self.cluster_id == o.cluster_id
338            && self.backend_id == o.backend_id
339            && self.sticky_id == o.sticky_id
340            && self.load_balancing_parameters == o.load_balancing_parameters
341            && self.backup == o.backup
342            && self.address == o.address;
343
344        let ordering = self
345            .cluster_id
346            .cmp(&o.cluster_id)
347            .then(self.backend_id.cmp(&o.backend_id))
348            .then(self.sticky_id.cmp(&o.sticky_id))
349            .then(
350                self.load_balancing_parameters
351                    .cmp(&o.load_balancing_parameters),
352            )
353            .then(self.backup.cmp(&o.backup))
354            .then(socketaddr_cmp(&self.address, &o.address));
355
356        debug_assert_eq!(
357            ordering == Ordering::Equal,
358            fields_all_equal,
359            "Backend::cmp returns Equal iff every keyed field is equal"
360        );
361        ordering
362    }
363}
364
365impl PartialOrd for Backend {
366    fn partial_cmp(&self, other: &Backend) -> Option<Ordering> {
367        Some(self.cmp(other))
368    }
369}
370
371impl Backend {
372    pub fn to_add_backend(self) -> AddBackend {
373        let source_address = self.address;
374        let source_backend_id = self.backend_id.clone();
375        let add_backend = AddBackend {
376            cluster_id: self.cluster_id,
377            address: self.address.into(),
378            sticky_id: self.sticky_id,
379            backend_id: self.backend_id,
380            load_balancing_parameters: self.load_balancing_parameters,
381            backup: self.backup,
382        };
383
384        // POST: identity (backend id) and wire address are preserved — same
385        // routing guarantee as the `From<Backend>` path, kept in lockstep.
386        debug_assert_eq!(
387            add_backend.backend_id, source_backend_id,
388            "backend_id must survive to_add_backend"
389        );
390        debug_assert_eq!(
391            SocketAddr::from(add_backend.address),
392            source_address,
393            "backend address must round-trip through to_add_backend"
394        );
395        add_backend
396    }
397}
398
399impl fmt::Display for RunState {
400    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
401        write!(f, "{self:?}")
402    }
403}
404
405pub type MessageId = String;
406
407impl WorkerResponse {
408    pub fn ok<T>(id: T) -> Self
409    where
410        T: ToString,
411    {
412        Self {
413            id: id.to_string(),
414            message: String::new(),
415            status: ResponseStatus::Ok.into(),
416            content: None,
417        }
418    }
419
420    pub fn ok_with_content<T>(id: T, content: ResponseContent) -> Self
421    where
422        T: ToString,
423    {
424        Self {
425            id: id.to_string(),
426            status: ResponseStatus::Ok.into(),
427            message: String::new(),
428            content: Some(content),
429        }
430    }
431
432    pub fn error<T, U>(id: T, error: U) -> Self
433    where
434        T: ToString,
435        U: ToString,
436    {
437        Self {
438            id: id.to_string(),
439            message: error.to_string(),
440            status: ResponseStatus::Failure.into(),
441            content: None,
442        }
443    }
444
445    pub fn processing<T>(id: T) -> Self
446    where
447        T: ToString,
448    {
449        Self {
450            id: id.to_string(),
451            message: String::new(),
452            status: ResponseStatus::Processing.into(),
453            content: None,
454        }
455    }
456
457    pub fn with_status<T>(id: T, status: ResponseStatus) -> Self
458    where
459        T: ToString,
460    {
461        Self {
462            id: id.to_string(),
463            message: String::new(),
464            status: status.into(),
465            content: None,
466        }
467    }
468
469    pub fn is_failure(&self) -> bool {
470        self.status == ResponseStatus::Failure as i32
471    }
472}
473
474impl fmt::Display for WorkerResponse {
475    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
476        write!(f, "{}-{:?}", self.id, self.status)
477    }
478}
479
480impl fmt::Display for FilteredTimeSerie {
481    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
482        write!(
483            f,
484            "FilteredTimeSerie {{\nlast_second: {},\nlast_minute:\n{:?}\n{:?}\n{:?}\n{:?}\n{:?}\n{:?}\nlast_hour:\n{:?}\n{:?}\n{:?}\n{:?}\n{:?}\n{:?}\n}}",
485            self.last_second,
486            &self.last_minute[0..10],
487            &self.last_minute[10..20],
488            &self.last_minute[20..30],
489            &self.last_minute[30..40],
490            &self.last_minute[40..50],
491            &self.last_minute[50..60],
492            &self.last_hour[0..10],
493            &self.last_hour[10..20],
494            &self.last_hour[20..30],
495            &self.last_hour[30..40],
496            &self.last_hour[40..50],
497            &self.last_hour[50..60]
498        )
499    }
500}
501
502fn socketaddr_cmp(a: &SocketAddr, b: &SocketAddr) -> Ordering {
503    let ordering = a.ip().cmp(&b.ip()).then(a.port().cmp(&b.port()));
504    // INV: two socket addresses compare Equal iff both IP and port match —
505    // the `.then` fold must not declare distinct (ip, port) pairs equal, which
506    // would make two different backends indistinguishable to the BTree key.
507    debug_assert_eq!(
508        ordering == Ordering::Equal,
509        a.ip() == b.ip() && a.port() == b.port(),
510        "socketaddr_cmp is Equal iff ip and port both match"
511    );
512    ordering
513}