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}