1use std::time::{SystemTime, UNIX_EPOCH};
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
7pub enum HttpMethod {
8 GET,
9 POST,
10 PUT,
11 DELETE,
12 PATCH,
13 HEAD,
14 OPTIONS,
15 CONNECT,
16 TRACE,
17 OTHER,
18}
19
20impl HttpMethod {
21 pub fn as_str(&self) -> &str {
22 match self {
23 Self::GET => "GET",
24 Self::POST => "POST",
25 Self::PUT => "PUT",
26 Self::DELETE => "DELETE",
27 Self::PATCH => "PATCH",
28 Self::HEAD => "HEAD",
29 Self::OPTIONS => "OPTIONS",
30 Self::CONNECT => "CONNECT",
31 Self::TRACE => "TRACE",
32 Self::OTHER => "OTHER",
33 }
34 }
35
36 pub fn from_str(s: &str) -> Option<Self> {
37 match s.to_uppercase().as_str() {
38 "GET" => Some(Self::GET),
39 "POST" => Some(Self::POST),
40 "PUT" => Some(Self::PUT),
41 "DELETE" => Some(Self::DELETE),
42 "PATCH" => Some(Self::PATCH),
43 "HEAD" => Some(Self::HEAD),
44 "OPTIONS" => Some(Self::OPTIONS),
45 "CONNECT" => Some(Self::CONNECT),
46 "TRACE" => Some(Self::TRACE),
47 _ => Some(Self::OTHER),
48 }
49 }
50}
51
52impl std::fmt::Display for HttpMethod {
53 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
54 write!(f, "{}", self.as_str())
55 }
56}
57
58impl From<&str> for HttpMethod {
59 fn from(s: &str) -> Self {
60 HttpMethod::from_str(s).unwrap_or_else(|| {
61 panic!("Invalid HTTP method: {}. Expected one of: GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS, CONNECT, TRACE", s)
62 })
63 }
64}
65
66impl From<&String> for HttpMethod {
67 fn from(s: &String) -> Self {
68 HttpMethod::from(s.as_str())
69 }
70}
71
72impl From<String> for HttpMethod {
73 fn from(s: String) -> Self {
74 HttpMethod::from(s.as_str())
75 }
76}
77
78#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
79pub enum Duration {
80 Seconds(u64),
81 Minutes(u64),
82 Hours(u64),
83 Days(u64),
84}
85
86impl Duration {
87 pub fn seconds(n: u64) -> Self {
88 Duration::Seconds(n)
89 }
90 pub fn minutes(n: u64) -> Self {
91 Duration::Minutes(n)
92 }
93 pub fn hours(n: u64) -> Self {
94 Duration::Hours(n)
95 }
96 pub fn days(n: u64) -> Self {
97 Duration::Days(n)
98 }
99
100 pub fn as_seconds(&self) -> u64 {
101 match self {
102 Duration::Seconds(n) => *n,
103 Duration::Minutes(n) => n * 60,
104 Duration::Hours(n) => n * 3600,
105 Duration::Days(n) => n * 86400,
106 }
107 }
108
109 pub fn is_short_interval(&self) -> bool {
110 self.as_seconds() <= 300 }
112}
113
114#[derive(Debug, Clone)]
115pub struct RuleConfig {
116 pub interval: Duration,
117 pub limit: u32,
118 pub is_prefix: bool,
119 pub methods: Option<Vec<HttpMethod>>,
120}
121
122impl RuleConfig {
123 pub fn new(interval: Duration, limit: u32) -> Self {
124 Self {
125 interval,
126 limit,
127 is_prefix: false,
128 methods: None,
129 }
130 }
131
132 pub fn match_prefix(mut self, is_prefix: bool) -> Self {
133 self.is_prefix = is_prefix;
134 self
135 }
136
137 pub fn for_methods(mut self, methods: Vec<HttpMethod>) -> Self {
138 self.methods = Some(methods);
139 self
140 }
141
142 pub fn matches_method(&self, method: &Option<HttpMethod>) -> bool {
144 match (&self.methods, method) {
145 (None, _) => true,
147 (Some(_), None) => false,
149 (Some(allowed), Some(m)) => allowed.contains(m),
151 }
152 }
153}
154
155#[derive(Debug, Clone)]
156pub struct RequestRecord {
157 pub count: u32,
158 pub window_start: u64,
159 pub timestamps: Vec<u64>,
160}
161
162impl RequestRecord {
163 pub fn new(is_short_interval: bool) -> Self {
164 Self {
165 count: 0,
166 window_start: current_timestamp(),
167 timestamps: if is_short_interval {
168 Vec::new()
169 } else {
170 Vec::with_capacity(16)
171 },
172 }
173 }
174
175 pub fn add_request(&mut self, is_short_interval: bool, window_size: u64) {
176 let now = current_timestamp();
177
178 if is_short_interval {
179 if now.saturating_sub(self.window_start) >= window_size {
180 self.window_start = now;
181 self.count = 1;
182 } else {
183 self.count += 1;
184 }
185 } else {
186 self.timestamps.push(now);
187 let cutoff = now.saturating_sub(window_size);
188 self.timestamps.retain(|&t| t > cutoff);
189 self.count = self.timestamps.len() as u32;
190 }
191 }
192
193 pub fn is_limit_exceeded(&self, limit: u32, is_short_interval: bool, window_size: u64) -> bool {
194 let now = current_timestamp();
195 if is_short_interval {
196 if now.saturating_sub(self.window_start) >= window_size {
197 false
198 } else {
199 self.count >= limit
200 }
201 } else {
202 let cutoff = now.saturating_sub(window_size);
203 let valid_requests = self.timestamps.iter().filter(|&&t| t > cutoff).count() as u32;
204 valid_requests >= limit
205 }
206 }
207
208 pub fn memory_usage(&self) -> usize {
209 std::mem::size_of::<Self>() + self.timestamps.capacity() * std::mem::size_of::<u64>()
210 }
211
212 pub fn should_cleanup(&self, max_age_seconds: u64) -> bool {
213 let now = current_timestamp();
214 let last_activity = if !self.timestamps.is_empty() {
215 *self.timestamps.last().unwrap_or(&self.window_start)
216 } else {
217 self.window_start
218 };
219 now.saturating_sub(last_activity) > max_age_seconds
220 }
221}
222
223pub fn current_timestamp() -> u64 {
224 SystemTime::now()
225 .duration_since(UNIX_EPOCH)
226 .expect("Time went backwards")
227 .as_secs()
228}