use std::path::{Component, Path, PathBuf};
pub const MAX_TRANSFER_BYTES: u64 = 64 * 1024 * 1024;
pub const DEFAULT_DROP_PORT: u16 = 8765;
pub const DROP_PORT_VAR: &str = "OPENCRABS_DROP_PORT";
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Response {
Ok { len: u64 },
Err { reason: String },
}
impl Response {
pub fn encode(&self) -> String {
match self {
Response::Ok { len } => format!("OK {len}\n"),
Response::Err { reason } => {
format!("ERR {}\n", reason.replace(['\n', '\r'], " "))
}
}
}
pub fn parse(line: &str) -> Option<Self> {
let line = line.trim_end_matches(['\n', '\r']);
if let Some(rest) = line.strip_prefix("OK ") {
return rest
.trim()
.parse::<u64>()
.ok()
.map(|len| Response::Ok { len });
}
line.strip_prefix("ERR ").map(|reason| Response::Err {
reason: reason.trim().to_string(),
})
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Refusal {
NotAbsolute,
Traversal,
OutsideRoots,
NotAFile,
TooLarge { bytes: u64 },
}
impl Refusal {
pub fn reason(&self) -> String {
match self {
Refusal::NotAbsolute => "path is not absolute".into(),
Refusal::Traversal => "path contains .. traversal".into(),
Refusal::OutsideRoots => "path is outside the directories this agent serves".into(),
Refusal::NotAFile => "not a regular file".into(),
Refusal::TooLarge { bytes } => {
format!("file is {bytes} bytes, over the {MAX_TRANSFER_BYTES} byte limit")
}
}
}
}
pub fn default_roots() -> Vec<PathBuf> {
let Some(home) = dirs::home_dir() else {
return Vec::new();
};
["Desktop", "Downloads", "Pictures", "Documents", "Movies"]
.iter()
.map(|d| home.join(d))
.collect()
}
pub fn authorize(requested: &str, roots: &[PathBuf]) -> Result<PathBuf, Refusal> {
let path = Path::new(requested);
if !path.is_absolute() {
return Err(Refusal::NotAbsolute);
}
if path.components().any(|c| c == Component::ParentDir) {
return Err(Refusal::Traversal);
}
let resolved = path.canonicalize().map_err(|_| Refusal::NotAFile)?;
let meta = std::fs::metadata(&resolved).map_err(|_| Refusal::NotAFile)?;
if !meta.is_file() {
return Err(Refusal::NotAFile);
}
if meta.len() > MAX_TRANSFER_BYTES {
return Err(Refusal::TooLarge { bytes: meta.len() });
}
let inside = roots.iter().any(|root| {
root.canonicalize()
.map(|r| resolved.starts_with(r))
.unwrap_or(false)
});
if !inside {
return Err(Refusal::OutsideRoots);
}
Ok(resolved)
}
pub const SSH_MARKERS: [&str; 3] = ["SSH_CONNECTION", "SSH_TTY", "SSH_CLIENT"];
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Tunnel {
pub port: u16,
pub declared: bool,
}
pub fn tunnel_from(var: impl Fn(&str) -> Option<String>) -> Option<Tunnel> {
if let Some(port) = var(DROP_PORT_VAR).and_then(|v| v.trim().parse().ok()) {
return Some(Tunnel {
port,
declared: true,
});
}
let over_ssh = SSH_MARKERS
.iter()
.any(|k| var(k).is_some_and(|v| !v.trim().is_empty()));
over_ssh.then_some(Tunnel {
port: DEFAULT_DROP_PORT,
declared: false,
})
}
pub fn tunnel() -> Option<Tunnel> {
tunnel_from(|k| std::env::var(k).ok())
}
pub fn ssh_hint(user_and_host: &str, port: u16) -> String {
format!("ssh -R 127.0.0.1:{port}:localhost:{port} {user_and_host}")
}