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                connect_time_ms: None,
129                ssl_time_ms: None,
130                },
131            };
132
133            if let Layer::Http(http) = &mut flow.layer {
134                http.response = Some(res);
135            } else if let Layer::WebSocket(ws) = &mut flow.layer {
136                ws.handshake_response = res;
137            }
138            ActionOutcome::Terminated(TerminalReason::Redirect)
139        }
140
141        // --- Request Header Actions ---
142        Action::AddRequestHeader { name, value } => {
143            let val = substitute_variables(value, flow, ctx, None);
144            if let Layer::Http(http) = &mut flow.layer {
145                http.request.headers.push((name.clone(), val));
146            }
147            ActionOutcome::Continue
148        }
149        Action::UpdateRequestHeader {
150            name,
151            value,
152            add_if_missing,
153        } => {
154            // First pass: find existing value to use as {{previous}}
155            let mut old_val = None;
156            if let Layer::Http(http) = &flow.layer {
157                 for (k, v) in &http.request.headers {
158                     if k.eq_ignore_ascii_case(name) {
159                         old_val = Some(v.clone());
160                         break;
161                     }
162                 }
163            }
164
165            // Compute new value
166            let new_val = substitute_variables(value, flow, ctx, old_val.as_deref());
167
168            // Second pass: apply change
169            if let Layer::Http(http) = &mut flow.layer {
170                let mut found = false;
171                for (k, v) in http.request.headers.iter_mut() {
172                    if k.eq_ignore_ascii_case(name) {
173                        *v = new_val.clone();
174                        found = true;
175                    }
176                }
177                if !found && *add_if_missing {
178                    http.request.headers.push((name.clone(), new_val));
179                }
180            }
181            ActionOutcome::Continue
182        }
183        Action::DeleteRequestHeader { name } => {
184            if let Layer::Http(http) = &mut flow.layer {
185                http.request.headers
186                    .retain(|(k, _)| !k.eq_ignore_ascii_case(name));
187            }
188            ActionOutcome::Continue
189        }
190
191        // --- Response Header Actions ---
192        Action::AddResponseHeader { name, value } => {
193            let val = substitute_variables(value, flow, ctx, None);
194            if let Layer::Http(http) = &mut flow.layer
195                && let Some(res) = &mut http.response {
196                    res.headers.push((name.clone(), val));
197                }
198            ActionOutcome::Continue
199        }
200        Action::UpdateResponseHeader { name, value, add_if_missing } => {
201            // First pass: find existing value
202            let mut old_val = None;
203            if let Layer::Http(http) = &flow.layer
204                 && let Some(res) = &http.response {
205                     for (k, v) in &res.headers {
206                         if k.eq_ignore_ascii_case(name) {
207                             old_val = Some(v.clone());
208                             break;
209                         }
210                     }
211                 }
212            
213            // Compute new value
214            let new_val = substitute_variables(value, flow, ctx, old_val.as_deref());
215
216            // Second pass: apply change
217            if let Layer::Http(http) = &mut flow.layer
218                && let Some(res) = &mut http.response {
219                    let mut found = false;
220                    for (k, v) in res.headers.iter_mut() {
221                        if k.eq_ignore_ascii_case(name) {
222                            *v = new_val.clone();
223                            found = true;
224                        }
225                    }
226                    if !found && *add_if_missing {
227                        res.headers.push((name.clone(), new_val));
228                    }
229                }
230            ActionOutcome::Continue
231        }
232         Action::DeleteResponseHeader { name } => {
233            if let Layer::Http(http) = &mut flow.layer
234                 && let Some(res) = &mut http.response {
235                    res.headers
236                    .retain(|(k, _)| !k.eq_ignore_ascii_case(name));
237                 }
238            ActionOutcome::Continue
239        }
240        
241        // --- Terminal Actions ---
242        Action::MockResponse { status, headers, body } => {
243             let body_data = if let Some(source) = body {
244                 resolve_body_source(source, ctx.policy.as_deref()).await
245             } else {
246                 None
247             };
248
249             if let Layer::Http(http) = &mut flow.layer {
250                 let mut res_headers = Vec::new();
251                 for (k, v) in headers {
252                     res_headers.push((k.clone(), v.clone()));
253                 }
254                 let cookies = parse_response_cookies(&res_headers);
255                 
256                 http.response = Some(HttpResponse {
257                    status: *status,
258                    status_text: status_text_for(*status),
259                    version: "HTTP/1.1".to_string(),
260                    headers: res_headers,
261                    cookies,
262                    body: body_data,
263                    timing: ResponseTiming {
264                        time_to_first_byte: Some(0),
265                        time_to_last_byte: Some(0),
266                connect_time_ms: None,
267                ssl_time_ms: None,
268                    },
269                 });
270             } else if let Layer::WebSocket(ws) = &mut flow.layer {
271                 let mut res_headers = Vec::new();
272                 for (k, v) in headers {
273                     res_headers.push((k.clone(), v.clone()));
274                 }
275                 let cookies = parse_response_cookies(&res_headers);
276                 
277                 ws.handshake_response = HttpResponse {
278                    status: *status,
279                    status_text: status_text_for(*status),
280                    version: "HTTP/1.1".to_string(),
281                    headers: res_headers,
282                    cookies,
283                    body: body_data,
284                    timing: ResponseTiming {
285                        time_to_first_byte: Some(0),
286                        time_to_last_byte: Some(0),
287                connect_time_ms: None,
288                ssl_time_ms: None,
289                    },
290                 };
291             }
292             ActionOutcome::Terminated(TerminalReason::Mock)
293        }
294        Action::MapLocal { path, content_type } => {
295            let body_data = resolve_body_source(&BodySource::File(path.clone()), ctx.policy.as_deref()).await;
296            
297            if let Some(body) = body_data {
298                 if let Layer::Http(http) = &mut flow.layer {
299                    let headers = content_type.as_ref()
300                        .map(|ct| vec![("Content-Type".to_string(), ct.clone())])
301                        .unwrap_or_default();
302
303                    http.response = Some(HttpResponse {
304                        status: 200,
305                        status_text: "OK".to_string(),
306                        version: "HTTP/1.1".to_string(),
307                        headers,
308                        cookies: vec![],
309                        body: Some(body),
310                        timing: ResponseTiming {
311                            time_to_first_byte: Some(0),
312                            time_to_last_byte: Some(0),
313                connect_time_ms: None,
314                ssl_time_ms: None,
315                        },
316                     });
317                }
318                ActionOutcome::Terminated(TerminalReason::Mock)
319            } else {
320                ActionOutcome::Failed(format!("Failed to load local file: {}", path))
321            }
322        }
323        
324        // --- Transformation Actions ---
325        Action::TransformRequestBody { transform } => {
326             if let Layer::Http(http) = &mut flow.layer
327                 && let Some(body) = &mut http.request.body {
328                     apply_transform(body, transform);
329                 }
330             ActionOutcome::Continue
331        }
332        Action::TransformResponseBody { transform } => {
333             if let Layer::Http(http) = &mut flow.layer
334                 && let Some(res) = &mut http.response
335                     && let Some(body) = &mut res.body {
336                         apply_transform(body, transform);
337                     }
338             ActionOutcome::Continue
339        }
340
341        _ => ActionOutcome::Failed(format!("Action {:?} not supported in http handler", action)),
342    }
343}
344
345#[cfg(test)]
346mod tests {
347    use super::{execute, parse_set_cookie_header};
348    use crate::rule::engine::actions::ActionOutcome;
349    use crate::rule::engine::executor::ExecutionContext;
350    use crate::rule::engine::state::InMemoryRuleStateStore;
351    use crate::rule::model::{Action, RuleTraceSummary};
352    use chrono::Utc;
353    use relay_core_api::flow::{Flow, HttpLayer, HttpRequest, Layer, NetworkInfo, TransportProtocol};
354    use std::collections::HashMap;
355    use std::sync::Arc;
356    use url::Url;
357    use uuid::Uuid;
358
359    fn sample_flow() -> Flow {
360        Flow {
361            id: Uuid::new_v4(),
362            start_time: Utc::now(),
363            end_time: None,
364            network: NetworkInfo {
365                client_ip: "127.0.0.1".to_string(),
366                client_port: 12345,
367                server_ip: "1.1.1.1".to_string(),
368                server_port: 443,
369                protocol: TransportProtocol::TCP,
370                tls: true,
371                tls_version: Some("TLS1.3".to_string()),
372                sni: Some("example.com".to_string()),
373            },
374            layer: Layer::Http(HttpLayer {
375                request: HttpRequest {
376                    method: "GET".to_string(),
377                    url: Url::parse("https://example.com/").expect("url"),
378                    version: "HTTP/1.1".to_string(),
379                    headers: vec![],
380                    cookies: vec![],
381                    query: vec![],
382                    body: None,
383                },
384                response: None,
385                error: None,
386            }),
387            tags: vec![],
388            meta: HashMap::new(),
389        }
390    }
391
392    fn sample_ctx() -> ExecutionContext {
393        ExecutionContext {
394            trace: vec![],
395            variables: HashMap::new(),
396            policy: None,
397            summary: RuleTraceSummary::NoMatch,
398            state_store: Arc::new(InMemoryRuleStateStore::new()),
399        }
400    }
401
402    #[test]
403    fn test_parse_set_cookie_header_with_attributes() {
404        let c = parse_set_cookie_header("sid=abc; Path=/; Domain=example.com; HttpOnly; Secure; Expires=Wed, 21 Oct 2026 07:28:00 GMT")
405            .expect("cookie");
406        assert_eq!(c.name, "sid");
407        assert_eq!(c.value, "abc");
408        assert_eq!(c.path.as_deref(), Some("/"));
409        assert_eq!(c.domain.as_deref(), Some("example.com"));
410        assert_eq!(c.http_only, Some(true));
411        assert_eq!(c.secure, Some(true));
412        assert!(c.expires.is_some());
413    }
414
415    #[tokio::test]
416    async fn test_mock_response_extracts_set_cookie() {
417        let mut headers = HashMap::new();
418        headers.insert(
419            "Set-Cookie".to_string(),
420            "token=xyz; Path=/; HttpOnly".to_string(),
421        );
422        let action = Action::MockResponse {
423            status: 200,
424            headers,
425            body: None,
426        };
427
428        let mut flow = sample_flow();
429        let mut ctx = sample_ctx();
430        let out = execute(&action, &mut flow, &mut ctx).await;
431        assert!(matches!(out, ActionOutcome::Terminated(_)));
432
433        let Layer::Http(http) = flow.layer else {
434            panic!("expected http layer");
435        };
436        let res = http.response.expect("mocked response");
437        assert_eq!(res.cookies.len(), 1);
438        assert_eq!(res.cookies[0].name, "token");
439        assert_eq!(res.cookies[0].value, "xyz");
440        assert_eq!(res.cookies[0].path.as_deref(), Some("/"));
441        assert_eq!(res.cookies[0].http_only, Some(true));
442    }
443}