ch_config/config/
captcha.rs1use super::{ConfigError, Result, variables};
2
3#[derive(Clone, Debug, Default, PartialEq)]
5pub struct CaptchaConfig {
6 pub charset: String,
8 pub noise_level: f32,
10 pub expires_after_secs: u64,
12 pub max_attempts_per_minute: u32,
14 pub max_failures: u32,
20 pub bypass: bool,
22}
23
24impl CaptchaConfig {
25 pub fn standard() -> Self {
27 Self {
28 charset: "ABCDEFGHJKLMNPQRSTUVWXYZ23456789".to_owned(),
29 noise_level: 0.35,
30 expires_after_secs: 300,
31 max_attempts_per_minute: 12,
32 max_failures: 3,
33 bypass: false,
34 }
35 }
36
37 pub(crate) fn apply_environment(
38 &mut self,
39 value: &mut impl FnMut(&'static str) -> Result<Option<String>>,
40 ) -> Result<()> {
41 if let Some(charset) = value("CAPTCHA_CHARSET")? {
42 self.charset = charset;
43 }
44 if let Some(noise_level) = variables::parsed(
45 "CAPTCHA_NOISE_LEVEL",
46 value("CAPTCHA_NOISE_LEVEL")?,
47 "must be a number between zero and one",
48 )? {
49 self.noise_level = noise_level;
50 }
51 if let Some(expires_after_secs) = variables::parsed(
52 "CAPTCHA_EXPIRES_AFTER_SECS",
53 value("CAPTCHA_EXPIRES_AFTER_SECS")?,
54 "must be a positive integer number of seconds",
55 )? {
56 self.expires_after_secs = expires_after_secs;
57 }
58 if let Some(max_attempts_per_minute) = variables::parsed(
59 "CAPTCHA_MAX_ATTEMPTS_PER_MINUTE",
60 value("CAPTCHA_MAX_ATTEMPTS_PER_MINUTE")?,
61 "must be a positive integer",
62 )? {
63 self.max_attempts_per_minute = max_attempts_per_minute;
64 }
65 if let Some(max_failures) = variables::parsed(
66 "CAPTCHA_MAX_FAILURES",
67 value("CAPTCHA_MAX_FAILURES")?,
68 "must be a positive integer",
69 )? {
70 self.max_failures = max_failures;
71 }
72 if let Some(bypass) = variables::parsed(
73 "CAPTCHA_BYPASS",
74 value("CAPTCHA_BYPASS")?,
75 "must be either `true` or `false`",
76 )? {
77 self.bypass = bypass;
78 }
79 Ok(())
80 }
81
82 pub fn is_configured(&self) -> bool {
84 !self.charset.is_empty()
85 || self.noise_level != 0.0
86 || self.expires_after_secs != 0
87 || self.max_attempts_per_minute != 0
88 || self.max_failures != 0
89 || self.bypass
90 }
91
92 pub fn validate(&self) -> Result<()> {
94 if !self.is_configured() {
95 return Ok(());
96 }
97 if self.charset.trim().is_empty() {
98 return Err(ConfigError::validation(
99 "captcha.charset",
100 "must not be empty",
101 ));
102 }
103 if !(0.0..=1.0).contains(&self.noise_level) {
104 return Err(ConfigError::validation(
105 "captcha.noise_level",
106 "must be between zero and one",
107 ));
108 }
109 for (field, value) in [
110 ("captcha.expires_after_secs", self.expires_after_secs),
111 (
112 "captcha.max_attempts_per_minute",
113 u64::from(self.max_attempts_per_minute),
114 ),
115 ("captcha.max_failures", u64::from(self.max_failures)),
116 ] {
117 if value == 0 {
118 return Err(ConfigError::validation(field, "must be greater than zero"));
119 }
120 }
121 Ok(())
122 }
123}
124
125#[cfg(test)]
126mod tests {
127 use super::*;
128
129 #[test]
130 fn standard_policy_is_complete_and_safe() {
131 let policy = CaptchaConfig::standard();
132 policy.validate().expect("standard policy should validate");
133 assert!(!policy.bypass);
134 assert!(!policy.charset.contains(['I', 'O', '0', '1']));
135 }
136}