fix: use SystemTime instead of Instant for session TTL

Instant is monotonic but drifts on sleep/hibernate common on NUC
hardware. SystemTime gives proper wall-clock expiry for sessions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-03-15 04:32:24 +00:00
parent 9d24e1f44b
commit 299357e908
2 changed files with 19 additions and 19 deletions

View File

@@ -2,7 +2,7 @@ use sha2::{Digest, Sha256};
use std::collections::HashMap;
use std::net::IpAddr;
use std::sync::Arc;
use std::time::Instant;
use std::time::SystemTime;
use tokio::sync::RwLock;
use zeroize::Zeroize;
@@ -30,8 +30,8 @@ impl Drop for SessionType {
#[derive(Clone)]
struct Session {
created_at: Instant,
last_activity: Instant,
created_at: SystemTime,
last_activity: SystemTime,
session_type: SessionType,
}
@@ -53,7 +53,7 @@ impl SessionStore {
let token_bytes: [u8; 32] = rand::random();
let token = hex::encode(token_bytes);
let hash = hash_token(&token);
let now = Instant::now();
let now = SystemTime::now();
let session = Session {
created_at: now,
last_activity: now,
@@ -72,7 +72,7 @@ impl SessionStore {
let token_bytes: [u8; 32] = rand::random();
let token = hex::encode(token_bytes);
let hash = hash_token(&token);
let now = Instant::now();
let now = SystemTime::now();
let session = Session {
created_at: now,
last_activity: now,
@@ -94,11 +94,11 @@ impl SessionStore {
if !matches!(session.session_type, SessionType::Full) {
return false;
}
if session.last_activity.elapsed().as_secs() >= FULL_SESSION_TTL {
if session.last_activity.elapsed().unwrap_or_default().as_secs() >= FULL_SESSION_TTL {
sessions.remove(&hash);
return false;
}
session.last_activity = Instant::now();
session.last_activity = SystemTime::now();
true
} else {
false
@@ -111,7 +111,7 @@ impl SessionStore {
let hash = hash_token(token);
let mut sessions = self.sessions.write().await;
if let Some(session) = sessions.get_mut(&hash) {
if session.created_at.elapsed().as_secs() >= PENDING_SESSION_TTL {
if session.created_at.elapsed().unwrap_or_default().as_secs() >= PENDING_SESSION_TTL {
sessions.remove(&hash);
return None;
}
@@ -137,7 +137,7 @@ impl SessionStore {
let mut sessions = self.sessions.write().await;
if let Some(session) = sessions.get_mut(&hash) {
session.session_type = SessionType::Full;
let now = Instant::now();
let now = SystemTime::now();
session.created_at = now;
session.last_activity = now;
}
@@ -163,7 +163,7 @@ impl SessionStore {
let new_token_bytes: [u8; 32] = rand::random();
let new_token = hex::encode(new_token_bytes);
let new_hash = hash_token(&new_token);
let now = Instant::now();
let now = SystemTime::now();
let mut sessions = self.sessions.write().await;
sessions.remove(&old_hash);
@@ -183,9 +183,9 @@ impl SessionStore {
let mut sessions = self.sessions.write().await;
sessions.retain(|_, session| {
match &session.session_type {
SessionType::Full => session.last_activity.elapsed().as_secs() < FULL_SESSION_TTL,
SessionType::Full => session.last_activity.elapsed().unwrap_or_default().as_secs() < FULL_SESSION_TTL,
SessionType::PendingTotp { .. } => {
session.created_at.elapsed().as_secs() < PENDING_SESSION_TTL
session.created_at.elapsed().unwrap_or_default().as_secs() < PENDING_SESSION_TTL
}
}
});
@@ -218,7 +218,7 @@ impl SessionStore {
.values()
.filter(|s| {
matches!(s.session_type, SessionType::Full)
&& s.last_activity.elapsed().as_secs() < FULL_SESSION_TTL
&& s.last_activity.elapsed().unwrap_or_default().as_secs() < FULL_SESSION_TTL
})
.count()
}
@@ -262,7 +262,7 @@ impl LoginRateLimiter {
pub async fn check(&self, ip: IpAddr) -> bool {
let mut attempts = self.attempts.write().await;
let now = Instant::now();
let now = SystemTime::now();
let entry = attempts.entry(ip).or_default();
entry.retain(|t| now.duration_since(*t).as_secs() < WINDOW_SECS);
entry.len() < MAX_ATTEMPTS
@@ -271,7 +271,7 @@ impl LoginRateLimiter {
pub async fn record_failure(&self, ip: IpAddr) {
let mut attempts = self.attempts.write().await;
let entry = attempts.entry(ip).or_default();
entry.push(Instant::now());
entry.push(SystemTime::now());
}
}
@@ -338,7 +338,7 @@ impl EndpointRateLimiter {
let key = (method.to_string(), ip);
let mut requests = self.requests.write().await;
let now = Instant::now();
let now = SystemTime::now();
let entry = requests.entry(key).or_default();
entry.retain(|t| now.duration_since(*t).as_secs() < window);
entry.len() < max_req
@@ -352,13 +352,13 @@ impl EndpointRateLimiter {
let key = (method.to_string(), ip);
let mut requests = self.requests.write().await;
let entry = requests.entry(key).or_default();
entry.push(Instant::now());
entry.push(SystemTime::now());
}
/// Periodic cleanup of expired entries.
pub async fn cleanup(&self) {
let mut requests = self.requests.write().await;
let now = Instant::now();
let now = SystemTime::now();
requests.retain(|(method, _), timestamps| {
let window = self
.limits