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, Response, ResponseContent,
7        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        RequestHttpFrontend {
81            cluster_id: val.cluster_id,
82            address: val.address.into(),
83            hostname: val.hostname,
84            path: val.path,
85            method: val.method,
86            position: val.position.into(),
87            tags: val.tags.unwrap_or_default(),
88            redirect: val.redirect,
89            redirect_scheme: val.redirect_scheme,
90            redirect_template: val.redirect_template,
91            rewrite_host: val.rewrite_host,
92            rewrite_path: val.rewrite_path,
93            rewrite_port: val.rewrite_port,
94            required_auth: val.required_auth,
95            headers: val.headers,
96            hsts: val.hsts,
97        }
98    }
99}
100
101impl From<Backend> for AddBackend {
102    fn from(val: Backend) -> Self {
103        AddBackend {
104            cluster_id: val.cluster_id,
105            backend_id: val.backend_id,
106            address: val.address.into(),
107            sticky_id: val.sticky_id,
108            load_balancing_parameters: val.load_balancing_parameters,
109            backup: val.backup,
110        }
111    }
112}
113
114impl PathRule {
115    pub fn prefix<S>(value: S) -> Self
116    where
117        S: ToString,
118    {
119        Self {
120            kind: PathRuleKind::Prefix.into(),
121            value: value.to_string(),
122        }
123    }
124
125    pub fn regex<S>(value: S) -> Self
126    where
127        S: ToString,
128    {
129        Self {
130            kind: PathRuleKind::Regex.into(),
131            value: value.to_string(),
132        }
133    }
134
135    pub fn equals<S>(value: S) -> Self
136    where
137        S: ToString,
138    {
139        Self {
140            kind: PathRuleKind::Equals.into(),
141            value: value.to_string(),
142        }
143    }
144
145    pub fn from_cli_options(
146        path_prefix: Option<String>,
147        path_regex: Option<String>,
148        path_equals: Option<String>,
149    ) -> Self {
150        match (path_prefix, path_regex, path_equals) {
151            (Some(prefix), _, _) => PathRule {
152                kind: PathRuleKind::Prefix as i32,
153                value: prefix,
154            },
155            (None, Some(regex), _) => PathRule {
156                kind: PathRuleKind::Regex as i32,
157                value: regex,
158            },
159            (None, None, Some(equals)) => PathRule {
160                kind: PathRuleKind::Equals as i32,
161                value: equals,
162            },
163            _ => PathRule::default(),
164        }
165    }
166}
167
168pub fn is_default_path_rule(p: &PathRule) -> bool {
169    PathRuleKind::try_from(p.kind) == Ok(PathRuleKind::Prefix) && p.value.is_empty()
170}
171
172impl fmt::Display for PathRule {
173    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
174        match PathRuleKind::try_from(self.kind) {
175            Ok(PathRuleKind::Prefix) => write!(f, "prefix '{}'", self.value),
176            Ok(PathRuleKind::Regex) => write!(f, "regexp '{}'", self.value),
177            Ok(PathRuleKind::Equals) => write!(f, "equals '{}'", self.value),
178            Err(_) => write!(f, ""),
179        }
180    }
181}
182
183/// A TCP frontend, as used *within* Sōzu
184#[derive(Debug, Clone, PartialOrd, Ord, PartialEq, Eq, Hash, Serialize, Deserialize)]
185pub struct TcpFrontend {
186    pub cluster_id: String,
187    pub address: SocketAddr,
188    /// custom tags to identify the frontend in the access logs
189    pub tags: BTreeMap<String, String>,
190}
191
192impl From<TcpFrontend> for RequestTcpFrontend {
193    fn from(val: TcpFrontend) -> Self {
194        RequestTcpFrontend {
195            cluster_id: val.cluster_id,
196            address: val.address.into(),
197            tags: val.tags,
198        }
199    }
200}
201
202/// A backend, as used *within* Sōzu
203#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
204pub struct Backend {
205    pub cluster_id: String,
206    pub backend_id: String,
207    pub address: SocketAddr,
208    #[serde(default)]
209    #[serde(skip_serializing_if = "Option::is_none")]
210    pub sticky_id: Option<String>,
211    #[serde(default)]
212    #[serde(skip_serializing_if = "Option::is_none")]
213    pub load_balancing_parameters: Option<LoadBalancingParams>,
214    #[serde(default)]
215    #[serde(skip_serializing_if = "Option::is_none")]
216    pub backup: Option<bool>,
217}
218
219impl Ord for Backend {
220    fn cmp(&self, o: &Backend) -> Ordering {
221        self.cluster_id
222            .cmp(&o.cluster_id)
223            .then(self.backend_id.cmp(&o.backend_id))
224            .then(self.sticky_id.cmp(&o.sticky_id))
225            .then(
226                self.load_balancing_parameters
227                    .cmp(&o.load_balancing_parameters),
228            )
229            .then(self.backup.cmp(&o.backup))
230            .then(socketaddr_cmp(&self.address, &o.address))
231    }
232}
233
234impl PartialOrd for Backend {
235    fn partial_cmp(&self, other: &Backend) -> Option<Ordering> {
236        Some(self.cmp(other))
237    }
238}
239
240impl Backend {
241    pub fn to_add_backend(self) -> AddBackend {
242        AddBackend {
243            cluster_id: self.cluster_id,
244            address: self.address.into(),
245            sticky_id: self.sticky_id,
246            backend_id: self.backend_id,
247            load_balancing_parameters: self.load_balancing_parameters,
248            backup: self.backup,
249        }
250    }
251}
252
253impl fmt::Display for RunState {
254    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
255        write!(f, "{self:?}")
256    }
257}
258
259pub type MessageId = String;
260
261impl WorkerResponse {
262    pub fn ok<T>(id: T) -> Self
263    where
264        T: ToString,
265    {
266        Self {
267            id: id.to_string(),
268            message: String::new(),
269            status: ResponseStatus::Ok.into(),
270            content: None,
271        }
272    }
273
274    pub fn ok_with_content<T>(id: T, content: ResponseContent) -> Self
275    where
276        T: ToString,
277    {
278        Self {
279            id: id.to_string(),
280            status: ResponseStatus::Ok.into(),
281            message: String::new(),
282            content: Some(content),
283        }
284    }
285
286    pub fn error<T, U>(id: T, error: U) -> Self
287    where
288        T: ToString,
289        U: ToString,
290    {
291        Self {
292            id: id.to_string(),
293            message: error.to_string(),
294            status: ResponseStatus::Failure.into(),
295            content: None,
296        }
297    }
298
299    pub fn processing<T>(id: T) -> Self
300    where
301        T: ToString,
302    {
303        Self {
304            id: id.to_string(),
305            message: String::new(),
306            status: ResponseStatus::Processing.into(),
307            content: None,
308        }
309    }
310
311    pub fn with_status<T>(id: T, status: ResponseStatus) -> Self
312    where
313        T: ToString,
314    {
315        Self {
316            id: id.to_string(),
317            message: String::new(),
318            status: status.into(),
319            content: None,
320        }
321    }
322
323    pub fn is_failure(&self) -> bool {
324        self.status == ResponseStatus::Failure as i32
325    }
326}
327
328impl fmt::Display for WorkerResponse {
329    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
330        write!(f, "{}-{:?}", self.id, self.status)
331    }
332}
333
334impl fmt::Display for FilteredTimeSerie {
335    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
336        write!(
337            f,
338            "FilteredTimeSerie {{\nlast_second: {},\nlast_minute:\n{:?}\n{:?}\n{:?}\n{:?}\n{:?}\n{:?}\nlast_hour:\n{:?}\n{:?}\n{:?}\n{:?}\n{:?}\n{:?}\n}}",
339            self.last_second,
340            &self.last_minute[0..10],
341            &self.last_minute[10..20],
342            &self.last_minute[20..30],
343            &self.last_minute[30..40],
344            &self.last_minute[40..50],
345            &self.last_minute[50..60],
346            &self.last_hour[0..10],
347            &self.last_hour[10..20],
348            &self.last_hour[20..30],
349            &self.last_hour[30..40],
350            &self.last_hour[40..50],
351            &self.last_hour[50..60]
352        )
353    }
354}
355
356fn socketaddr_cmp(a: &SocketAddr, b: &SocketAddr) -> Ordering {
357    a.ip().cmp(&b.ip()).then(a.port().cmp(&b.port()))
358}