1pub 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
19pub 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
41pub 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
74pub 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
90pub 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}