gix_transport/client/blocking_io/ssh/
mod.rs

1use 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/// The error used in [`connect()`].
11#[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/// The kind of SSH programs we have built-in support for.
23///
24/// Various different programs exists with different capabilities, and we have a few built in.
25#[derive(Debug, Copy, Clone, Eq, PartialEq)]
26pub enum ProgramKind {
27    /// The standard linux ssh program
28    Ssh,
29    /// The `(plink|putty).exe` binaries, typically only on windows.
30    Plink,
31    /// The `putty.exe` binary, typically only on windows.
32    Putty,
33    /// The `tortoiseplink.exe` binary, only on windows.
34    TortoisePlink,
35    /// A minimal ssh client that supports on options.
36    Simple,
37}
38
39mod program_kind;
40
41///
42pub mod invocation {
43    use std::ffi::OsString;
44
45    /// The error returned when producing ssh invocation arguments based on a selected invocation kind.
46    #[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            /// The simple command that should have been invoked.
56            command: OsString,
57            /// The function that was unsupported
58            function: &'static str,
59        },
60    }
61}
62
63///
64pub mod connect {
65    use std::ffi::{OsStr, OsString};
66
67    use crate::client::blocking_io::ssh::ProgramKind;
68
69    /// The options for use when [connecting][super::connect()] via the `ssh` protocol.
70    #[derive(Debug, Clone, Default)]
71    pub struct Options {
72        /// The program or script to use.
73        /// If unset, it defaults to `ssh` or `ssh.exe`, or the program implied by `kind` if that one is set.
74        pub command: Option<OsString>,
75        /// If `true`, a shell must not be used to execute `command`.
76        /// This defaults to `false`, and a shell can then be used if `command` seems to require it, but won't be
77        /// used unnecessarily.
78        pub disallow_shell: bool,
79        /// The ssh variant further identifying `program`. This determines which arguments will be used
80        /// when invoking the program.
81        /// If unset, the `program` basename determines the variant, or an invocation of the `command` itself.
82        pub kind: Option<ProgramKind>,
83    }
84
85    impl Options {
86        /// Return the configured ssh command, defaulting to `ssh` if neither the `command` nor the `kind` fields are set.
87        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/// Connect to `host` using the ssh program to obtain data from the repository at `path` on the remote.
97///
98/// The optional `user` identifies the user's account to which to connect, while `port` allows to specify non-standard
99/// ssh ports.
100///
101/// The `desired_version` is the preferred protocol version when establishing the connection, but note that it can be
102/// downgraded by servers not supporting it.
103/// If `trace` is `true`, all packetlines received or sent will be passed to the facilities of the `gix-trace` crate.
104#[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;