openssh/
session.rs

1use super::{Error, ForwardType, KnownHosts, OwningCommand, SessionBuilder, Socket};
2
3#[cfg(feature = "process-mux")]
4use super::process_impl;
5
6#[cfg(feature = "native-mux")]
7use super::native_mux_impl;
8
9use std::borrow::Cow;
10use std::ffi::OsStr;
11use std::ops::Deref;
12use std::path::Path;
13
14use tempfile::TempDir;
15
16#[derive(Debug)]
17pub(crate) enum SessionImp {
18    #[cfg(feature = "process-mux")]
19    ProcessImpl(process_impl::Session),
20
21    #[cfg(feature = "native-mux")]
22    NativeMuxImpl(native_mux_impl::Session),
23}
24
25#[cfg(any(feature = "process-mux", feature = "native-mux"))]
26macro_rules! delegate {
27    ($impl:expr, $var:ident, $then:block) => {{
28        match $impl {
29            #[cfg(feature = "process-mux")]
30            SessionImp::ProcessImpl($var) => $then,
31
32            #[cfg(feature = "native-mux")]
33            SessionImp::NativeMuxImpl($var) => $then,
34        }
35    }};
36}
37
38#[cfg(not(any(feature = "process-mux", feature = "native-mux")))]
39macro_rules! delegate {
40    ($impl:expr, $var:ident, $then:block) => {{
41        unreachable!("Neither feature process-mux nor native-mux is enabled")
42    }};
43}
44
45/// A single SSH session to a remote host.
46///
47/// You can use [`command`](Session::command) to start a new command on the connected machine.
48///
49/// When the `Session` is dropped, the connection to the remote host is severed, and any errors
50/// silently ignored. To disconnect and be alerted to errors, use [`close`](Session::close).
51#[derive(Debug)]
52pub struct Session(SessionImp);
53
54// TODO: UserKnownHostsFile for custom known host fingerprint.
55
56impl Session {
57    /// The method for creating a [`Session`] and externally control the creation of TempDir.
58    ///
59    /// By using the built-in [`SessionBuilder`] in openssh, or a custom SessionBuilder,
60    /// create a TempDir.
61    ///
62    /// # Examples
63    ///
64    /// ```rust,no_run
65    /// # use std::error::Error;
66    /// # #[cfg(feature = "process-mux")]
67    /// # #[tokio::main]
68    /// # async fn main() -> Result<(), Box<dyn Error>> {
69    ///
70    /// use openssh::{Session, Stdio, SessionBuilder};
71    /// use openssh_sftp_client::Sftp;
72    ///
73    /// let builder = SessionBuilder::default();
74    /// let (builder, destination) = builder.resolve("ssh://jon@ssh.thesquareplanet.com:222");
75    /// let tempdir = builder.launch_master(destination).await?;
76    ///
77    /// let session = Session::new_process_mux(tempdir);
78    ///
79    /// let mut child = session
80    ///     .subsystem("sftp")
81    ///     .stdin(Stdio::piped())
82    ///     .stdout(Stdio::piped())
83    ///     .spawn()
84    ///     .await?;
85    ///
86    /// Sftp::new(
87    ///     child.stdin().take().unwrap(),
88    ///     child.stdout().take().unwrap(),
89    ///     Default::default(),
90    /// )
91    /// .await?
92    /// .close()
93    /// .await?;
94    ///
95    /// # Ok(()) }
96    /// ```
97    #[cfg(feature = "process-mux")]
98    #[cfg_attr(docsrs, doc(cfg(feature = "process-mux")))]
99    pub fn new_process_mux(tempdir: TempDir) -> Self {
100        Self(SessionImp::ProcessImpl(process_impl::Session::new(tempdir)))
101    }
102
103    /// The method for creating a [`Session`] and externally control the creation of TempDir.
104    ///
105    /// By using the built-in [`SessionBuilder`] in openssh, or a custom SessionBuilder,
106    /// create a TempDir.
107    ///
108    /// # Examples
109    ///
110    /// ```rust,no_run
111    /// # use std::error::Error;
112    /// # #[cfg(feature = "native-mux")]
113    /// # #[tokio::main]
114    /// # async fn main() -> Result<(), Box<dyn Error>> {
115    ///
116    /// use openssh::{Session, Stdio, SessionBuilder};
117    /// use openssh_sftp_client::Sftp;
118    ///
119    /// let builder = SessionBuilder::default();
120    /// let (builder, destination) = builder.resolve("ssh://jon@ssh.thesquareplanet.com:222");
121    /// let tempdir = builder.launch_master(destination).await?;
122    ///
123    /// let session = Session::new_native_mux(tempdir);
124    /// let mut child = session
125    ///     .subsystem("sftp")
126    ///     .stdin(Stdio::piped())
127    ///     .stdout(Stdio::piped())
128    ///     .spawn()
129    ///     .await?;
130    ///
131    /// Sftp::new(
132    ///     child.stdin().take().unwrap(),
133    ///     child.stdout().take().unwrap(),
134    ///     Default::default(),
135    /// )
136    /// .await?
137    /// .close()
138    /// .await?;
139    ///
140    /// # Ok(()) }
141    /// ```
142    #[cfg(feature = "native-mux")]
143    #[cfg_attr(docsrs, doc(cfg(feature = "native-mux")))]
144    pub fn new_native_mux(tempdir: TempDir) -> Self {
145        Self(SessionImp::NativeMuxImpl(native_mux_impl::Session::new(
146            tempdir,
147        )))
148    }
149
150    /// Resume the connection using path to control socket and
151    /// path to ssh multiplex output log.
152    ///
153    /// If you do not use `-E` option (or redirection) to write
154    /// the log of the ssh multiplex master to the disk, you can
155    /// simply pass `None` to `master_log`.
156    ///
157    /// [`Session`] created this way will not be terminated on drop,
158    /// but can be forced terminated by [`Session::close`].
159    ///
160    /// This connects to the ssh multiplex master using process mux impl.
161    #[cfg(feature = "process-mux")]
162    #[cfg_attr(docsrs, doc(cfg(feature = "process-mux")))]
163    pub fn resume(ctl: Box<Path>, master_log: Option<Box<Path>>) -> Self {
164        Self(SessionImp::ProcessImpl(process_impl::Session::resume(
165            ctl, master_log,
166        )))
167    }
168
169    /// Same as [`Session::resume`] except that it connects to
170    /// the ssh multiplex master using native mux impl.
171    #[cfg(feature = "native-mux")]
172    #[cfg_attr(docsrs, doc(cfg(feature = "native-mux")))]
173    pub fn resume_mux(ctl: Box<Path>, master_log: Option<Box<Path>>) -> Self {
174        Self(SessionImp::NativeMuxImpl(native_mux_impl::Session::resume(
175            ctl, master_log,
176        )))
177    }
178
179    /// Connect to the host at the given `host` over SSH using process impl, which will
180    /// spawn a new ssh process for each `Child` created.
181    ///
182    /// The format of `destination` is the same as the `destination` argument to `ssh`. It may be
183    /// specified as either `[user@]hostname` or a URI of the form `ssh://[user@]hostname[:port]`.
184    ///
185    /// If connecting requires interactive authentication based on `STDIN` (such as reading a
186    /// password), the connection will fail. Consider setting up keypair-based authentication
187    /// instead.
188    ///
189    /// For more options, see [`SessionBuilder`].
190    #[cfg(feature = "process-mux")]
191    #[cfg_attr(docsrs, doc(cfg(feature = "process-mux")))]
192    pub async fn connect<S: AsRef<str>>(destination: S, check: KnownHosts) -> Result<Self, Error> {
193        SessionBuilder::default()
194            .known_hosts_check(check)
195            .connect(destination)
196            .await
197    }
198
199    /// Connect to the host at the given `host` over SSH using native mux impl, which
200    /// will create a new socket connection for each `Child` created.
201    ///
202    /// See the crate-level documentation for more details on the difference between native and process-based mux.
203    ///
204    /// The format of `destination` is the same as the `destination` argument to `ssh`. It may be
205    /// specified as either `[user@]hostname` or a URI of the form `ssh://[user@]hostname[:port]`.
206    ///
207    /// If connecting requires interactive authentication based on `STDIN` (such as reading a
208    /// password), the connection will fail. Consider setting up keypair-based authentication
209    /// instead.
210    ///
211    /// For more options, see [`SessionBuilder`].
212    #[cfg(feature = "native-mux")]
213    #[cfg_attr(docsrs, doc(cfg(feature = "native-mux")))]
214    pub async fn connect_mux<S: AsRef<str>>(
215        destination: S,
216        check: KnownHosts,
217    ) -> Result<Self, Error> {
218        SessionBuilder::default()
219            .known_hosts_check(check)
220            .connect_mux(destination)
221            .await
222    }
223
224    /// Check the status of the underlying SSH connection.
225    #[cfg(not(windows))]
226    #[cfg_attr(docsrs, doc(cfg(not(windows))))]
227    pub async fn check(&self) -> Result<(), Error> {
228        delegate!(&self.0, imp, { imp.check().await })
229    }
230
231    /// Get the SSH connection's control socket path.
232    #[cfg(not(windows))]
233    #[cfg_attr(docsrs, doc(cfg(not(windows))))]
234    pub fn control_socket(&self) -> &Path {
235        delegate!(&self.0, imp, { imp.ctl() })
236    }
237
238    /// Constructs a new [`OwningCommand`] for launching the program at path `program` on the remote
239    /// host.
240    ///
241    /// Before it is passed to the remote host, `program` is escaped so that special characters
242    /// aren't evaluated by the remote shell. If you do not want this behavior, use
243    /// [`raw_command`](Session::raw_command).
244    ///
245    /// The returned `OwningCommand` is a builder, with the following default configuration:
246    ///
247    /// * No arguments to the program
248    /// * Empty stdin and discard stdout/stderr for `spawn` or `status`, but create output pipes for
249    ///   `output`
250    ///
251    /// Builder methods are provided to change these defaults and otherwise configure the process.
252    ///
253    /// If `program` is not an absolute path, the `PATH` will be searched in an OS-defined way on
254    /// the host.
255    pub fn command<'a, S: Into<Cow<'a, str>>>(&self, program: S) -> OwningCommand<&'_ Self> {
256        Self::to_command(self, program)
257    }
258
259    /// Constructs a new [`OwningCommand`] for launching the program at path `program` on the remote
260    /// host.
261    ///
262    /// Unlike [`command`](Session::command), this method does not shell-escape `program`, so it may be evaluated in
263    /// unforeseen ways by the remote shell.
264    ///
265    /// The returned `OwningCommand` is a builder, with the following default configuration:
266    ///
267    /// * No arguments to the program
268    /// * Empty stdin and dsicard stdout/stderr for `spawn` or `status`, but create output pipes for
269    ///   `output`
270    ///
271    /// Builder methods are provided to change these defaults and otherwise configure the process.
272    ///
273    /// If `program` is not an absolute path, the `PATH` will be searched in an OS-defined way on
274    /// the host.
275    pub fn raw_command<S: AsRef<OsStr>>(&self, program: S) -> OwningCommand<&'_ Self> {
276        Self::to_raw_command(self, program)
277    }
278
279    /// Version of [`command`](Self::command) which stores an
280    /// `Arc<Session>` instead of a reference, making the resulting
281    /// [`OwningCommand`] independent from the source [`Session`] and
282    /// simplifying lifetime management and concurrent usage:
283    ///
284    /// ```rust,no_run
285    /// # use std::sync::Arc;
286    /// # use tokio::io::AsyncReadExt;
287    /// # use openssh::{Session, KnownHosts};
288    /// # #[cfg(feature = "native-mux")]
289    /// # #[tokio::main]
290    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
291    ///
292    /// let session = Arc::new(Session::connect_mux("me@ssh.example.com", KnownHosts::Strict).await?);
293    ///
294    /// let mut log = session.arc_command("less").arg("+F").arg("./some-log-file").spawn().await?;
295    /// # let t: tokio::task::JoinHandle<Result<(), std::io::Error>> =
296    /// tokio::spawn(async move {
297    ///     // can move the child around
298    ///     let mut stdout = log.stdout().take().unwrap();
299    ///     let mut buf = vec![0;100];
300    ///     loop {
301    ///         let n = stdout.read(&mut buf).await?;
302    ///         if n == 0 {
303    ///             return Ok(())
304    ///         }
305    ///         println!("read {:?}", &buf[..n]);
306    ///     }
307    /// });
308    /// # t.await??;
309    /// # Ok(()) }
310    pub fn arc_command<'a, P: Into<Cow<'a, str>>>(
311        self: std::sync::Arc<Session>,
312        program: P,
313    ) -> OwningCommand<std::sync::Arc<Session>> {
314        Self::to_command(self, program)
315    }
316
317    /// Version of [`raw_command`](Self::raw_command) which stores an
318    /// `Arc<Session>`, similar to [`arc_command`](Self::arc_command).
319    pub fn arc_raw_command<P: AsRef<OsStr>>(
320        self: std::sync::Arc<Session>,
321        program: P,
322    ) -> OwningCommand<std::sync::Arc<Session>> {
323        Self::to_raw_command(self, program)
324    }
325
326    /// Version of [`command`](Self::command) which stores an
327    /// arbitrary shared-ownership smart pointer to a [`Session`],
328    /// more generic but less convenient than
329    /// [`arc_command`](Self::arc_command).
330    pub fn to_command<'a, S, P>(session: S, program: P) -> OwningCommand<S>
331    where
332        P: Into<Cow<'a, str>>,
333        S: Deref<Target = Session> + Clone,
334    {
335        Self::to_raw_command(session, &*shell_escape::unix::escape(program.into()))
336    }
337
338    /// Version of [`raw_command`](Self::raw_command) which stores an
339    /// arbitrary shared-ownership smart pointer to a [`Session`],
340    /// more generic but less convenient than
341    /// [`arc_raw_command`](Self::arc_raw_command).
342    pub fn to_raw_command<S, P>(session: S, program: P) -> OwningCommand<S>
343    where
344        P: AsRef<OsStr>,
345        S: Deref<Target = Session> + Clone,
346    {
347        let session_impl = delegate!(&session.0, imp, {
348            imp.raw_command(program.as_ref()).into()
349        });
350        OwningCommand::new(session, session_impl)
351    }
352
353    /// Constructs a new [`OwningCommand`] for launching subsystem `program` on the remote
354    /// host.
355    ///
356    /// Unlike [`command`](Session::command), this method does not shell-escape `program`, so it may be evaluated in
357    /// unforeseen ways by the remote shell.
358    ///
359    /// The returned `OwningCommand` is a builder, with the following default configuration:
360    ///
361    /// * No arguments to the program
362    /// * Empty stdin and dsicard stdout/stderr for `spawn` or `status`, but create output pipes for
363    ///   `output`
364    ///
365    /// Builder methods are provided to change these defaults and otherwise configure the process.
366    ///
367    /// ## Sftp subsystem
368    ///
369    /// To use the sftp subsystem, you'll want to use [`openssh-sftp-client`],
370    /// then use the following code to construct a sftp instance:
371    ///
372    /// [`openssh-sftp-client`]: https://crates.io/crates/openssh-sftp-client
373    ///
374    /// ```rust,no_run
375    /// # use std::error::Error;
376    /// # #[cfg(feature = "native-mux")]
377    /// # #[tokio::main]
378    /// # async fn main() -> Result<(), Box<dyn Error>> {
379    ///
380    /// use openssh::{Session, KnownHosts, Stdio};
381    /// use openssh_sftp_client::Sftp;
382    ///
383    /// let session = Session::connect_mux("me@ssh.example.com", KnownHosts::Strict).await?;
384    ///
385    /// let mut child = session
386    ///     .subsystem("sftp")
387    ///     .stdin(Stdio::piped())
388    ///     .stdout(Stdio::piped())
389    ///     .spawn()
390    ///     .await?;
391    ///
392    /// Sftp::new(
393    ///     child.stdin().take().unwrap(),
394    ///     child.stdout().take().unwrap(),
395    ///     Default::default(),
396    /// )
397    /// .await?
398    /// .close()
399    /// .await?;
400    ///
401    /// # Ok(()) }
402    /// ```
403    pub fn subsystem<S: AsRef<OsStr>>(&self, program: S) -> OwningCommand<&'_ Self> {
404        Self::to_subsystem(self, program)
405    }
406
407    /// Version of [`subsystem`](Self::subsystem) which stores an
408    /// arbitrary shared-ownership pointer to a session making the
409    /// resulting [`OwningCommand`] independent from the source
410    /// [`Session`] and simplifying lifetime management and concurrent
411    /// usage:
412    pub fn to_subsystem<S, P>(session: S, program: P) -> OwningCommand<S>
413    where
414        P: AsRef<OsStr>,
415        S: Deref<Target = Session> + Clone,
416    {
417        let session_impl = delegate!(&session.0, imp, { imp.subsystem(program.as_ref()).into() });
418        OwningCommand::new(session, session_impl)
419    }
420
421    /// Constructs a new [`OwningCommand`] that runs the provided shell command on the remote host.
422    ///
423    /// The provided command is passed as a single, escaped argument to `sh -c`, and from that
424    /// point forward the behavior is up to `sh`. Since this executes a shell command, keep in mind
425    /// that you are subject to the shell's rules around argument parsing, such as whitespace
426    /// splitting, variable expansion, and other funkyness. I _highly_ recommend you read
427    /// [this article] if you observe strange things.
428    ///
429    /// While the returned `OwningCommand` is a builder, like for [`command`](Session::command), you should not add
430    /// additional arguments to it, since the arguments are already passed within the shell
431    /// command.
432    ///
433    /// # Non-standard Remote Shells
434    ///
435    /// It is worth noting that there are really _two_ shells at work here: the one that sshd
436    /// launches for the session, and that launches are command; and the instance of `sh` that we
437    /// launch _in_ that session. This method tries hard to ensure that the provided `command` is
438    /// passed exactly as-is to `sh`, but this is complicated by the presence of the "outer" shell.
439    /// That outer shell may itself perform argument splitting, variable expansion, and the like,
440    /// which might produce unintuitive results. For example, the outer shell may try to expand a
441    /// variable that is only defined in the inner shell, and simply produce an empty string in the
442    /// variable's place by the time it gets to `sh`.
443    ///
444    /// To counter this, this method assumes that the remote shell (the one launched by `sshd`) is
445    /// [POSIX compliant]. This is more or less equivalent to "supports `bash` syntax" if you don't
446    /// look too closely. It uses [`shell-escape`] to escape `command` before sending it to the
447    /// remote shell, with the expectation that the remote shell will only end up undoing that one
448    /// "level" of escaping, thus producing the original `command` as an argument to `sh`. This
449    /// works _most of the time_.
450    ///
451    /// With sufficiently complex or weird commands, the escaping of `shell-escape` may not fully
452    /// match the "un-escaping" of the remote shell. This will manifest as escape characters
453    /// appearing in the `sh` command that you did not intend to be there. If this happens, try
454    /// changing the remote shell if you can, or fall back to [`command`](Session::command)
455    /// and do the escaping manually instead.
456    ///
457    ///   [POSIX compliant]: https://pubs.opengroup.org/onlinepubs/9699919799/xrat/V4_xcu_chap02.html
458    ///   [this article]: https://mywiki.wooledge.org/Arguments
459    ///   [`shell-escape`]: https://crates.io/crates/shell-escape
460    pub fn shell<S: AsRef<str>>(&self, command: S) -> OwningCommand<&'_ Self> {
461        let mut cmd = self.command("sh");
462        cmd.arg("-c").arg(command.as_ref());
463        cmd
464    }
465
466    /// Request to open a local/remote port forwarding.
467    /// The `Socket` can be either a unix socket or a tcp socket.
468    ///
469    /// If `forward_type` == Local, then `listen_socket` on local machine will be
470    /// forwarded to `connect_socket` on remote machine.
471    ///
472    /// Otherwise, `listen_socket` on the remote machine will be forwarded to `connect_socket`
473    /// on the local machine.
474    pub async fn request_port_forward(
475        &self,
476        forward_type: impl Into<ForwardType>,
477        listen_socket: impl Into<Socket<'_>>,
478        connect_socket: impl Into<Socket<'_>>,
479    ) -> Result<(), Error> {
480        delegate!(&self.0, imp, {
481            imp.request_port_forward(
482                forward_type.into(),
483                listen_socket.into(),
484                connect_socket.into(),
485            )
486            .await
487        })
488    }
489
490    /// Close a previously established local/remote port forwarding.
491    ///
492    /// The same set of arguments should be passed as when the port forwarding was requested.
493    pub async fn close_port_forward(
494        &self,
495        forward_type: impl Into<ForwardType>,
496        listen_socket: impl Into<Socket<'_>>,
497        connect_socket: impl Into<Socket<'_>>,
498    ) -> Result<(), Error> {
499        delegate!(&self.0, imp, {
500            imp.close_port_forward(
501                forward_type.into(),
502                listen_socket.into(),
503                connect_socket.into(),
504            )
505            .await
506        })
507    }
508
509    /// Terminate the remote connection.
510    ///
511    /// This destructor terminates the ssh multiplex server
512    /// regardless of how it was created.
513    pub async fn close(self) -> Result<(), Error> {
514        let res: Result<Option<TempDir>, Error> = delegate!(self.0, imp, { imp.close().await });
515
516        res?.map(TempDir::close)
517            .transpose()
518            .map_err(Error::Cleanup)
519            .map(|_| ())
520    }
521
522    /// Detach the lifetime of underlying ssh multiplex master
523    /// from this `Session`.
524    ///
525    /// Return (path to control socket, path to ssh multiplex output log)
526    pub fn detach(self) -> (Box<Path>, Option<Box<Path>>) {
527        delegate!(self.0, imp, { imp.detach() })
528    }
529}