pub fn validate_container_name(container: &str) -> Result<(), String> {
let ok = matches!(container.chars().next(), Some(c) if c.is_ascii_alphanumeric())
&& container.len() <= 128
&& container
.chars()
.all(|c| c.is_ascii_alphanumeric() || matches!(c, '_' | '.' | '-'));
if ok {
Ok(())
} else {
Err(format!(
"invalid container name: {container:?} (must match Docker naming rules)"
))
}
}
pub fn validate_remote_host(host: &str) -> Result<(), String> {
let ok = !host.is_empty()
&& host.len() <= 253
&& host
.chars()
.all(|c| c.is_ascii_alphanumeric() || matches!(c, '.' | '-'));
if ok {
Ok(())
} else {
Err(format!("invalid remote host: {host:?}"))
}
}
pub fn validate_remote_port(port: u16) -> Result<(), String> {
if (1..=65535).contains(&port) {
Ok(())
} else {
Err(format!("invalid remote port: {port}"))
}
}
pub fn bridge_argv(
container: &str,
tool: crate::config::BridgeTool,
remote_host: &str,
remote_port: u16,
) -> Result<Vec<String>, String> {
validate_container_name(container)?;
validate_remote_host(remote_host)?;
validate_remote_port(remote_port)?;
let mut argv = vec![
"docker".to_string(),
"exec".to_string(),
"-i".to_string(),
container.to_string(),
tool.as_str().to_string(),
];
match tool {
crate::config::BridgeTool::Socat => {
argv.push("-".into());
argv.push(format!("TCP:{remote_host}:{remote_port}"));
}
crate::config::BridgeTool::Nc | crate::config::BridgeTool::Ncat => {
argv.push(remote_host.to_string());
argv.push(remote_port.to_string());
}
}
Ok(argv)
}
pub fn tool_probe_argv(
container: &str,
tool: crate::config::BridgeTool,
) -> Result<Vec<String>, String> {
validate_container_name(container)?;
Ok(vec![
"docker".into(),
"exec".into(),
container.to_string(),
"which".into(),
tool.as_str().to_string(),
])
}
pub fn inspect_argv(container: &str) -> Result<Vec<String>, String> {
validate_container_name(container)?;
Ok(vec![
"docker".into(),
"inspect".into(),
"--format".into(),
"{{.Config.Image}}|{{.State.StartedAt}}|{{.State.Running}}".into(),
container.to_string(),
])
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn container_rules() {
assert!(validate_container_name("db-1.x_y").is_ok());
assert!(validate_container_name("-bad").is_err());
assert!(validate_container_name("bad;rm").is_err());
assert!(validate_container_name("$(x)").is_err());
assert!(validate_container_name("").is_err());
assert!(validate_container_name(&"a".repeat(129)).is_err());
}
#[test]
fn host_rules() {
assert!(validate_remote_host("db.internal-1.example.invalid").is_ok());
assert!(validate_remote_host("10.0.0.5").is_ok());
assert!(validate_remote_host("h; rm -rf").is_err());
assert!(validate_remote_host("").is_err());
}
#[test]
fn bridge_argv_forms() {
let nc = bridge_argv("c1", crate::config::BridgeTool::Nc, "h", 3306).unwrap();
assert_eq!(nc, vec!["docker", "exec", "-i", "c1", "nc", "h", "3306"]);
let socat = bridge_argv("c1", crate::config::BridgeTool::Socat, "h", 3306).unwrap();
assert_eq!(
socat,
vec!["docker", "exec", "-i", "c1", "socat", "-", "TCP:h:3306"]
);
let ncat = bridge_argv("c1", crate::config::BridgeTool::Ncat, "h", 3306).unwrap();
assert_eq!(
ncat,
vec!["docker", "exec", "-i", "c1", "ncat", "h", "3306"]
);
assert!(bridge_argv("c;1", crate::config::BridgeTool::Nc, "h", 3306).is_err());
assert!(bridge_argv("c1", crate::config::BridgeTool::Nc, "h", 0).is_err());
}
#[test]
fn probe_uses_which_without_shell() {
let probe = tool_probe_argv("c1", crate::config::BridgeTool::Socat).unwrap();
assert_eq!(probe, vec!["docker", "exec", "c1", "which", "socat"]);
assert!(!probe.iter().any(|a| a.contains("sh -c")));
}
}