1use crate::fetch::{FetchKind, Terminator};
2use crate::middleware::MiddlewareKind;
3
4#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug, serde::Serialize, serde::Deserialize)]
5pub enum Phase {
6 L4Raw,
7 L4Peeked,
8 L7Request,
9 L7Response,
10 Tunnel,
11}
12
13#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]
14pub enum PhaseNodeKind {
15 Check,
16 Middleware(MiddlewareKind),
17 Upgrade,
18 Fetch(FetchKind),
19 Terminate(Terminator),
20}
21
22#[derive(Copy, Clone, Eq, PartialEq, Debug)]
23pub enum Transition {
24 PassThrough,
25 Into(Phase),
26 BiOutcome { response: Phase, tunnel: Phase },
27 Terminal,
28}
29
30#[derive(Copy, Clone, Eq, PartialEq, Debug)]
31pub struct PhaseError {
32 pub expected: &'static [Phase],
33 pub got: Phase,
34}
35
36const L4_ANY: &[Phase] = &[Phase::L4Raw, Phase::L4Peeked];
37const L7_REQ: &[Phase] = &[Phase::L7Request];
38const L7_RESP: &[Phase] = &[Phase::L7Response];
39const TUNNEL: &[Phase] = &[Phase::Tunnel];
40const ANY_PHASE: &[Phase] =
41 &[Phase::L4Raw, Phase::L4Peeked, Phase::L7Request, Phase::L7Response, Phase::Tunnel];
42
43#[must_use]
47#[allow(clippy::match_same_arms)]
48pub const fn accepted_in_phases(kind: PhaseNodeKind) -> &'static [Phase] {
49 match kind {
50 PhaseNodeKind::Check => ANY_PHASE,
51 PhaseNodeKind::Middleware(MiddlewareKind::L4Peek) => L4_ANY,
52 PhaseNodeKind::Middleware(MiddlewareKind::L4Bytes) => L4_ANY,
53 PhaseNodeKind::Middleware(MiddlewareKind::L7Request) => L7_REQ,
54 PhaseNodeKind::Middleware(MiddlewareKind::L7Response) => L7_RESP,
55 PhaseNodeKind::Upgrade => L4_ANY,
60 PhaseNodeKind::Fetch(FetchKind::L4Forward) => L4_ANY,
61 PhaseNodeKind::Fetch(FetchKind::HttpProxy) => L7_REQ,
62 PhaseNodeKind::Fetch(FetchKind::HttpSynthesize) => L7_REQ,
63 PhaseNodeKind::Fetch(FetchKind::WebSocketUpgrade) => L7_REQ,
64 PhaseNodeKind::Fetch(FetchKind::AcmeChallenge) => L7_REQ,
65 PhaseNodeKind::Terminate(Terminator::WriteHttpResponse) => L7_RESP,
66 PhaseNodeKind::Terminate(Terminator::ByteTunnel) => TUNNEL,
67 PhaseNodeKind::Terminate(Terminator::Close) => ANY_PHASE,
70 }
71}
72
73#[allow(clippy::match_same_arms)]
79pub fn transition(kind: PhaseNodeKind, cur: Phase) -> Result<Transition, PhaseError> {
80 let accepted = accepted_in_phases(kind);
81 if !accepted.contains(&cur) {
82 return Err(PhaseError { expected: accepted, got: cur });
83 }
84 Ok(match kind {
85 PhaseNodeKind::Check => Transition::PassThrough,
86 PhaseNodeKind::Middleware(MiddlewareKind::L4Peek) => Transition::Into(Phase::L4Peeked),
87 PhaseNodeKind::Middleware(MiddlewareKind::L4Bytes) => Transition::PassThrough,
88 PhaseNodeKind::Middleware(MiddlewareKind::L7Request) => Transition::Into(Phase::L7Request),
89 PhaseNodeKind::Middleware(MiddlewareKind::L7Response) => Transition::Into(Phase::L7Response),
90 PhaseNodeKind::Upgrade => Transition::Into(Phase::L7Request),
91 PhaseNodeKind::Fetch(FetchKind::L4Forward) => Transition::Into(Phase::Tunnel),
92 PhaseNodeKind::Fetch(FetchKind::HttpProxy) => Transition::Into(Phase::L7Response),
93 PhaseNodeKind::Fetch(FetchKind::HttpSynthesize) => Transition::Into(Phase::L7Response),
94 PhaseNodeKind::Fetch(FetchKind::AcmeChallenge) => Transition::Into(Phase::L7Response),
95 PhaseNodeKind::Fetch(FetchKind::WebSocketUpgrade) => {
96 Transition::BiOutcome { response: Phase::L7Response, tunnel: Phase::Tunnel }
97 }
98 PhaseNodeKind::Terminate(_) => Transition::Terminal,
99 })
100}
101
102#[cfg(test)]
103mod tests {
104 use super::*;
105
106 const ALL_PHASES: [Phase; 5] =
107 [Phase::L4Raw, Phase::L4Peeked, Phase::L7Request, Phase::L7Response, Phase::Tunnel];
108
109 #[test]
110 fn phase_serde_round_trip_per_variant() {
111 for p in ALL_PHASES {
112 let encoded = serde_json::to_string(&p).expect("serialize");
113 let decoded: Phase = serde_json::from_str(&encoded).expect("deserialize");
114 assert_eq!(decoded, p);
115 }
116 }
117
118 #[test]
119 fn check_accepts_any_phase() {
120 assert_eq!(accepted_in_phases(PhaseNodeKind::Check), ANY_PHASE);
121 }
122
123 #[test]
124 fn l4_peek_accepts_l4_phases_only() {
125 assert_eq!(
126 accepted_in_phases(PhaseNodeKind::Middleware(MiddlewareKind::L4Peek)),
127 &[Phase::L4Raw, Phase::L4Peeked] as &[Phase],
128 );
129 }
130
131 #[test]
132 fn l4_bytes_accepts_l4_phases_only() {
133 assert_eq!(
134 accepted_in_phases(PhaseNodeKind::Middleware(MiddlewareKind::L4Bytes)),
135 &[Phase::L4Raw, Phase::L4Peeked] as &[Phase],
136 );
137 }
138
139 #[test]
140 fn l7_request_middleware_accepts_only_l7_request() {
141 assert_eq!(
142 accepted_in_phases(PhaseNodeKind::Middleware(MiddlewareKind::L7Request)),
143 &[Phase::L7Request] as &[Phase],
144 );
145 }
146
147 #[test]
148 fn l7_response_middleware_accepts_only_l7_response() {
149 assert_eq!(
150 accepted_in_phases(PhaseNodeKind::Middleware(MiddlewareKind::L7Response)),
151 &[Phase::L7Response] as &[Phase],
152 );
153 }
154
155 #[test]
156 fn upgrade_accepts_both_l4_phases() {
157 assert_eq!(
160 accepted_in_phases(PhaseNodeKind::Upgrade),
161 &[Phase::L4Raw, Phase::L4Peeked] as &[Phase],
162 );
163 }
164
165 #[test]
166 fn l4_forward_fetch_accepts_l4_phases() {
167 assert_eq!(
168 accepted_in_phases(PhaseNodeKind::Fetch(FetchKind::L4Forward)),
169 &[Phase::L4Raw, Phase::L4Peeked] as &[Phase],
170 );
171 }
172
173 #[test]
174 fn http_fetches_accept_only_l7_request() {
175 for f in [
176 FetchKind::HttpProxy,
177 FetchKind::HttpSynthesize,
178 FetchKind::WebSocketUpgrade,
179 FetchKind::AcmeChallenge,
180 ] {
181 assert_eq!(accepted_in_phases(PhaseNodeKind::Fetch(f)), &[Phase::L7Request] as &[Phase],);
182 }
183 }
184
185 #[test]
186 fn write_http_response_accepts_only_l7_response() {
187 assert_eq!(
188 accepted_in_phases(PhaseNodeKind::Terminate(Terminator::WriteHttpResponse)),
189 &[Phase::L7Response] as &[Phase],
190 );
191 }
192
193 #[test]
194 fn byte_tunnel_accepts_only_tunnel() {
195 assert_eq!(
196 accepted_in_phases(PhaseNodeKind::Terminate(Terminator::ByteTunnel)),
197 &[Phase::Tunnel] as &[Phase],
198 );
199 }
200
201 #[test]
202 fn check_is_pass_through_at_every_phase() {
203 for cur in ALL_PHASES {
204 assert_eq!(transition(PhaseNodeKind::Check, cur), Ok(Transition::PassThrough));
205 }
206 }
207
208 #[test]
209 fn l4_peek_forces_out_to_l4_peeked() {
210 for cur in [Phase::L4Raw, Phase::L4Peeked] {
211 assert_eq!(
212 transition(PhaseNodeKind::Middleware(MiddlewareKind::L4Peek), cur),
213 Ok(Transition::Into(Phase::L4Peeked)),
214 );
215 }
216 }
217
218 #[test]
219 fn l4_bytes_is_pass_through_on_l4_phases() {
220 for cur in [Phase::L4Raw, Phase::L4Peeked] {
221 assert_eq!(
222 transition(PhaseNodeKind::Middleware(MiddlewareKind::L4Bytes), cur),
223 Ok(Transition::PassThrough),
224 );
225 }
226 }
227
228 #[test]
229 fn upgrade_transitions_to_l7_request_from_any_l4_phase() {
230 for cur in [Phase::L4Raw, Phase::L4Peeked] {
231 assert_eq!(transition(PhaseNodeKind::Upgrade, cur), Ok(Transition::Into(Phase::L7Request)),);
232 }
233 }
234
235 #[test]
236 fn l7_request_middleware_stays_in_l7_request() {
237 assert_eq!(
238 transition(PhaseNodeKind::Middleware(MiddlewareKind::L7Request), Phase::L7Request),
239 Ok(Transition::Into(Phase::L7Request)),
240 );
241 }
242
243 #[test]
244 fn l7_response_middleware_stays_in_l7_response() {
245 assert_eq!(
246 transition(PhaseNodeKind::Middleware(MiddlewareKind::L7Response), Phase::L7Response),
247 Ok(Transition::Into(Phase::L7Response)),
248 );
249 }
250
251 #[test]
252 fn l4_forward_fetch_goes_to_tunnel_from_any_l4_phase() {
253 for cur in [Phase::L4Raw, Phase::L4Peeked] {
254 assert_eq!(
255 transition(PhaseNodeKind::Fetch(FetchKind::L4Forward), cur),
256 Ok(Transition::Into(Phase::Tunnel)),
257 );
258 }
259 }
260
261 #[test]
262 fn http_fetch_variants_go_to_l7_response() {
263 for f in [FetchKind::HttpProxy, FetchKind::HttpSynthesize, FetchKind::AcmeChallenge] {
264 assert_eq!(
265 transition(PhaseNodeKind::Fetch(f), Phase::L7Request),
266 Ok(Transition::Into(Phase::L7Response)),
267 );
268 }
269 }
270
271 #[test]
272 fn websocket_fetch_is_bi_outcome() {
273 assert_eq!(
274 transition(PhaseNodeKind::Fetch(FetchKind::WebSocketUpgrade), Phase::L7Request),
275 Ok(Transition::BiOutcome { response: Phase::L7Response, tunnel: Phase::Tunnel }),
276 );
277 }
278
279 #[test]
280 fn write_http_response_is_terminal() {
281 assert_eq!(
282 transition(PhaseNodeKind::Terminate(Terminator::WriteHttpResponse), Phase::L7Response),
283 Ok(Transition::Terminal),
284 );
285 }
286
287 #[test]
288 fn byte_tunnel_is_terminal() {
289 assert_eq!(
290 transition(PhaseNodeKind::Terminate(Terminator::ByteTunnel), Phase::Tunnel),
291 Ok(Transition::Terminal),
292 );
293 }
294
295 #[test]
296 fn close_is_terminal_at_every_phase() {
297 for p in ALL_PHASES {
298 assert_eq!(
299 transition(PhaseNodeKind::Terminate(Terminator::Close), p),
300 Ok(Transition::Terminal),
301 );
302 }
303 }
304
305 #[test]
306 fn close_accepts_any_phase() {
307 assert_eq!(accepted_in_phases(PhaseNodeKind::Terminate(Terminator::Close)), ANY_PHASE,);
308 }
309
310 #[test]
311 fn rejects_out_of_phase_attempts() {
312 let cases: &[(PhaseNodeKind, Phase)] = &[
313 (PhaseNodeKind::Upgrade, Phase::L7Request),
314 (PhaseNodeKind::Upgrade, Phase::L7Response),
315 (PhaseNodeKind::Middleware(MiddlewareKind::L7Request), Phase::L4Raw),
316 (PhaseNodeKind::Middleware(MiddlewareKind::L7Response), Phase::L7Request),
317 (PhaseNodeKind::Fetch(FetchKind::HttpProxy), Phase::L7Response),
318 (PhaseNodeKind::Fetch(FetchKind::L4Forward), Phase::L7Request),
319 (PhaseNodeKind::Terminate(Terminator::WriteHttpResponse), Phase::Tunnel),
320 (PhaseNodeKind::Terminate(Terminator::ByteTunnel), Phase::L7Response),
321 ];
322 for (kind, cur) in cases.iter().copied() {
323 let err = transition(kind, cur).expect_err("out-of-phase must error");
324 assert_eq!(err.got, cur);
325 assert_eq!(err.expected, accepted_in_phases(kind));
326 }
327 }
328}