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 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 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 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 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 let new_val = substitute_variables(value, flow, ctx, old_val.as_deref());
167
168 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 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 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 let new_val = substitute_variables(value, flow, ctx, old_val.as_deref());
215
216 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 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 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}