use std::{
num::NonZeroU32,
sync::{
atomic::{AtomicU64, Ordering},
Arc,
},
time::{Duration, SystemTime, UNIX_EPOCH},
};
use governor::{Quota, RateLimiter};
use once_cell::sync::Lazy;
use tokio::sync::Semaphore;
static LOCKOUT_UNTIL: AtomicU64 = AtomicU64::new(0);
static WEB_AUTH_LIMITER: Lazy<RateLimiter<governor::state::NotKeyed, governor::state::InMemoryState, governor::clock::QuantaClock, governor::middleware::NoOpMiddleware<governor::clock::QuantaInstant>>> = Lazy::new(|| RateLimiter::direct(Quota::per_minute(NonZeroU32::new(3).unwrap()).allow_burst(NonZeroU32::new(5).unwrap())));
static CM_CONNECTION_LIMITER: Lazy<RateLimiter<governor::state::NotKeyed, governor::state::InMemoryState, governor::clock::QuantaClock, governor::middleware::NoOpMiddleware<governor::clock::QuantaInstant>>> = Lazy::new(|| RateLimiter::direct(Quota::per_minute(NonZeroU32::new(30).unwrap()).allow_burst(NonZeroU32::new(50).unwrap())));
static WEB_AUTH_SEMAPHORE: Lazy<Arc<Semaphore>> = Lazy::new(|| Arc::new(Semaphore::new(3)));
pub enum LoginType {
WebAuth,
CMConnection,
}
pub struct WebAuthPermit {
_permit: tokio::sync::OwnedSemaphorePermit,
}
pub async fn acquire_web_auth_slot() -> WebAuthPermit {
let permit = WEB_AUTH_SEMAPHORE.clone().acquire_owned().await.expect("semaphore closed");
WebAuthPermit { _permit: permit }
}
pub async fn wait_for_permit(login_type: LoginType) {
loop {
let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_millis() as u64;
let lockout = LOCKOUT_UNTIL.load(Ordering::Acquire);
if lockout > now {
let wait_ms = lockout - now;
tracing::warn!("Steam login rate-limit LOCKOUT active. Waiting {}ms...", wait_ms);
tokio::time::sleep(Duration::from_millis(wait_ms)).await;
let wake_jitter = rand::random::<u64>() % 1000 + 100;
tokio::time::sleep(Duration::from_millis(wake_jitter)).await;
continue;
}
break;
}
let start = std::time::Instant::now();
match login_type {
LoginType::WebAuth => WEB_AUTH_LIMITER.until_ready().await,
LoginType::CMConnection => CM_CONNECTION_LIMITER.until_ready().await,
}
let wait_time = start.elapsed();
if wait_time > Duration::from_secs(5) {
tracing::warn!("Steam login throttled for {:?}", wait_time);
} else {
tracing::trace!("Steam login permit granted after {:?}", wait_time);
}
let jitter_ms = rand::random::<u64>() % 200 + 50;
tokio::time::sleep(Duration::from_millis(jitter_ms)).await;
}
pub fn penalize_abuse(duration: Duration, reason: &str) {
let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_millis() as u64;
let until = now + duration.as_millis() as u64;
let current = LOCKOUT_UNTIL.load(Ordering::Acquire);
if until > current {
LOCKOUT_UNTIL.store(until, Ordering::Release);
}
tracing::error!("Received {reason}. Locking global Steam login limiter for {:?}", duration);
}