use super::{ConfigError, Result, variables};
#[derive(Clone, Debug, Default, PartialEq)]
pub struct CaptchaConfig {
pub charset: String,
pub noise_level: f32,
pub expires_after_secs: u64,
pub max_attempts_per_minute: u32,
pub max_failures: u32,
pub bypass: bool,
}
impl CaptchaConfig {
pub fn standard() -> Self {
Self {
charset: "ABCDEFGHJKLMNPQRSTUVWXYZ23456789".to_owned(),
noise_level: 0.35,
expires_after_secs: 300,
max_attempts_per_minute: 12,
max_failures: 3,
bypass: false,
}
}
pub(crate) fn apply_environment(
&mut self,
value: &mut impl FnMut(&'static str) -> Result<Option<String>>,
) -> Result<()> {
if let Some(charset) = value("CAPTCHA_CHARSET")? {
self.charset = charset;
}
if let Some(noise_level) = variables::parsed(
"CAPTCHA_NOISE_LEVEL",
value("CAPTCHA_NOISE_LEVEL")?,
"must be a number between zero and one",
)? {
self.noise_level = noise_level;
}
if let Some(expires_after_secs) = variables::parsed(
"CAPTCHA_EXPIRES_AFTER_SECS",
value("CAPTCHA_EXPIRES_AFTER_SECS")?,
"must be a positive integer number of seconds",
)? {
self.expires_after_secs = expires_after_secs;
}
if let Some(max_attempts_per_minute) = variables::parsed(
"CAPTCHA_MAX_ATTEMPTS_PER_MINUTE",
value("CAPTCHA_MAX_ATTEMPTS_PER_MINUTE")?,
"must be a positive integer",
)? {
self.max_attempts_per_minute = max_attempts_per_minute;
}
if let Some(max_failures) = variables::parsed(
"CAPTCHA_MAX_FAILURES",
value("CAPTCHA_MAX_FAILURES")?,
"must be a positive integer",
)? {
self.max_failures = max_failures;
}
if let Some(bypass) = variables::parsed(
"CAPTCHA_BYPASS",
value("CAPTCHA_BYPASS")?,
"must be either `true` or `false`",
)? {
self.bypass = bypass;
}
Ok(())
}
pub fn is_configured(&self) -> bool {
!self.charset.is_empty()
|| self.noise_level != 0.0
|| self.expires_after_secs != 0
|| self.max_attempts_per_minute != 0
|| self.max_failures != 0
|| self.bypass
}
pub fn validate(&self) -> Result<()> {
if !self.is_configured() {
return Ok(());
}
if self.charset.trim().is_empty() {
return Err(ConfigError::validation(
"captcha.charset",
"must not be empty",
));
}
if !(0.0..=1.0).contains(&self.noise_level) {
return Err(ConfigError::validation(
"captcha.noise_level",
"must be between zero and one",
));
}
for (field, value) in [
("captcha.expires_after_secs", self.expires_after_secs),
(
"captcha.max_attempts_per_minute",
u64::from(self.max_attempts_per_minute),
),
("captcha.max_failures", u64::from(self.max_failures)),
] {
if value == 0 {
return Err(ConfigError::validation(field, "must be greater than zero"));
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn standard_policy_is_complete_and_safe() {
let policy = CaptchaConfig::standard();
policy.validate().expect("standard policy should validate");
assert!(!policy.bypass);
assert!(!policy.charset.contains(['I', 'O', '0', '1']));
}
}