steam_client/internal/
limiter.rs1use std::{
2 num::NonZeroU32,
3 sync::{
4 atomic::{AtomicU64, Ordering},
5 Arc,
6 },
7 time::{Duration, SystemTime, UNIX_EPOCH},
8};
9
10use governor::{Quota, RateLimiter};
11use once_cell::sync::Lazy;
12use tokio::sync::Semaphore;
13
14static LOCKOUT_UNTIL: AtomicU64 = AtomicU64::new(0);
16
17static 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())));
19
20static 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())));
22
23static WEB_AUTH_SEMAPHORE: Lazy<Arc<Semaphore>> = Lazy::new(|| Arc::new(Semaphore::new(3)));
27
28pub enum LoginType {
29 WebAuth,
30 CMConnection,
31}
32
33pub struct WebAuthPermit {
36 _permit: tokio::sync::OwnedSemaphorePermit,
37}
38
39pub async fn acquire_web_auth_slot() -> WebAuthPermit {
43 let permit = WEB_AUTH_SEMAPHORE.clone().acquire_owned().await.expect("semaphore closed");
44 WebAuthPermit { _permit: permit }
45}
46
47pub async fn wait_for_permit(login_type: LoginType) {
49 loop {
51 let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_millis() as u64;
52 let lockout = LOCKOUT_UNTIL.load(Ordering::Acquire);
53
54 if lockout > now {
55 let wait_ms = lockout - now;
56 tracing::warn!("Steam login rate-limit LOCKOUT active. Waiting {}ms...", wait_ms);
57 tokio::time::sleep(Duration::from_millis(wait_ms)).await;
58
59 let wake_jitter = rand::random::<u64>() % 1000 + 100;
62 tokio::time::sleep(Duration::from_millis(wake_jitter)).await;
63 continue;
64 }
65 break;
66 }
67
68 let start = std::time::Instant::now();
70 match login_type {
71 LoginType::WebAuth => WEB_AUTH_LIMITER.until_ready().await,
72 LoginType::CMConnection => CM_CONNECTION_LIMITER.until_ready().await,
73 }
74 let wait_time = start.elapsed();
75
76 if wait_time > Duration::from_secs(5) {
77 tracing::warn!("Steam login throttled for {:?}", wait_time);
78 } else {
79 tracing::trace!("Steam login permit granted after {:?}", wait_time);
80 }
81
82 let jitter_ms = rand::random::<u64>() % 200 + 50;
85 tokio::time::sleep(Duration::from_millis(jitter_ms)).await;
86}
87
88pub fn penalize_abuse(duration: Duration, reason: &str) {
90 let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_millis() as u64;
91 let until = now + duration.as_millis() as u64;
92
93 let current = LOCKOUT_UNTIL.load(Ordering::Acquire);
95 if until > current {
96 LOCKOUT_UNTIL.store(until, Ordering::Release);
97 }
98 tracing::error!("Received {reason}. Locking global Steam login limiter for {:?}", duration);
99}