1use crate::error::{Error, Result};
4
5#[derive(Debug, Clone)]
7pub enum Action {
8 Disruptive(DisruptiveAction),
10 Flow(FlowAction),
12 Metadata(MetadataAction),
14 Data(DataAction),
16 Logging(LoggingAction),
18 Control(ControlAction),
20 Transformation(String),
22}
23
24#[derive(Debug, Clone)]
26pub enum DisruptiveAction {
27 Deny,
29 Block,
31 Pass,
33 Allow,
35 AllowPhase,
37 AllowRequest,
39 Redirect(String),
41 Drop,
43}
44
45#[derive(Debug, Clone)]
47pub enum FlowAction {
48 Chain,
50 Skip(u32),
52 SkipAfter(String),
54 MultiMatch,
56}
57
58#[derive(Debug, Clone)]
60pub enum MetadataAction {
61 Id(u64),
63 Phase(u8),
65 Severity(u8),
67 Msg(String),
69 Tag(String),
71 Rev(String),
73 Ver(String),
75 Maturity(u8),
77 Accuracy(u8),
79 LogData(String),
81 Status(u16),
83}
84
85#[derive(Debug, Clone)]
87pub enum DataAction {
88 SetVar(SetVarSpec),
90 Capture,
92 InitCol { collection: String, key: String },
94 SetUid(String),
96 SetSid(String),
98 ExpireVar { var: String, seconds: u64 },
100 DeprecateVar(String),
102 Exec(String),
104 Prepend(String),
106 Append(String),
108}
109
110#[derive(Debug, Clone)]
112pub struct SetVarSpec {
113 pub collection: String,
115 pub key: String,
117 pub value: SetVarValue,
119}
120
121#[derive(Debug, Clone)]
123pub enum SetVarValue {
124 String(String),
126 Int(i64),
128 Increment(i64),
130 Decrement(i64),
132 Delete,
134}
135
136#[derive(Debug, Clone)]
138pub enum LoggingAction {
139 Log,
141 NoLog,
143 AuditLog,
145 NoAuditLog,
147 SanitiseMatched,
149 SanitizeMatched,
151 SanitiseArg(String),
153 SanitiseRequestHeader(String),
155 SanitiseResponseHeader(String),
157}
158
159#[derive(Debug, Clone)]
161pub struct ControlAction {
162 pub directive: String,
164 pub value: String,
166}
167
168fn normalize_line_continuations(input: &str) -> String {
171 let mut result = String::with_capacity(input.len());
172 let mut chars = input.chars().peekable();
173
174 while let Some(c) = chars.next() {
175 if c == '\\' {
176 if chars.peek() == Some(&'\n') {
178 chars.next();
180 while chars.peek().map(|c| c.is_whitespace() && *c != '\n').unwrap_or(false) {
182 chars.next();
183 }
184 continue;
185 } else if chars.peek() == Some(&'\r') {
186 chars.next();
188 if chars.peek() == Some(&'\n') {
189 chars.next();
190 }
191 while chars.peek().map(|c| c.is_whitespace() && *c != '\n').unwrap_or(false) {
193 chars.next();
194 }
195 continue;
196 }
197 }
198 result.push(c);
199 }
200
201 result
202}
203
204pub fn parse_actions(input: &str) -> Result<Vec<Action>> {
206 let normalized = normalize_line_continuations(input);
208
209 let mut actions = Vec::new();
210 let mut chars = normalized.chars().peekable();
211 let mut current = String::new();
212 let mut in_quotes = false;
213 let mut quote_char = '"';
214 let mut paren_depth: u32 = 0;
215
216 while let Some(c) = chars.next() {
217 match c {
218 '"' | '\'' if !in_quotes => {
219 in_quotes = true;
220 quote_char = c;
221 current.push(c);
222 }
223 c if in_quotes && c == quote_char => {
224 in_quotes = false;
225 current.push(c);
226 }
227 '(' if !in_quotes => {
228 paren_depth += 1;
229 current.push(c);
230 }
231 ')' if !in_quotes => {
232 paren_depth = paren_depth.saturating_sub(1);
233 current.push(c);
234 }
235 ',' if !in_quotes && paren_depth == 0 => {
236 if !current.trim().is_empty() {
237 actions.push(parse_single_action(current.trim())?);
238 }
239 current.clear();
240 }
241 _ => {
242 current.push(c);
243 }
244 }
245 }
246
247 if !current.trim().is_empty() {
249 actions.push(parse_single_action(current.trim())?);
250 }
251
252 Ok(actions)
253}
254
255fn parse_single_action(input: &str) -> Result<Action> {
257 let input = input.trim();
258
259 if input.starts_with("t:") {
261 return Ok(Action::Transformation(input[2..].to_string()));
262 }
263
264 let (name, argument) = if let Some(pos) = input.find(':') {
266 let name = &input[..pos];
267 let arg = &input[pos + 1..];
268 (name.to_lowercase(), Some(arg.to_string()))
269 } else {
270 (input.to_lowercase(), None)
271 };
272
273 match name.as_str() {
274 "deny" => Ok(Action::Disruptive(DisruptiveAction::Deny)),
276 "block" => Ok(Action::Disruptive(DisruptiveAction::Block)),
277 "pass" => Ok(Action::Disruptive(DisruptiveAction::Pass)),
278 "allow" => Ok(Action::Disruptive(DisruptiveAction::Allow)),
279 "drop" => Ok(Action::Disruptive(DisruptiveAction::Drop)),
280 "redirect" => {
281 let url = argument.ok_or_else(|| Error::InvalidActionArgument {
282 action: "redirect".to_string(),
283 message: "missing URL".to_string(),
284 })?;
285 Ok(Action::Disruptive(DisruptiveAction::Redirect(url)))
286 }
287
288 "chain" => Ok(Action::Flow(FlowAction::Chain)),
290 "skip" => {
291 let count: u32 = argument
292 .as_ref()
293 .and_then(|s| s.parse().ok())
294 .ok_or_else(|| Error::InvalidActionArgument {
295 action: "skip".to_string(),
296 message: "invalid count".to_string(),
297 })?;
298 Ok(Action::Flow(FlowAction::Skip(count)))
299 }
300 "skipafter" => {
301 let marker = argument.ok_or_else(|| Error::InvalidActionArgument {
302 action: "skipAfter".to_string(),
303 message: "missing marker name".to_string(),
304 })?;
305 Ok(Action::Flow(FlowAction::SkipAfter(marker)))
306 }
307
308 "id" => {
310 let id: u64 = argument
311 .as_ref()
312 .and_then(|s| s.parse().ok())
313 .ok_or_else(|| Error::InvalidActionArgument {
314 action: "id".to_string(),
315 message: "invalid ID".to_string(),
316 })?;
317 Ok(Action::Metadata(MetadataAction::Id(id)))
318 }
319 "phase" => {
320 let phase: u8 = argument
321 .as_ref()
322 .and_then(|s| s.parse().ok())
323 .ok_or_else(|| Error::InvalidActionArgument {
324 action: "phase".to_string(),
325 message: "invalid phase".to_string(),
326 })?;
327 Ok(Action::Metadata(MetadataAction::Phase(phase)))
328 }
329 "severity" => {
330 let sev: u8 = argument
331 .as_ref()
332 .map(|s| s.trim_matches(|c| c == '\'' || c == '"'))
333 .and_then(|s| parse_severity(s))
334 .ok_or_else(|| Error::InvalidActionArgument {
335 action: "severity".to_string(),
336 message: "invalid severity".to_string(),
337 })?;
338 Ok(Action::Metadata(MetadataAction::Severity(sev)))
339 }
340 "msg" => {
341 let msg = argument.unwrap_or_default();
342 let msg = msg.trim_matches(|c| c == '\'' || c == '"');
344 Ok(Action::Metadata(MetadataAction::Msg(msg.to_string())))
345 }
346 "tag" => {
347 let tag = argument.unwrap_or_default();
348 let tag = tag.trim_matches(|c| c == '\'' || c == '"');
349 Ok(Action::Metadata(MetadataAction::Tag(tag.to_string())))
350 }
351 "rev" => {
352 let rev = argument.unwrap_or_default();
353 let rev = rev.trim_matches(|c| c == '\'' || c == '"');
354 Ok(Action::Metadata(MetadataAction::Rev(rev.to_string())))
355 }
356 "ver" => {
357 let ver = argument.unwrap_or_default();
358 let ver = ver.trim_matches(|c| c == '\'' || c == '"');
359 Ok(Action::Metadata(MetadataAction::Ver(ver.to_string())))
360 }
361 "maturity" => {
362 let mat: u8 = argument
363 .as_ref()
364 .and_then(|s| s.parse().ok())
365 .ok_or_else(|| Error::InvalidActionArgument {
366 action: "maturity".to_string(),
367 message: "invalid maturity".to_string(),
368 })?;
369 Ok(Action::Metadata(MetadataAction::Maturity(mat)))
370 }
371 "accuracy" => {
372 let acc: u8 = argument
373 .as_ref()
374 .and_then(|s| s.parse().ok())
375 .ok_or_else(|| Error::InvalidActionArgument {
376 action: "accuracy".to_string(),
377 message: "invalid accuracy".to_string(),
378 })?;
379 Ok(Action::Metadata(MetadataAction::Accuracy(acc)))
380 }
381 "logdata" => {
382 let data = argument.unwrap_or_default();
383 let data = data.trim_matches(|c| c == '\'' || c == '"');
384 Ok(Action::Metadata(MetadataAction::LogData(data.to_string())))
385 }
386 "status" => {
387 let status: u16 = argument
388 .as_ref()
389 .and_then(|s| s.parse().ok())
390 .ok_or_else(|| Error::InvalidActionArgument {
391 action: "status".to_string(),
392 message: "invalid status code".to_string(),
393 })?;
394 Ok(Action::Metadata(MetadataAction::Status(status)))
395 }
396
397 "setvar" => {
399 let spec = argument.ok_or_else(|| Error::InvalidActionArgument {
400 action: "setvar".to_string(),
401 message: "missing variable specification".to_string(),
402 })?;
403 let setvar = parse_setvar(&spec)?;
404 Ok(Action::Data(DataAction::SetVar(setvar)))
405 }
406 "capture" => Ok(Action::Data(DataAction::Capture)),
407
408 "log" => Ok(Action::Logging(LoggingAction::Log)),
410 "nolog" => Ok(Action::Logging(LoggingAction::NoLog)),
411 "auditlog" => Ok(Action::Logging(LoggingAction::AuditLog)),
412 "noauditlog" => Ok(Action::Logging(LoggingAction::NoAuditLog)),
413 "sanitisematched" | "sanitizematched" => Ok(Action::Logging(LoggingAction::SanitiseMatched)),
414
415 "ctl" => {
417 let spec = argument.ok_or_else(|| Error::InvalidActionArgument {
418 action: "ctl".to_string(),
419 message: "missing control specification".to_string(),
420 })?;
421 let (directive, value) = if let Some(pos) = spec.find('=') {
422 (spec[..pos].to_string(), spec[pos + 1..].to_string())
423 } else {
424 (spec, String::new())
425 };
426 Ok(Action::Control(ControlAction { directive, value }))
427 }
428
429 "initcol" => {
431 let spec = argument.ok_or_else(|| Error::InvalidActionArgument {
432 action: "initcol".to_string(),
433 message: "missing collection specification".to_string(),
434 })?;
435 let (collection, key) = if let Some(pos) = spec.find('=') {
436 (spec[..pos].to_string(), spec[pos + 1..].to_string())
437 } else {
438 (spec, String::new())
439 };
440 Ok(Action::Data(DataAction::InitCol { collection, key }))
441 }
442
443 "setsid" | "setuid" => {
445 Ok(Action::Logging(LoggingAction::NoAuditLog)) }
448
449 "deprecatevar" => {
451 Ok(Action::Logging(LoggingAction::NoAuditLog)) }
453
454 "expirevar" => {
456 let spec = argument.unwrap_or_default();
457 let (var, seconds) = if let Some(pos) = spec.find('=') {
458 let var = spec[..pos].to_string();
459 let secs: u64 = spec[pos + 1..].parse().unwrap_or(0);
460 (var, secs)
461 } else {
462 (spec, 0)
463 };
464 Ok(Action::Data(DataAction::ExpireVar { var, seconds }))
465 }
466
467 "multimatch" => Ok(Action::Flow(FlowAction::MultiMatch)),
469
470 "exec" => Ok(Action::Logging(LoggingAction::NoAuditLog)), "append" | "prepend" => Ok(Action::Logging(LoggingAction::NoAuditLog)), "proxy" => Ok(Action::Logging(LoggingAction::NoAuditLog)), "pause" => Ok(Action::Logging(LoggingAction::NoAuditLog)), "xmlns" => Ok(Action::Logging(LoggingAction::NoAuditLog)), _ => Err(Error::UnknownAction {
486 name: name.to_string(),
487 }),
488 }
489}
490
491fn parse_setvar(input: &str) -> Result<SetVarSpec> {
493 let input = input.trim();
494
495 if input.starts_with('!') {
497 let var = &input[1..];
498 let (collection, key) = parse_var_name(var)?;
499 return Ok(SetVarSpec {
500 collection,
501 key,
502 value: SetVarValue::Delete,
503 });
504 }
505
506 let (var, value_str) = if let Some(pos) = input.find('=') {
508 (&input[..pos], Some(&input[pos + 1..]))
509 } else {
510 (input, None)
511 };
512
513 let (collection, key) = parse_var_name(var)?;
514
515 let value = if let Some(val) = value_str {
516 if val.starts_with('+') {
517 let amount: i64 = val[1..].parse().unwrap_or(1);
519 SetVarValue::Increment(amount)
520 } else if val.starts_with('-') {
521 let amount: i64 = val[1..].parse().unwrap_or(1);
523 SetVarValue::Decrement(amount)
524 } else if let Ok(n) = val.parse::<i64>() {
525 SetVarValue::Int(n)
526 } else {
527 SetVarValue::String(val.to_string())
528 }
529 } else {
530 SetVarValue::String("1".to_string())
531 };
532
533 Ok(SetVarSpec {
534 collection,
535 key,
536 value,
537 })
538}
539
540fn parse_var_name(input: &str) -> Result<(String, String)> {
542 if let Some(pos) = input.find('.') {
543 Ok((input[..pos].to_lowercase(), input[pos + 1..].to_string()))
544 } else {
545 Ok(("tx".to_string(), input.to_string()))
547 }
548}
549
550fn parse_severity(s: &str) -> Option<u8> {
552 if let Ok(n) = s.parse::<u8>() {
554 return Some(n);
555 }
556
557 match s.to_lowercase().as_str() {
559 "emergency" => Some(0),
560 "alert" => Some(1),
561 "critical" => Some(2),
562 "error" => Some(3),
563 "warning" => Some(4),
564 "notice" => Some(5),
565 "info" => Some(6),
566 "debug" => Some(7),
567 _ => None,
568 }
569}
570
571#[cfg(test)]
572mod tests {
573 use super::*;
574
575 #[test]
576 fn test_parse_simple_actions() {
577 let actions = parse_actions("id:1,deny,status:403").unwrap();
578 assert_eq!(actions.len(), 3);
579 }
580
581 #[test]
582 fn test_parse_action_with_msg() {
583 let actions = parse_actions("id:1,msg:'Hello world',deny").unwrap();
584 assert_eq!(actions.len(), 3);
585 }
586
587 #[test]
588 fn test_parse_setvar() {
589 let actions = parse_actions("setvar:tx.score=+5").unwrap();
590 assert_eq!(actions.len(), 1);
591 match &actions[0] {
592 Action::Data(DataAction::SetVar(spec)) => {
593 assert_eq!(spec.collection, "tx");
594 assert_eq!(spec.key, "score");
595 assert!(matches!(spec.value, SetVarValue::Increment(5)));
596 }
597 _ => panic!("expected SetVar"),
598 }
599 }
600
601 #[test]
602 fn test_parse_chain() {
603 let actions = parse_actions("id:1,phase:2,chain").unwrap();
604 assert!(actions.iter().any(|a| matches!(a, Action::Flow(FlowAction::Chain))));
605 }
606
607 #[test]
608 fn test_parse_transformation() {
609 let actions = parse_actions("id:1,t:lowercase,t:urlDecode").unwrap();
610 let transforms: Vec<_> = actions
611 .iter()
612 .filter(|a| matches!(a, Action::Transformation(_)))
613 .collect();
614 assert_eq!(transforms.len(), 2);
615 }
616}