gix_transport/client/blocking_io/ssh/
mod.rs1use std::{
2 ffi::OsStr,
3 process::{Command, Stdio},
4};
5
6use gix_url::{ArgumentSafety::*, Url};
7
8use crate::{client::blocking_io::file::SpawnProcessOnDemand, Protocol};
9
10#[derive(Debug, thiserror::Error)]
12#[allow(missing_docs)]
13pub enum Error {
14 #[error("The scheme in \"{}\" is not usable for an ssh connection", .0.to_bstring())]
15 UnsupportedScheme(gix_url::Url),
16 #[error("Host name '{host}' could be mistaken for a command-line argument")]
17 AmbiguousHostName { host: String },
18}
19
20impl crate::IsSpuriousError for Error {}
21
22#[derive(Debug, Copy, Clone, Eq, PartialEq)]
26pub enum ProgramKind {
27 Ssh,
29 Plink,
31 Putty,
33 TortoisePlink,
35 Simple,
37}
38
39mod program_kind;
40
41pub mod invocation {
43 use std::ffi::OsString;
44
45 #[derive(Debug, thiserror::Error)]
47 #[allow(missing_docs)]
48 pub enum Error {
49 #[error("Username '{user}' could be mistaken for a command-line argument")]
50 AmbiguousUserName { user: String },
51 #[error("Host name '{host}' could be mistaken for a command-line argument")]
52 AmbiguousHostName { host: String },
53 #[error("The 'Simple' ssh variant doesn't support {function}")]
54 Unsupported {
55 command: OsString,
57 function: &'static str,
59 },
60 }
61}
62
63pub mod connect {
65 use std::ffi::{OsStr, OsString};
66
67 use crate::client::blocking_io::ssh::ProgramKind;
68
69 #[derive(Debug, Clone, Default)]
71 pub struct Options {
72 pub command: Option<OsString>,
75 pub disallow_shell: bool,
79 pub kind: Option<ProgramKind>,
83 }
84
85 impl Options {
86 pub fn ssh_command(&self) -> &OsStr {
88 self.command
89 .as_deref()
90 .or_else(|| self.kind.and_then(|kind| kind.exe()))
91 .unwrap_or_else(|| OsStr::new("ssh"))
92 }
93 }
94}
95
96#[allow(clippy::result_large_err)]
105pub fn connect(
106 url: Url,
107 desired_version: Protocol,
108 options: connect::Options,
109 trace: bool,
110) -> Result<SpawnProcessOnDemand, Error> {
111 if url.scheme != gix_url::Scheme::Ssh || url.host().is_none() {
112 return Err(Error::UnsupportedScheme(url));
113 }
114 let ssh_cmd = options.ssh_command();
115 let kind = determine_client_kind(options.kind, ssh_cmd, &url, options.disallow_shell)?;
116 let path = gix_url::expand_path::for_shell(url.path.clone());
117 Ok(SpawnProcessOnDemand::new_ssh(
118 url,
119 ssh_cmd,
120 path,
121 kind,
122 options.disallow_shell,
123 desired_version,
124 trace,
125 ))
126}
127
128#[allow(clippy::result_large_err)]
129fn determine_client_kind(
130 known_kind: Option<ProgramKind>,
131 ssh_cmd: &OsStr,
132 url: &Url,
133 disallow_shell: bool,
134) -> Result<ProgramKind, Error> {
135 let mut kind = known_kind.unwrap_or_else(|| ProgramKind::from(ssh_cmd));
136 if known_kind.is_none() && kind == ProgramKind::Simple {
137 let mut cmd = build_client_feature_check_command(ssh_cmd, url, disallow_shell)?;
138 gix_features::trace::debug!(cmd = ?cmd, "invoking `ssh` for feature check");
139 kind = if cmd.status().ok().is_some_and(|status| status.success()) {
140 ProgramKind::Ssh
141 } else {
142 ProgramKind::Simple
143 };
144 }
145 Ok(kind)
146}
147
148#[allow(clippy::result_large_err)]
149fn build_client_feature_check_command(ssh_cmd: &OsStr, url: &Url, disallow_shell: bool) -> Result<Command, Error> {
150 let mut prepare = gix_command::prepare(ssh_cmd)
151 .stderr(Stdio::null())
152 .stdout(Stdio::null())
153 .stdin(Stdio::null())
154 .command_may_be_shell_script()
155 .arg("-G")
156 .arg(match url.host_as_argument() {
157 Usable(host) => host,
158 Dangerous(host) => Err(Error::AmbiguousHostName { host: host.into() })?,
159 Absent => panic!("BUG: host should always be present in SSH URLs"),
160 });
161 if disallow_shell {
162 prepare.use_shell = false;
163 }
164 Ok(prepare.into())
165}
166
167#[cfg(test)]
168mod tests;