use std::io::{BufRead, BufReader, Read, Write};
use std::net::{Ipv4Addr, SocketAddr, TcpListener, TcpStream};
use std::path::PathBuf;
use std::time::Duration;
use anyhow::{Context, Result, bail};
use super::drop_transfer::{MAX_TRANSFER_BYTES, Response, authorize, default_roots};
const DIAL_TIMEOUT: Duration = Duration::from_secs(5);
const READ_TIMEOUT: Duration = Duration::from_secs(120);
pub fn serve(port: u16, roots: Vec<PathBuf>) -> Result<()> {
let roots = if roots.is_empty() {
default_roots()
} else {
roots
};
if roots.is_empty() {
bail!("no servable directories resolved; pass --root explicitly");
}
let addr = SocketAddr::from((Ipv4Addr::LOCALHOST, port));
let listener = TcpListener::bind(addr)
.with_context(|| format!("cannot bind {addr}; is another agent already running?"))?;
println!("OpenCrabs drop agent listening on {addr}");
println!("Serving files from:");
for root in &roots {
println!(" {}", root.display());
}
println!(
"\nOn the machine running the TUI, connect with:\n \
ssh -R {port}:localhost:{port} <you>@<host>\n"
);
for stream in listener.incoming() {
match stream {
Ok(stream) => {
if let Err(e) = handle(stream, &roots) {
tracing::warn!("drop agent: request failed: {e:#}");
}
}
Err(e) => tracing::warn!("drop agent: accept failed: {e}"),
}
}
Ok(())
}
fn handle(mut stream: TcpStream, roots: &[PathBuf]) -> Result<()> {
stream.set_read_timeout(Some(READ_TIMEOUT))?;
stream.set_write_timeout(Some(READ_TIMEOUT))?;
let mut requested = String::new();
BufReader::new(stream.try_clone()?).read_line(&mut requested)?;
let requested = requested.trim();
if requested.is_empty() {
return Ok(());
}
match authorize(requested, roots) {
Ok(path) => {
let bytes = std::fs::read(&path)?;
println!(
"drop agent: serving {} ({} bytes)",
path.display(),
bytes.len()
);
stream.write_all(
Response::Ok {
len: bytes.len() as u64,
}
.encode()
.as_bytes(),
)?;
stream.write_all(&bytes)?;
}
Err(refusal) => {
println!("drop agent: REFUSED {requested} ({})", refusal.reason());
stream.write_all(
Response::Err {
reason: refusal.reason(),
}
.encode()
.as_bytes(),
)?;
}
}
stream.flush()?;
Ok(())
}
pub fn fetch(port: u16, client_path: &str) -> Result<Vec<u8>> {
let addr = SocketAddr::from((Ipv4Addr::LOCALHOST, port));
let mut stream = TcpStream::connect_timeout(&addr, DIAL_TIMEOUT)
.with_context(|| format!("no drop agent answering on {addr}"))?;
stream.set_read_timeout(Some(READ_TIMEOUT))?;
stream.set_write_timeout(Some(READ_TIMEOUT))?;
writeln!(stream, "{client_path}")?;
stream.flush()?;
let mut reader = BufReader::new(stream);
let mut line = String::new();
reader.read_line(&mut line)?;
let Some(response) = Response::parse(&line) else {
bail!("drop agent sent an unreadable response: {line:?}");
};
match response {
Response::Err { reason } => bail!("drop agent refused: {reason}"),
Response::Ok { len } => {
if len > MAX_TRANSFER_BYTES {
bail!("drop agent offered {len} bytes, over the transfer limit");
}
let mut bytes = vec![0u8; len as usize];
reader
.read_exact(&mut bytes)
.with_context(|| format!("transfer cut short before {len} bytes"))?;
Ok(bytes)
}
}
}