//! `cubec` — a tiny Unix-domain-socket client for the `cube-server` daemon. //! //! It speaks the same framed wire protocol as the server (`net` module): a //! 4-byte length prefix + UTF-8 payload per frame, one request → one reply. //! //! When the daemon was started with `--auth-key`, the connection opens with a //! `CHALLENGE ` frame (plan R4). `cubec` proves possession of the same //! pre-shared key by replying `HELLO `. Without a key //! (legacy daemon) `cubec` behaves exactly as before — it sends its command //! first and reads the reply. use cubesys::net::{read_stream_frame, write_frame}; use std::io::{BufRead, Write}; use std::os::unix::net::UnixStream; /// Resolve `--key VALUE` from argv, or None. fn arg_value(args: &[String], key: &str) -> Option { args.iter() .position(|a| a == key) .and_then(|i| args.get(i + 1).cloned()) } fn main() { let args: Vec = std::env::args().skip(1).collect(); let socket = arg_value(&args, "--socket") .or_else(|| std::env::var("CUBE_SOCKET").ok()) .unwrap_or_else(|| "/run/cube/demo.sock".to_string()); let tenant = arg_value(&args, "--tenant").unwrap_or_else(|| "default".to_string()); let owner = arg_value(&args, "--owner") .or_else(|| std::env::var("USER").ok()) .unwrap_or_else(|| "cubec".to_string()); // Pre-shared key for the signed-HELLO handshake (plan R4). `--auth-key PATH` // reads a file; `--auth-key-env VAR` reads an env var; otherwise `CUBE_AUTH_KEY`. let psk: Option> = if let Some(path) = arg_value(&args, "--auth-key") { std::fs::read_to_string(&path) .map(|s| s.trim().as_bytes().to_vec()) .ok() } else if let Some(var) = arg_value(&args, "--auth-key-env") { std::env::var(&var) .ok() .map(|s| s.trim().as_bytes().to_vec()) } else { std::env::var("CUBE_AUTH_KEY") .ok() .map(|s| s.trim().as_bytes().to_vec()) }; let mut stream = match UnixStream::connect(&socket) { Ok(s) => s, Err(e) => { eprintln!("cubec: cannot connect to {socket}: {e}"); std::process::exit(1); } }; // Complete the challenge-response handshake if we (and the server) are in // auth mode. We only expect a CHALLENGE when we have a key; reading first // unconditionally would deadlock against a legacy (un-keyed) server. if let Some(key) = psk.as_deref() { let frame = match read_stream_frame(&mut stream) { Ok(f) => f, Err(e) => { eprintln!("cubec: handshake read failed: {e}"); std::process::exit(1); } }; let line = frame.trim(); if !line.to_lowercase().starts_with("challenge ") { eprintln!("cubec: expected CHALLENGE from an auth-enabled server, got: {line}"); std::process::exit(1); } let nonce = line["challenge ".len()..].trim(); // HELLO + HMAC over (nonce|tenant|owner|remote). No remote asserted here. let sig = cubecrypt::sign_hello(key, nonce, &tenant, &owner, None); let hello = format!("HELLO {tenant} {owner} {sig}"); if write_frame(&mut stream, &hello).is_err() { eprintln!("cubec: handshake write failed"); std::process::exit(1); } } if args.is_empty() { // REPL mode (legacy behaviour, now after an optional handshake). let stdin = std::io::stdin(); for line in stdin.lock().lines().map_while(Result::ok) { let line = line.trim(); if line.is_empty() { continue; } if write_frame(&mut stream, line).is_err() { eprintln!("cubec: write failed"); return; } match read_stream_frame(&mut stream) { Ok(reply) => println!("{reply}"), Err(e) => { eprintln!("cubec: read failed: {e}"); return; } } } } else { // One-shot: join the remaining args as the command line, send, print. let cmd = args.join(" "); if write_frame(&mut stream, &cmd).is_err() { eprintln!("cubec: write failed"); std::process::exit(1); } match read_stream_frame(&mut stream) { Ok(reply) => println!("{reply}"), Err(e) => { eprintln!("cubec: read failed: {e}"); std::process::exit(1); } } } let _ = stream.flush(); }