Add SSH port forwards to settings (#24474)

Closes #6920

Release Notes:

- Added ability to specify port forwarding settings for remote
connections
This commit is contained in:
Richard Weber 2025-02-14 23:52:24 +02:00 committed by GitHub
parent 58b0a6c4af
commit 5f6311171f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 158 additions and 6 deletions

View file

@ -30,6 +30,7 @@ paths.workspace = true
parking_lot.workspace = true
prost.workspace = true
rpc = { workspace = true, features = ["gpui"] }
schemars.workspace = true
serde.workspace = true
serde_json.workspace = true
shlex.workspace = true

View file

@ -29,6 +29,8 @@ use rpc::{
AnyProtoClient, EntityMessageSubscriber, ErrorExt, ProtoClient, ProtoMessageHandlerSet,
RpcError,
};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use smol::{
fs,
process::{self, Child, Stdio},
@ -59,6 +61,16 @@ pub struct SshSocket {
socket_path: PathBuf,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize, Serialize, JsonSchema)]
pub struct SshPortForwardOption {
#[serde(skip_serializing_if = "Option::is_none")]
pub local_host: Option<String>,
pub local_port: u16,
#[serde(skip_serializing_if = "Option::is_none")]
pub remote_host: Option<String>,
pub remote_port: u16,
}
#[derive(Debug, Default, Clone, PartialEq, Eq, Hash)]
pub struct SshConnectionOptions {
pub host: String,
@ -66,6 +78,7 @@ pub struct SshConnectionOptions {
pub port: Option<u16>,
pub password: Option<String>,
pub args: Option<Vec<String>>,
pub port_forwards: Option<Vec<SshPortForwardOption>>,
pub nickname: Option<String>,
pub upload_binary_over_ssh: bool,
@ -83,6 +96,42 @@ macro_rules! shell_script {
}};
}
fn parse_port_number(port_str: &str) -> Result<u16> {
port_str
.parse()
.map_err(|e| anyhow!("Invalid port number: {}: {}", port_str, e))
}
fn parse_port_forward_spec(spec: &str) -> Result<SshPortForwardOption> {
let parts: Vec<&str> = spec.split(':').collect();
match parts.len() {
4 => {
let local_port = parse_port_number(parts[1])?;
let remote_port = parse_port_number(parts[3])?;
Ok(SshPortForwardOption {
local_host: Some(parts[0].to_string()),
local_port,
remote_host: Some(parts[2].to_string()),
remote_port,
})
}
3 => {
let local_port = parse_port_number(parts[0])?;
let remote_port = parse_port_number(parts[2])?;
Ok(SshPortForwardOption {
local_host: None,
local_port,
remote_host: Some(parts[1].to_string()),
remote_port,
})
}
_ => anyhow::bail!("Invalid port forward format"),
}
}
impl SshConnectionOptions {
pub fn parse_command_line(input: &str) -> Result<Self> {
let input = input.trim_start_matches("ssh ");
@ -90,14 +139,14 @@ impl SshConnectionOptions {
let mut username: Option<String> = None;
let mut port: Option<u16> = None;
let mut args = Vec::new();
let mut port_forwards: Vec<SshPortForwardOption> = Vec::new();
// disallowed: -E, -e, -F, -f, -G, -g, -M, -N, -n, -O, -q, -S, -s, -T, -t, -V, -v, -W
const ALLOWED_OPTS: &[&str] = &[
"-4", "-6", "-A", "-a", "-C", "-K", "-k", "-X", "-x", "-Y", "-y",
];
const ALLOWED_ARGS: &[&str] = &[
"-B", "-b", "-c", "-D", "-I", "-i", "-J", "-L", "-l", "-m", "-o", "-P", "-p", "-R",
"-w",
"-B", "-b", "-c", "-D", "-I", "-i", "-J", "-l", "-m", "-o", "-P", "-p", "-R", "-w",
];
let mut tokens = shlex::split(input)
@ -123,6 +172,20 @@ impl SshConnectionOptions {
username = Some(l.to_string());
continue;
}
if arg == "-L" || arg.starts_with("-L") {
let forward_spec = if arg == "-L" {
tokens.next()
} else {
Some(arg.strip_prefix("-L").unwrap().to_string())
};
if let Some(spec) = forward_spec {
port_forwards.push(parse_port_forward_spec(&spec)?);
} else {
anyhow::bail!("Missing port forward format");
}
}
for a in ALLOWED_ARGS {
if arg == *a {
args.push(arg);
@ -154,10 +217,16 @@ impl SshConnectionOptions {
anyhow::bail!("missing hostname");
};
let port_forwards = match port_forwards.len() {
0 => None,
_ => Some(port_forwards),
};
Ok(Self {
host: hostname.to_string(),
username: username.clone(),
port,
port_forwards,
args: Some(args),
password: None,
nickname: None,
@ -179,8 +248,28 @@ impl SshConnectionOptions {
result
}
pub fn additional_args(&self) -> Option<&Vec<String>> {
self.args.as_ref()
pub fn additional_args(&self) -> Vec<String> {
let mut args = self.args.iter().flatten().cloned().collect::<Vec<String>>();
if let Some(forwards) = &self.port_forwards {
args.extend(forwards.iter().map(|pf| {
let local_host = match &pf.local_host {
Some(host) => host,
None => "localhost",
};
let remote_host = match &pf.remote_host {
Some(host) => host,
None => "localhost",
};
format!(
"-L{}:{}:{}:{}",
local_host, pf.local_port, remote_host, pf.remote_port
)
}));
}
args
}
fn scp_url(&self) -> String {
@ -1454,7 +1543,7 @@ impl SshRemoteConnection {
.stderr(Stdio::piped())
.env("SSH_ASKPASS_REQUIRE", "force")
.env("SSH_ASKPASS", &askpass_script_path)
.args(connection_options.additional_args().unwrap_or(&Vec::new()))
.args(connection_options.additional_args())
.args([
"-N",
"-o",