aboutsummaryrefslogtreecommitdiffstats
path: root/src/bin
diff options
context:
space:
mode:
authormurilo ijanc2026-03-25 02:07:37 -0300
committermurilo ijanc2026-03-25 02:07:37 -0300
commit7aff2e1d279a4e442b32f49ca0a0eca065355787 (patch)
treebc987ece7eb78bb8375de1b20123ecd0f90472ba /src/bin
downloadtesseras-paste-7aff2e1d279a4e442b32f49ca0a0eca065355787.tar.gz
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.
Diffstat (limited to 'src/bin')
-rw-r--r--src/bin/tp.rs206
-rw-r--r--src/bin/tpd.rs293
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);
+ }
+}