From d29a37e213edfeefd037b068651cc8007ad8096a Mon Sep 17 00:00:00 2001 From: baoyachi Date: Mon, 31 Jul 2023 14:59:25 +0800 Subject: [PATCH 1/5] By using the built-in SessionBuilder in openssh, or a custom SessionBuilder, create a TempDir. --- .gitignore | 2 +- src/builder.rs | 31 +++++++++++++++++-- src/session.rs | 84 ++++++++++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 111 insertions(+), 6 deletions(-) diff --git a/.gitignore b/.gitignore index 203223e52..ba9ddd03f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ /target /ci-target Cargo.lock - +.idea/ .DS_Store diff --git a/src/builder.rs b/src/builder.rs index ffb603640..a81988581 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -93,6 +93,16 @@ impl Default for SessionBuilder { } impl SessionBuilder { + /// Return the user set in builder. + pub fn get_user(&self) -> Option<&str> { + self.user.as_deref() + } + + /// Return the port set in builder. + pub fn get_port(&self) -> Option<&str> { + self.port.as_deref() + } + /// Set the ssh user (`ssh -l`). /// /// Defaults to `None`. @@ -286,7 +296,21 @@ impl SessionBuilder { Ok(f(tempdir)) } - fn resolve<'a, 'b>(&'a self, mut destination: &'b str) -> (Cow<'a, Self>, &'b str) { + /// [`SessionBuilder`] support for `destination` parsing. + /// The format of `destination` is the same as the `destination` argument to `ssh`. + /// + /// # Examples + /// + /// ```rust + /// use std::borrow::Cow; + /// use openssh::SessionBuilder; + /// let b = SessionBuilder::default(); + /// let (b, d) = b.resolve("ssh://test-user@127.0.0.1:2222"); + /// assert_eq!(b.get_port().as_deref(), Some("2222")); + /// assert_eq!(b.get_user().as_deref(), Some("test-user")); + /// assert_eq!(d, "127.0.0.1"); + /// ``` + pub fn resolve<'a, 'b>(&'a self, mut destination: &'b str) -> (Cow<'a, Self>, &'b str) { // the "new" ssh://user@host:port form is not supported by all versions of ssh, // so we always translate it into the option form. let mut user = None; @@ -324,7 +348,8 @@ impl SessionBuilder { (Cow::Owned(with_overrides), destination) } - async fn launch_master(&self, destination: &str) -> Result { + /// Create ssh master session and return [`TempDir`] + pub async fn launch_master(&self, destination: &str) -> Result { let socketdir = if let Some(socketdir) = self.control_dir.as_ref() { socketdir } else { @@ -334,7 +359,7 @@ impl SessionBuilder { let prefix = ".ssh-connection"; if self.clean_history_control_dir { - let _ = clean_history_control_dir(&socketdir, prefix); + let _ = clean_history_control_dir(socketdir, prefix); } let dir = Builder::new() diff --git a/src/session.rs b/src/session.rs index be0018d01..d023cc8bc 100644 --- a/src/session.rs +++ b/src/session.rs @@ -53,13 +53,93 @@ pub struct Session(SessionImp); // TODO: UserKnownHostsFile for custom known host fingerprint. impl Session { + /// The method for creating a [`Session`] and externally control the creation of TempDir. + /// + /// By using the built-in [`SessionBuilder`] in openssh, or a custom SessionBuilder, + /// create a TempDir. + /// + /// # Examples + /// + /// ```rust,no_run + /// # use std::error::Error; + /// # #[cfg(feature = "process-mux")] + /// # #[tokio::main] + /// # async fn main() -> Result<(), Box> { + /// + /// use openssh::{Session, Stdio, SessionBuilder}; + /// use openssh_sftp_client::Sftp; + /// + /// let builder = SessionBuilder::default(); + /// let (builder, destination) = builder.resolve("ssh://jon@ssh.thesquareplanet.com:222"); + /// let tempdir = builder.launch_master(destination).await?; + /// + /// let session = Session::new_process_mux(tempdir); + /// + /// let mut child = session + /// .subsystem("sftp") + /// .stdin(Stdio::piped()) + /// .stdout(Stdio::piped()) + /// .spawn() + /// .await?; + /// + /// Sftp::new( + /// child.stdin().take().unwrap(), + /// child.stdout().take().unwrap(), + /// Default::default(), + /// ) + /// .await? + /// .close() + /// .await?; + /// + /// # Ok(()) } + /// ``` #[cfg(feature = "process-mux")] - pub(super) fn new_process_mux(tempdir: TempDir) -> Self { + pub fn new_process_mux(tempdir: TempDir) -> Self { Self(SessionImp::ProcessImpl(process_impl::Session::new(tempdir))) } + /// The method for creating a [`Session`] and externally control the creation of TempDir. + /// + /// By using the built-in [`SessionBuilder`] in openssh, or a custom SessionBuilder, + /// create a TempDir. + /// + /// # Examples + /// + /// ```rust,no_run + /// # use std::error::Error; + /// # #[cfg(feature = "native-mux")] + /// # #[tokio::main] + /// # async fn main() -> Result<(), Box> { + /// + /// use openssh::{Session, Stdio, SessionBuilder}; + /// use openssh_sftp_client::Sftp; + /// + /// let builder = SessionBuilder::default(); + /// let (builder, destination) = builder.resolve("ssh://jon@ssh.thesquareplanet.com:222"); + /// let tempdir = builder.launch_master(destination).await?; + /// + /// let session = Session::new_native_mux(tempdir); + /// + /// let mut child = session + /// .subsystem("sftp") + /// .stdin(Stdio::piped()) + /// .stdout(Stdio::piped()) + /// .spawn() + /// .await?; + /// + /// Sftp::new( + /// child.stdin().take().unwrap(), + /// child.stdout().take().unwrap(), + /// Default::default(), + /// ) + /// .await? + /// .close() + /// .await?; + /// + /// # Ok(()) } + /// ``` #[cfg(feature = "native-mux")] - pub(super) fn new_native_mux(tempdir: TempDir) -> Self { + pub fn new_native_mux(tempdir: TempDir) -> Self { Self(SessionImp::NativeMuxImpl(native_mux_impl::Session::new( tempdir, ))) From 5d4042da827e8dc3c6c50cbebfe64a946b050df0 Mon Sep 17 00:00:00 2001 From: "baoyachi. Aka Rust Hairy crabs" Date: Mon, 31 Jul 2023 22:05:30 +0800 Subject: [PATCH 2/5] Update src/builder.rs Co-authored-by: Jiahao XU --- src/builder.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/builder.rs b/src/builder.rs index a81988581..f912f4c15 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -348,7 +348,8 @@ impl SessionBuilder { (Cow::Owned(with_overrides), destination) } - /// Create ssh master session and return [`TempDir`] + /// Create ssh master session and return [`TempDir`] which + /// contains the ssh control socket. pub async fn launch_master(&self, destination: &str) -> Result { let socketdir = if let Some(socketdir) = self.control_dir.as_ref() { socketdir From 40840c5b0e96ead3be75624c97102b2996379954 Mon Sep 17 00:00:00 2001 From: baoyachi Date: Mon, 31 Jul 2023 22:06:33 +0800 Subject: [PATCH 3/5] remove code --- src/builder.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/builder.rs b/src/builder.rs index f912f4c15..8ff2dda94 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -302,7 +302,6 @@ impl SessionBuilder { /// # Examples /// /// ```rust - /// use std::borrow::Cow; /// use openssh::SessionBuilder; /// let b = SessionBuilder::default(); /// let (b, d) = b.resolve("ssh://test-user@127.0.0.1:2222"); From 8ba00de6cf7b94f6831ba4ba12869f34c955c893 Mon Sep 17 00:00:00 2001 From: baoyachi Date: Thu, 3 Aug 2023 15:35:45 +0800 Subject: [PATCH 4/5] support user password login ssh --- src/builder.rs | 51 ++++++++++++++++++++++++++++-------- src/session.rs | 70 +++++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 106 insertions(+), 15 deletions(-) diff --git a/src/builder.rs b/src/builder.rs index 8ff2dda94..6f28d9455 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -11,7 +11,7 @@ use std::{fs, io}; use dirs::state_dir; use once_cell::sync::OnceCell; use tempfile::{Builder, TempDir}; -use tokio::process; +use tokio::process::Command; /// The returned `&'static Path` can be coreced to any lifetime. fn get_default_control_dir<'a>() -> Result<&'a Path, Error> { @@ -60,6 +60,7 @@ pub struct SessionBuilder { user: Option, port: Option, keyfile: Option, + password: Option, connect_timeout: Option, server_alive_interval: Option, known_hosts_check: KnownHosts, @@ -78,6 +79,7 @@ impl Default for SessionBuilder { user: None, port: None, keyfile: None, + password: None, connect_timeout: None, server_alive_interval: None, known_hosts_check: KnownHosts::Add, @@ -106,8 +108,8 @@ impl SessionBuilder { /// Set the ssh user (`ssh -l`). /// /// Defaults to `None`. - pub fn user(&mut self, user: String) -> &mut Self { - self.user = Some(user); + pub fn user(&mut self, user: impl AsRef) -> &mut Self { + self.user = Some(user.as_ref().to_string()); self } @@ -127,6 +129,14 @@ impl SessionBuilder { self } + /// Set the ssh password. + /// + /// Defaults to `None`. + pub fn password(&mut self, p: impl AsRef) -> &mut Self { + self.password = Some(p.as_ref().to_string()); + self + } + /// See [`KnownHosts`]. /// /// Default `KnownHosts::Add`. @@ -337,7 +347,7 @@ impl SessionBuilder { let mut with_overrides = self.clone(); if let Some(user) = user { - with_overrides.user(user.to_owned()); + with_overrides.user(user); } if let Some(port) = port { @@ -347,6 +357,28 @@ impl SessionBuilder { (Cow::Owned(with_overrides), destination) } + /// Create ssh master Command + fn init_command(&self) -> Command { + if let Some(pass) = &self.password { + let mut init = Command::new("sshpass"); + + init.stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .arg("-p") + .arg(pass) + .arg("ssh"); + return init; + } + + let mut init = Command::new("ssh"); + + init.stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::null()); + init + } + /// Create ssh master session and return [`TempDir`] which /// contains the ssh control socket. pub async fn launch_master(&self, destination: &str) -> Result { @@ -368,16 +400,13 @@ impl SessionBuilder { .map_err(Error::Master)?; let log = dir.path().join("log"); + let master = dir.path().join("master"); - let mut init = process::Command::new("ssh"); - - init.stdin(Stdio::null()) - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .arg("-E") + let mut init = self.init_command(); + init.arg("-E") .arg(&log) .arg("-S") - .arg(dir.path().join("master")) + .arg(&master) .arg("-M") .arg("-f") .arg("-N") diff --git a/src/session.rs b/src/session.rs index d023cc8bc..b95902699 100644 --- a/src/session.rs +++ b/src/session.rs @@ -180,9 +180,10 @@ impl Session { /// The format of `destination` is the same as the `destination` argument to `ssh`. It may be /// specified as either `[user@]hostname` or a URI of the form `ssh://[user@]hostname[:port]`. /// + /// The function provide keypair-based authentication. + /// /// If connecting requires interactive authentication based on `STDIN` (such as reading a - /// password), the connection will fail. Consider setting up keypair-based authentication - /// instead. + /// password), consider use [`Session::connect_with_pass`] function instead. /// /// For more options, see [`SessionBuilder`]. #[cfg(feature = "process-mux")] @@ -194,6 +195,35 @@ impl Session { .await } + /// Connect to the host at the given `host` over SSH using process impl, which will + /// spawn a new ssh process for each `Child` created. + /// + /// The format of `destination` is the same as the `destination` argument to `ssh`. It may be + /// specified as either `[user@]hostname` or a URI of the form `ssh://[user@]hostname[:port]`. + /// + /// The function provide interactive authentication based on `STDIN` (such as reading a + /// password). + /// + /// Warning ⚠️: current use sshpass non-interactive ssh password auth. + /// See detail: + /// + /// The current implementation of sshpass is only temporary, and the functionality will be + /// optimized later. If using this feature, pay attention to security risks. + /// + /// For more options, see [`SessionBuilder`]. + #[cfg(feature = "process-mux")] + #[cfg_attr(docsrs, doc(cfg(feature = "process-mux")))] + pub async fn connect_with_pass>( + destination: S, + pass: impl AsRef, + ) -> Result { + SessionBuilder::default() + .password(pass) + .known_hosts_check(KnownHosts::Accept) + .connect(destination) + .await + } + /// Connect to the host at the given `host` over SSH using native mux impl, which /// will create a new socket connection for each `Child` created. /// @@ -202,9 +232,10 @@ impl Session { /// The format of `destination` is the same as the `destination` argument to `ssh`. It may be /// specified as either `[user@]hostname` or a URI of the form `ssh://[user@]hostname[:port]`. /// + /// The function provide keypair-based authentication. + /// /// If connecting requires interactive authentication based on `STDIN` (such as reading a - /// password), the connection will fail. Consider setting up keypair-based authentication - /// instead. + /// password), consider use [`Session::connect_mux_with_pass`] function instead. /// /// For more options, see [`SessionBuilder`]. #[cfg(feature = "native-mux")] @@ -219,6 +250,37 @@ impl Session { .await } + /// Connect to the host at the given `host` over SSH using native mux impl, which + /// will create a new socket connection for each `Child` created. + /// + /// See the crate-level documentation for more details on the difference between native and process-based mux. + /// + /// The format of `destination` is the same as the `destination` argument to `ssh`. It may be + /// specified as either `[user@]hostname` or a URI of the form `ssh://[user@]hostname[:port]`. + /// + /// The function provide interactive authentication based on `STDIN` (such as reading a + /// password). + /// + /// Warning ⚠️: current use sshpass non-interactive ssh password auth. + /// See detail: + /// + /// The current implementation of sshpass is only temporary, and the functionality will be + /// optimized later. If using this feature, pay attention to security risks. + /// + /// For more options, see [`SessionBuilder`]. + #[cfg(feature = "native-mux")] + #[cfg_attr(docsrs, doc(cfg(feature = "native-mux")))] + pub async fn connect_mux_with_pass>( + destination: S, + pass: impl AsRef, + ) -> Result { + SessionBuilder::default() + .password(pass) + .known_hosts_check(KnownHosts::Accept) + .connect_mux(destination) + .await + } + /// Check the status of the underlying SSH connection. #[cfg(not(windows))] #[cfg_attr(docsrs, doc(cfg(not(windows))))] From 385f70de082151122fe71543f7cdc9b18f47c1dd Mon Sep 17 00:00:00 2001 From: baoyachi Date: Thu, 3 Aug 2023 15:46:16 +0800 Subject: [PATCH 5/5] support user password login ssh --- src/builder.rs | 47 ++++++++++++++++++++++++++++++----------------- 1 file changed, 30 insertions(+), 17 deletions(-) diff --git a/src/builder.rs b/src/builder.rs index 6f28d9455..21f7e7bf8 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -358,7 +358,7 @@ impl SessionBuilder { } /// Create ssh master Command - fn init_command(&self) -> Command { + fn init_command(&self, log: &PathBuf, master: &PathBuf) -> Command { if let Some(pass) = &self.password { let mut init = Command::new("sshpass"); @@ -367,7 +367,20 @@ impl SessionBuilder { .stderr(Stdio::null()) .arg("-p") .arg(pass) - .arg("ssh"); + .arg("ssh") + .arg("-E") + .arg(log) + .arg("-S") + .arg(master) + .arg("-M") + .arg("-f") + .arg("-N") + .arg("-o") + .arg("ControlPersist=yes") + .arg("-o") + .arg("BatchMode=no") + .arg("-o") + .arg(self.known_hosts_check.as_option()); return init; } @@ -375,7 +388,20 @@ impl SessionBuilder { init.stdin(Stdio::null()) .stdout(Stdio::null()) - .stderr(Stdio::null()); + .stderr(Stdio::null()) + .arg("-E") + .arg(log) + .arg("-S") + .arg(master) + .arg("-M") + .arg("-f") + .arg("-N") + .arg("-o") + .arg("ControlPersist=yes") + .arg("-o") + .arg("BatchMode=yes") + .arg("-o") + .arg(self.known_hosts_check.as_option()); init } @@ -402,20 +428,7 @@ impl SessionBuilder { let log = dir.path().join("log"); let master = dir.path().join("master"); - let mut init = self.init_command(); - init.arg("-E") - .arg(&log) - .arg("-S") - .arg(&master) - .arg("-M") - .arg("-f") - .arg("-N") - .arg("-o") - .arg("ControlPersist=yes") - .arg("-o") - .arg("BatchMode=yes") - .arg("-o") - .arg(self.known_hosts_check.as_option()); + let mut init = self.init_command(&log, &master); if let Some(ref timeout) = self.connect_timeout { init.arg("-o").arg(format!("ConnectTimeout={}", timeout));