diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/daemon.rs | 34 | ||||
| -rw-r--r-- | src/ops.rs | 12 | ||||
| -rw-r--r-- | src/paste.rs | 2 | ||||
| -rw-r--r-- | src/store.rs | 26 |
4 files changed, 59 insertions, 15 deletions
diff --git a/src/daemon.rs b/src/daemon.rs index 313a4aa..c578e5c 100644 --- a/src/daemon.rs +++ b/src/daemon.rs @@ -6,7 +6,7 @@ //! over a Unix socket using a line-oriented text protocol //! (see [`crate::protocol`]). -use std::io::{BufRead, BufReader, Write}; +use std::io::{BufRead, BufReader, Read, Write}; use std::path::Path; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::mpsc; @@ -196,7 +196,7 @@ fn republish(node: &mut Node, store: &PasteStore) { .duration_since(std::time::UNIX_EPOCH) .unwrap_or_default() .as_secs(); - let expires = paste.created_at + paste.ttl_secs; + let expires = paste.created_at.saturating_add(paste.ttl_secs); let rem = expires.saturating_sub(now); std::cmp::min(rem, u16::MAX as u64) as u16 }; @@ -259,6 +259,10 @@ pub fn run_unix_listener( let _ = std::fs::remove_file(sock_path); } +/// Maximum protocol line size (128 KiB covers the 64 KiB paste +/// limit after base58 expansion plus command overhead). +const MAX_LINE_SIZE: usize = 128 * 1024; + /// Read requests line-by-line from a connected Unix socket /// client, forwarding each to the daemon main loop via `tx`. fn handle_client( @@ -268,11 +272,29 @@ fn handle_client( stream.set_nonblocking(false)?; stream.set_read_timeout(Some(Duration::from_secs(60)))?; - let reader = BufReader::new(&stream); + let mut reader = BufReader::new(&stream); let mut writer = &stream; - - for line in reader.lines() { - let line = line?; + let mut line = String::new(); + + loop { + line.clear(); + // Limit read to MAX_LINE_SIZE to prevent a client from + // exhausting memory with an unbounded request line. + let n = (&mut reader).take(MAX_LINE_SIZE as u64).read_line(&mut line)?; + if n == 0 { + break; + } + if !line.ends_with('\n') && n >= MAX_LINE_SIZE { + let resp = protocol::format_response(&Response::Err( + "request too large".into(), + )); + writer.write_all(resp.as_bytes())?; + // Drain remaining bytes until newline + let mut discard = String::new(); + let _ = reader.read_line(&mut discard); + continue; + } + let line = line.trim(); let cmd = match protocol::parse_request(&line) { Ok(c) => c, Err(e) => { @@ -118,7 +118,17 @@ pub fn get_paste( if vals.is_empty() { return Err(PasteError::NotFound); } - vals[0].clone() + // Verify DHT result: the content hash must match the + // requested key to prevent a malicious node from + // injecting arbitrary data. + match vals.iter().find(|v| { + Paste::from_bytes(v) + .map(|p| Paste::content_key(&p.content) == *hash) + .unwrap_or(false) + }) { + Some(v) => v.clone(), + None => return Err(PasteError::NotFound), + } }; let paste = Paste::from_bytes(&data).ok_or(PasteError::InvalidKey)?; diff --git a/src/paste.rs b/src/paste.rs index 50b32b1..8bfe979 100644 --- a/src/paste.rs +++ b/src/paste.rs @@ -81,7 +81,7 @@ impl Paste { .duration_since(std::time::UNIX_EPOCH) .unwrap_or_default() .as_secs(); - now > self.created_at + self.ttl_secs + now > self.created_at.saturating_add(self.ttl_secs) } } diff --git a/src/store.rs b/src/store.rs index 18a7641..98c5481 100644 --- a/src/store.rs +++ b/src/store.rs @@ -45,14 +45,12 @@ impl PasteStore { // ── Paste CRUD ────────────────────────────────── - /// Write a paste to disk. The key (32 bytes) is prepended - /// to the file so [`original_keys`] can reconstruct it. + /// Write a paste to disk atomically (write-to-temp + rename). + /// The key (32 bytes) is prepended to the file so + /// [`original_keys`] can reconstruct it. pub fn put_paste(&self, key: &[u8], value: &[u8]) -> std::io::Result<()> { let path = self.paste_path(key); - let mut f = fs::File::create(path)?; - f.write_all(key)?; - f.write_all(value)?; - Ok(()) + atomic_write(&path, &[key, value]) } /// Read a paste from disk. Returns `None` if the paste @@ -179,6 +177,20 @@ impl PasteStore { } } +/// Write data to `path` atomically: write to a temporary file in +/// the same directory, then rename over the target. This prevents +/// corruption if the process is killed mid-write. +fn atomic_write(path: &Path, chunks: &[&[u8]]) -> std::io::Result<()> { + let parent = path.parent().unwrap_or(Path::new(".")); + let tmp = parent.join(format!(".tmp.{}", std::process::id())); + let mut f = fs::File::create(&tmp)?; + for chunk in chunks { + f.write_all(chunk)?; + } + f.sync_all()?; + fs::rename(&tmp, path) +} + // ── tesseras-dht persistence traits ───────────────── impl tesseras_dht::persist::RoutingPersistence for PasteStore { @@ -198,7 +210,7 @@ impl tesseras_dht::persist::RoutingPersistence for PasteStore { buf.extend_from_slice(id); buf.extend_from_slice(addr_bytes); } - fs::write(&path, &buf).map_err(tesseras_dht::Error::Io)?; + atomic_write(&path, &[&buf]).map_err(tesseras_dht::Error::Io)?; log::info!("store: persisted {} routing contacts", contacts.len()); Ok(()) } |