Skip to main content

camel_api/
delayer.rs

1use std::time::Duration;
2
3/// Default cap for header-derived delays (1 hour). A header value larger than
4/// this is clamped down to it before the cast to `u64`. The H12 vulnerability
5/// was: a header `CamelDelayMs: 1e20` cast to `u64` saturates to `u64::MAX`,
6/// then `Duration::from_millis(u64::MAX)` + `Instant` overflows and panics.
7pub const DEFAULT_MAX_DELAY_MS: u64 = 3_600_000;
8
9#[derive(Debug, Clone)]
10pub struct DelayConfig {
11    pub delay_ms: u64,
12    pub dynamic_header: Option<String>,
13    /// Hard cap for header-derived delay values. A header value larger than
14    /// this is clamped to `max_delay_ms` before the cast. Defaults to
15    /// `DEFAULT_MAX_DELAY_MS` (1 hour). The processor MUST consult this cap
16    /// before constructing the `Duration` it sleeps on.
17    pub max_delay_ms: u64,
18}
19
20impl DelayConfig {
21    pub fn new(delay_ms: u64) -> Self {
22        Self {
23            delay_ms,
24            dynamic_header: None,
25            max_delay_ms: DEFAULT_MAX_DELAY_MS,
26        }
27    }
28
29    pub fn with_dynamic_header(mut self, header: impl Into<String>) -> Self {
30        self.dynamic_header = Some(header.into());
31        self
32    }
33
34    /// Override the cap on header-derived delays. Pass a value larger than
35    /// `DEFAULT_MAX_DELAY_MS` only when the operator has a real reason to
36    /// sleep longer; the value is a hard ceiling, not a soft target.
37    pub fn with_max_delay_ms(mut self, max_ms: u64) -> Self {
38        self.max_delay_ms = max_ms;
39        self
40    }
41
42    pub fn from_duration(duration: Duration) -> Self {
43        Self {
44            delay_ms: duration.as_millis() as u64,
45            dynamic_header: None,
46            max_delay_ms: DEFAULT_MAX_DELAY_MS,
47        }
48    }
49
50    pub fn from_duration_with_header(duration: Duration, header: impl Into<String>) -> Self {
51        Self {
52            delay_ms: duration.as_millis() as u64,
53            dynamic_header: Some(header.into()),
54            max_delay_ms: DEFAULT_MAX_DELAY_MS,
55        }
56    }
57}
58
59#[cfg(test)]
60mod tests {
61    use super::*;
62
63    #[test]
64    fn new_sets_delay_no_header() {
65        let cfg = DelayConfig::new(500);
66        assert_eq!(cfg.delay_ms, 500);
67        assert!(cfg.dynamic_header.is_none());
68    }
69
70    #[test]
71    fn with_dynamic_header_sets_header() {
72        let cfg = DelayConfig::new(200).with_dynamic_header("X-Delay");
73        assert_eq!(cfg.delay_ms, 200);
74        assert_eq!(cfg.dynamic_header.as_deref(), Some("X-Delay"));
75    }
76
77    #[test]
78    fn from_duration_converts_ms() {
79        let cfg = DelayConfig::from_duration(Duration::from_millis(1500));
80        assert_eq!(cfg.delay_ms, 1500);
81        assert!(cfg.dynamic_header.is_none());
82    }
83
84    #[test]
85    fn from_duration_with_header() {
86        let cfg = DelayConfig::from_duration_with_header(Duration::from_secs(2), "MyHeader");
87        assert_eq!(cfg.delay_ms, 2000);
88        assert_eq!(cfg.dynamic_header.as_deref(), Some("MyHeader"));
89    }
90
91    #[test]
92    fn clone_preserves_values() {
93        let cfg = DelayConfig::new(100).with_dynamic_header("H");
94        let cloned = cfg.clone();
95        assert_eq!(cloned.delay_ms, 100);
96        assert_eq!(cloned.dynamic_header.as_deref(), Some("H"));
97    }
98
99    /// H12: `DelayConfig` carries a `max_delay_ms` field with a sensible
100    /// default. The default is 3_600_000 (1 hour) so a typo or a malicious
101    /// header value cannot pin a task to `u64::MAX` ms.
102    #[test]
103    fn test_delay_config_max_delay_ms_default() {
104        let cfg = DelayConfig::new(100);
105        assert_eq!(cfg.max_delay_ms, 3_600_000);
106    }
107
108    /// `with_max_delay_ms` overrides the default and is preserved by `clone`.
109    #[test]
110    fn test_delay_config_with_max_delay_ms() {
111        let cfg = DelayConfig::new(100).with_max_delay_ms(50);
112        assert_eq!(cfg.max_delay_ms, 50);
113        let cloned = cfg.clone();
114        assert_eq!(cloned.max_delay_ms, 50);
115    }
116}