Skip to main content

sequel_mcp/sql/
docker.rs

1//! Docker bridge validation and command construction (no shell strings).
2
3/// Container names: alnum start, then alnum/`_`/`.`/`-`, ≤128 total.
4pub fn validate_container_name(container: &str) -> Result<(), String> {
5    let ok = matches!(container.chars().next(), Some(c) if c.is_ascii_alphanumeric())
6        && container.len() <= 128
7        && container
8            .chars()
9            .all(|c| c.is_ascii_alphanumeric() || matches!(c, '_' | '.' | '-'));
10    if ok {
11        Ok(())
12    } else {
13        Err(format!(
14            "invalid container name: {container:?} (must match Docker naming rules)"
15        ))
16    }
17}
18
19/// Remote hosts reachable through the bridge: alnum/`.`/`-`, ≤253.
20pub fn validate_remote_host(host: &str) -> Result<(), String> {
21    let ok = !host.is_empty()
22        && host.len() <= 253
23        && host
24            .chars()
25            .all(|c| c.is_ascii_alphanumeric() || matches!(c, '.' | '-'));
26    if ok {
27        Ok(())
28    } else {
29        Err(format!("invalid remote host: {host:?}"))
30    }
31}
32
33pub fn validate_remote_port(port: u16) -> Result<(), String> {
34    if (1..=65535).contains(&port) {
35        Ok(())
36    } else {
37        Err(format!("invalid remote port: {port}"))
38    }
39}
40
41/// The argv for the remote `docker exec` bridge — a structured argument
42/// vector, never a shell string. Executed over an SSH exec channel.
43/// Per-tool forms match the legacy bridge commands:
44/// `socat - TCP:h:p` and `nc|ncat h p`.
45pub fn bridge_argv(
46    container: &str,
47    tool: crate::config::BridgeTool,
48    remote_host: &str,
49    remote_port: u16,
50) -> Result<Vec<String>, String> {
51    validate_container_name(container)?;
52    validate_remote_host(remote_host)?;
53    validate_remote_port(remote_port)?;
54    let mut argv = vec![
55        "docker".to_string(),
56        "exec".to_string(),
57        "-i".to_string(),
58        container.to_string(),
59        tool.as_str().to_string(),
60    ];
61    match tool {
62        crate::config::BridgeTool::Socat => {
63            argv.push("-".into());
64            argv.push(format!("TCP:{remote_host}:{remote_port}"));
65        }
66        crate::config::BridgeTool::Nc | crate::config::BridgeTool::Ncat => {
67            argv.push(remote_host.to_string());
68            argv.push(remote_port.to_string());
69        }
70    }
71    Ok(argv)
72}
73
74/// Argv probing the bridge tool's presence inside the container. `which` is
75/// executed directly (an exec argv, no `sh -c` string).
76pub fn tool_probe_argv(
77    container: &str,
78    tool: crate::config::BridgeTool,
79) -> Result<Vec<String>, String> {
80    validate_container_name(container)?;
81    Ok(vec![
82        "docker".into(),
83        "exec".into(),
84        container.to_string(),
85        "which".into(),
86        tool.as_str().to_string(),
87    ])
88}
89
90/// Argv for `docker inspect` with a structured output format.
91pub fn inspect_argv(container: &str) -> Result<Vec<String>, String> {
92    validate_container_name(container)?;
93    Ok(vec![
94        "docker".into(),
95        "inspect".into(),
96        "--format".into(),
97        "{{.Config.Image}}|{{.State.StartedAt}}|{{.State.Running}}".into(),
98        container.to_string(),
99    ])
100}
101
102#[cfg(test)]
103mod tests {
104    use super::*;
105
106    #[test]
107    fn container_rules() {
108        assert!(validate_container_name("db-1.x_y").is_ok());
109        assert!(validate_container_name("-bad").is_err());
110        assert!(validate_container_name("bad;rm").is_err());
111        assert!(validate_container_name("$(x)").is_err());
112        assert!(validate_container_name("").is_err());
113        assert!(validate_container_name(&"a".repeat(129)).is_err());
114    }
115
116    #[test]
117    fn host_rules() {
118        assert!(validate_remote_host("db.internal-1.example.invalid").is_ok());
119        assert!(validate_remote_host("10.0.0.5").is_ok());
120        assert!(validate_remote_host("h; rm -rf").is_err());
121        assert!(validate_remote_host("").is_err());
122    }
123
124    #[test]
125    fn bridge_argv_forms() {
126        let nc = bridge_argv("c1", crate::config::BridgeTool::Nc, "h", 3306).unwrap();
127        assert_eq!(nc, vec!["docker", "exec", "-i", "c1", "nc", "h", "3306"]);
128        let socat = bridge_argv("c1", crate::config::BridgeTool::Socat, "h", 3306).unwrap();
129        assert_eq!(
130            socat,
131            vec!["docker", "exec", "-i", "c1", "socat", "-", "TCP:h:3306"]
132        );
133        let ncat = bridge_argv("c1", crate::config::BridgeTool::Ncat, "h", 3306).unwrap();
134        assert_eq!(
135            ncat,
136            vec!["docker", "exec", "-i", "c1", "ncat", "h", "3306"]
137        );
138        assert!(bridge_argv("c;1", crate::config::BridgeTool::Nc, "h", 3306).is_err());
139        assert!(bridge_argv("c1", crate::config::BridgeTool::Nc, "h", 0).is_err());
140    }
141
142    #[test]
143    fn probe_uses_which_without_shell() {
144        let probe = tool_probe_argv("c1", crate::config::BridgeTool::Socat).unwrap();
145        assert_eq!(probe, vec!["docker", "exec", "c1", "which", "socat"]);
146        assert!(!probe.iter().any(|a| a.contains("sh -c")));
147    }
148}