bug fixing and deploy and build diagnostics

This commit is contained in:
Dorian
2026-03-22 03:30:21 +00:00
parent 01942cea95
commit 13e4a738be
198 changed files with 21703 additions and 19587 deletions
+3 -200
View File
@@ -2,10 +2,9 @@ use hmac::{Hmac, Mac};
use rand::RngCore;
use sha2::{Digest, Sha256};
use std::collections::HashMap;
use std::net::IpAddr;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::time::{Instant, SystemTime, UNIX_EPOCH};
use std::time::{SystemTime, UNIX_EPOCH};
use tokio::sync::RwLock;
use zeroize::Zeroize;
@@ -129,25 +128,6 @@ impl SessionStore {
}
}
/// Async wrapper for save — spawns to avoid blocking RPC.
fn schedule_save(&self, sessions: &HashMap<[u8; 32], Session>) {
let persisted: Vec<PersistedSession> = sessions
.iter()
.filter(|(_, s)| matches!(s.session_type, SessionType::Full))
.map(|(hash, s)| PersistedSession {
hash_hex: hex::encode(hash),
created_at: s.created_at.duration_since(UNIX_EPOCH).unwrap_or_default().as_secs(),
last_activity: s.last_activity.duration_since(UNIX_EPOCH).unwrap_or_default().as_secs(),
})
.collect();
let path = self.persist_path.clone();
tokio::spawn(async move {
if let Ok(json) = serde_json::to_string(&persisted) {
let _ = tokio::fs::write(path, json).await;
}
});
}
/// Create a full (authenticated) session. Returns the plaintext token.
/// Enforces max concurrent sessions by evicting the oldest if limit reached.
pub async fn create(&self) -> String {
@@ -303,6 +283,7 @@ impl SessionStore {
}
/// Remove all expired sessions (cleanup).
#[cfg(test)]
pub async fn cleanup_expired(&self) {
let mut sessions = self.sessions.write().await;
sessions.retain(|_, session| {
@@ -336,6 +317,7 @@ impl SessionStore {
}
/// Get the number of active full sessions.
#[cfg(test)]
pub async fn active_session_count(&self) -> usize {
let sessions = self.sessions.read().await;
sessions
@@ -447,147 +429,6 @@ pub fn extract_session_cookie(headers: &hyper::HeaderMap) -> Option<String> {
.filter(|v| !v.is_empty())
}
/// Rate limiter for login attempts: max 5 failures per 60 seconds per IP.
#[derive(Clone)]
pub struct LoginRateLimiter {
attempts: Arc<RwLock<HashMap<IpAddr, Vec<Instant>>>>,
}
const MAX_ATTEMPTS: usize = 5;
const WINDOW_SECS: u64 = 60;
impl LoginRateLimiter {
pub fn new() -> Self {
Self {
attempts: Arc::new(RwLock::new(HashMap::new())),
}
}
pub async fn check(&self, ip: IpAddr) -> bool {
let mut attempts = self.attempts.write().await;
let now = Instant::now();
let entry = attempts.entry(ip).or_default();
entry.retain(|t| now.duration_since(*t).as_secs() < WINDOW_SECS);
entry.len() < MAX_ATTEMPTS
}
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());
}
/// 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.
/// Tracks request counts per (method, IP) with configurable limits and windows.
#[derive(Clone)]
pub struct EndpointRateLimiter {
/// Map of (method, ip) -> list of request timestamps
requests: Arc<RwLock<HashMap<(String, IpAddr), Vec<Instant>>>>, // Instant for monotonic rate limiting
/// Per-method configuration: (max_requests, window_secs)
limits: Arc<HashMap<String, (usize, u64)>>,
}
impl EndpointRateLimiter {
pub fn new() -> Self {
let mut limits = HashMap::new();
// Financial operations: strict limits
limits.insert("wallet.send".to_string(), (5usize, 300u64));
limits.insert("wallet.ecash-send".to_string(), (10, 300));
limits.insert("lnd.sendcoins".to_string(), (5, 300));
limits.insert("lnd.payinvoice".to_string(), (10, 300));
limits.insert("lnd.openchannel".to_string(), (3, 300));
limits.insert("lnd.closechannel".to_string(), (3, 300));
limits.insert("lnd.create-psbt".to_string(), (5, 300));
limits.insert("lnd.finalize-psbt".to_string(), (5, 300));
// Identity/credential operations
limits.insert("identity.create".to_string(), (10, 300));
limits.insert("identity.issue-credential".to_string(), (20, 300));
// Backup operations (resource-intensive)
limits.insert("backup.create".to_string(), (10, 600));
limits.insert("backup.restore".to_string(), (5, 600));
// Container operations
limits.insert("container-install".to_string(), (5, 300));
limits.insert("package.install".to_string(), (5, 300));
// S3 backup operations (resource-intensive)
limits.insert("backup.upload-s3".to_string(), (3, 600));
limits.insert("backup.download-s3".to_string(), (3, 600));
// System operations
limits.insert("update.apply".to_string(), (2, 600));
limits.insert("system.reboot".to_string(), (2, 300));
limits.insert("system.shutdown".to_string(), (2, 300));
// Password and TOTP changes
limits.insert("auth.changePassword".to_string(), (3, 300));
limits.insert("auth.totp.setup".to_string(), (3, 300));
limits.insert("auth.totp.confirm".to_string(), (5, 300));
// Federation join: prevent invite-code brute force
limits.insert("federation.join".to_string(), (5, 60));
limits.insert("federation.invite".to_string(), (10, 300));
// Inter-node federation RPCs (unauthenticated, need stricter limits)
limits.insert("federation.peer-joined".to_string(), (10, 60));
limits.insert("federation.peer-address-changed".to_string(), (10, 60));
limits.insert("federation.peer-did-changed".to_string(), (5, 60));
limits.insert("federation.get-state".to_string(), (30, 60));
// DID rotation: sensitive identity operation
limits.insert("node.rotate-did".to_string(), (3, 600));
Self {
requests: Arc::new(RwLock::new(HashMap::new())),
limits: Arc::new(limits),
}
}
/// Check if a request is allowed. Returns true if within limits.
pub async fn check(&self, method: &str, ip: IpAddr) -> bool {
let (max_req, window) = match self.limits.get(method) {
Some(config) => *config,
None => return true, // Not rate-limited
};
let key = (method.to_string(), ip);
let mut requests = self.requests.write().await;
let now = Instant::now();
let entry = requests.entry(key).or_default();
entry.retain(|t| now.duration_since(*t).as_secs() < window);
entry.len() < max_req
}
/// Record a request for rate limiting purposes.
pub async fn record(&self, method: &str, ip: IpAddr) {
if !self.limits.contains_key(method) {
return; // Not rate-limited, skip tracking
}
let key = (method.to_string(), ip);
let mut requests = self.requests.write().await;
let entry = requests.entry(key).or_default();
entry.push(Instant::now());
}
/// Periodic cleanup of expired entries.
pub async fn cleanup(&self) {
let mut requests = self.requests.write().await;
let now = Instant::now();
requests.retain(|(method, _), timestamps| {
let window = self
.limits
.get(method)
.map(|(_, w)| *w)
.unwrap_or(300);
timestamps.retain(|t| now.duration_since(*t).as_secs() < window);
!timestamps.is_empty()
});
}
}
#[cfg(test)]
mod tests {
@@ -669,44 +510,6 @@ mod tests {
assert_eq!(extract_session_cookie(&headers), None);
}
#[tokio::test]
async fn test_rate_limiter_allows_under_limit() {
let limiter = LoginRateLimiter::new();
let ip: IpAddr = "127.0.0.1".parse().unwrap_or(std::net::IpAddr::V4(std::net::Ipv4Addr::LOCALHOST));
for _ in 0..MAX_ATTEMPTS {
assert!(limiter.check(ip).await);
limiter.record_failure(ip).await;
}
}
#[tokio::test]
async fn test_rate_limiter_blocks_over_limit() {
let limiter = LoginRateLimiter::new();
let ip: IpAddr = "127.0.0.1".parse().unwrap_or(std::net::IpAddr::V4(std::net::Ipv4Addr::LOCALHOST));
for _ in 0..MAX_ATTEMPTS {
limiter.record_failure(ip).await;
}
assert!(!limiter.check(ip).await);
}
#[tokio::test]
async fn test_rate_limiter_different_ips() {
let limiter = LoginRateLimiter::new();
let ip1: IpAddr = "127.0.0.1".parse().unwrap_or(std::net::IpAddr::V4(std::net::Ipv4Addr::LOCALHOST));
let ip2: IpAddr = "192.168.1.1".parse().unwrap_or(std::net::IpAddr::V4(std::net::Ipv4Addr::LOCALHOST));
for _ in 0..MAX_ATTEMPTS {
limiter.record_failure(ip1).await;
}
// ip1 should be blocked
assert!(!limiter.check(ip1).await);
// ip2 should still be allowed
assert!(limiter.check(ip2).await);
}
#[tokio::test]
async fn test_session_activity_updates_on_validate() {