tesseras-paste 0.1.3

Decentralized pastebin built on tesseras-dht
//! 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 = "../dns.rs"]
mod dns;
#[path = "../ops.rs"]
mod ops;
#[path = "../paste.rs"]
mod paste;
#[path = "../protocol.rs"]
mod protocol;
#[path = "../sandbox.rs"]
mod sandbox;
#[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] [-n] [-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!("  -n            no auto-bootstrap (skip DNS SRV)");
    eprintln!("  -b host:port  bootstrap peer (repeatable)");
    eprintln!("  -v            verbose (debug logging)");
    eprintln!("  -h            show this help");
}

fn main() {
    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 no_auto_bootstrap = false;
    let mut verbose = 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,
            "-n" => no_auto_bootstrap = true,
            "-v" => verbose = 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 default_level = if verbose { "debug" } else { "info" };
    env_logger::Builder::from_env(
        env_logger::Env::default().default_filter_or(default_level),
    )
    .format(|buf, record| {
        use std::io::Write;
        writeln!(buf, "[{}]: {}", record.level(), record.args())
    })
    .init();

    let sock_path = sock.unwrap_or_else(|| dir.join("daemon.sock"));

    // Ensure directories exist
    if let Err(e) = std::fs::create_dir_all(&dir) {
        eprintln!("error: cannot create {}: {e}", dir.display());
        std::process::exit(1);
    }
    if let Some(parent) = sock_path.parent()
        && let Err(e) = std::fs::create_dir_all(parent)
    {
        eprintln!("error: cannot create {}: {e}", parent.display());
        std::process::exit(1);
    }

    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);

    // ── Sandbox ─────────────────────────────────────
    // Apply unveil(2) to restrict filesystem visibility,
    // then pledge(2) to restrict syscalls.
    sandbox::do_unveil(&dir, "rwc");
    if sock_path.parent() != Some(dir.as_ref())
        && let Some(parent) = sock_path.parent()
    {
        sandbox::do_unveil(parent, "rwc");
    }
    if !no_auto_bootstrap || !bootstrap.is_empty() {
        sandbox::do_unveil(std::path::Path::new("/etc/resolv.conf"), "r");
    }
    sandbox::unveil_lock();

    sandbox::do_pledge("stdio rpath wpath cpath fattr inet unix dns");

    // If no explicit peers given and auto-bootstrap is enabled,
    // discover peers via DNS SRV (_tesseras._udp.tesseras.net).
    if bootstrap.is_empty() && !no_auto_bootstrap {
        log::info!("bootstrap: resolving SRV records");
        let srv = dns::lookup_bootstrap();
        if srv.is_empty() {
            log::warn!("bootstrap: no SRV records found");
        }
        for rec in &srv {
            bootstrap.push(format!("{}:{}", rec.host, rec.port));
        }
    }

    for peer in &bootstrap {
        let parts: Vec<&str> = peer.rsplitn(2, ':').collect();
        if parts.len() != 2 {
            eprintln!("warning: bad bootstrap address: {peer}");
            continue;
        }
        let host = parts[1];
        let p: u16 = match parts[0].parse() {
            Ok(p) => p,
            Err(_) => {
                eprintln!("warning: bad bootstrap port: {peer}");
                continue;
            }
        };
        if let Err(e) = node.join(host, p) {
            log::warn!("bootstrap: failed to join {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 — Arc::into_raw intentionally leaks the
    // refcount so the pointer remains valid for the process
    // lifetime. No matching Arc::from_raw needed.
    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, &bootstrap);

    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);
    match write_private_file(path, &seed) {
        Ok(()) => {
            log::info!("identity: generated new keypair at {}", path.display());
        }
        Err(e) => {
            log::warn!("identity: failed to save to {}: {e}", path.display());
        }
    }
    seed.to_vec()
}

/// Write data to a file with mode 0600 (owner read/write only).
fn write_private_file(
    path: &std::path::Path,
    data: &[u8],
) -> std::io::Result<()> {
    use std::io::Write;
    use std::os::unix::fs::OpenOptionsExt;
    let mut f = std::fs::OpenOptions::new()
        .write(true)
        .create(true)
        .truncate(true)
        .mode(0o600)
        .open(path)?;
    f.write_all(data)?;
    f.sync_all()
}

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);
    }
}