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