diff options
Diffstat (limited to 'src/bin')
| -rw-r--r-- | src/bin/tp.rs | 206 | ||||
| -rw-r--r-- | src/bin/tpd.rs | 293 |
2 files changed, 499 insertions, 0 deletions
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] <command> [args]"); + eprintln!(); + eprintln!("commands:"); + eprintln!(" put [-t ttl] [-p] read stdin, store paste"); + eprintln!(" -p public (no encryption)"); + eprintln!(" get <key> retrieve paste to stdout"); + eprintln!(" del <key> delete paste"); + eprintln!(" pin <key> pin (never expires)"); + eprintln!(" unpin <key> 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<u64, String> { + let s = s.trim(); + if let Some(h) = s.strip_suffix('h') { + h.parse::<u64>() + .map(|v| v * 3600) + .map_err(|e| e.to_string()) + } else if let Some(m) = s.strip_suffix('m') { + m.parse::<u64>().map(|v| v * 60).map_err(|e| e.to_string()) + } else if let Some(sec) = s.strip_suffix('s') { + sec.parse::<u64>().map_err(|e| e.to_string()) + } else { + s.parse::<u64>().map_err(|e| e.to_string()) + } +} + +fn main() { + let args: Vec<String> = 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); + } + } +} diff --git a/src/bin/tpd.rs b/src/bin/tpd.rs new file mode 100644 index 0000000..15e7d9b --- /dev/null +++ b/src/bin/tpd.rs @@ -0,0 +1,293 @@ +//! tpd — tesseras-paste daemon. +//! +//! Runs a DHT node that stores and serves encrypted pastes. +//! Communicates with the CLI (`tp`) over a Unix socket and +//! optionally serves pastes via HTTP. + +#[path = "../base58.rs"] +mod base58; +#[path = "../crypto.rs"] +mod crypto; +#[path = "../daemon.rs"] +mod daemon; +#[path = "../ops.rs"] +mod ops; +#[path = "../paste.rs"] +mod paste; +#[path = "../protocol.rs"] +mod protocol; +#[path = "../store.rs"] +mod store; + +use std::path::PathBuf; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::{Arc, mpsc}; + +use tesseras_dht::nat::NatState; +use tesseras_dht::node::NodeBuilder; + +use store::PasteStore; + +fn default_dir() -> PathBuf { + PathBuf::from("/var/tesseras-paste") +} + +fn usage() { + eprintln!( + "usage: tpd [-p port] [-d dir] [-s sock] \ + [-w http_port] [-g] [-b host:port] [-h]" + ); + eprintln!(); + eprintln!(" -p port UDP port (0 = random)"); + eprintln!(" -d dir data directory"); + eprintln!(" -s sock Unix socket path"); + eprintln!(" -w port HTTP server port"); + eprintln!(" -g global NAT (public server)"); + eprintln!(" -b host:port bootstrap peer (repeatable)"); + eprintln!(" -h show this help"); +} + +fn main() { + env_logger::Builder::from_env( + env_logger::Env::default().default_filter_or("info"), + ) + .format(|buf, record| { + use std::io::Write; + writeln!(buf, "[{}]: {}", record.level(), record.args()) + }) + .init(); + + let mut port: u16 = 0; + let mut dir = default_dir(); + let mut sock: Option<PathBuf> = None; + let mut http_port: Option<u16> = None; + let mut global = false; + let mut bootstrap: Vec<String> = Vec::new(); + + let args: Vec<String> = std::env::args().collect(); + let mut i = 1; + while i < args.len() { + match args[i].as_str() { + "-p" => { + i += 1; + port = args.get(i).and_then(|s| s.parse().ok()).unwrap_or_else( + || { + eprintln!("error: -p requires a port"); + std::process::exit(1); + }, + ); + } + "-d" => { + i += 1; + dir = args.get(i).map(PathBuf::from).unwrap_or_else(|| { + eprintln!("error: -d requires a path"); + std::process::exit(1); + }); + } + "-s" => { + i += 1; + sock = args.get(i).map(PathBuf::from); + if sock.is_none() { + eprintln!("error: -s requires a path"); + std::process::exit(1); + } + } + "-w" => { + i += 1; + http_port = Some( + args.get(i).and_then(|s| s.parse().ok()).unwrap_or_else( + || { + eprintln!("error: -w requires a port"); + std::process::exit(1); + }, + ), + ); + } + "-g" => global = true, + "-b" => { + i += 1; + if let Some(addr) = args.get(i) { + bootstrap.push(addr.clone()); + } else { + eprintln!("error: -b requires host:port"); + std::process::exit(1); + } + } + "-h" | "--help" => { + usage(); + return; + } + other => { + eprintln!("unknown option: {other}"); + usage(); + std::process::exit(1); + } + } + i += 1; + } + + let sock_path = sock.unwrap_or_else(|| dir.join("daemon.sock")); + + // Ensure directories exist + let _ = std::fs::create_dir_all(&dir); + if let Some(parent) = sock_path.parent() { + let _ = std::fs::create_dir_all(parent); + } + + let store = match PasteStore::open(&dir) { + Ok(s) => s, + Err(e) => { + eprintln!("error: {e}"); + std::process::exit(1); + } + }; + + // Load or generate persistent identity + let identity_path = dir.join("identity.key"); + let identity_seed = load_or_create_identity(&identity_path); + + let mut builder = NodeBuilder::new().port(port).seed(&identity_seed); + if global { + builder = builder.nat(NatState::Global); + } + + let cfg = tesseras_dht::config::Config { + default_ttl: 65535, + max_value_size: 128 * 1024, + require_signatures: true, + ..Default::default() + }; + builder = builder.config(cfg); + + let mut node = match builder.build() { + Ok(n) => n, + Err(e) => { + eprintln!("error: {e}"); + std::process::exit(1); + } + }; + + node.set_routing_persistence(Box::new(store.clone())); + node.set_data_persistence(Box::new(store.clone())); + node.load_persisted(); + + let addr = match node.local_addr() { + Ok(a) => a, + Err(e) => { + eprintln!("error: could not determine local address: {e}"); + std::process::exit(1); + } + }; + let id = node.id_hex(); + eprintln!("tpd {addr} id={:.8}", id); + + for peer in &bootstrap { + let parts: Vec<&str> = peer.rsplitn(2, ':').collect(); + if parts.len() != 2 { + eprintln!("warning: bad bootstrap: {peer}"); + continue; + } + let host = parts[1]; + let p: u16 = match parts[0].parse() { + Ok(p) => p, + Err(_) => { + eprintln!("warning: bad port: {peer}"); + continue; + } + }; + if let Err(e) = node.join(host, p) { + eprintln!("warning: bootstrap {peer}: {e}"); + } else { + log::info!("bootstrap: connected to {peer}"); + } + } + + for _ in 0..10 { + let _ = node.poll(); + } + + eprintln!( + "peers={} socket={}", + node.routing_table_size(), + sock_path.display() + ); + + let shutdown = Arc::new(AtomicBool::new(false)); + + // Signal handler + let sig = Arc::clone(&shutdown); + unsafe { + SHUTDOWN_PTR.store( + Arc::into_raw(sig) as *mut AtomicBool as usize, + Ordering::SeqCst, + ); + signal(SIGINT, sig_handler as *const () as usize); + signal(SIGTERM, sig_handler as *const () as usize); + } + + let (tx, rx) = mpsc::channel(); + + let listener_shutdown = Arc::clone(&shutdown); + let listener_path = sock_path.clone(); + let handle = std::thread::spawn(move || { + daemon::run_unix_listener(&listener_path, tx, &listener_shutdown); + }); + + // HTTP server thread (optional) + let http_handle = http_port.map(|hp| { + let http_store = store.clone(); + let http_shutdown = Arc::clone(&shutdown); + let http_sock = sock_path.clone(); + eprintln!("http on 0.0.0.0:{hp}"); + std::thread::spawn(move || { + daemon::run_http(hp, &http_sock, &http_store, &http_shutdown); + }) + }); + + daemon::run_daemon(&mut node, &store, &rx, &shutdown); + + let _ = std::fs::remove_file(&sock_path); + let _ = handle.join(); + if let Some(h) = http_handle { + let _ = h.join(); + } + eprintln!("shutdown complete"); +} + +/// Load identity seed from file, or generate and save +/// a new one. This ensures the node keeps the same +/// Ed25519 keypair (and NodeId) across restarts. +fn load_or_create_identity(path: &std::path::Path) -> Vec<u8> { + if let Ok(data) = std::fs::read(path) + && data.len() == 32 + { + log::info!("identity: loaded from {}", path.display()); + return data; + } + let mut seed = [0u8; 32]; + tesseras_dht::sys::random_bytes(&mut seed); + if let Err(e) = std::fs::write(path, seed) { + log::warn!("identity: failed to save to {}: {e}", path.display()); + } else { + log::info!("identity: generated new keypair at {}", path.display()); + } + seed.to_vec() +} + +const SIGINT: i32 = 2; +const SIGTERM: i32 = 15; + +unsafe extern "C" { + fn signal(sig: i32, handler: usize) -> usize; +} + +static SHUTDOWN_PTR: std::sync::atomic::AtomicUsize = + std::sync::atomic::AtomicUsize::new(0); + +extern "C" fn sig_handler(_sig: i32) { + let ptr = SHUTDOWN_PTR.load(Ordering::SeqCst); + if ptr != 0 { + let flag = unsafe { &*(ptr as *const AtomicBool) }; + flag.store(true, Ordering::SeqCst); + } +} |