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:
co-authored by
Claude Opus 4.6
parent
38dc845f57
commit
4d17c60da7
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user