use super::funejson::Value; /// Allowed or locked, by replaying the failures in time order: max_attempts /// failures within window_seconds of the latest lock the account for /// lockout_seconds from that failure, and the count starts again. /// /// # Panics /// Panics on a policy number below 1. pub fn login_throttle(failures: &[i64], now: i64, policy: &ThrottlePolicy) -> ThrottleDecision { if policy.max_attempts < 1 { panic!("maxAttempts must be a whole number of at least 1"); } if policy.window_seconds < 1 { panic!("windowSeconds must be a whole number of at least 1"); } if policy.lockout_seconds < 1 { panic!("lockoutSeconds must be a whole number of at least 1"); } let mut times: Vec = failures.iter().copied().filter(|&t| t <= now).collect(); times.sort_unstable(); let mut locked_until: Option = None; let mut streak: Vec = Vec::new(); for t in times { if let Some(until) = locked_until { if t < until { continue; } } streak.retain(|&s| s > t - policy.window_seconds); streak.push(t); if streak.len() as i64 >= policy.max_attempts { locked_until = Some(t + policy.lockout_seconds); streak.clear(); } } if let Some(until) = locked_until { if now < until { return ThrottleDecision { allowed: false, retry_after_seconds: until - now, remaining_attempts: 0, locked_until: Some(until), }; } } let live = streak.iter().filter(|&&s| s > now - policy.window_seconds).count() as i64; ThrottleDecision { allowed: true, retry_after_seconds: 0, remaining_attempts: policy.max_attempts - live, locked_until: None, } } pub fn throttle_decision_to_value(decision: &ThrottleDecision) -> Value { Value::obj(vec![ ("allowed", Value::Bool(decision.allowed)), ("retryAfterSeconds", Value::Int(decision.retry_after_seconds)), ("remainingAttempts", Value::Int(decision.remaining_attempts)), ("lockedUntil", decision.locked_until.map(Value::Int).unwrap_or(Value::Null)), ]) } pub fn fune_vector(args: &[Value]) -> Value { let failures: Vec = args[0] .as_arr() .iter() .map(|v| match v { Value::Int(i) => *i, _ => panic!("failures must be whole Unix seconds"), }) .collect(); let now = match &args[1] { Value::Int(i) => *i, _ => panic!("now must be a whole number of Unix seconds"), }; let field = |key: &str| match args[2].get(key) { Value::Int(i) => *i, _ => panic!("{} must be a whole number of at least 1", key), }; let policy = ThrottlePolicy { max_attempts: field("maxAttempts"), window_seconds: field("windowSeconds"), lockout_seconds: field("lockoutSeconds"), }; throttle_decision_to_value(&login_throttle(&failures, now, &policy)) }