From 7aff2e1d279a4e442b32f49ca0a0eca065355787 Mon Sep 17 00:00:00 2001 From: murilo ijanc Date: Wed, 25 Mar 2026 02:07:37 -0300 Subject: Initial commit: tesseras-paste decentralized pastebin DHT-backed encrypted pastebin with two binaries (tp/tpd), XChaCha20-Poly1305 encryption, content-addressed storage, and Unix socket + HTTP interfaces. --- src/bin/tp.rs | 206 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 206 insertions(+) create mode 100644 src/bin/tp.rs (limited to 'src/bin/tp.rs') diff --git a/src/bin/tp.rs b/src/bin/tp.rs new file mode 100644 index 0000000..e33c357 --- /dev/null +++ b/src/bin/tp.rs @@ -0,0 +1,206 @@ +//! tp — tesseras-paste CLI client. +//! +//! Sends commands to the `tpd` daemon over a Unix socket. +//! Reads paste content from stdin (put) and writes it to +//! stdout (get). + +use std::io::{BufRead, BufReader, Read, Write}; +use std::os::unix::net::UnixStream; +use std::path::PathBuf; + +#[path = "../base58.rs"] +mod base58; + +fn default_socket() -> PathBuf { + PathBuf::from("/var/tesseras-paste/daemon.sock") +} + +fn usage() { + eprintln!("usage: tp [-s sock] [args]"); + eprintln!(); + eprintln!("commands:"); + eprintln!(" put [-t ttl] [-p] read stdin, store paste"); + eprintln!(" -p public (no encryption)"); + eprintln!(" get retrieve paste to stdout"); + eprintln!(" del delete paste"); + eprintln!(" pin pin (never expires)"); + eprintln!(" unpin unpin"); + eprintln!(" status show daemon status"); + eprintln!(); + eprintln!(" -s sock Unix socket path"); + eprintln!(" -t ttl time-to-live (e.g. 24h 30m 3600)"); +} + +fn parse_ttl(s: &str) -> Result { + let s = s.trim(); + if let Some(h) = s.strip_suffix('h') { + h.parse::() + .map(|v| v * 3600) + .map_err(|e| e.to_string()) + } else if let Some(m) = s.strip_suffix('m') { + m.parse::().map(|v| v * 60).map_err(|e| e.to_string()) + } else if let Some(sec) = s.strip_suffix('s') { + sec.parse::().map_err(|e| e.to_string()) + } else { + s.parse::().map_err(|e| e.to_string()) + } +} + +fn main() { + let args: Vec = std::env::args().collect(); + + let mut sock_path = default_socket(); + let mut cmd_start = 1; + + // Parse global options before command + let mut i = 1; + while i < args.len() { + match args[i].as_str() { + "-s" => { + i += 1; + sock_path = + args.get(i).map(PathBuf::from).unwrap_or_else(|| { + eprintln!("error: -s requires path"); + std::process::exit(1); + }); + cmd_start = i + 1; + } + "-h" | "--help" => { + usage(); + return; + } + _ => break, + } + i += 1; + } + + let cmd_args = &args[cmd_start..]; + if cmd_args.is_empty() { + usage(); + std::process::exit(1); + } + + let command = &cmd_args[0]; + let mut is_get = false; + + let request = match command.as_str() { + "put" => { + let mut ttl = "24h".to_string(); + let mut public = false; + let mut j = 1; + while j < cmd_args.len() { + match cmd_args[j].as_str() { + "-t" => { + j += 1; + if j < cmd_args.len() { + ttl = cmd_args[j].clone(); + } + } + "-p" => public = true, + _ => {} + } + j += 1; + } + let ttl_secs = match parse_ttl(&ttl) { + Ok(s) => s, + Err(e) => { + eprintln!("error: bad TTL: {e}"); + std::process::exit(1); + } + }; + let mut content = Vec::new(); + if let Err(e) = std::io::stdin().read_to_end(&mut content) { + eprintln!("error: reading stdin: {e}"); + std::process::exit(1); + } + if content.is_empty() { + eprintln!("error: empty input"); + std::process::exit(1); + } + let cmd = if public { "PUTP" } else { "PUT" }; + format!("{cmd} {ttl_secs} {}\n", base58::encode(&content)) + } + "get" => { + let key = cmd_args.get(1).unwrap_or_else(|| { + eprintln!("error: get requires a key"); + std::process::exit(1); + }); + is_get = true; + format!("GET {key}\n") + } + "del" => { + let key = cmd_args.get(1).unwrap_or_else(|| { + eprintln!("error: del requires a key"); + std::process::exit(1); + }); + format!("DEL {key}\n") + } + "pin" => { + let key = cmd_args.get(1).unwrap_or_else(|| { + eprintln!("error: pin requires a key"); + std::process::exit(1); + }); + format!("PIN {key}\n") + } + "unpin" => { + let key = cmd_args.get(1).unwrap_or_else(|| { + eprintln!("error: unpin requires a key"); + std::process::exit(1); + }); + format!("UNPIN {key}\n") + } + "status" => "STATUS\n".to_string(), + other => { + eprintln!("unknown command: {other}"); + usage(); + std::process::exit(1); + } + }; + + let stream = match UnixStream::connect(&sock_path) { + Ok(s) => s, + Err(e) => { + eprintln!("error: cannot connect to {}: {e}", sock_path.display(),); + eprintln!("hint: is tpd running?"); + std::process::exit(1); + } + }; + + stream + .set_read_timeout(Some(std::time::Duration::from_secs(60))) + .ok(); + + let mut writer = &stream; + if let Err(e) = writer.write_all(request.as_bytes()) { + eprintln!("error: writing to socket: {e}"); + std::process::exit(1); + } + + let reader = BufReader::new(&stream); + for line in reader.lines() { + let line = match line { + Ok(l) => l, + Err(_) => break, + }; + if let Some(data) = line.strip_prefix("OK ") { + if is_get { + // Decode base58 → raw bytes → stdout + match base58::decode(data) { + Some(bytes) => { + if let Err(e) = std::io::stdout().write_all(&bytes) { + eprintln!("error: writing to stdout: {e}"); + std::process::exit(1); + } + } + None => println!("{data}"), + } + } else { + println!("{data}"); + } + break; + } else if let Some(msg) = line.strip_prefix("ERR ") { + eprintln!("error: {msg}"); + std::process::exit(1); + } + } +} -- cgit v1.2.3