1use std::collections::{BTreeMap, BTreeSet, HashMap};
46use std::sync::Arc;
47
48use crate::behavior::BehaviorRule;
49use crate::custom_checks::CustomValidatorFn;
50use crate::detection_exclusions::RouteDetectionExclusions;
51use crate::headers_auth::AuthVerifier;
52use crate::rate_limit::RouteRateLimits;
53
54#[derive(Clone)]
57pub struct RouteConfig {
58 pub rate_limit: Option<u32>,
60 pub rate_limit_window: Option<u64>,
62 pub ip_whitelist: Option<Vec<String>>,
64 pub ip_blacklist: Option<Vec<String>>,
66 pub blocked_countries: Option<Vec<String>>,
68 pub whitelist_countries: Option<Vec<String>>,
70 pub bypassed_checks: BTreeSet<String>,
72 pub require_https: bool,
74 pub auth_required: Option<String>,
76 pub custom_validators: Vec<CustomValidatorFn>,
78 pub blocked_user_agents: Vec<String>,
80 pub required_headers: BTreeMap<String, String>,
82 pub behavior_rules: Vec<BehaviorRule>,
84 pub block_cloud_providers: BTreeSet<String>,
86 pub max_request_size: Option<u64>,
88 pub allowed_content_types: Option<Vec<String>>,
90 pub time_restrictions: Option<BTreeMap<String, String>>,
92 pub enable_suspicious_detection: bool,
94 pub require_referrer: Option<Vec<String>>,
96 pub api_key_required: bool,
98 pub auth_verifier: Option<AuthVerifier>,
100 pub api_key_verifier: Option<AuthVerifier>,
102 pub api_key_header: Option<String>,
104 pub authorization_header_required: Option<String>,
106 pub geo_rate_limits: Option<BTreeMap<String, (u32, u64)>>,
108 pub excluded_detection_headers: Option<BTreeSet<String>>,
110 pub excluded_semantic_detection_headers: Option<BTreeSet<String>>,
112 pub excluded_detection_params: Option<BTreeSet<String>>,
114 pub excluded_detection_body_fields: Option<BTreeSet<String>>,
116 pub enabled_detection_categories: Option<BTreeSet<String>>,
118 pub detection_scan_body: Option<bool>,
120}
121
122impl Default for RouteConfig {
123 fn default() -> Self {
126 Self {
127 rate_limit: None,
128 rate_limit_window: None,
129 ip_whitelist: None,
130 ip_blacklist: None,
131 blocked_countries: None,
132 whitelist_countries: None,
133 bypassed_checks: BTreeSet::new(),
134 require_https: false,
135 auth_required: None,
136 custom_validators: Vec::new(),
137 blocked_user_agents: Vec::new(),
138 required_headers: BTreeMap::new(),
139 behavior_rules: Vec::new(),
140 block_cloud_providers: BTreeSet::new(),
141 max_request_size: None,
142 allowed_content_types: None,
143 time_restrictions: None,
144 enable_suspicious_detection: true,
145 require_referrer: None,
146 api_key_required: false,
147 auth_verifier: None,
148 api_key_verifier: None,
149 api_key_header: None,
150 authorization_header_required: None,
151 geo_rate_limits: None,
152 excluded_detection_headers: None,
153 excluded_semantic_detection_headers: None,
154 excluded_detection_params: None,
155 excluded_detection_body_fields: None,
156 enabled_detection_categories: None,
157 detection_scan_body: None,
158 }
159 }
160}
161
162impl RouteConfig {
163 pub fn rate_limits(
169 &self,
170 ) -> Result<Option<RouteRateLimits>, crate::rate_limit::RateLimitConfigError> {
171 if self.rate_limit.is_none() && self.geo_rate_limits.is_none() {
172 return Ok(None);
173 }
174 let geo = self.geo_rate_limits.as_ref().map(|geo| {
175 geo.iter()
176 .map(|(country, (requests, window))| {
177 (
178 country.clone(),
179 crate::rate_limit::RateLimitEntry {
180 requests: *requests,
181 window: *window,
182 },
183 )
184 })
185 .collect::<HashMap<_, _>>()
186 });
187 RouteRateLimits::new(self.rate_limit, self.rate_limit_window, geo).map(Some)
188 }
189
190 #[must_use]
193 pub fn detection_exclusions(&self) -> RouteDetectionExclusions {
194 RouteDetectionExclusions {
195 excluded_detection_headers: self
196 .excluded_detection_headers
197 .as_ref()
198 .map(|set| set.iter().cloned().collect()),
199 excluded_semantic_detection_headers: self
200 .excluded_semantic_detection_headers
201 .as_ref()
202 .map(|set| set.iter().cloned().collect()),
203 excluded_detection_params: self
204 .excluded_detection_params
205 .as_ref()
206 .map(|set| set.iter().cloned().collect()),
207 excluded_detection_body_fields: self
208 .excluded_detection_body_fields
209 .as_ref()
210 .map(|set| set.iter().cloned().collect()),
211 enabled_detection_categories: self
212 .enabled_detection_categories
213 .as_ref()
214 .map(|set| set.iter().cloned().collect()),
215 detection_scan_body: self.detection_scan_body,
216 }
217 }
218}
219
220pub type RouteConfigResolver = Arc<dyn Fn(&str, &str) -> Option<Arc<RouteConfig>> + Send + Sync>;
227
228pub const UNRESOLVED_ROUTE_STATUS: u16 = 500;
230
231pub const UNRESOLVED_ROUTE_MESSAGE: &str = "Route resolution failed";
234
235pub const UNRESOLVED_ROUTE_REASON: &str =
238 "Route resolution failed; per-route decorator config could not be applied";
239
240pub const UNRESOLVED_ROUTE_CHECK_NAME: &str = "route_config";
243
244#[cfg(test)]
245mod tests {
246 use super::*;
247
248 fn with(mutate: impl FnOnce(&mut RouteConfig)) -> RouteConfig {
251 let mut config = RouteConfig::default();
252 mutate(&mut config);
253 config
254 }
255
256 #[test]
257 fn defaults_inherit_everything() {
258 let config = RouteConfig::default();
259 assert_eq!(config.rate_limit, None);
260 assert_eq!(config.rate_limit_window, None);
261 assert_eq!(config.ip_whitelist, None);
262 assert_eq!(config.ip_blacklist, None);
263 assert_eq!(config.blocked_countries, None);
264 assert_eq!(config.whitelist_countries, None);
265 assert!(config.bypassed_checks.is_empty());
266 assert!(!config.require_https);
267 assert_eq!(config.auth_required, None);
268 assert!(config.custom_validators.is_empty());
269 assert!(config.blocked_user_agents.is_empty());
270 assert!(config.required_headers.is_empty());
271 assert!(config.behavior_rules.is_empty());
272 assert!(config.block_cloud_providers.is_empty());
273 assert_eq!(config.max_request_size, None);
274 assert_eq!(config.allowed_content_types, None);
275 assert_eq!(config.time_restrictions, None);
276 assert!(config.enable_suspicious_detection);
277 assert_eq!(config.require_referrer, None);
278 assert!(!config.api_key_required);
279 assert!(config.auth_verifier.is_none());
280 assert!(config.api_key_verifier.is_none());
281 assert_eq!(config.api_key_header, None);
282 assert_eq!(config.authorization_header_required, None);
283 assert_eq!(config.geo_rate_limits, None);
284 assert_eq!(config.excluded_detection_headers, None);
285 assert_eq!(config.excluded_detection_params, None);
286 assert_eq!(config.excluded_detection_body_fields, None);
287 assert_eq!(config.enabled_detection_categories, None);
288 assert_eq!(config.detection_scan_body, None);
289 }
290
291 #[test]
292 fn the_rate_limit_view_carries_both_knobs_or_nothing() {
293 let config = RouteConfig {
294 rate_limit_window: Some(60),
295 ..RouteConfig::default()
296 };
297 assert!(config.rate_limits().expect("valid").is_none());
299
300 let config = RouteConfig {
301 rate_limit: Some(5),
302 rate_limit_window: Some(60),
303 geo_rate_limits: Some(BTreeMap::from([(String::from("BR"), (10, 30))])),
304 ..RouteConfig::default()
305 };
306 let limits = config
307 .rate_limits()
308 .expect("valid")
309 .expect("tier configured");
310 assert_eq!(limits.rate_limit(), Some(5));
311 assert_eq!(limits.rate_limit_window(), Some(60));
312 assert_eq!(
313 limits.geo_rate_limits().map(std::collections::HashMap::len),
314 Some(1)
315 );
316 }
317
318 #[test]
319 fn the_detection_view_maps_the_five_knobs() {
320 let config = RouteConfig::default();
321 let view = config.detection_exclusions();
322 assert_eq!(view.excluded_detection_headers, None);
323 assert_eq!(view.excluded_detection_params, None);
324 assert_eq!(view.excluded_detection_body_fields, None);
325 assert_eq!(view.enabled_detection_categories, None);
326 assert_eq!(view.detection_scan_body, None);
327
328 let config = with(|config| {
329 config.excluded_detection_headers = Some(BTreeSet::from([String::from("x-api-key")]));
330 config.detection_scan_body = Some(false);
331 });
332 let view = config.detection_exclusions();
333 assert_eq!(
334 view.excluded_detection_headers,
335 Some(vec![String::from("x-api-key")])
336 );
337 assert_eq!(view.detection_scan_body, Some(false));
338 }
339
340 #[test]
341 fn a_zero_route_limit_is_rejected_by_the_engine_validator() {
342 let config = with(|config| config.rate_limit = Some(0));
343 assert!(config.rate_limits().is_err());
344 }
345
346 #[test]
347 fn the_resolver_seams_method_and_path() {
348 let resolver: RouteConfigResolver = Arc::new(|method, path| {
349 (method == "POST" && path == "/login").then(|| {
350 Arc::new(RouteConfig {
351 rate_limit: Some(5),
352 require_https: true,
353 ..RouteConfig::default()
354 })
355 })
356 });
357 let config = resolver("POST", "/login").expect("route config");
358 assert_eq!(config.rate_limit, Some(5));
359 assert!(config.require_https);
360 assert!(resolver("GET", "/login").is_none());
361 assert!(resolver("POST", "/signup").is_none());
362 }
363}