libssh_rs/
channel.rs

1use crate::{opt_cstring_to_cstr, opt_str_to_cstring, Error, SessionHolder, SshResult};
2use libssh_rs_sys as sys;
3use std::convert::TryInto;
4use std::ffi::{CStr, CString};
5use std::os::raw::c_int;
6use std::sync::{Arc, Mutex, MutexGuard};
7use std::time::Duration;
8
9/// Represents a channel in a `Session`.
10///
11/// A `Session` can have multiple channels; there is typically one
12/// for the shell/program being run, but additional channels can
13/// be opened to forward TCP or other connections.
14///
15/// [open_session](#method.open_session) is often the first
16/// thing you will call on the `Channel` after creating it; this establishes
17/// the channel for executing commands.
18///
19/// Then you will typically use either [request_exec](#method.request_exec)
20/// to run a non-interactive command, or [request_pty](#method.request_pty)
21/// followed [request_shell](#method.request_shell) to set up an interactive
22/// remote shell.
23///
24/// # Thread Safety
25///
26/// `Channel` is strongly associated with the `Session` to which it belongs.
27/// libssh doesn't allow using anything associated with a given `Session`
28/// from multiple threads concurrently.  These Rust bindings encapsulate
29/// the underlying `Session` in an internal mutex, which allows you to
30/// safely operate on the various elements of the session and even move
31/// them to other threads, but you need to be aware that calling methods
32/// on any of those structs will attempt to lock the underlying session,
33/// and this can lead to blocking in surprising situations.
34pub struct Channel {
35    pub(crate) sess: Arc<Mutex<SessionHolder>>,
36    pub(crate) chan_inner: sys::ssh_channel,
37    _callbacks: Box<sys::ssh_channel_callbacks_struct>,
38    callback_state: Box<CallbackState>,
39}
40
41unsafe impl Send for Channel {}
42
43impl Drop for Channel {
44    fn drop(&mut self) {
45        unsafe {
46            // Prevent any callbacks firing as part the remainder of this drop operation
47            sys::ssh_remove_channel_callbacks(self.chan_inner, self._callbacks.as_mut());
48        }
49        let (_sess, chan) = self.lock_session();
50        unsafe {
51            sys::ssh_channel_free(chan);
52        }
53    }
54}
55
56/// State visible to the callbacks
57struct CallbackState {
58    signal_state: Mutex<Option<SignalState>>,
59}
60
61#[derive(Clone, Debug)]
62pub struct SignalState {
63    pub signal_name: Option<String>,
64    pub core_dumped: bool,
65    pub error_message: Option<String>,
66    pub language: Option<String>,
67}
68
69fn cstr_to_opt_string(cstr: *const ::std::os::raw::c_char) -> Option<String> {
70    if cstr.is_null() {
71        return None;
72    }
73
74    Some(
75        unsafe { CStr::from_ptr(cstr) }
76            .to_string_lossy()
77            .to_string(),
78    )
79}
80
81unsafe extern "C" fn handle_exit_signal(
82    _session: sys::ssh_session,
83    _channel: sys::ssh_channel,
84    signal: *const ::std::os::raw::c_char,
85    core_dumped: ::std::os::raw::c_int,
86    errmsg: *const ::std::os::raw::c_char,
87    lang: *const ::std::os::raw::c_char,
88    userdata: *mut ::std::os::raw::c_void,
89) {
90    let callback_state: &CallbackState = &*(userdata as *const CallbackState);
91
92    let signal_name = cstr_to_opt_string(signal);
93    let error_message = cstr_to_opt_string(errmsg);
94    let language = cstr_to_opt_string(lang);
95
96    callback_state
97        .signal_state
98        .lock()
99        .unwrap()
100        .replace(SignalState {
101            signal_name,
102            core_dumped: if core_dumped == 0 { false } else { true },
103            error_message,
104            language,
105        });
106}
107
108impl Channel {
109    /// Accept an X11 forwarding channel.
110    /// Returns a newly created `Channel`, or `None` if no X11 request from the server.
111    pub fn accept_x11(&self, timeout: std::time::Duration) -> Option<Self> {
112        let (_sess, chan) = self.lock_session();
113        let timeout = timeout.as_millis();
114        let chan = unsafe { sys::ssh_channel_accept_x11(chan, timeout.try_into().unwrap()) };
115        if chan.is_null() {
116            None
117        } else {
118            Some(Self::new(&self.sess, chan))
119        }
120    }
121
122    pub(crate) fn new(sess: &Arc<Mutex<SessionHolder>>, chan: sys::ssh_channel) -> Self {
123        let callback_state = Box::new(CallbackState {
124            signal_state: Mutex::new(None),
125        });
126
127        let callbacks = Box::new(sys::ssh_channel_callbacks_struct {
128            size: std::mem::size_of::<sys::ssh_channel_callbacks_struct>(),
129            userdata: callback_state.as_ref() as *const CallbackState as *mut _,
130            channel_data_function: None,
131            channel_eof_function: None,
132            channel_close_function: None,
133            channel_signal_function: None,
134            channel_exit_status_function: None,
135            channel_exit_signal_function: Some(handle_exit_signal),
136            channel_pty_request_function: None,
137            channel_shell_request_function: None,
138            channel_auth_agent_req_function: None,
139            channel_x11_req_function: None,
140            channel_pty_window_change_function: None,
141            channel_exec_request_function: None,
142            channel_env_request_function: None,
143            channel_subsystem_request_function: None,
144            channel_write_wontblock_function: None,
145            channel_open_response_function: None,
146            channel_request_response_function: None,
147        });
148
149        unsafe { sys::ssh_set_channel_callbacks(chan, callbacks.as_ref() as *const _ as *mut _) };
150
151        Self {
152            sess: Arc::clone(&sess),
153            chan_inner: chan,
154            callback_state,
155            _callbacks: callbacks,
156        }
157    }
158
159    fn lock_session(&self) -> (MutexGuard<SessionHolder>, sys::ssh_channel) {
160        (self.sess.lock().unwrap(), self.chan_inner)
161    }
162
163    /// Close a channel.
164    /// This sends an end of file and then closes the channel.
165    /// You won't be able to recover any data the server was going
166    /// to send or was in buffers.
167    pub fn close(&self) -> SshResult<()> {
168        let (sess, chan) = self.lock_session();
169        let res = unsafe { sys::ssh_channel_close(chan) };
170        sess.basic_status(res, "error closing channel")
171    }
172
173    /// Get the exit status of the channel
174    /// (error code from the executed instruction).
175    /// This function may block until a timeout (or never) if the other
176    /// side is not willing to close the channel.
177    pub fn get_exit_status(&self) -> Option<c_int> {
178        let (_sess, chan) = self.lock_session();
179        let res = unsafe { sys::ssh_channel_get_exit_status(chan) };
180        if res == -1 {
181            None
182        } else {
183            Some(res)
184        }
185    }
186
187    /// Get the exit signal status of the channel.
188    /// If the channel was closed/terminated due to a signal, and the
189    /// remote system supports the signal concept, the signal state
190    /// will be set and reported here.
191    pub fn get_exit_signal(&self) -> Option<SignalState> {
192        self.callback_state.signal_state.lock().unwrap().clone()
193    }
194
195    /// Check if the channel is closed or not.
196    pub fn is_closed(&self) -> bool {
197        let (_sess, chan) = self.lock_session();
198        unsafe { sys::ssh_channel_is_closed(chan) != 0 }
199    }
200
201    /// Check if remote has sent an EOF.
202    pub fn is_eof(&self) -> bool {
203        let (_sess, chan) = self.lock_session();
204        unsafe { sys::ssh_channel_is_eof(chan) != 0 }
205    }
206
207    /// Send an end of file on the channel.
208    ///
209    /// You should call this when you have no additional data to send
210    /// to the channel to signal that information to the remote host.
211    ///
212    /// This doesn't close the channel.
213    /// You may still read from it but not write.
214    pub fn send_eof(&self) -> SshResult<()> {
215        let (sess, chan) = self.lock_session();
216        let res = unsafe { sys::ssh_channel_send_eof(chan) };
217        sess.basic_status(res, "ssh_channel_send_eof failed")
218    }
219
220    /// Check if the channel is open or not.
221    pub fn is_open(&self) -> bool {
222        let (_sess, chan) = self.lock_session();
223        unsafe { sys::ssh_channel_is_open(chan) != 0 }
224    }
225
226    /// Open an agent authentication forwarding channel.
227    /// This type of channel can be opened by a *server* towards a
228    /// client in order to provide SSH-Agent services to the server-side
229    /// process. This channel can only be opened if the client claimed
230    /// support by sending a channel request beforehand.
231    pub fn open_auth_agent(&self) -> SshResult<()> {
232        let (sess, chan) = self.lock_session();
233        let res = unsafe { sys::ssh_channel_open_auth_agent(chan) };
234        sess.basic_status(res, "ssh_channel_open_auth_agent failed")
235    }
236
237    /// Send an `"auth-agent-req"` channel request over an existing session channel.
238    ///
239    /// This client-side request will enable forwarding the agent
240    /// over a secure tunnel. When the server is ready to open one
241    /// authentication agent channel, an
242    /// ssh_channel_open_request_auth_agent_callback event will be generated.
243    pub fn request_auth_agent(&self) -> SshResult<()> {
244        let (sess, chan) = self.lock_session();
245        let res = unsafe { sys::ssh_channel_request_auth_agent(chan) };
246        sess.basic_status(res, "ssh_channel_request_auth_agent failed")
247    }
248
249    /// Set environment variable.
250    /// Some environment variables may be refused by security reasons.
251    pub fn request_env(&self, name: &str, value: &str) -> SshResult<()> {
252        let (sess, chan) = self.lock_session();
253        let name = CString::new(name)?;
254        let value = CString::new(value)?;
255        let res = unsafe { sys::ssh_channel_request_env(chan, name.as_ptr(), value.as_ptr()) };
256        sess.basic_status(res, "ssh_channel_request_env failed")
257    }
258
259    /// Requests a shell; asks the server to spawn the user's shell,
260    /// rather than directly executing a command specified by the client.
261    ///
262    /// The channel must be a session channel; you need to have called
263    /// [open_session](#method.open_session) before this will succeed.
264    pub fn request_shell(&self) -> SshResult<()> {
265        let (sess, chan) = self.lock_session();
266        let res = unsafe { sys::ssh_channel_request_shell(chan) };
267        sess.basic_status(res, "ssh_channel_request_shell failed")
268    }
269
270    /// Run a shell command without an interactive shell.
271    /// This is similar to 'sh -c command'.
272    ///
273    /// The channel must be a session channel; you need to have called
274    /// [open_session](#method.open_session) before this will succeed.
275    pub fn request_exec(&self, command: &str) -> SshResult<()> {
276        let (sess, chan) = self.lock_session();
277        let command = CString::new(command)?;
278        let res = unsafe { sys::ssh_channel_request_exec(chan, command.as_ptr()) };
279        sess.basic_status(res, "ssh_channel_request_exec failed")
280    }
281
282    /// Request a subsystem.
283    ///
284    /// You probably don't need this unless you know what you are doing!
285    pub fn request_subsystem(&self, subsys: &str) -> SshResult<()> {
286        let (sess, chan) = self.lock_session();
287        let subsys = CString::new(subsys)?;
288        let res = unsafe { sys::ssh_channel_request_subsystem(chan, subsys.as_ptr()) };
289        sess.basic_status(res, "ssh_channel_request_subsystem failed")
290    }
291
292    /// Request a PTY with a specific type and size.
293    /// A PTY is useful when you want to run an interactive program on
294    /// the remote host.
295    ///
296    /// `term` is the initial value for the `TERM` environment variable.
297    /// If you're not sure what to fill for the values,
298    /// `term = "xterm"`, `columns = 80` and `rows = 24` are reasonable
299    /// defaults.
300    pub fn request_pty(&self, term: &str, columns: u32, rows: u32) -> SshResult<()> {
301        let (sess, chan) = self.lock_session();
302        let term = CString::new(term)?;
303        let res = unsafe {
304            sys::ssh_channel_request_pty_size(
305                chan,
306                term.as_ptr(),
307                columns.try_into().unwrap(),
308                rows.try_into().unwrap(),
309            )
310        };
311        sess.basic_status(res, "ssh_channel_request_pty_size failed")
312    }
313
314    /// Informs the server that the local size of the PTY has changed
315    pub fn change_pty_size(&self, columns: u32, rows: u32) -> SshResult<()> {
316        let (sess, chan) = self.lock_session();
317        let res = unsafe {
318            sys::ssh_channel_change_pty_size(
319                chan,
320                columns.try_into().unwrap(),
321                rows.try_into().unwrap(),
322            )
323        };
324        sess.basic_status(res, "ssh_channel_change_pty_size failed")
325    }
326
327    /// Send a break signal to the server (as described in RFC 4335).
328    /// Sends a break signal to the remote process. Note, that remote
329    /// system may not support breaks. In such a case this request will
330    /// be silently ignored.
331    pub fn request_send_break(&self, length: Duration) -> SshResult<()> {
332        let (sess, chan) = self.lock_session();
333        let res = unsafe { sys::ssh_channel_request_send_break(chan, length.as_millis() as _) };
334        sess.basic_status(res, "ssh_channel_request_send_break failed")
335    }
336
337    /// Send a signal to remote process (as described in RFC 4254, section 6.9).
338    /// Sends a signal to the remote process.
339    /// Note, that remote system may not support signals concept.
340    /// In such a case this request will be silently ignored.
341    ///
342    /// `signal` is the name of the signal, without the `"SIG"` prefix.
343    /// For example, `"ABRT"`, `"INT"`, `"KILL"` and so on.
344    ///
345    /// The OpenSSH server has only supported signals since OpenSSH version 8.1,
346    /// released in 2019.
347    /// <https://bugzilla.mindrot.org/show_bug.cgi?id=1424>
348    pub fn request_send_signal(&self, signal: &str) -> SshResult<()> {
349        let (sess, chan) = self.lock_session();
350        let signal = CString::new(signal)?;
351        let res = unsafe { sys::ssh_channel_request_send_signal(chan, signal.as_ptr()) };
352        sess.basic_status(res, "ssh_channel_request_send_signal failed")
353    }
354
355    /// Open a TCP/IP forwarding channel.
356    /// `remote_host`, `remote_port` identify the destination for the
357    /// connection.
358    /// `source_host`, `source_port` identify the origin of the connection
359    /// on the client side; these are used primarily for logging purposes.
360    ///
361    /// This function does not bind the source port and does not
362    /// automatically forward the content of a socket to the channel.
363    /// You still have to read/write this channel object to achieve that.
364    pub fn open_forward(
365        &self,
366        remote_host: &str,
367        remote_port: u16,
368        source_host: &str,
369        source_port: u16,
370    ) -> SshResult<()> {
371        let (sess, chan) = self.lock_session();
372        let remote_host = CString::new(remote_host)?;
373        let source_host = CString::new(source_host)?;
374        let res = unsafe {
375            sys::ssh_channel_open_forward(
376                chan,
377                remote_host.as_ptr(),
378                remote_port as i32,
379                source_host.as_ptr(),
380                source_port as i32,
381            )
382        };
383        sess.basic_status(res, "ssh_channel_open_forward failed")
384    }
385
386    /// Open a UNIX domain socket forwarding channel.
387    /// `remote_path` is the path to the unix socket to open on the remote
388    /// machine.
389    /// `source_host` and `source_port` identify the originating connection
390    /// from the client machine and are used for logging purposes.
391    ///
392    /// This function does not bind the source and does not
393    /// automatically forward the content of a socket to the channel.
394    /// You still have to read/write this channel object to achieve that.
395    pub fn open_forward_unix(
396        &self,
397        remote_path: &str,
398        source_host: &str,
399        source_port: u16,
400    ) -> SshResult<()> {
401        let (sess, chan) = self.lock_session();
402        let remote_path = CString::new(remote_path)?;
403        let source_host = CString::new(source_host)?;
404        let res = unsafe {
405            sys::ssh_channel_open_forward_unix(
406                chan,
407                remote_path.as_ptr(),
408                source_host.as_ptr(),
409                source_port as i32,
410            )
411        };
412        sess.basic_status(res, "ssh_channel_open_forward_unix failed")
413    }
414
415    /// Sends the `"x11-req"` channel request over an existing session channel.
416    /// This will enable redirecting the display of the remote X11 applications
417    /// to local X server over an secure tunnel.
418    pub fn request_x11(
419        &self,
420        single_connection: bool,
421        protocol: Option<&str>,
422        cookie: Option<&str>,
423        screen_number: c_int,
424    ) -> SshResult<()> {
425        let (sess, chan) = self.lock_session();
426        let protocol = opt_str_to_cstring(protocol);
427        let cookie = opt_str_to_cstring(cookie);
428        let res = unsafe {
429            sys::ssh_channel_request_x11(
430                chan,
431                if single_connection { 1 } else { 0 },
432                opt_cstring_to_cstr(&protocol),
433                opt_cstring_to_cstr(&cookie),
434                screen_number,
435            )
436        };
437
438        sess.basic_status(res, "ssh_channel_open_forward failed")
439    }
440
441    /// Open a session channel (suited for a shell, not TCP forwarding).
442    pub fn open_session(&self) -> SshResult<()> {
443        let (sess, chan) = self.lock_session();
444        let res = unsafe { sys::ssh_channel_open_session(chan) };
445        sess.basic_status(res, "ssh_channel_open_session failed")
446    }
447
448    /// Polls a channel for data to read.
449    /// Returns the number of bytes available for reading.
450    /// If `timeout` is None, then blocks until data is available.
451    pub fn poll_timeout(
452        &self,
453        is_stderr: bool,
454        timeout: Option<Duration>,
455    ) -> SshResult<PollStatus> {
456        let (sess, chan) = self.lock_session();
457        let timeout = match timeout {
458            Some(t) => t.as_millis() as c_int,
459            None => -1,
460        };
461        let res =
462            unsafe { sys::ssh_channel_poll_timeout(chan, if is_stderr { 1 } else { 0 }, timeout) };
463        match res {
464            sys::SSH_ERROR => {
465                if let Some(err) = sess.last_error() {
466                    Err(err)
467                } else {
468                    Err(Error::fatal("ssh_channel_poll failed"))
469                }
470            }
471            sys::SSH_EOF => Ok(PollStatus::EndOfFile),
472            n if n >= 0 => Ok(PollStatus::AvailableBytes(n as u32)),
473            n => Err(Error::Fatal(format!(
474                "ssh_channel_poll returned unexpected {} value",
475                n
476            ))),
477        }
478    }
479
480    /// Reads data from a channel.
481    /// This function may fewer bytes than the buf size.
482    pub fn read_timeout(
483        &self,
484        buf: &mut [u8],
485        is_stderr: bool,
486        timeout: Option<Duration>,
487    ) -> SshResult<usize> {
488        let (sess, chan) = self.lock_session();
489
490        let timeout = match timeout {
491            Some(t) => t.as_millis() as c_int,
492            None => -1,
493        };
494        let res = unsafe {
495            sys::ssh_channel_read_timeout(
496                chan,
497                buf.as_mut_ptr() as _,
498                buf.len() as u32,
499                if is_stderr { 1 } else { 0 },
500                timeout,
501            )
502        };
503        match res {
504            sys::SSH_ERROR => {
505                if let Some(err) = sess.last_error() {
506                    Err(err)
507                } else {
508                    Err(Error::fatal("ssh_channel_read_timeout failed"))
509                }
510            }
511            sys::SSH_AGAIN => Err(Error::TryAgain),
512            n if n < 0 => Err(Error::Fatal(format!(
513                "ssh_channel_read_timeout returned unexpected {} value",
514                n
515            ))),
516            0 if !sess.is_blocking() => Err(Error::TryAgain),
517            n => Ok(n as usize),
518        }
519    }
520
521    /// Do a nonblocking read on the channel.
522    /// A nonblocking read on the specified channel. it will return <= count bytes of data read atomically.
523    pub fn read_nonblocking(&self, buf: &mut [u8], is_stderr: bool) -> SshResult<usize> {
524        let (sess, chan) = self.lock_session();
525
526        let res = unsafe {
527            sys::ssh_channel_read_nonblocking(
528                chan,
529                buf.as_mut_ptr() as _,
530                buf.len() as u32,
531                if is_stderr { 1 } else { 0 },
532            )
533        };
534        match res {
535            sys::SSH_ERROR => {
536                if let Some(err) = sess.last_error() {
537                    Err(err)
538                } else {
539                    Err(Error::fatal("ssh_channel_read_timeout failed"))
540                }
541            }
542            sys::SSH_EOF => Ok(0 as usize),
543            n if n < 0 => Err(Error::Fatal(format!(
544                "ssh_channel_read_timeout returned unexpected value: {n}"
545            ))),
546            n => Ok(n as usize),
547        }
548    }
549
550    /// Get the remote window size.
551    /// This is the maximum amounts of bytes the remote side expects us to send
552    /// before growing the window again.
553    /// A nonzero return value does not guarantee the socket is ready to send that much data.
554    /// Buffering may happen in the local SSH packet buffer, so beware of really big window sizes.
555    /// A zero return value means that a write will block (if the session is in blocking mode)
556    /// until the window grows back.
557    pub fn window_size(&self) -> usize {
558        let (_sess, chan) = self.lock_session();
559        unsafe { sys::ssh_channel_window_size(chan).try_into().unwrap() }
560    }
561
562    fn read_impl(&self, buf: &mut [u8], is_stderr: bool) -> std::io::Result<usize> {
563        Ok(self.read_timeout(buf, is_stderr, None)?)
564    }
565
566    fn write_impl(&self, buf: &[u8], is_stderr: bool) -> SshResult<usize> {
567        let (sess, chan) = self.lock_session();
568
569        let res = unsafe {
570            (if is_stderr {
571                sys::ssh_channel_write_stderr
572            } else {
573                sys::ssh_channel_write
574            })(chan, buf.as_ptr() as _, buf.len() as _)
575        };
576
577        match res {
578            sys::SSH_ERROR => {
579                if let Some(err) = sess.last_error() {
580                    Err(err)
581                } else {
582                    Err(Error::fatal("ssh_channel_read_timeout failed"))
583                }
584            }
585            sys::SSH_AGAIN => Err(Error::TryAgain),
586            n if n < 0 => Err(Error::Fatal(format!(
587                "ssh_channel_read_timeout returned unexpected {} value",
588                n
589            ))),
590            n => Ok(n as usize),
591        }
592    }
593
594    /// Returns a struct that implements `std::io::Read`
595    /// and that will read data from the stdout channel.
596    pub fn stdout(&self) -> impl std::io::Read + '_ {
597        ChannelStdout { chan: self }
598    }
599
600    /// Returns a struct that implements `std::io::Read`
601    /// and that will read data from the stderr channel.
602    pub fn stderr(&self) -> impl std::io::Read + '_ {
603        ChannelStderr { chan: self }
604    }
605
606    /// Returns a struct that implements `std::io::Write`
607    /// and that will write data to the stdin channel
608    pub fn stdin(&self) -> impl std::io::Write + '_ {
609        ChannelStdin { chan: self }
610    }
611}
612
613/// Represents the stdin stream for the channel.
614/// Implements std::io::Write; writing to this struct
615/// will write to the stdin of the channel.
616struct ChannelStdin<'a> {
617    chan: &'a Channel,
618}
619
620impl<'a> std::io::Write for ChannelStdin<'a> {
621    fn flush(&mut self) -> std::io::Result<()> {
622        Ok(self.chan.sess.lock().unwrap().blocking_flush(None)?)
623    }
624
625    fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
626        Ok(self.chan.write_impl(buf, false)?)
627    }
628}
629
630/// Represents the stdout stream for the channel.
631/// Implements std::io::Read; reading from this struct
632/// will read from the stdout of the channel.
633struct ChannelStdout<'a> {
634    chan: &'a Channel,
635}
636
637impl<'a> std::io::Read for ChannelStdout<'a> {
638    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
639        self.chan.read_impl(buf, false)
640    }
641}
642
643/// Represents the stderr stream for the channel.
644/// Implements std::io::Read; reading from this struct
645/// will read from the stderr of the channel.
646struct ChannelStderr<'a> {
647    chan: &'a Channel,
648}
649
650impl<'a> std::io::Read for ChannelStderr<'a> {
651    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
652        self.chan.read_impl(buf, true)
653    }
654}
655
656/// Indicates available data for the stdout or stderr on a `Channel`.
657#[derive(Debug, Clone, Copy, PartialEq, Eq)]
658pub enum PollStatus {
659    /// The available bytes to read; may be 0
660    AvailableBytes(u32),
661    /// The channel is in the EOF state
662    EndOfFile,
663}