1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
use std::process::Stdio;

use gix_url::ArgumentSafety::*;

use crate::{client::blocking_io, Protocol};

/// The error used in [`connect()`].
#[derive(Debug, thiserror::Error)]
#[allow(missing_docs)]
pub enum Error {
    #[error("The scheme in \"{}\" is not usable for an ssh connection", .0.to_bstring())]
    UnsupportedScheme(gix_url::Url),
    #[error("Host name '{host}' could be mistaken for a command-line argument")]
    AmbiguousHostName { host: String },
}

impl crate::IsSpuriousError for Error {}

/// The kind of SSH programs we have built-in support for.
///
/// Various different programs exists with different capabilities, and we have a few built in.
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub enum ProgramKind {
    /// The standard linux ssh program
    Ssh,
    /// The `(plink|putty).exe` binaries, typically only on windows.
    Plink,
    /// The `putty.exe` binary, typically only on windows.
    Putty,
    /// The `tortoiseplink.exe` binary, only on windows.
    TortoisePlink,
    /// A minimal ssh client that supports on options.
    Simple,
}

mod program_kind;

///
#[allow(clippy::empty_docs)]
pub mod invocation {
    use std::ffi::OsString;

    /// The error returned when producing ssh invocation arguments based on a selected invocation kind.
    #[derive(Debug, thiserror::Error)]
    #[allow(missing_docs)]
    pub enum Error {
        #[error("Username '{user}' could be mistaken for a command-line argument")]
        AmbiguousUserName { user: String },
        #[error("Host name '{host}' could be mistaken for a command-line argument")]
        AmbiguousHostName { host: String },
        #[error("The 'Simple' ssh variant doesn't support {function}")]
        Unsupported {
            /// The simple command that should have been invoked.
            command: OsString,
            /// The function that was unsupported
            function: &'static str,
        },
    }
}

///
#[allow(clippy::empty_docs)]
pub mod connect {
    use std::ffi::{OsStr, OsString};

    use crate::client::ssh::ProgramKind;

    /// The options for use when [connecting][super::connect()] via the `ssh` protocol.
    #[derive(Debug, Clone, Default)]
    pub struct Options {
        /// The program or script to use.
        /// If unset, it defaults to `ssh` or `ssh.exe`, or the program implied by `kind` if that one is set.
        pub command: Option<OsString>,
        /// If `true`, a shell must not be used to execute `command`.
        /// This defaults to `false`, and a shell can then be used if `command` seems to require it, but won't be
        /// used unnecessarily.
        pub disallow_shell: bool,
        /// The ssh variant further identifying `program`. This determines which arguments will be used
        /// when invoking the program.
        /// If unset, the `program` basename determines the variant, or an invocation of the `command` itself.
        pub kind: Option<ProgramKind>,
    }

    impl Options {
        /// Return the configured ssh command, defaulting to `ssh` if neither the `command` nor the `kind` fields are set.
        pub fn ssh_command(&self) -> &OsStr {
            self.command
                .as_deref()
                .or_else(|| self.kind.and_then(|kind| kind.exe()))
                .unwrap_or_else(|| OsStr::new("ssh"))
        }
    }
}

/// Connect to `host` using the ssh program to obtain data from the repository at `path` on the remote.
///
/// The optional `user` identifies the user's account to which to connect, while `port` allows to specify non-standard
/// ssh ports.
///
/// The `desired_version` is the preferred protocol version when establishing the connection, but note that it can be
/// downgraded by servers not supporting it.
/// If `trace` is `true`, all packetlines received or sent will be passed to the facilities of the `gix-trace` crate.
#[allow(clippy::result_large_err)]
pub fn connect(
    url: gix_url::Url,
    desired_version: Protocol,
    options: connect::Options,
    trace: bool,
) -> Result<blocking_io::file::SpawnProcessOnDemand, Error> {
    if url.scheme != gix_url::Scheme::Ssh || url.host().is_none() {
        return Err(Error::UnsupportedScheme(url));
    }
    let ssh_cmd = options.ssh_command();
    let mut kind = options.kind.unwrap_or_else(|| ProgramKind::from(ssh_cmd));
    if options.kind.is_none() && kind == ProgramKind::Simple {
        let mut cmd = std::process::Command::from(
            gix_command::prepare(ssh_cmd)
                .stderr(Stdio::null())
                .stdout(Stdio::null())
                .stdin(Stdio::null())
                .with_shell()
                .arg("-G")
                .arg(match url.host_as_argument() {
                    Usable(host) => host,
                    Dangerous(host) => Err(Error::AmbiguousHostName { host: host.into() })?,
                    Absent => panic!("BUG: host should always be present in SSH URLs"),
                }),
        );
        gix_features::trace::debug!(cmd = ?cmd, "invoking `ssh` for feature check");
        kind = if cmd.status().ok().map_or(false, |status| status.success()) {
            ProgramKind::Ssh
        } else {
            ProgramKind::Simple
        };
    }

    let path = gix_url::expand_path::for_shell(url.path.clone());
    Ok(blocking_io::file::SpawnProcessOnDemand::new_ssh(
        url,
        ssh_cmd,
        path,
        kind,
        options.disallow_shell,
        desired_version,
        trace,
    ))
}

#[cfg(test)]
mod tests;