use std::io::{BufRead, BufReader, Read, Write};
use std::os::unix::net::UnixStream;
use std::path::PathBuf;
#[path = "../base58.rs"]
mod base58;
#[path = "../sandbox.rs"]
mod sandbox;
fn default_socket() -> PathBuf {
PathBuf::from("/var/tesseras-paste/daemon.sock")
}
fn usage() {
eprintln!("usage: tp [-s sock] [-v] <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!(" -v verbose output");
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 verbose = false;
let mut cmd_start = 1;
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;
}
"-v" => {
verbose = true;
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);
}
};
const MAX_PASTE: usize = 64 * 1024;
let mut content = Vec::new();
match std::io::stdin()
.take((MAX_PASTE + 1) as u64)
.read_to_end(&mut content)
{
Ok(0) => {
eprintln!("error: empty input");
std::process::exit(1);
}
Ok(n) if n > MAX_PASTE => {
eprintln!("error: input exceeds 64 KiB limit");
std::process::exit(1);
}
Err(e) => {
eprintln!("error: reading stdin: {e}");
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);
}
};
sandbox::do_unveil(&sock_path, "rw");
sandbox::unveil_lock();
sandbox::do_pledge("stdio unix rpath");
if verbose {
eprintln!("socket: {}", sock_path.display());
eprintln!(">> {}", request.trim());
}
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 verbose {
eprintln!("<< {}", line);
}
if let Some(data) = line.strip_prefix("OK ") {
if is_get {
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);
}
}
}