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}