Skip to content

Commit 85fa7e9

Browse files
authored
Merge pull request #65 from openssh-rust/refactor
Refactor crate
2 parents d659d21 + a32e168 commit 85fa7e9

File tree

4 files changed

+264
-293
lines changed

4 files changed

+264
-293
lines changed

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ native-mux = ["openssh-mux-client"]
3838
[dependencies]
3939
tempfile = "3.1.0"
4040
shell-escape = "0.1.5"
41+
thiserror = "1.0.30"
42+
4143
tokio = { version = "1", features = [ "process", "io-util", "macros" ] }
4244
tokio-pipe = "0.2.8"
4345

src/error.rs

Lines changed: 18 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,38 @@
1-
use std::fmt;
21
use std::io;
32

43
/// Errors that occur when interacting with a remote process.
5-
#[derive(Debug)]
4+
#[derive(Debug, thiserror::Error)]
65
#[non_exhaustive]
76
pub enum Error {
87
/// The master connection failed.
9-
Master(io::Error),
8+
#[error("the master connection failed")]
9+
Master(#[source] io::Error),
1010

1111
/// Failed to establish initial connection to the remote host.
12-
Connect(io::Error),
12+
#[error("failed to connect to the remote host")]
13+
Connect(#[source] io::Error),
1314

1415
/// Failed to run the `ssh` command locally.
1516
#[cfg(feature = "process-mux")]
1617
#[cfg_attr(docsrs, doc(cfg(feature = "process-mux")))]
17-
Ssh(io::Error),
18+
#[error("the local ssh command could not be executed")]
19+
Ssh(#[source] io::Error),
1820

1921
/// Failed to connect to the ssh multiplex server.
2022
#[cfg(feature = "native-mux")]
2123
#[cfg_attr(docsrs, doc(cfg(feature = "native-mux")))]
22-
SshMux(openssh_mux_client::Error),
24+
#[error("failed to connect to the ssh multiplex server")]
25+
SshMux(#[source] openssh_mux_client::Error),
2326

2427
/// Invalid command that contains null byte.
2528
#[cfg(feature = "native-mux")]
2629
#[cfg_attr(docsrs, doc(cfg(feature = "native-mux")))]
30+
#[error("invalid command: Command contains null byte.")]
2731
InvalidCommand,
2832

2933
/// The remote process failed.
30-
Remote(io::Error),
34+
#[error("the remote command could not be executed")]
35+
Remote(#[source] io::Error),
3136

3237
/// The connection to the remote host was severed.
3338
///
@@ -36,6 +41,7 @@ pub enum Error {
3641
///
3742
/// You should call [`Session::check`](crate::Session::check) to verify if you get
3843
/// this error back.
44+
#[error("the connection was terminated")]
3945
Disconnected,
4046

4147
/// Remote process is terminated.
@@ -51,13 +57,16 @@ pub enum Error {
5157
/// instead of `Disconnect`ed.
5258
///
5359
/// It is thus recommended to create your own workaround for your particular use cases.
60+
#[error("the remote process has terminated")]
5461
RemoteProcessTerminated,
5562

5663
/// Failed to remove temporary dir where ssh socket and output is stored.
57-
Cleanup(io::Error),
64+
#[error("failed to remove temporary ssh session directory")]
65+
Cleanup(#[source] io::Error),
5866

5967
/// IO Error when creating/reading/writing from ChildStdin, ChildStdout, ChildStderr.
60-
ChildIo(io::Error),
68+
#[error("failure while accessing standard i/o of remote process")]
69+
ChildIo(#[source] io::Error),
6170
}
6271

6372
#[cfg(feature = "native-mux")]
@@ -83,56 +92,6 @@ impl From<openssh_mux_client::Error> for Error {
8392
}
8493
}
8594

86-
impl fmt::Display for Error {
87-
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
88-
match *self {
89-
Error::Master(_) => write!(f, "the master connection failed"),
90-
Error::Connect(_) => write!(f, "failed to connect to the remote host"),
91-
92-
#[cfg(feature = "process-mux")]
93-
Error::Ssh(_) => write!(f, "the local ssh command could not be executed"),
94-
95-
Error::Remote(_) => write!(f, "the remote command could not be executed"),
96-
Error::Disconnected => write!(f, "the connection was terminated"),
97-
Error::Cleanup(_) => write!(f, "failed to remove temporary ssh session directory"),
98-
Error::ChildIo(_) => {
99-
write!(f, "failure while accessing standard I/O of remote process")
100-
}
101-
102-
Error::RemoteProcessTerminated => write!(f, "the remote process has terminated"),
103-
104-
#[cfg(feature = "native-mux")]
105-
Error::SshMux(_) => write!(f, "failed to connect to the ssh multiplex server"),
106-
107-
#[cfg(feature = "native-mux")]
108-
Error::InvalidCommand => write!(f, "invalid command: Command contains null byte."),
109-
}
110-
}
111-
}
112-
113-
impl std::error::Error for Error {
114-
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
115-
match *self {
116-
Error::Master(ref e)
117-
| Error::Connect(ref e)
118-
| Error::Remote(ref e)
119-
| Error::Cleanup(ref e)
120-
| Error::ChildIo(ref e) => Some(e),
121-
122-
Error::RemoteProcessTerminated | Error::Disconnected => None,
123-
124-
#[cfg(feature = "native-mux")]
125-
Error::InvalidCommand => None,
126-
127-
#[cfg(feature = "process-mux")]
128-
Error::Ssh(ref e) => Some(e),
129-
130-
#[cfg(feature = "native-mux")]
131-
Error::SshMux(ref e) => Some(e),
132-
}
133-
}
134-
}
135-
13695
impl Error {
13796
pub(crate) fn interpret_ssh_error(stderr: &str) -> Self {
13897
// we want to turn the string-only ssh error into something a little more "handleable".

src/lib.rs

Lines changed: 3 additions & 234 deletions
Original file line numberDiff line numberDiff line change
@@ -153,13 +153,12 @@
153153
#[cfg(not(unix))]
154154
compile_error!("This crate can only be used on unix");
155155

156-
use std::borrow::Cow;
157-
use std::ffi::OsStr;
158-
use std::path::Path;
159-
160156
mod stdio;
161157
pub use stdio::{ChildStderr, ChildStdin, ChildStdout, Stdio};
162158

159+
mod session;
160+
pub use session::Session;
161+
163162
mod builder;
164163
pub use builder::{KnownHosts, SessionBuilder};
165164

@@ -189,233 +188,3 @@ pub use port_forwarding::*;
189188
pub mod process {
190189
pub use super::{ChildStderr, ChildStdin, ChildStdout, Command, RemoteChild, Stdio};
191190
}
192-
193-
#[derive(Debug)]
194-
pub(crate) enum SessionImp {
195-
#[cfg(feature = "process-mux")]
196-
ProcessImpl(process_impl::Session),
197-
198-
#[cfg(feature = "native-mux")]
199-
NativeMuxImpl(native_mux_impl::Session),
200-
}
201-
202-
#[cfg(any(feature = "process-mux", feature = "native-mux"))]
203-
macro_rules! delegate {
204-
($impl:expr, $var:ident, $then:block) => {{
205-
match $impl {
206-
#[cfg(feature = "process-mux")]
207-
SessionImp::ProcessImpl($var) => $then,
208-
209-
#[cfg(feature = "native-mux")]
210-
SessionImp::NativeMuxImpl($var) => $then,
211-
}
212-
}};
213-
}
214-
215-
#[cfg(not(any(feature = "process-mux", feature = "native-mux")))]
216-
macro_rules! delegate {
217-
($impl:expr, $var:ident, $then:block) => {{
218-
unreachable!("Neither feature process-mux nor native-mux is enabled")
219-
}};
220-
}
221-
222-
/// A single SSH session to a remote host.
223-
///
224-
/// You can use [`command`](Session::command) to start a new command on the connected machine.
225-
///
226-
/// When the `Session` is dropped, the connection to the remote host is severed, and any errors
227-
/// silently ignored. To disconnect and be alerted to errors, use [`close`](Session::close).
228-
#[derive(Debug)]
229-
pub struct Session(SessionImp);
230-
231-
#[cfg(feature = "process-mux")]
232-
impl From<process_impl::Session> for Session {
233-
fn from(imp: process_impl::Session) -> Self {
234-
Self(SessionImp::ProcessImpl(imp))
235-
}
236-
}
237-
238-
#[cfg(feature = "native-mux")]
239-
impl From<native_mux_impl::Session> for Session {
240-
fn from(imp: native_mux_impl::Session) -> Self {
241-
Self(SessionImp::NativeMuxImpl(imp))
242-
}
243-
}
244-
245-
// TODO: UserKnownHostsFile for custom known host fingerprint.
246-
247-
impl Session {
248-
/// Connect to the host at the given `host` over SSH using process impl, which will
249-
/// spawn a new ssh process for each `Child` created.
250-
///
251-
/// The format of `destination` is the same as the `destination` argument to `ssh`. It may be
252-
/// specified as either `[user@]hostname` or a URI of the form `ssh://[user@]hostname[:port]`.
253-
///
254-
/// If connecting requires interactive authentication based on `STDIN` (such as reading a
255-
/// password), the connection will fail. Consider setting up keypair-based authentication
256-
/// instead.
257-
///
258-
/// For more options, see [`SessionBuilder`].
259-
#[cfg(feature = "process-mux")]
260-
#[cfg_attr(docsrs, doc(cfg(feature = "process-mux")))]
261-
pub async fn connect<S: AsRef<str>>(destination: S, check: KnownHosts) -> Result<Self, Error> {
262-
let mut s = SessionBuilder::default();
263-
s.known_hosts_check(check);
264-
s.connect(destination.as_ref()).await
265-
}
266-
267-
/// Connect to the host at the given `host` over SSH using native mux impl, which
268-
/// will create a new socket connection for each `Child` created.
269-
///
270-
/// See the crate-level documentation for more details on the difference between native and process-based mux.
271-
///
272-
/// The format of `destination` is the same as the `destination` argument to `ssh`. It may be
273-
/// specified as either `[user@]hostname` or a URI of the form `ssh://[user@]hostname[:port]`.
274-
///
275-
/// If connecting requires interactive authentication based on `STDIN` (such as reading a
276-
/// password), the connection will fail. Consider setting up keypair-based authentication
277-
/// instead.
278-
///
279-
/// For more options, see [`SessionBuilder`].
280-
#[cfg(feature = "native-mux")]
281-
#[cfg_attr(docsrs, doc(cfg(feature = "native-mux")))]
282-
pub async fn connect_mux<S: AsRef<str>>(
283-
destination: S,
284-
check: KnownHosts,
285-
) -> Result<Self, Error> {
286-
let mut s = SessionBuilder::default();
287-
s.known_hosts_check(check);
288-
s.connect_mux(destination.as_ref()).await
289-
}
290-
291-
/// Check the status of the underlying SSH connection.
292-
#[cfg(not(windows))]
293-
#[cfg_attr(docsrs, doc(cfg(not(windows))))]
294-
pub async fn check(&self) -> Result<(), Error> {
295-
delegate!(&self.0, imp, { imp.check().await })
296-
}
297-
298-
/// Get the SSH connection's control socket path.
299-
#[cfg(not(windows))]
300-
#[cfg_attr(docsrs, doc(cfg(not(windows))))]
301-
pub fn control_socket(&self) -> &Path {
302-
delegate!(&self.0, imp, { imp.ctl() })
303-
}
304-
305-
/// Constructs a new [`Command`] for launching the program at path `program` on the remote
306-
/// host.
307-
///
308-
/// Before it is passed to the remote host, `program` is escaped so that special characters
309-
/// aren't evaluated by the remote shell. If you do not want this behavior, use
310-
/// [`raw_command`](Session::raw_command).
311-
///
312-
/// The returned `Command` is a builder, with the following default configuration:
313-
///
314-
/// * No arguments to the program
315-
/// * Empty stdin and dsicard stdout/stderr for `spawn` or `status`, but create output pipes for
316-
/// `output`
317-
///
318-
/// Builder methods are provided to change these defaults and otherwise configure the process.
319-
///
320-
/// If `program` is not an absolute path, the `PATH` will be searched in an OS-defined way on
321-
/// the host.
322-
pub fn command<'a, S: Into<Cow<'a, str>>>(&self, program: S) -> Command<'_> {
323-
self.raw_command(&*shell_escape::unix::escape(program.into()))
324-
}
325-
326-
/// Constructs a new [`Command`] for launching the program at path `program` on the remote
327-
/// host.
328-
///
329-
/// Unlike [`command`](Session::command), this method does not shell-escape `program`, so it may be evaluated in
330-
/// unforeseen ways by the remote shell.
331-
///
332-
/// The returned `Command` is a builder, with the following default configuration:
333-
///
334-
/// * No arguments to the program
335-
/// * Empty stdin and dsicard stdout/stderr for `spawn` or `status`, but create output pipes for
336-
/// `output`
337-
///
338-
/// Builder methods are provided to change these defaults and otherwise configure the process.
339-
///
340-
/// If `program` is not an absolute path, the `PATH` will be searched in an OS-defined way on
341-
/// the host.
342-
pub fn raw_command<S: AsRef<OsStr>>(&self, program: S) -> Command<'_> {
343-
Command::new(
344-
self,
345-
delegate!(&self.0, imp, { imp.raw_command(program).into() }),
346-
)
347-
}
348-
349-
/// Constructs a new [`Command`] that runs the provided shell command on the remote host.
350-
///
351-
/// The provided command is passed as a single, escaped argument to `sh -c`, and from that
352-
/// point forward the behavior is up to `sh`. Since this executes a shell command, keep in mind
353-
/// that you are subject to the shell's rules around argument parsing, such as whitespace
354-
/// splitting, variable expansion, and other funkyness. I _highly_ recommend you read
355-
/// [this article] if you observe strange things.
356-
///
357-
/// While the returned `Command` is a builder, like for [`command`](Session::command), you should not add
358-
/// additional arguments to it, since the arguments are already passed within the shell
359-
/// command.
360-
///
361-
/// # Non-standard Remote Shells
362-
///
363-
/// It is worth noting that there are really _two_ shells at work here: the one that sshd
364-
/// launches for the session, and that launches are command; and the instance of `sh` that we
365-
/// launch _in_ that session. This method tries hard to ensure that the provided `command` is
366-
/// passed exactly as-is to `sh`, but this is complicated by the presence of the "outer" shell.
367-
/// That outer shell may itself perform argument splitting, variable expansion, and the like,
368-
/// which might produce unintuitive results. For example, the outer shell may try to expand a
369-
/// variable that is only defined in the inner shell, and simply produce an empty string in the
370-
/// variable's place by the time it gets to `sh`.
371-
///
372-
/// To counter this, this method assumes that the remote shell (the one launched by `sshd`) is
373-
/// [POSIX compliant]. This is more or less equivalent to "supports `bash` syntax" if you don't
374-
/// look too closely. It uses [`shell-escape`] to escape `command` before sending it to the
375-
/// remote shell, with the expectation that the remote shell will only end up undoing that one
376-
/// "level" of escaping, thus producing the original `command` as an argument to `sh`. This
377-
/// works _most of the time_.
378-
///
379-
/// With sufficiently complex or weird commands, the escaping of `shell-escape` may not fully
380-
/// match the "un-escaping" of the remote shell. This will manifest as escape characters
381-
/// appearing in the `sh` command that you did not intend to be there. If this happens, try
382-
/// changing the remote shell if you can, or fall back to [`command`](Session::command)
383-
/// and do the escaping manually instead.
384-
///
385-
/// [POSIX compliant]: https://pubs.opengroup.org/onlinepubs/9699919799/xrat/V4_xcu_chap02.html
386-
/// [this article]: https://mywiki.wooledge.org/Arguments
387-
/// [`shell-escape`]: https://crates.io/crates/shell-escape
388-
pub fn shell<S: AsRef<str>>(&self, command: S) -> Command<'_> {
389-
let mut cmd = self.command("sh");
390-
cmd.arg("-c").arg(command);
391-
cmd
392-
}
393-
394-
/// Request to open a local/remote port forwarding.
395-
/// The `Socket` can be either a unix socket or a tcp socket.
396-
///
397-
/// If `forward_type` == Local, then `listen_socket` on local machine will be
398-
/// forwarded to `connect_socket` on remote machine.
399-
///
400-
/// Otherwise, `listen_socket` on the remote machine will be forwarded to `connect_socket`
401-
/// on the local machine.
402-
///
403-
/// Currently, there is no way of stopping a port forwarding due to the fact that
404-
/// openssh multiplex server/master does not support this.
405-
pub async fn request_port_forward(
406-
&self,
407-
forward_type: ForwardType,
408-
listen_socket: Socket<'_>,
409-
connect_socket: Socket<'_>,
410-
) -> Result<(), Error> {
411-
delegate!(&self.0, imp, {
412-
imp.request_port_forward(forward_type, listen_socket, connect_socket)
413-
.await
414-
})
415-
}
416-
417-
/// Terminate the remote connection.
418-
pub async fn close(self) -> Result<(), Error> {
419-
delegate!(self.0, imp, { imp.close().await })
420-
}
421-
}

0 commit comments

Comments
 (0)