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;