1use std::sync::Arc;
22
23use waf_core::{Config, Decision, Phase, RequestContext, WafModule};
24
25use crate::{CaptureConfig, CaptureRing, CapturedRequest, RedactionConfig};
26
27pub struct CaptureModule {
31 enabled: bool,
32 max_body_bytes: usize,
33 redaction: RedactionConfig,
34 ring: Arc<CaptureRing>,
35}
36
37impl CaptureModule {
38 pub fn new(cfg: &CaptureConfig, ring: Arc<CaptureRing>) -> Self {
40 Self {
41 enabled: cfg.enabled,
42 max_body_bytes: cfg.max_body_bytes,
43 redaction: cfg.redaction.clone(),
44 ring,
45 }
46 }
47}
48
49impl WafModule for CaptureModule {
50 fn id(&self) -> &str {
51 "request-capture"
52 }
53
54 fn phase(&self) -> Phase {
55 Phase::RequestLine
56 }
57
58 fn structural(&self) -> bool {
60 true
61 }
62
63 fn init(&mut self, _cfg: &Config) {}
64
65 fn inspect(&self, ctx: &RequestContext) -> Decision {
66 if self.enabled {
67 let capture = CapturedRequest::from_ctx(ctx, self.max_body_bytes, &self.redaction);
68 self.ring.insert(capture);
69 }
70 Decision::Allow
72 }
73}
74
75#[cfg(test)]
76mod tests {
77 use super::*;
78 use std::time::Duration;
79 use waf_core::testkit::Request;
80
81 fn ring() -> Arc<CaptureRing> {
82 Arc::new(CaptureRing::new(16, 1 << 20, Duration::from_secs(5)))
83 }
84
85 #[test]
86 fn enabled_module_captures_into_ring_and_allows() {
87 let ring = ring();
88 let cfg = CaptureConfig { enabled: true, ..Default::default() };
89 let m = CaptureModule::new(&cfg, Arc::clone(&ring));
90
91 let ctx = Request::new().request_id("req-1").method("POST").path("/x").json_body("{}").build();
92 assert!(matches!(m.inspect(&ctx), Decision::Allow), "observer never denies");
93
94 let cap = ring.take("req-1").expect("captured");
95 assert_eq!(cap.method, "POST");
96 assert_eq!(cap.path, "/x");
97 }
98
99 #[test]
100 fn disabled_module_captures_nothing() {
101 let ring = ring();
102 let cfg = CaptureConfig { enabled: false, ..Default::default() };
103 let m = CaptureModule::new(&cfg, Arc::clone(&ring));
104
105 let ctx = Request::new().request_id("req-2").build();
106 assert!(matches!(m.inspect(&ctx), Decision::Allow));
107 assert!(ring.is_empty(), "disabled → nothing captured");
108 }
109
110 #[test]
111 fn module_metadata_is_structural_requestline() {
112 let m = CaptureModule::new(&CaptureConfig::default(), ring());
113 assert_eq!(m.id(), "request-capture");
114 assert_eq!(m.phase(), Phase::RequestLine);
115 assert!(m.structural(), "must run on the content fast-path skip");
116 }
117}