@@ -1518,7 +1518,7 @@ impl RemoteServerProjects {
.ssh_connections
.get_or_insert(Default::default())
.push(SshConnection {
- host: SharedString::from(connection_options.host),
+ host: SharedString::from(connection_options.host.to_string()),
username: connection_options.username,
port: connection_options.port,
projects: BTreeSet::new(),
@@ -1983,7 +1983,7 @@ impl RemoteServerProjects {
.size_full()
.child(match &options {
ViewServerOptionsState::Ssh { connection, .. } => SshConnectionHeader {
- connection_string: connection.host.clone().into(),
+ connection_string: connection.host.to_string().into(),
paths: Default::default(),
nickname: connection.nickname.clone().map(|s| s.into()),
is_wsl: false,
@@ -2148,7 +2148,7 @@ impl RemoteServerProjects {
window: &mut Window,
cx: &mut Context<Self>,
) -> impl IntoElement {
- let connection_string = SharedString::new(connection.host.clone());
+ let connection_string = SharedString::new(connection.host.to_string());
v_flex()
.child({
@@ -2659,7 +2659,7 @@ impl RemoteServerProjects {
self.add_ssh_server(
SshConnectionOptions {
- host: ssh_config_host.to_string(),
+ host: ssh_config_host.to_string().into(),
..SshConnectionOptions::default()
},
cx,
@@ -23,6 +23,7 @@ use smol::{
process::{self, Child, Stdio},
};
use std::{
+ net::IpAddr,
path::{Path, PathBuf},
sync::Arc,
time::Instant,
@@ -47,9 +48,58 @@ pub(crate) struct SshRemoteConnection {
_temp_dir: TempDir,
}
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+pub enum SshConnectionHost {
+ IpAddr(IpAddr),
+ Hostname(String),
+}
+
+impl SshConnectionHost {
+ pub fn to_bracketed_string(&self) -> String {
+ match self {
+ Self::IpAddr(IpAddr::V4(ip)) => ip.to_string(),
+ Self::IpAddr(IpAddr::V6(ip)) => format!("[{}]", ip),
+ Self::Hostname(hostname) => hostname.clone(),
+ }
+ }
+
+ pub fn to_string(&self) -> String {
+ match self {
+ Self::IpAddr(ip) => ip.to_string(),
+ Self::Hostname(hostname) => hostname.clone(),
+ }
+ }
+}
+
+impl From<&str> for SshConnectionHost {
+ fn from(value: &str) -> Self {
+ if let Ok(address) = value.parse() {
+ Self::IpAddr(address)
+ } else {
+ Self::Hostname(value.to_string())
+ }
+ }
+}
+
+impl From<String> for SshConnectionHost {
+ fn from(value: String) -> Self {
+ if let Ok(address) = value.parse() {
+ Self::IpAddr(address)
+ } else {
+ Self::Hostname(value)
+ }
+ }
+}
+
+impl Default for SshConnectionHost {
+ fn default() -> Self {
+ Self::Hostname(Default::default())
+ }
+}
+
#[derive(Debug, Default, Clone, PartialEq, Eq, Hash)]
pub struct SshConnectionOptions {
- pub host: String,
+ pub host: SshConnectionHost,
pub username: Option<String>,
pub port: Option<u16>,
pub password: Option<String>,
@@ -64,7 +114,7 @@ pub struct SshConnectionOptions {
impl From<settings::SshConnection> for SshConnectionOptions {
fn from(val: settings::SshConnection) -> Self {
SshConnectionOptions {
- host: val.host.into(),
+ host: val.host.to_string().into(),
username: val.username,
port: val.port,
password: None,
@@ -96,7 +146,7 @@ impl MasterProcess {
askpass_script_path: &std::ffi::OsStr,
additional_args: Vec<String>,
socket_path: &std::path::Path,
- url: &str,
+ destination: &str,
) -> Result<Self> {
let args = [
"-N",
@@ -120,7 +170,7 @@ impl MasterProcess {
master_process.arg(format!("ControlPath={}", socket_path.display()));
- let process = master_process.arg(&url).spawn()?;
+ let process = master_process.arg(&destination).spawn()?;
Ok(MasterProcess { process })
}
@@ -143,7 +193,7 @@ impl MasterProcess {
pub fn new(
askpass_script_path: &std::ffi::OsStr,
additional_args: Vec<String>,
- url: &str,
+ destination: &str,
) -> Result<Self> {
// On Windows, `ControlMaster` and `ControlPath` are not supported:
// https://github.com/PowerShell/Win32-OpenSSH/issues/405
@@ -165,7 +215,7 @@ impl MasterProcess {
.env("SSH_ASKPASS_REQUIRE", "force")
.env("SSH_ASKPASS", askpass_script_path)
.args(additional_args)
- .arg(url)
+ .arg(destination)
.args(args);
let process = master_process.spawn()?;
@@ -412,7 +462,7 @@ impl SshRemoteConnection {
) -> Result<Self> {
use askpass::AskPassResult;
- let url = connection_options.ssh_url();
+ let destination = connection_options.ssh_destination();
let temp_dir = tempfile::Builder::new()
.prefix("zed-ssh-session")
@@ -437,14 +487,14 @@ impl SshRemoteConnection {
let mut master_process = MasterProcess::new(
askpass.script_path().as_ref(),
connection_options.additional_args(),
- &url,
+ &destination,
)?;
#[cfg(not(target_os = "windows"))]
let mut master_process = MasterProcess::new(
askpass.script_path().as_ref(),
connection_options.additional_args(),
&socket_path,
- &url,
+ &destination,
)?;
let result = select_biased! {
@@ -840,7 +890,7 @@ impl SshRemoteConnection {
}
command.arg(src_path).arg(format!(
"{}:{}",
- self.socket.connection_options.scp_url(),
+ self.socket.connection_options.scp_destination(),
dest_path_str
));
command
@@ -856,7 +906,7 @@ impl SshRemoteConnection {
.unwrap_or_default(),
);
command.arg("-b").arg("-");
- command.arg(self.socket.connection_options.scp_url());
+ command.arg(self.socket.connection_options.scp_destination());
command.stdin(Stdio::piped());
command
}
@@ -986,7 +1036,7 @@ impl SshSocket {
let separator = shell_kind.sequential_commands_separator();
let to_run = format!("cd{separator} {to_run}");
self.ssh_options(&mut command, true)
- .arg(self.connection_options.ssh_url());
+ .arg(self.connection_options.ssh_destination());
if !allow_pseudo_tty {
command.arg("-T");
}
@@ -1063,7 +1113,7 @@ impl SshSocket {
"ControlMaster=no".to_string(),
"-o".to_string(),
format!("ControlPath={}", self.socket_path.display()),
- self.connection_options.ssh_url(),
+ self.connection_options.ssh_destination(),
]);
arguments
}
@@ -1071,7 +1121,7 @@ impl SshSocket {
#[cfg(target_os = "windows")]
fn ssh_args(&self) -> Vec<String> {
let mut arguments = self.connection_options.additional_args();
- arguments.push(self.connection_options.ssh_url());
+ arguments.push(self.connection_options.ssh_destination());
arguments
}
@@ -1208,10 +1258,24 @@ impl SshConnectionOptions {
input = rest;
username = Some(u.to_string());
}
- if let Some((rest, p)) = input.split_once(':') {
+
+ // Handle port parsing, accounting for IPv6 addresses
+ // IPv6 addresses can be: 2001:db8::1 or [2001:db8::1]:22
+ if input.starts_with('[') {
+ if let Some((rest, p)) = input.rsplit_once("]:") {
+ input = rest.strip_prefix('[').unwrap_or(rest);
+ port = p.parse().ok();
+ } else if input.ends_with(']') {
+ input = input.strip_prefix('[').unwrap_or(input);
+ input = input.strip_suffix(']').unwrap_or(input);
+ }
+ } else if let Some((rest, p)) = input.rsplit_once(':')
+ && !rest.contains(":")
+ {
input = rest;
- port = p.parse().ok()
+ port = p.parse().ok();
}
+
hostname = Some(input.to_string())
}
@@ -1225,7 +1289,7 @@ impl SshConnectionOptions {
};
Ok(Self {
- host: hostname,
+ host: hostname.into(),
username,
port,
port_forwards,
@@ -1237,19 +1301,16 @@ impl SshConnectionOptions {
})
}
- pub fn ssh_url(&self) -> String {
- let mut result = String::from("ssh://");
+ pub fn ssh_destination(&self) -> String {
+ let mut result = String::default();
if let Some(username) = &self.username {
// Username might be: username1@username2@ip2
let username = urlencoding::encode(username);
result.push_str(&username);
result.push('@');
}
- result.push_str(&self.host);
- if let Some(port) = self.port {
- result.push(':');
- result.push_str(&port.to_string());
- }
+
+ result.push_str(&self.host.to_string());
result
}
@@ -1264,6 +1325,11 @@ impl SshConnectionOptions {
args.extend(["-o".to_string(), format!("ConnectTimeout={}", timeout)]);
}
+ if let Some(port) = self.port {
+ args.push("-p".to_string());
+ args.push(port.to_string());
+ }
+
if let Some(forwards) = &self.port_forwards {
args.extend(forwards.iter().map(|pf| {
let local_host = match &pf.local_host {
@@ -1285,22 +1351,23 @@ impl SshConnectionOptions {
args
}
- fn scp_url(&self) -> String {
+ fn scp_destination(&self) -> String {
if let Some(username) = &self.username {
- format!("{}@{}", username, self.host)
+ format!("{}@{}", username, self.host.to_bracketed_string())
} else {
- self.host.clone()
+ self.host.to_string()
}
}
pub fn connection_string(&self) -> String {
- let host = if let Some(username) = &self.username {
- format!("{}@{}", username, self.host)
+ let host = if let Some(port) = &self.port {
+ format!("{}:{}", self.host.to_bracketed_string(), port)
} else {
- self.host.clone()
+ self.host.to_string()
};
- if let Some(port) = &self.port {
- format!("{}:{}", host, port)
+
+ if let Some(username) = &self.username {
+ format!("{}@{}", username, host)
} else {
host
}
@@ -1510,4 +1577,44 @@ mod tests {
]
);
}
+
+ #[test]
+ fn test_host_parsing() -> Result<()> {
+ let opts = SshConnectionOptions::parse_command_line("user@2001:db8::1")?;
+ assert_eq!(opts.host, "2001:db8::1".into());
+ assert_eq!(opts.username, Some("user".to_string()));
+ assert_eq!(opts.port, None);
+
+ let opts = SshConnectionOptions::parse_command_line("user@[2001:db8::1]:2222")?;
+ assert_eq!(opts.host, "2001:db8::1".into());
+ assert_eq!(opts.username, Some("user".to_string()));
+ assert_eq!(opts.port, Some(2222));
+
+ let opts = SshConnectionOptions::parse_command_line("user@[2001:db8::1]")?;
+ assert_eq!(opts.host, "2001:db8::1".into());
+ assert_eq!(opts.username, Some("user".to_string()));
+ assert_eq!(opts.port, None);
+
+ let opts = SshConnectionOptions::parse_command_line("2001:db8::1")?;
+ assert_eq!(opts.host, "2001:db8::1".into());
+ assert_eq!(opts.username, None);
+ assert_eq!(opts.port, None);
+
+ let opts = SshConnectionOptions::parse_command_line("[2001:db8::1]:2222")?;
+ assert_eq!(opts.host, "2001:db8::1".into());
+ assert_eq!(opts.username, None);
+ assert_eq!(opts.port, Some(2222));
+
+ let opts = SshConnectionOptions::parse_command_line("user@example.com:2222")?;
+ assert_eq!(opts.host, "example.com".into());
+ assert_eq!(opts.username, Some("user".to_string()));
+ assert_eq!(opts.port, Some(2222));
+
+ let opts = SshConnectionOptions::parse_command_line("user@192.168.1.1:2222")?;
+ assert_eq!(opts.host, "192.168.1.1".into());
+ assert_eq!(opts.username, Some("user".to_string()));
+ assert_eq!(opts.port, Some(2222));
+
+ Ok(())
+ }
}
@@ -1273,7 +1273,7 @@ impl WorkspaceDb {
match options {
RemoteConnectionOptions::Ssh(options) => {
kind = RemoteConnectionKind::Ssh;
- host = Some(options.host);
+ host = Some(options.host.to_string());
port = options.port;
user = options.username;
}
@@ -1486,7 +1486,7 @@ impl WorkspaceDb {
user: user,
})),
RemoteConnectionKind::Ssh => Some(RemoteConnectionOptions::Ssh(SshConnectionOptions {
- host: host?,
+ host: host?.into(),
port,
username: user,
..Default::default()
@@ -2757,7 +2757,7 @@ mod tests {
let connection_id = db
.get_or_create_remote_connection(RemoteConnectionOptions::Ssh(SshConnectionOptions {
- host: "my-host".to_string(),
+ host: "my-host".into(),
port: Some(1234),
..Default::default()
}))
@@ -2946,7 +2946,7 @@ mod tests {
.into_iter()
.map(|(host, user)| async {
let options = RemoteConnectionOptions::Ssh(SshConnectionOptions {
- host: host.to_string(),
+ host: host.into(),
username: Some(user.to_string()),
..Default::default()
});
@@ -3037,7 +3037,7 @@ mod tests {
let connection_id = db
.get_or_create_remote_connection(RemoteConnectionOptions::Ssh(SshConnectionOptions {
- host: host.clone(),
+ host: host.clone().into(),
port,
username: user.clone(),
..Default::default()
@@ -3048,7 +3048,7 @@ mod tests {
// Test that calling the function again with the same parameters returns the same project
let same_connection = db
.get_or_create_remote_connection(RemoteConnectionOptions::Ssh(SshConnectionOptions {
- host: host.clone(),
+ host: host.clone().into(),
port,
username: user.clone(),
..Default::default()
@@ -3065,7 +3065,7 @@ mod tests {
let different_connection = db
.get_or_create_remote_connection(RemoteConnectionOptions::Ssh(SshConnectionOptions {
- host: host2.clone(),
+ host: host2.clone().into(),
port: port2,
username: user2.clone(),
..Default::default()
@@ -3084,7 +3084,7 @@ mod tests {
let connection_id = db
.get_or_create_remote_connection(RemoteConnectionOptions::Ssh(SshConnectionOptions {
- host: host.clone(),
+ host: host.clone().into(),
port,
username: None,
..Default::default()
@@ -3094,7 +3094,7 @@ mod tests {
let same_connection_id = db
.get_or_create_remote_connection(RemoteConnectionOptions::Ssh(SshConnectionOptions {
- host: host.clone(),
+ host: host.clone().into(),
port,
username: user.clone(),
..Default::default()
@@ -3124,7 +3124,7 @@ mod tests {
ids.push(
db.get_or_create_remote_connection(RemoteConnectionOptions::Ssh(
SshConnectionOptions {
- host: host.clone(),
+ host: host.clone().into(),
port: *port,
username: user.clone(),
..Default::default()