refactor: replace blocking std::fs and TCP I/O with async tokio equivalents

- R6: Convert 6 std::fs calls in session.rs to tokio::fs async
- R7: Convert std::fs::read_to_string in docker_packages.rs to async
- R8: Convert 3 std::fs calls in port_allocator.rs to async, switch to tokio::sync::Mutex
- R9+R10+R11: Fix blocking I/O in node_message.rs and nostr_discovery.rs
- R12: Convert electrs_status.rs from sync TCP to async tokio::net with 5s timeouts
- R4+R5: Spawn periodic cleanup tasks for endpoint and login rate limiters

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-03-21 01:21:08 +00:00
co-authored by Claude Opus 4.6
parent 38dc845f57
commit 4d17c60da7
12 changed files with 161 additions and 117 deletions
+29 -19
View File
@@ -58,9 +58,9 @@ struct PersistedSession {
}
impl SessionStore {
pub fn new() -> Self {
pub async fn new() -> Self {
let persist_path = PathBuf::from(SESSIONS_FILE);
let sessions = Self::load_from_disk(&persist_path);
let sessions = Self::load_from_disk(&persist_path).await;
let count = sessions.len();
if count > 0 {
tracing::info!("Restored {} sessions from disk", count);
@@ -72,9 +72,9 @@ impl SessionStore {
}
/// Load persisted sessions from disk (only Full sessions).
fn load_from_disk(path: &Path) -> HashMap<[u8; 32], Session> {
async fn load_from_disk(path: &Path) -> HashMap<[u8; 32], Session> {
let mut map = HashMap::new();
let data = match std::fs::read_to_string(path) {
let data = match tokio::fs::read_to_string(path).await {
Ok(d) => d,
Err(_) => return map,
};
@@ -114,7 +114,7 @@ impl SessionStore {
}
/// Save all Full sessions to disk. Called after mutations.
fn save_to_disk_sync(sessions: &HashMap<[u8; 32], Session>, path: &Path) {
async fn save_to_disk(sessions: &HashMap<[u8; 32], Session>, path: &Path) {
let persisted: Vec<PersistedSession> = sessions
.iter()
.filter(|(_, s)| matches!(s.session_type, SessionType::Full))
@@ -125,7 +125,7 @@ impl SessionStore {
})
.collect();
if let Ok(json) = serde_json::to_string(&persisted) {
let _ = std::fs::write(path, json);
let _ = tokio::fs::write(path, json).await;
}
}
@@ -166,7 +166,7 @@ impl SessionStore {
sessions.insert(hash, session);
// Sync save — must complete before returning the token to the client.
// Async save risks losing the session if the process is killed (e.g., deploy restart).
Self::save_to_disk_sync(&sessions, &self.persist_path);
Self::save_to_disk(&sessions, &self.persist_path).await;
token
}
@@ -256,7 +256,7 @@ impl SessionStore {
session_type: SessionType::Full,
},
);
Self::save_to_disk_sync(&sessions, &self.persist_path);
Self::save_to_disk(&sessions, &self.persist_path).await;
Some(new_token)
} else {
None
@@ -267,7 +267,7 @@ impl SessionStore {
let hash = hash_token(token);
let mut sessions = self.sessions.write().await;
sessions.remove(&hash);
Self::save_to_disk_sync(&sessions, &self.persist_path);
Self::save_to_disk(&sessions, &self.persist_path).await;
}
/// Invalidate all sessions except the one matching the given token.
@@ -276,7 +276,7 @@ impl SessionStore {
let keep_hash = hash_token(keep_token);
let mut sessions = self.sessions.write().await;
sessions.retain(|hash, _| *hash == keep_hash);
Self::save_to_disk_sync(&sessions, &self.persist_path);
Self::save_to_disk(&sessions, &self.persist_path).await;
}
/// Rotate a session: invalidate the old token and create a new one.
@@ -298,7 +298,7 @@ impl SessionStore {
session_type: SessionType::Full,
},
);
Self::save_to_disk_sync(&sessions, &self.persist_path);
Self::save_to_disk(&sessions, &self.persist_path).await;
new_token
}
@@ -352,8 +352,8 @@ impl SessionStore {
// Format: "timestamp_hex:hmac_hex"
/// Create a remember-me token. Returns the cookie value.
pub fn create_remember_token(&self) -> String {
let secret = Self::load_or_create_remember_secret();
pub async fn create_remember_token(&self) -> String {
let secret = Self::load_or_create_remember_secret().await;
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
@@ -366,8 +366,8 @@ impl SessionStore {
}
/// Validate a remember-me token. Returns true if valid and not expired.
pub fn validate_remember_token(token: &str) -> bool {
let secret = match std::fs::read(REMEMBER_SECRET_FILE) {
pub async fn validate_remember_token(token: &str) -> bool {
let secret = match tokio::fs::read(REMEMBER_SECRET_FILE).await {
Ok(s) if s.len() == 32 => s,
_ => return false,
};
@@ -408,9 +408,9 @@ impl SessionStore {
now.saturating_sub(ts_bytes) < REMEMBER_TTL
}
pub fn load_or_create_remember_secret() -> Vec<u8> {
pub async fn load_or_create_remember_secret() -> Vec<u8> {
// Try existing secret file first
if let Ok(secret) = std::fs::read(REMEMBER_SECRET_FILE) {
if let Ok(secret) = tokio::fs::read(REMEMBER_SECRET_FILE).await {
if secret.len() == 32 {
return secret;
}
@@ -420,9 +420,9 @@ impl SessionStore {
rand::rngs::OsRng.fill_bytes(&mut secret);
// Ensure parent directory exists
if let Some(parent) = std::path::Path::new(REMEMBER_SECRET_FILE).parent() {
let _ = std::fs::create_dir_all(parent);
let _ = tokio::fs::create_dir_all(parent).await;
}
let _ = std::fs::write(REMEMBER_SECRET_FILE, &secret);
let _ = tokio::fs::write(REMEMBER_SECRET_FILE, &secret).await;
secret.to_vec()
}
}
@@ -476,6 +476,16 @@ impl LoginRateLimiter {
let entry = attempts.entry(ip).or_default();
entry.push(Instant::now());
}
/// Periodic cleanup of expired entries for IPs that are no longer active.
pub async fn cleanup(&self) {
let mut attempts = self.attempts.write().await;
let now = Instant::now();
attempts.retain(|_, timestamps| {
timestamps.retain(|t| now.duration_since(*t).as_secs() < WINDOW_SECS);
!timestamps.is_empty()
});
}
}
/// General-purpose rate limiter for sensitive endpoints.