Skip to main content

relay_core_lib/rule/engine/actions/
http.rs

1use crate::rule::model::{Action, BodySource, TerminalReason};
2use crate::rule::engine::executor::ExecutionContext;
3use crate::rule::engine::actions::utils::{substitute_variables, resolve_body_source};
4use crate::rule::engine::actions::transform::apply_transform;
5use crate::rule::engine::actions::ActionOutcome;
6use relay_core_api::flow::{Cookie, Flow, Layer, HttpResponse, ResponseTiming};
7use url::Url;
8
9fn status_text_for(code: u16) -> String {
10    http::StatusCode::from_u16(code)
11        .ok()
12        .and_then(|s| s.canonical_reason().map(|r| r.to_string()))
13        .unwrap_or_else(|| "Mocked".to_string())
14}
15
16fn parse_set_cookie_header(value: &str) -> Option<Cookie> {
17    let mut segments = value.split(';');
18    let first = segments.next()?.trim();
19    let (name, cookie_value) = first.split_once('=')?;
20    if name.trim().is_empty() {
21        return None;
22    }
23
24    let mut cookie = Cookie {
25        name: name.trim().to_string(),
26        value: cookie_value.trim().to_string(),
27        path: None,
28        domain: None,
29        expires: None,
30        http_only: None,
31        secure: None,
32    };
33
34    for seg in segments {
35        let attr = seg.trim();
36        if attr.eq_ignore_ascii_case("httponly") {
37            cookie.http_only = Some(true);
38            continue;
39        }
40        if attr.eq_ignore_ascii_case("secure") {
41            cookie.secure = Some(true);
42            continue;
43        }
44        if let Some((k, v)) = attr.split_once('=') {
45            let key = k.trim();
46            let val = v.trim().to_string();
47            if key.eq_ignore_ascii_case("path") {
48                cookie.path = Some(val);
49            } else if key.eq_ignore_ascii_case("domain") {
50                cookie.domain = Some(val);
51            } else if key.eq_ignore_ascii_case("expires") {
52                cookie.expires = Some(val);
53            }
54        }
55    }
56
57    Some(cookie)
58}
59
60fn parse_response_cookies(headers: &[(String, String)]) -> Vec<Cookie> {
61    headers
62        .iter()
63        .filter(|(k, _)| k.eq_ignore_ascii_case("set-cookie"))
64        .filter_map(|(_, v)| parse_set_cookie_header(v))
65        .collect()
66}
67
68pub async fn execute(
69    action: &Action,
70    flow: &mut Flow,
71    ctx: &mut ExecutionContext,
72) -> ActionOutcome {
73    match action {
74        // --- Request Modification Actions ---
75        Action::SetRequestMethod { method } => {
76            let val = substitute_variables(method, flow, ctx, None);
77            if let Layer::Http(http) = &mut flow.layer {
78                http.request.method = val;
79            }
80            ActionOutcome::Continue
81        }
82        Action::SetRequestUrl { url } => {
83            let val = substitute_variables(url, flow, ctx, None);
84            if let Ok(new_url) = Url::parse(&val)
85                && let Layer::Http(http) = &mut flow.layer {
86                    http.request.url = new_url;
87                }
88            ActionOutcome::Continue
89        }
90        Action::SetRequestBody { body } => {
91            if let Some(body_data) = resolve_body_source(body, ctx.policy.as_deref()).await
92                && let Layer::Http(http) = &mut flow.layer {
93                    http.request.body = Some(body_data);
94                }
95            ActionOutcome::Continue
96        }
97
98        // --- Response Modification Actions ---
99        Action::SetResponseStatus { status } => {
100            if let Layer::Http(http) = &mut flow.layer
101                && let Some(res) = &mut http.response {
102                    res.status = *status;
103                }
104            ActionOutcome::Continue
105        }
106        Action::SetResponseBody { body } => {
107            if let Some(body_data) = resolve_body_source(body, ctx.policy.as_deref()).await
108                && let Layer::Http(http) = &mut flow.layer
109                     && let Some(res) = &mut http.response {
110                         res.body = Some(body_data);
111                     }
112            ActionOutcome::Continue
113        }
114        Action::Redirect { location, status } => {
115            let val = substitute_variables(location, flow, ctx, None);
116            let headers = vec![("Location".to_string(), val)];
117            
118            let res = HttpResponse {
119                status: *status,
120                status_text: "Redirect".to_string(),
121                version: "HTTP/1.1".to_string(),
122                headers,
123                cookies: vec![],
124                body: None,
125                timing: ResponseTiming {
126                    time_to_first_byte: Some(0),
127                    time_to_last_byte: Some(0),
128                },
129            };
130
131            if let Layer::Http(http) = &mut flow.layer {
132                http.response = Some(res);
133            } else if let Layer::WebSocket(ws) = &mut flow.layer {
134                ws.handshake_response = res;
135            }
136            ActionOutcome::Terminated(TerminalReason::Redirect)
137        }
138
139        // --- Request Header Actions ---
140        Action::AddRequestHeader { name, value } => {
141            let val = substitute_variables(value, flow, ctx, None);
142            if let Layer::Http(http) = &mut flow.layer {
143                http.request.headers.push((name.clone(), val));
144            }
145            ActionOutcome::Continue
146        }
147        Action::UpdateRequestHeader {
148            name,
149            value,
150            add_if_missing,
151        } => {
152            // First pass: find existing value to use as {{previous}}
153            let mut old_val = None;
154            if let Layer::Http(http) = &flow.layer {
155                 for (k, v) in &http.request.headers {
156                     if k.eq_ignore_ascii_case(name) {
157                         old_val = Some(v.clone());
158                         break;
159                     }
160                 }
161            }
162
163            // Compute new value
164            let new_val = substitute_variables(value, flow, ctx, old_val.as_deref());
165
166            // Second pass: apply change
167            if let Layer::Http(http) = &mut flow.layer {
168                let mut found = false;
169                for (k, v) in http.request.headers.iter_mut() {
170                    if k.eq_ignore_ascii_case(name) {
171                        *v = new_val.clone();
172                        found = true;
173                    }
174                }
175                if !found && *add_if_missing {
176                    http.request.headers.push((name.clone(), new_val));
177                }
178            }
179            ActionOutcome::Continue
180        }
181        Action::DeleteRequestHeader { name } => {
182            if let Layer::Http(http) = &mut flow.layer {
183                http.request.headers
184                    .retain(|(k, _)| !k.eq_ignore_ascii_case(name));
185            }
186            ActionOutcome::Continue
187        }
188
189        // --- Response Header Actions ---
190        Action::AddResponseHeader { name, value } => {
191            let val = substitute_variables(value, flow, ctx, None);
192            if let Layer::Http(http) = &mut flow.layer
193                && let Some(res) = &mut http.response {
194                    res.headers.push((name.clone(), val));
195                }
196            ActionOutcome::Continue
197        }
198        Action::UpdateResponseHeader { name, value, add_if_missing } => {
199            // First pass: find existing value
200            let mut old_val = None;
201            if let Layer::Http(http) = &flow.layer
202                 && let Some(res) = &http.response {
203                     for (k, v) in &res.headers {
204                         if k.eq_ignore_ascii_case(name) {
205                             old_val = Some(v.clone());
206                             break;
207                         }
208                     }
209                 }
210            
211            // Compute new value
212            let new_val = substitute_variables(value, flow, ctx, old_val.as_deref());
213
214            // Second pass: apply change
215            if let Layer::Http(http) = &mut flow.layer
216                && let Some(res) = &mut http.response {
217                    let mut found = false;
218                    for (k, v) in res.headers.iter_mut() {
219                        if k.eq_ignore_ascii_case(name) {
220                            *v = new_val.clone();
221                            found = true;
222                        }
223                    }
224                    if !found && *add_if_missing {
225                        res.headers.push((name.clone(), new_val));
226                    }
227                }
228            ActionOutcome::Continue
229        }
230         Action::DeleteResponseHeader { name } => {
231            if let Layer::Http(http) = &mut flow.layer
232                 && let Some(res) = &mut http.response {
233                    res.headers
234                    .retain(|(k, _)| !k.eq_ignore_ascii_case(name));
235                 }
236            ActionOutcome::Continue
237        }
238        
239        // --- Terminal Actions ---
240        Action::MockResponse { status, headers, body } => {
241             let body_data = if let Some(source) = body {
242                 resolve_body_source(source, ctx.policy.as_deref()).await
243             } else {
244                 None
245             };
246
247             if let Layer::Http(http) = &mut flow.layer {
248                 let mut res_headers = Vec::new();
249                 for (k, v) in headers {
250                     res_headers.push((k.clone(), v.clone()));
251                 }
252                 let cookies = parse_response_cookies(&res_headers);
253                 
254                 http.response = Some(HttpResponse {
255                    status: *status,
256                    status_text: status_text_for(*status),
257                    version: "HTTP/1.1".to_string(),
258                    headers: res_headers,
259                    cookies,
260                    body: body_data,
261                    timing: ResponseTiming {
262                        time_to_first_byte: Some(0),
263                        time_to_last_byte: Some(0),
264                    },
265                 });
266             } else if let Layer::WebSocket(ws) = &mut flow.layer {
267                 let mut res_headers = Vec::new();
268                 for (k, v) in headers {
269                     res_headers.push((k.clone(), v.clone()));
270                 }
271                 let cookies = parse_response_cookies(&res_headers);
272                 
273                 ws.handshake_response = HttpResponse {
274                    status: *status,
275                    status_text: status_text_for(*status),
276                    version: "HTTP/1.1".to_string(),
277                    headers: res_headers,
278                    cookies,
279                    body: body_data,
280                    timing: ResponseTiming {
281                        time_to_first_byte: Some(0),
282                        time_to_last_byte: Some(0),
283                    },
284                 };
285             }
286             ActionOutcome::Terminated(TerminalReason::Mock)
287        }
288        Action::MapLocal { path, content_type } => {
289            let body_data = resolve_body_source(&BodySource::File(path.clone()), ctx.policy.as_deref()).await;
290            
291            if let Some(body) = body_data {
292                 if let Layer::Http(http) = &mut flow.layer {
293                    let headers = content_type.as_ref()
294                        .map(|ct| vec![("Content-Type".to_string(), ct.clone())])
295                        .unwrap_or_default();
296
297                    http.response = Some(HttpResponse {
298                        status: 200,
299                        status_text: "OK".to_string(),
300                        version: "HTTP/1.1".to_string(),
301                        headers,
302                        cookies: vec![],
303                        body: Some(body),
304                        timing: ResponseTiming {
305                            time_to_first_byte: Some(0),
306                            time_to_last_byte: Some(0),
307                        },
308                     });
309                }
310                ActionOutcome::Terminated(TerminalReason::Mock)
311            } else {
312                ActionOutcome::Failed(format!("Failed to load local file: {}", path))
313            }
314        }
315        
316        // --- Transformation Actions ---
317        Action::TransformRequestBody { transform } => {
318             if let Layer::Http(http) = &mut flow.layer
319                 && let Some(body) = &mut http.request.body {
320                     apply_transform(body, transform);
321                 }
322             ActionOutcome::Continue
323        }
324        Action::TransformResponseBody { transform } => {
325             if let Layer::Http(http) = &mut flow.layer
326                 && let Some(res) = &mut http.response
327                     && let Some(body) = &mut res.body {
328                         apply_transform(body, transform);
329                     }
330             ActionOutcome::Continue
331        }
332
333        _ => ActionOutcome::Failed(format!("Action {:?} not supported in http handler", action)),
334    }
335}
336
337#[cfg(test)]
338mod tests {
339    use super::{execute, parse_set_cookie_header};
340    use crate::rule::engine::actions::ActionOutcome;
341    use crate::rule::engine::executor::ExecutionContext;
342    use crate::rule::engine::state::InMemoryRuleStateStore;
343    use crate::rule::model::{Action, RuleTraceSummary};
344    use chrono::Utc;
345    use relay_core_api::flow::{Flow, HttpLayer, HttpRequest, Layer, NetworkInfo, TransportProtocol};
346    use std::collections::HashMap;
347    use std::sync::Arc;
348    use url::Url;
349    use uuid::Uuid;
350
351    fn sample_flow() -> Flow {
352        Flow {
353            id: Uuid::new_v4(),
354            start_time: Utc::now(),
355            end_time: None,
356            network: NetworkInfo {
357                client_ip: "127.0.0.1".to_string(),
358                client_port: 12345,
359                server_ip: "1.1.1.1".to_string(),
360                server_port: 443,
361                protocol: TransportProtocol::TCP,
362                tls: true,
363                tls_version: Some("TLS1.3".to_string()),
364                sni: Some("example.com".to_string()),
365            },
366            layer: Layer::Http(HttpLayer {
367                request: HttpRequest {
368                    method: "GET".to_string(),
369                    url: Url::parse("https://example.com/").expect("url"),
370                    version: "HTTP/1.1".to_string(),
371                    headers: vec![],
372                    cookies: vec![],
373                    query: vec![],
374                    body: None,
375                },
376                response: None,
377                error: None,
378            }),
379            tags: vec![],
380            meta: HashMap::new(),
381        }
382    }
383
384    fn sample_ctx() -> ExecutionContext {
385        ExecutionContext {
386            trace: vec![],
387            variables: HashMap::new(),
388            policy: None,
389            summary: RuleTraceSummary::NoMatch,
390            state_store: Arc::new(InMemoryRuleStateStore::new()),
391        }
392    }
393
394    #[test]
395    fn test_parse_set_cookie_header_with_attributes() {
396        let c = parse_set_cookie_header("sid=abc; Path=/; Domain=example.com; HttpOnly; Secure; Expires=Wed, 21 Oct 2026 07:28:00 GMT")
397            .expect("cookie");
398        assert_eq!(c.name, "sid");
399        assert_eq!(c.value, "abc");
400        assert_eq!(c.path.as_deref(), Some("/"));
401        assert_eq!(c.domain.as_deref(), Some("example.com"));
402        assert_eq!(c.http_only, Some(true));
403        assert_eq!(c.secure, Some(true));
404        assert!(c.expires.is_some());
405    }
406
407    #[tokio::test]
408    async fn test_mock_response_extracts_set_cookie() {
409        let mut headers = HashMap::new();
410        headers.insert(
411            "Set-Cookie".to_string(),
412            "token=xyz; Path=/; HttpOnly".to_string(),
413        );
414        let action = Action::MockResponse {
415            status: 200,
416            headers,
417            body: None,
418        };
419
420        let mut flow = sample_flow();
421        let mut ctx = sample_ctx();
422        let out = execute(&action, &mut flow, &mut ctx).await;
423        assert!(matches!(out, ActionOutcome::Terminated(_)));
424
425        let Layer::Http(http) = flow.layer else {
426            panic!("expected http layer");
427        };
428        let res = http.response.expect("mocked response");
429        assert_eq!(res.cookies.len(), 1);
430        assert_eq!(res.cookies[0].name, "token");
431        assert_eq!(res.cookies[0].value, "xyz");
432        assert_eq!(res.cookies[0].path.as_deref(), Some("/"));
433        assert_eq!(res.cookies[0].http_only, Some(true));
434    }
435}