1use std::collections::{BTreeMap, BTreeSet, HashMap};
39use std::sync::Arc;
40
41use crate::behavior::BehaviorRule;
42use crate::custom_checks::CustomValidatorFn;
43use crate::detection_exclusions::RouteDetectionExclusions;
44use crate::headers_auth::AuthVerifier;
45use crate::rate_limit::RouteRateLimits;
46
47#[derive(Clone)]
50pub struct RouteConfig {
51 pub rate_limit: Option<u32>,
53 pub rate_limit_window: Option<u64>,
55 pub ip_whitelist: Option<Vec<String>>,
57 pub ip_blacklist: Option<Vec<String>>,
59 pub blocked_countries: Option<Vec<String>>,
61 pub whitelist_countries: Option<Vec<String>>,
63 pub bypassed_checks: BTreeSet<String>,
65 pub require_https: bool,
67 pub auth_required: Option<String>,
69 pub custom_validators: Vec<CustomValidatorFn>,
71 pub blocked_user_agents: Vec<String>,
73 pub required_headers: BTreeMap<String, String>,
75 pub behavior_rules: Vec<BehaviorRule>,
77 pub block_cloud_providers: BTreeSet<String>,
79 pub max_request_size: Option<u64>,
81 pub allowed_content_types: Option<Vec<String>>,
83 pub time_restrictions: Option<BTreeMap<String, String>>,
85 pub enable_suspicious_detection: bool,
87 pub require_referrer: Option<Vec<String>>,
89 pub api_key_required: bool,
91 pub auth_verifier: Option<AuthVerifier>,
93 pub api_key_verifier: Option<AuthVerifier>,
95 pub api_key_header: Option<String>,
97 pub authorization_header_required: Option<String>,
99 pub geo_rate_limits: Option<BTreeMap<String, (u32, u64)>>,
101 pub excluded_detection_headers: Option<BTreeSet<String>>,
103 pub excluded_detection_params: Option<BTreeSet<String>>,
105 pub excluded_detection_body_fields: Option<BTreeSet<String>>,
107 pub enabled_detection_categories: Option<BTreeSet<String>>,
109 pub detection_scan_body: Option<bool>,
111}
112
113impl Default for RouteConfig {
114 fn default() -> Self {
117 Self {
118 rate_limit: None,
119 rate_limit_window: None,
120 ip_whitelist: None,
121 ip_blacklist: None,
122 blocked_countries: None,
123 whitelist_countries: None,
124 bypassed_checks: BTreeSet::new(),
125 require_https: false,
126 auth_required: None,
127 custom_validators: Vec::new(),
128 blocked_user_agents: Vec::new(),
129 required_headers: BTreeMap::new(),
130 behavior_rules: Vec::new(),
131 block_cloud_providers: BTreeSet::new(),
132 max_request_size: None,
133 allowed_content_types: None,
134 time_restrictions: None,
135 enable_suspicious_detection: true,
136 require_referrer: None,
137 api_key_required: false,
138 auth_verifier: None,
139 api_key_verifier: None,
140 api_key_header: None,
141 authorization_header_required: None,
142 geo_rate_limits: None,
143 excluded_detection_headers: None,
144 excluded_detection_params: None,
145 excluded_detection_body_fields: None,
146 enabled_detection_categories: None,
147 detection_scan_body: None,
148 }
149 }
150}
151
152impl RouteConfig {
153 pub fn rate_limits(
159 &self,
160 ) -> Result<Option<RouteRateLimits>, crate::rate_limit::RateLimitConfigError> {
161 if self.rate_limit.is_none() && self.geo_rate_limits.is_none() {
162 return Ok(None);
163 }
164 let geo = self.geo_rate_limits.as_ref().map(|geo| {
165 geo.iter()
166 .map(|(country, (requests, window))| {
167 (
168 country.clone(),
169 crate::rate_limit::RateLimitEntry {
170 requests: *requests,
171 window: *window,
172 },
173 )
174 })
175 .collect::<HashMap<_, _>>()
176 });
177 RouteRateLimits::new(self.rate_limit, self.rate_limit_window, geo).map(Some)
178 }
179
180 #[must_use]
183 pub fn detection_exclusions(&self) -> RouteDetectionExclusions {
184 RouteDetectionExclusions {
185 excluded_detection_headers: self
186 .excluded_detection_headers
187 .as_ref()
188 .map(|set| set.iter().cloned().collect()),
189 excluded_detection_params: self
190 .excluded_detection_params
191 .as_ref()
192 .map(|set| set.iter().cloned().collect()),
193 excluded_detection_body_fields: self
194 .excluded_detection_body_fields
195 .as_ref()
196 .map(|set| set.iter().cloned().collect()),
197 enabled_detection_categories: self
198 .enabled_detection_categories
199 .as_ref()
200 .map(|set| set.iter().cloned().collect()),
201 detection_scan_body: self.detection_scan_body,
202 }
203 }
204}
205
206pub type RouteConfigResolver = Arc<dyn Fn(&str, &str) -> Option<Arc<RouteConfig>> + Send + Sync>;
213
214#[cfg(test)]
215mod tests {
216 use super::*;
217
218 fn with(mutate: impl FnOnce(&mut RouteConfig)) -> RouteConfig {
221 let mut config = RouteConfig::default();
222 mutate(&mut config);
223 config
224 }
225
226 #[test]
227 fn defaults_inherit_everything() {
228 let config = RouteConfig::default();
229 assert_eq!(config.rate_limit, None);
230 assert_eq!(config.rate_limit_window, None);
231 assert_eq!(config.ip_whitelist, None);
232 assert_eq!(config.ip_blacklist, None);
233 assert_eq!(config.blocked_countries, None);
234 assert_eq!(config.whitelist_countries, None);
235 assert!(config.bypassed_checks.is_empty());
236 assert!(!config.require_https);
237 assert_eq!(config.auth_required, None);
238 assert!(config.custom_validators.is_empty());
239 assert!(config.blocked_user_agents.is_empty());
240 assert!(config.required_headers.is_empty());
241 assert!(config.behavior_rules.is_empty());
242 assert!(config.block_cloud_providers.is_empty());
243 assert_eq!(config.max_request_size, None);
244 assert_eq!(config.allowed_content_types, None);
245 assert_eq!(config.time_restrictions, None);
246 assert!(config.enable_suspicious_detection);
247 assert_eq!(config.require_referrer, None);
248 assert!(!config.api_key_required);
249 assert!(config.auth_verifier.is_none());
250 assert!(config.api_key_verifier.is_none());
251 assert_eq!(config.api_key_header, None);
252 assert_eq!(config.authorization_header_required, None);
253 assert_eq!(config.geo_rate_limits, None);
254 assert_eq!(config.excluded_detection_headers, None);
255 assert_eq!(config.excluded_detection_params, None);
256 assert_eq!(config.excluded_detection_body_fields, None);
257 assert_eq!(config.enabled_detection_categories, None);
258 assert_eq!(config.detection_scan_body, None);
259 }
260
261 #[test]
262 fn the_rate_limit_view_carries_both_knobs_or_nothing() {
263 let config = RouteConfig {
264 rate_limit_window: Some(60),
265 ..RouteConfig::default()
266 };
267 assert!(config.rate_limits().expect("valid").is_none());
269
270 let config = RouteConfig {
271 rate_limit: Some(5),
272 rate_limit_window: Some(60),
273 geo_rate_limits: Some(BTreeMap::from([(String::from("BR"), (10, 30))])),
274 ..RouteConfig::default()
275 };
276 let limits = config
277 .rate_limits()
278 .expect("valid")
279 .expect("tier configured");
280 assert_eq!(limits.rate_limit(), Some(5));
281 assert_eq!(limits.rate_limit_window(), Some(60));
282 assert_eq!(
283 limits.geo_rate_limits().map(std::collections::HashMap::len),
284 Some(1)
285 );
286 }
287
288 #[test]
289 fn the_detection_view_maps_the_five_knobs() {
290 let config = RouteConfig::default();
291 let view = config.detection_exclusions();
292 assert_eq!(view.excluded_detection_headers, None);
293 assert_eq!(view.excluded_detection_params, None);
294 assert_eq!(view.excluded_detection_body_fields, None);
295 assert_eq!(view.enabled_detection_categories, None);
296 assert_eq!(view.detection_scan_body, None);
297
298 let config = with(|config| {
299 config.excluded_detection_headers = Some(BTreeSet::from([String::from("x-api-key")]));
300 config.detection_scan_body = Some(false);
301 });
302 let view = config.detection_exclusions();
303 assert_eq!(
304 view.excluded_detection_headers,
305 Some(vec![String::from("x-api-key")])
306 );
307 assert_eq!(view.detection_scan_body, Some(false));
308 }
309
310 #[test]
311 fn a_zero_route_limit_is_rejected_by_the_engine_validator() {
312 let config = with(|config| config.rate_limit = Some(0));
313 assert!(config.rate_limits().is_err());
314 }
315
316 #[test]
317 fn the_resolver_seams_method_and_path() {
318 let resolver: RouteConfigResolver = Arc::new(|method, path| {
319 (method == "POST" && path == "/login").then(|| {
320 Arc::new(RouteConfig {
321 rate_limit: Some(5),
322 require_https: true,
323 ..RouteConfig::default()
324 })
325 })
326 });
327 let config = resolver("POST", "/login").expect("route config");
328 assert_eq!(config.rate_limit, Some(5));
329 assert!(config.require_https);
330 assert!(resolver("GET", "/login").is_none());
331 assert!(resolver("POST", "/signup").is_none());
332 }
333}