Skip to main content

async_ssh2_russh/
lib.rs

1#![cfg_attr(docsrs, feature(doc_cfg))]
2#![warn(missing_docs)]
3#![doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/", env!("CARGO_PKG_README")))]
4
5use std::collections::HashMap;
6use std::convert::Infallible as Never;
7use std::ops::Deref;
8use std::path::Path;
9use std::sync::Arc;
10
11use russh::client::{connect, Config, Handle, Handler, Msg};
12use russh::keys::{load_secret_key, ssh_key, PrivateKeyWithHashAlg};
13use russh::{ChannelMsg, ChannelWriteHalf, CryptoVec};
14use tokio::io::AsyncWrite;
15use tokio::net::ToSocketAddrs;
16use tokio::sync::mpsc;
17use tokio::task::JoinHandle;
18
19// `pub` items
20#[cfg(feature = "sftp")]
21#[cfg_attr(docsrs, doc(cfg(feature = "sftp")))]
22pub mod sftp;
23use async_promise::Promise;
24#[doc(no_inline)]
25pub use russh::Error as SshError;
26#[cfg(feature = "sftp")]
27#[cfg_attr(docsrs, doc(cfg(feature = "sftp")))]
28pub use russh_sftp;
29use tracing::Instrument;
30pub use {async_promise, russh, tokio};
31mod read_stream;
32pub use read_stream::ReadStream;
33
34/// A handler that does NOT check the server's public key.
35///
36/// This should NOT be used unless you are certain that the SSH server is trusted and you are aware of the security
37/// implications of not verifying the server's public key, particularly the risk of man-in-the-middle (MITM) attacks.
38///
39/// This should only be used with public key authentication, as it provides
40/// [some protection against MITM attacks](https://security.stackexchange.com/questions/67242/does-public-key-auth-in-ssh-prevent-most-mitm-attacks).
41pub struct NoCheckHandler;
42impl Handler for NoCheckHandler {
43    type Error = SshError;
44
45    async fn check_server_key(&mut self, _server_public_key: &ssh_key::PublicKey) -> Result<bool, Self::Error> {
46        Ok(true)
47    }
48}
49
50/// An SSH session, which may open multiple [`AsyncChannel`]s.
51///
52/// This struct is a thin wrapper around [`russh::client::Handle`] which provides basic authentication and channel
53/// management for a SSH session. Implements [`Deref`] to allow access to the underlying [`russh::client::Handle`].
54pub struct AsyncSession<H: Handler> {
55    session: Handle<H>,
56}
57impl<H: 'static + Handler> AsyncSession<H> {
58    /// Connect to an SSH server using the provided configuration and handler, without beginning
59    /// authentication.
60    pub async fn connect_unauthenticated(
61        config: Arc<Config>,
62        addrs: impl ToSocketAddrs,
63        handler: H,
64    ) -> Result<Self, H::Error> {
65        let session = connect(config, addrs, handler).await?;
66        Ok(Self { session })
67    }
68
69    /// Opens an [`AsyncChannel`] in this session.
70    ///
71    /// [`AsyncChannel`] is the asnyc wrapper for [`russh::Channel`].
72    pub async fn open_channel(&self) -> Result<AsyncChannel, SshError> {
73        let russh_channel = self.session.channel_open_session().await?;
74        Ok(AsyncChannel::from(russh_channel))
75    }
76}
77
78impl AsyncSession<NoCheckHandler> {
79    /// Connect to an SSH server and authenticate with the given `user` and `key_path` via publickey
80    /// authentication.
81    ///
82    /// Uses [`NoCheckHandler`] to skip server public key verification, as publickey authentication provides protection
83    /// against MITM attacks.
84    pub async fn connect_publickey(
85        config: impl Into<Arc<Config>>,
86        addrs: impl ToSocketAddrs,
87        user: impl Into<String>,
88        key_path: impl AsRef<Path>,
89    ) -> Result<Self, SshError> {
90        let key_pair = load_secret_key(key_path, None)?;
91
92        let mut session = connect(config.into(), addrs, NoCheckHandler).await?;
93
94        // use publickey authentication.
95        let auth_res = session
96            .authenticate_publickey(
97                user,
98                PrivateKeyWithHashAlg::new(Arc::new(key_pair), session.best_supported_rsa_hash().await?.flatten()),
99            )
100            .await?;
101
102        if auth_res.success() {
103            Ok(Self { session })
104        } else {
105            Err(SshError::NotAuthenticated)
106        }
107    }
108}
109
110impl<H: Handler> Deref for AsyncSession<H> {
111    type Target = Handle<H>;
112    fn deref(&self) -> &Self::Target {
113        &self.session
114    }
115}
116
117/// An asynchronous SSH channel, one of possibly many within a single SSH [`AsyncSession`]. Each channel represents a
118/// separate command, shell, SFTP session, X11 forwarding, or other SSH subsystem.
119///
120/// This struct is a thin wrapper around [`russh::Channel`] which provides access to async read/write streams
121/// (stdout/stderr/stdin) and async event handling Implements [`Deref`] to allow access to the underlying
122/// [`russh::ChannelWriteHalf`].
123///
124/// # Shutdown Lifecycle
125///
126/// During shutdown, events _may_ be received in the following order. However this should not be relied upon, as the
127/// order may be different and none of these events are guaranteed to occur, except for [`Self::closed`] which will
128/// always happen last.
129///
130/// 1. [`Self::recv_success_failure`].
131/// 2. [`Self::recv_eof`] - Guarantees all stream data has been received, i.e. stdout/stderr will produce no more data.
132///    Channels may be closed without sending EOF; see [this StackOverflow answer](https://stackoverflow.com/a/23257958).
133/// 3. [`Self::recv_exit_status`] - The exit status of the command run, if applicable.
134/// 4. [`Self::closed`] - This channel is closed, no more events will occur.
135pub struct AsyncChannel {
136    write_half: ChannelWriteHalf<Msg>,
137    subscribe_send: mpsc::UnboundedSender<(Option<u32>, mpsc::UnboundedSender<CryptoVec>)>,
138    success_failure: Promise<bool>,
139    eof: Promise<()>,
140    exit_status: Promise<u32>,
141    closed: Promise<Never>,
142    _reader: JoinHandle<()>,
143}
144
145impl From<russh::Channel<Msg>> for AsyncChannel {
146    fn from(inner: russh::Channel<Msg>) -> Self {
147        let (mut read_half, write_half) = inner.split();
148        let (mut resolve_success_failure, success_failure) = async_promise::channel();
149        let (mut resolve_eof, eof) = async_promise::channel();
150        let (mut resolve_exit_status, exit_status) = async_promise::channel();
151        let (resolve_closed, closed) = async_promise::channel();
152        let (subscribe_send, mut subscribe_recv) = mpsc::unbounded_channel();
153
154        let reader = async move {
155            // When the reader exits, (even due to panic, which would be a bug), this will resolve dropped.
156            let _resolve_closed_drop = resolve_closed;
157
158            // Map from `ext` to a sender for `CryptoVec`s of data.
159            type Subscribers = HashMap<Option<u32>, mpsc::UnboundedSender<CryptoVec>>;
160            let mut subscribers = Some(Subscribers::new());
161
162            #[tracing::instrument(level = "INFO", skip_all, fields(?ext))]
163            fn receive_data(subscribers: &Option<Subscribers>, ext: Option<u32>, data: CryptoVec) {
164                if let Some(subscribers) = &subscribers {
165                    if let Some(send) = subscribers.get(&ext) {
166                        if let Err(e) = send.send(data) {
167                            tracing::warn!("Failed to send data to subscriber: {e}");
168                        } else {
169                            tracing::debug!("Successfully sent data to subscriber.");
170                        }
171                    } else {
172                        tracing::debug!("No subscriber for ext, dropping data.");
173                    }
174                } else {
175                    tracing::warn!("Unexpectedly received data from server after receiving EOF.");
176                }
177            }
178
179            loop {
180                tokio::select! {
181                    biased;
182                    Some((ext, send)) = subscribe_recv.recv() => {
183                        if let Some(subscribers) = &mut subscribers {
184                            subscribers.insert(ext, send);
185                        } else {
186                            tracing::debug!(ext, "Received stream subscriber after EOF, ignoring.");
187                        }
188                    },
189                    opt_msg = read_half.wait() => {
190                        let Some(msg) = opt_msg else {
191                            // No more messages, exit!
192                            break;
193                        };
194
195                        tracing::info_span!("Message", ?msg).in_scope(|| {
196                            match msg {
197                                ChannelMsg::Data { data } => receive_data(&subscribers, None, data),
198                                ChannelMsg::ExtendedData { data, ext } => receive_data(&subscribers, Some(ext), data),
199                                ChannelMsg::Success | ChannelMsg::Failure => {
200                                    tracing::debug!("Resolving success/failure.");
201                                    let is_success = matches!(msg, ChannelMsg::Success);
202                                    if resolve_success_failure.resolve(is_success).is_err() {
203                                        tracing::warn!("Success/failure already resolved, ignoring.");
204                                    }
205                                }
206                                // The command has indicated no more `ChannelMsg::Data`/`ChannelMsg::ExtendedData` will be
207                                // sent.
208                                ChannelMsg::Eof => {
209                                    tracing::debug!("Resolving EOF and dropping stream subscribers.");
210                                    if resolve_eof.resolve(()).is_err() {
211                                        tracing::warn!("EOF already resolved, ignoring.");
212                                    }
213                                    // Disconnect all subscribers.
214                                    drop(std::mem::take(&mut subscribers));
215                                }
216                                // The command has returned an exit code
217                                ChannelMsg::ExitStatus { exit_status } => {
218                                    tracing::debug!(exit_status, "Resolving exit status.");
219                                    if resolve_exit_status.resolve(exit_status).is_err() {
220                                        tracing::warn!("Exit status already resolved, ignoring.");
221                                    }
222                                }
223                                // Other
224                                _ => {
225                                    tracing::trace!("Ignoring message.");
226                                }
227                            }
228                        });
229                    },
230                }
231            }
232            tracing::debug!("Channel read half finished, reader exiting.");
233        };
234        let reader = tokio::task::spawn(reader.instrument(tracing::info_span!("Reader")));
235
236        Self {
237            write_half,
238            subscribe_send,
239            success_failure,
240            eof,
241            exit_status,
242            closed,
243            _reader: reader,
244        }
245    }
246}
247
248impl AsyncChannel {
249    /// Returns the specified stream as a [`ReadStream`].
250    ///
251    /// Note that the returned stream will only receive data after this call, so call this before calling
252    /// [`exec`](ChannelWriteHalf::exec).
253    ///
254    /// When this is called for the same `ext` more than once, the later call will disconnect the
255    /// first.
256    pub fn read_stream(&self, ext: Option<u32>) -> ReadStream {
257        let (send, recv) = mpsc::unbounded_channel();
258        let _ = self.subscribe_send.send((ext, send));
259        ReadStream::from_recv(recv)
260    }
261
262    /// Returns stdout as a [`ReadStream`].
263    ///
264    /// Note that the returned stream will only receive data after this call, so call this before calling
265    /// [`exec`](ChannelWriteHalf::exec).
266    ///
267    /// When this is called more than once, the later call will disconnect the first.
268    pub fn stdout(&self) -> ReadStream {
269        self.read_stream(None)
270    }
271
272    /// Returns stderr as a [`ReadStream`].
273    ///
274    /// Note that the returned stream will only receive data after this call, so call this before calling
275    /// [`exec`](ChannelWriteHalf::exec).
276    ///
277    /// When this is called more than once, the later call will disconnect the first.
278    pub fn stderr(&self) -> ReadStream {
279        self.read_stream(Some(1))
280    }
281
282    /// Returns the specified stream as an [`impl AsyncWrite`](AsyncWrite).
283    ///
284    /// When this is called for the same `ext` more than once, writes to each may be interleaved.
285    pub fn write_stream(&self, ext: Option<u32>) -> impl use<'_> + 'static + AsyncWrite {
286        self.write_half.make_writer_ext(ext)
287    }
288
289    /// Returns stdin as an [`impl AsyncWrite`](AsyncWrite).
290    ///
291    /// When this is called more than once, writes to each may be interleaved.
292    pub fn stdin(&self) -> impl use<'_> + 'static + AsyncWrite {
293        self.write_stream(None)
294    }
295
296    /// Resolves when success or failure has been received, where `true` indicates success.
297    pub fn recv_success_failure(&self) -> &Promise<bool> {
298        &self.success_failure
299    }
300
301    /// Resolves when EOF has been received, indicating all stream data is complete.
302    ///
303    /// At that point, any streams from [`Self::stdout`]/[`Self::stderr`]/[`Self::read_stream`]
304    /// will return no additional data.
305    pub fn recv_eof(&self) -> &Promise<()> {
306        &self.eof
307    }
308
309    /// Resolves when the command exit status has been received.
310    pub fn recv_exit_status(&self) -> &Promise<u32> {
311        &self.exit_status
312    }
313
314    /// Resolves as dropped when the channel reader exits.
315    ///
316    /// Calling `.wait()` on this promise will return `None` when the reader exits, since `Never` cannot be constructed.
317    /// The expected usage pattern is `channel.closed().wait().await` to wait for the channel to close.
318    /// After this point, no more events will resolve.
319    ///
320    /// Use `channel.closed().is_done()` to check if the channel is closed, without waiting.
321    pub fn closed(&self) -> &Promise<Never> {
322        &self.closed
323    }
324}
325
326impl Deref for AsyncChannel {
327    type Target = ChannelWriteHalf<Msg>;
328    fn deref(&self) -> &Self::Target {
329        &self.write_half
330    }
331}