openssh-mux-client-0.17.3/.cargo_vcs_info.json0000644000000001570000000000100146710ustar { "git": { "sha1": "aa047bfc0c6d664574d6abf8566d4c7638eeee58" }, "path_in_vcs": "crates/mux-client" }openssh-mux-client-0.17.3/Cargo.toml0000644000000031570000000000100126720ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2018" name = "openssh-mux-client" version = "0.17.3" authors = ["Jiahao XU "] description = "openssh mux client." readme = "README.md" keywords = [ "ssh", "openssh", "multiplex", "async", "network", ] categories = [ "asynchronous", "network-programming", "api-bindings", ] license = "MIT" repository = "https://github.com/openssh-rust/openssh-mux-client" [dependencies.cfg-if] version = "1.0.0" [dependencies.non-zero-byte-slice] version = "0.1.0" [dependencies.once_cell] version = "1.10.0" [dependencies.openssh-mux-client-error] version = "0.1" [dependencies.sendfd] version = "0.4.1" features = ["tokio"] [dependencies.serde] version = "1.0.103" features = ["derive"] [dependencies.ssh_format] version = "0.14.1" [dependencies.tokio] version = "1.11.0" features = [ "net", "io-util", ] [dependencies.tokio-io-utility] version = "0.7.1" [dependencies.typed-builder] version = "0.18.0" [dev-dependencies.assert_matches] version = "1.5.0" [dev-dependencies.tokio] version = "1.11.0" features = [ "rt", "macros", "time", ] [dev-dependencies.tokio-pipe] version = "0.2.1" openssh-mux-client-0.17.3/Cargo.toml.orig000064400000000000000000000017031046102023000163460ustar 00000000000000[package] name = "openssh-mux-client" version = "0.17.3" edition = "2018" authors = ["Jiahao XU "] license = "MIT" description = "openssh mux client." repository = "https://github.com/openssh-rust/openssh-mux-client" keywords = ["ssh", "openssh", "multiplex", "async", "network"] categories = ["asynchronous", "network-programming", "api-bindings"] [dependencies] openssh-mux-client-error = { version = "0.1", path = "../mux-client-error" } cfg-if = "1.0.0" serde = { version = "1.0.103", features = ["derive"] } ssh_format = "0.14.1" typed-builder = "0.18.0" once_cell = "1.10.0" sendfd = { version = "0.4.1", features = ["tokio"] } tokio = { version = "1.11.0", features = ["net", "io-util"] } tokio-io-utility = "0.7.1" non-zero-byte-slice = { version = "0.1.0", path = "../non-zero-byte-slice" } [dev-dependencies] tokio = { version = "1.11.0", features = ["rt", "macros", "time"] } tokio-pipe = "0.2.1" assert_matches = "1.5.0" openssh-mux-client-0.17.3/LICENSE000064400000000000000000000020521046102023000144620ustar 00000000000000MIT License Copyright (c) 2021 Jiahao XU Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. openssh-mux-client-0.17.3/README.md000064400000000000000000000033071046102023000147400ustar 00000000000000# openssh-mux-client [![Rust](https://github.com/openssh-rust/openssh-mux-client/actions/workflows/rust.yml/badge.svg)](https://github.com/openssh-rust/openssh-mux-client/actions/workflows/rust.yml) [![crate.io downloads](https://img.shields.io/crates/d/openssh-mux-client)](https://crates.io/crates/openssh-mux-client) [![crate.io version](https://img.shields.io/crates/v/openssh-mux-client)](https://crates.io/crates/openssh-mux-client) [![docs](https://docs.rs/openssh-mux-client/badge.svg)](https://docs.rs/openssh-mux-client) Rust library to communicate with openssh-mux-server using [ssh_format]. The entire crate is built upon [official document on ssh multiplex protocol][protocol doc]. Currently, I have written a few test cases to make sure the - health check - session opening - remote port forwarding - graceful shutdown of the ssh multiplex server - local port forwarding are working as intended, while features - dynamic forwarding are implemented but not tested. There are also two features that I didn't implement: - forward stdio (stdin + stdout) to remote port (not that useful) - closure of port forwarding (according to the [document], it is not implemented yet by ssh) - terminating the ssh multiplex server for the ssh implementation is buggy (the server does not reply with the Ok message before it terminates). While it is extremely likely there are bugs in my code, I think it is ready for testing. ## Development To run tests, make sure you have bash, ssh and docker installed on your computer and run: ``` /path/to/repository/run_test.sh ``` [ssh_format]: https://github.com/openssh-rust/ssh_format [protocol doc]: https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.mux openssh-mux-client-0.17.3/src/connection.rs000064400000000000000000000552511046102023000167620ustar 00000000000000#![forbid(unsafe_code)] use crate::{ constants, request::{Fwd, Request, SessionZeroCopy}, shutdown_mux_master::shutdown_mux_master_from, utils::{serialize_u32, SliceExt}, Error, ErrorExt, EstablishedSession, Response, Result, Session, Socket, }; use std::{ borrow::Cow, convert::TryInto, io, io::IoSlice, num::{NonZeroU32, Wrapping}, os::unix::io::RawFd, path::Path, }; use sendfd::SendWithFd; use serde::{de::DeserializeOwned, Serialize}; use ssh_format::{from_bytes, Serializer}; use tokio::net::UnixStream; use tokio_io_utility::{read_to_vec_rng, write_vectored_all}; #[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] pub enum ForwardType { Local, Remote, } /// # Cancel safety /// /// All methods of this struct is not cancellation safe. #[derive(Debug)] pub struct Connection { raw_conn: UnixStream, serializer: Serializer, read_buffer: Vec, request_id: Wrapping, } impl Connection { fn reset_serializer(&mut self) { self.serializer.reset_counter(); self.serializer.output.clear(); } async fn write(&mut self, value: &Request) -> Result<()> { self.reset_serializer(); value.serialize(&mut self.serializer)?; let header = self.serializer.create_header(0)?; write_vectored_all( &mut self.raw_conn, &mut [IoSlice::new(&header), IoSlice::new(&self.serializer.output)], ) .await?; Ok(()) } fn deserialize(read_buffer: &[u8]) -> Result { // Ignore any trailing bytes to be forward compatible Ok(from_bytes(read_buffer)?.0) } pub(crate) async fn read_response(&mut self) -> Result { let buffer = &mut self.read_buffer; if buffer.len() < 4 { let n = 4 - buffer.len(); read_to_vec_rng(&mut self.raw_conn, buffer, n..).await?; } // Read in the header let packet_len: u32 = Self::deserialize(&buffer[..4])?; let packet_len: usize = packet_len.try_into().unwrap(); // The first 4 bytes are not counted as the packet body let buffer_len = buffer.len() - 4; if buffer_len < packet_len { // Read in rest of the packet let n = packet_len - buffer_len; read_to_vec_rng(&mut self.raw_conn, buffer, n..).await?; } // Deserialize the response let response = Self::deserialize(&buffer[4..(4 + packet_len)])?; // Remove the packet from buffer buffer.drain(..(4 + packet_len)); Ok(response) } /// Send fds with "\0" async fn send_with_fds(&self, fds: &[RawFd]) -> Result<()> { let byte = &[0]; loop { self.raw_conn.writable().await?; // send_with_fd calls `UnixStream::try_io` match SendWithFd::send_with_fd(&self.raw_conn, byte, fds) { Ok(n) => { if n == 1 { break Ok(()); } else { debug_assert_eq!(n, 0); break Err(io::Error::from(io::ErrorKind::UnexpectedEof).into()); } } Err(e) => { if e.kind() != io::ErrorKind::WouldBlock { break Err(e.into()); } } } } } fn get_request_id(&mut self) -> u32 { let request_id = self.request_id.0; self.request_id += Wrapping(1); request_id } fn check_response_id(request_id: u32, response_id: u32) -> Result<()> { if request_id != response_id { Err(Error::UnmatchedRequestId) } else { Ok(()) } } async fn exchange_hello(mut self) -> Result { self.write(&Request::Hello { version: constants::SSHMUX_VER, }) .await?; let response = self.read_response().await?; if let Response::Hello { version } = response { if version != constants::SSHMUX_VER { Err(Error::UnsupportedMuxProtocol) } else { Ok(self) } } else { Err(Error::invalid_server_response(&"Hello message", &response)) } } pub async fn connect>(path: P) -> Result { Self { raw_conn: UnixStream::connect(path).await?, // All request packets are at least 12 bytes large, // and variant [`Request::NewSession`] takes 36 bytes to // serialize. serializer: Serializer::new(Vec::with_capacity(36)), // All reponse packets are at least 16 bytes large. read_buffer: Vec::with_capacity(32), request_id: Wrapping(0), } .exchange_hello() .await } /// Send a ping to the server and return pid of the ssh mux server /// if it is still alive. pub async fn send_alive_check(&mut self) -> Result { let request_id = self.get_request_id(); self.write(&Request::AliveCheck { request_id }).await?; let response = self.read_response().await?; if let Response::Alive { response_id, server_pid, } = response { Self::check_response_id(request_id, response_id)?; NonZeroU32::new(server_pid).ok_or(Error::InvalidPid) } else { Err(Error::invalid_server_response( &"Response::Alive", &response, )) } } /// Return session_id async fn open_new_session_impl( &mut self, session: &Session<'_>, fds: &[RawFd; 3], ) -> Result { use Response::*; let request_id = self.get_request_id(); // Prepare to serialize let term = session.term.as_ref().into_inner(); let cmd = session.cmd.as_ref().into_inner(); let term_len: u32 = term.get_len_as_u32()?; let cmd_len: u32 = cmd.get_len_as_u32()?; let request = Request::NewSession { request_id, session: SessionZeroCopy { tty: session.tty, x11_forwarding: session.x11_forwarding, agent: session.agent, subsystem: session.subsystem, escape_ch: session.escape_ch, }, }; // Serialize self.reset_serializer(); request.serialize(&mut self.serializer)?; let serialized_header = self.serializer.create_header( /* len of term */ 4 + term_len + /* len of cmd */ 4 + cmd_len, )?; let serialized_cmd_len = serialize_u32(cmd_len); let serialized_term_len = serialize_u32(term_len); // Write them to self.raw_conn let mut io_slices = [ IoSlice::new(&serialized_header), IoSlice::new(&self.serializer.output), IoSlice::new(&serialized_term_len), IoSlice::new(term), IoSlice::new(&serialized_cmd_len), IoSlice::new(cmd), ]; write_vectored_all(&mut self.raw_conn, &mut io_slices).await?; for fd in fds { self.send_with_fds(&[*fd]).await?; } let session_id = match self.read_response().await? { SessionOpened { response_id, session_id, } => { Self::check_response_id(request_id, response_id)?; session_id } PermissionDenied { response_id, reason, } => { Self::check_response_id(request_id, response_id)?; return Err(Error::PermissionDenied(reason)); } Failure { response_id, reason, } => { Self::check_response_id(request_id, response_id)?; return Err(Error::RequestFailure(reason)); } response => { return Err(Error::invalid_server_response( &"SessionOpened, PermissionDenied or Failure", &response, )) } }; Result::Ok(session_id) } /// Opens a new session. /// /// Consumes `self` so that users would not be able to create multiple sessions /// or perform other operations during the session that might complicates the /// handling of packets received from the ssh mux server. /// /// Two additional cases that the client must cope with are it receiving /// a signal itself (from the ssh mux server) and the server disconnecting /// without sending an exit message. /// /// * `fds` - must be in blocking mode pub async fn open_new_session( mut self, session: &Session<'_>, fds: &[RawFd; 3], ) -> Result { let session_id = self.open_new_session_impl(session, fds).await?; // EstablishedSession does not send any request // It merely wait for response. self.serializer.output = Vec::new(); Ok(EstablishedSession { conn: self, session_id, }) } /// Convenient function for opening a new sftp session, uses /// `open_new_session` underlying. pub async fn sftp(self, fds: &[RawFd; 3]) -> Result { let session = Session::builder() .subsystem(true) .term(Cow::Borrowed("".try_into().unwrap())) .cmd(Cow::Borrowed("sftp".try_into().unwrap())) .build(); self.open_new_session(&session, fds).await } async fn send_fwd_request(&mut self, request_id: u32, fwd: &Fwd<'_>) -> Result<()> { let (fwd_mode, listen_socket, connect_socket) = fwd.as_serializable(); let (listen_addr, listen_port) = listen_socket.as_serializable(); let (connect_addr, connect_port) = connect_socket.as_ref().as_serializable(); let serialized_listen_port = serialize_u32(listen_port); let serialized_connect_port = serialize_u32(connect_port); let listen_addr_len: u32 = listen_addr.get_len_as_u32()?; let connect_addr_len: u32 = connect_addr.get_len_as_u32()?; let request = Request::OpenFwd { request_id, fwd_mode, }; // Serialize self.reset_serializer(); request.serialize(&mut self.serializer)?; let serialized_header = self.serializer.create_header( // len 4 + listen_addr_len + // port 4 + // len 4 + connect_addr_len // port + 4, )?; let serialized_listen_addr_len = serialize_u32(listen_addr_len); let serialized_connect_addr_len = serialize_u32(connect_addr_len); // Write them to self.raw_conn let mut io_slices = [ IoSlice::new(&serialized_header), IoSlice::new(&self.serializer.output), IoSlice::new(&serialized_listen_addr_len), IoSlice::new(listen_addr.into_inner()), IoSlice::new(&serialized_listen_port), IoSlice::new(&serialized_connect_addr_len), IoSlice::new(connect_addr.into_inner()), IoSlice::new(&serialized_connect_port), ]; write_vectored_all(&mut self.raw_conn, &mut io_slices).await?; Ok(()) } /// Request for local/remote port forwarding. /// /// # Warning /// /// Local port forwarding hasn't been tested yet. pub async fn request_port_forward( &mut self, forward_type: ForwardType, listen_socket: &Socket<'_>, connect_socket: &Socket<'_>, ) -> Result<()> { use ForwardType::*; use Response::*; let fwd = match forward_type { Local => Fwd::Local { listen_socket, connect_socket, }, Remote => Fwd::Remote { listen_socket, connect_socket, }, }; let request_id = self.get_request_id(); self.send_fwd_request(request_id, &fwd).await?; match self.read_response().await? { Ok { response_id } => Self::check_response_id(request_id, response_id), PermissionDenied { response_id, reason, } => { Self::check_response_id(request_id, response_id)?; Err(Error::PermissionDenied(reason)) } Failure { response_id, reason, } => { Self::check_response_id(request_id, response_id)?; Err(Error::RequestFailure(reason)) } response => Err(Error::invalid_server_response( &"Ok, PermissionDenied or Failure", &response, )), } } /// **UNTESTED** Return remote port opened for dynamic forwarding. pub async fn request_dynamic_forward( &mut self, listen_socket: &Socket<'_>, ) -> Result { use Response::*; let fwd = Fwd::Dynamic { listen_socket }; let request_id = self.get_request_id(); self.send_fwd_request(request_id, &fwd).await?; match self.read_response().await? { RemotePort { response_id, remote_port, } => { Self::check_response_id(request_id, response_id)?; NonZeroU32::new(remote_port).ok_or(Error::InvalidPort) } PermissionDenied { response_id, reason, } => { Self::check_response_id(request_id, response_id)?; Err(Error::PermissionDenied(reason)) } Failure { response_id, reason, } => { Self::check_response_id(request_id, response_id)?; Err(Error::RequestFailure(reason)) } response => Err(Error::invalid_server_response( &"RemotePort, PermissionDenied or Failure", &response, )), } } /// Request the master to stop accepting new multiplexing requests /// and remove its listener socket. pub async fn request_stop_listening(&mut self) -> Result<()> { use Response::*; let request_id = self.get_request_id(); self.write(&Request::StopListening { request_id }).await?; match self.read_response().await? { Ok { response_id } => { Self::check_response_id(request_id, response_id)?; Result::Ok(()) } PermissionDenied { response_id, reason, } => { Self::check_response_id(request_id, response_id)?; Err(Error::PermissionDenied(reason)) } Failure { response_id, reason, } => { Self::check_response_id(request_id, response_id)?; Err(Error::RequestFailure(reason)) } response => Err(Error::invalid_server_response( &"Ok, PermissionDenied or Failure", &response, )), } } /// Request the master to stop accepting new multiplexing requests /// and remove its listener socket. /// /// **Only suitable to use in `Drop::drop`.** pub fn request_stop_listening_sync(self) -> Result<()> { shutdown_mux_master_from(self.raw_conn.into_std()?) } } #[cfg(test)] mod tests { use super::*; use crate::SessionStatus; use std::convert::TryInto; use std::env; use std::io; use std::os::unix::io::AsRawFd; use std::time::Duration; use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio::net::{TcpListener, TcpStream}; use tokio::time::sleep; use tokio_pipe::{pipe, PipeRead, PipeWrite}; const PATH: &str = "/tmp/openssh-mux-client-test.socket"; macro_rules! run_test { ( $test_name:ident, $func:ident ) => { #[tokio::test(flavor = "current_thread")] async fn $test_name() { $func(Connection::connect(PATH).await.unwrap()).await; } }; } macro_rules! run_test2 { ( $test_name:ident, $func:ident ) => { #[tokio::test(flavor = "current_thread")] async fn $test_name() { $func( Connection::connect(PATH).await.unwrap(), Connection::connect(PATH).await.unwrap(), ) .await; } }; } async fn test_connect_impl(_conn: Connection) {} run_test!(test_unordered_connect, test_connect_impl); async fn test_alive_check_impl(mut conn: Connection) { let expected_pid = env::var("ControlMasterPID").unwrap(); let expected_pid: u32 = expected_pid.parse().unwrap(); let actual_pid = conn.send_alive_check().await.unwrap().get(); assert_eq!(expected_pid, actual_pid); } run_test!(test_unordered_alive_check, test_alive_check_impl); async fn test_roundtrip( stdios: &mut (PipeWrite, PipeRead), data: &'static [u8; SIZE], ) { stdios.0.write_all(data).await.unwrap(); let mut buffer = [0_u8; SIZE]; stdios.1.read_exact(&mut buffer).await.unwrap(); assert_eq!(data, &buffer); } async fn create_remote_process( conn: Connection, cmd: &str, ) -> (EstablishedSession, (PipeWrite, PipeRead)) { let session = Session::builder() .cmd(Cow::Borrowed(cmd.try_into().unwrap())) .build(); // pipe() returns (PipeRead, PipeWrite) let (stdin_read, stdin_write) = pipe().unwrap(); let (stdout_read, stdout_write) = pipe().unwrap(); let established_session = conn .open_new_session( &session, &[ stdin_read.as_raw_fd(), stdout_write.as_raw_fd(), io::stderr().as_raw_fd(), ], ) .await .unwrap(); (established_session, (stdin_write, stdout_read)) } async fn test_open_new_session_impl(conn: Connection) { let (established_session, mut stdios) = create_remote_process(conn, "/bin/cat").await; // All test data here must end with '\n', otherwise cat would output nothing // and the test would hang forever. test_roundtrip(&mut stdios, b"0134131dqwdqdx13as\n").await; test_roundtrip(&mut stdios, b"Whats' Up?\n").await; drop(stdios); let session_status = established_session.wait().await.unwrap(); assert_matches!( session_status, SessionStatus::Exited { exit_value, .. } if exit_value.unwrap() == 0 ); } run_test!(test_unordered_open_new_session, test_open_new_session_impl); async fn test_remote_socket_forward_impl(mut conn: Connection) { let path = Path::new("/tmp/openssh-remote-forward.socket"); let output_listener = TcpListener::bind(("127.0.0.1", 1234)).await.unwrap(); eprintln!("Requesting port forward"); conn.request_port_forward( ForwardType::Remote, &Socket::UnixSocket { path: path.into() }, &Socket::TcpSocket { port: 1234, host: "127.0.0.1".into(), }, ) .await .unwrap(); eprintln!("Creating remote process"); let cmd = format!("/usr/bin/socat OPEN:/data,rdonly UNIX-CONNECT:{:#?}", path); let (established_session, stdios) = create_remote_process(conn, &cmd).await; eprintln!("Waiting for connection"); let (mut output, _addr) = output_listener.accept().await.unwrap(); eprintln!("Reading"); const DATA: &[u8] = "0\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n".as_bytes(); let mut buffer = [0_u8; DATA.len()]; output.read_exact(&mut buffer).await.unwrap(); assert_eq!(DATA, &buffer); drop(output); drop(output_listener); drop(stdios); eprintln!("Waiting for session to end"); let session_status = established_session.wait().await.unwrap(); assert_matches!( session_status, SessionStatus::Exited { exit_value, .. } if exit_value.unwrap() == 0 ); } run_test!( test_unordered_remote_socket_forward, test_remote_socket_forward_impl ); async fn test_local_socket_forward_impl(conn0: Connection, mut conn1: Connection) { let path = Path::new("/tmp/openssh-local-forward.socket").into(); eprintln!("Creating remote process"); let cmd = format!("socat -u OPEN:/data UNIX-LISTEN:{:#?} >/dev/stderr", path); let (established_session, stdios) = create_remote_process(conn0, &cmd).await; sleep(Duration::from_secs(1)).await; eprintln!("Requesting port forward"); conn1 .request_port_forward( ForwardType::Local, &Socket::TcpSocket { port: 1235, host: "127.0.0.1".into(), }, &Socket::UnixSocket { path }, ) .await .unwrap(); eprintln!("Connecting to forwarded socket"); let mut output = TcpStream::connect(("127.0.0.1", 1235)).await.unwrap(); eprintln!("Reading"); const DATA: &[u8] = "0\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n".as_bytes(); let mut buffer = [0_u8; DATA.len()]; output.read_exact(&mut buffer).await.unwrap(); assert_eq!(DATA, buffer); drop(output); drop(stdios); eprintln!("Waiting for session to end"); let session_status = established_session.wait().await.unwrap(); assert_matches!( session_status, SessionStatus::Exited { exit_value, .. } if exit_value.unwrap() == 0 ); } run_test2!( test_unordered_local_socket_forward, test_local_socket_forward_impl ); async fn test_request_stop_listening_impl(mut conn: Connection) { conn.request_stop_listening().await.unwrap(); eprintln!("Verify that existing connection is still usable."); test_open_new_session_impl(conn).await; eprintln!( "Verify that after the last connection is dropped, the multiplex server \ indeed shutdown." ); assert_matches!(Connection::connect(PATH).await, Err(_)); } run_test!( test_request_stop_listening, test_request_stop_listening_impl ); } openssh-mux-client-0.17.3/src/constants.rs000064400000000000000000000020571046102023000166330ustar 00000000000000macro_rules! def_constants { ( $name:ident, $val:literal ) => { pub const $name: u32 = $val; }; } def_constants!(SSHMUX_VER, 4); def_constants!(MUX_MSG_HELLO, 0x00000001); def_constants!(MUX_C_NEW_SESSION, 0x10000002); def_constants!(MUX_C_ALIVE_CHECK, 0x10000004); def_constants!(MUX_C_OPEN_FWD, 0x10000006); def_constants!(MUX_C_STOP_LISTENING, 0x10000009); def_constants!(MUX_S_OK, 0x80000001); def_constants!(MUX_S_PERMISSION_DENIED, 0x80000002); def_constants!(MUX_S_FAILURE, 0x80000003); def_constants!(MUX_S_EXIT_MESSAGE, 0x80000004); def_constants!(MUX_S_ALIVE, 0x80000005); def_constants!(MUX_S_SESSION_OPENED, 0x80000006); def_constants!(MUX_S_REMOTE_PORT, 0x80000007); def_constants!(MUX_S_TTY_ALLOC_FAIL, 0x80000008); // MUX_C_CLOSE_FWD is not yet supported by openssh // MUX_C_NEW_STDIO_FWD is not supported by this crate //def_constants!(MUX_C_CLOSE_FWD, 0x10000007); //def_constants!(MUX_C_NEW_STDIO_FWD, 0x10000008); def_constants!(MUX_FWD_LOCAL, 1); def_constants!(MUX_FWD_REMOTE, 2); def_constants!(MUX_FWD_DYNAMIC, 3); openssh-mux-client-0.17.3/src/default_config.rs000064400000000000000000000010631046102023000175640ustar 00000000000000use std::{env, os::unix::ffi::OsStringExt}; use once_cell::sync::OnceCell; use crate::{NonZeroByteSlice, NonZeroByteVec}; /// Return environment variable `$TERM` if set. /// Otherwise, returns empty string. pub fn get_term() -> &'static NonZeroByteSlice { static TERM: OnceCell> = OnceCell::new(); TERM.get_or_init(|| { env::var_os("TERM") .map(OsStringExt::into_vec) .map(NonZeroByteVec::from_bytes_remove_nul) }) .as_deref() .unwrap_or_else(|| NonZeroByteSlice::new(&[]).unwrap()) } openssh-mux-client-0.17.3/src/lib.rs000064400000000000000000000016471046102023000153710ustar 00000000000000#[cfg(not(unix))] compile_error!("This crate can only be used on unix"); pub use non_zero_byte_slice::*; pub use error::Error; pub use openssh_mux_client_error as error; pub type Result = std::result::Result; trait ErrorExt { fn invalid_server_response(package_type: &'static &'static str, response: &Response) -> Self; } impl ErrorExt for Error { fn invalid_server_response(package_type: &'static &'static str, response: &Response) -> Self { Error::InvalidServerResponse(package_type, format!("{:#?}", response).into_boxed_str()) } } pub mod default_config; mod connection; pub use connection::*; mod constants; mod request; pub use request::{Session, Socket}; mod response; pub use response::Response; mod session; pub use session::*; mod shutdown_mux_master; pub use shutdown_mux_master::shutdown_mux_master; mod utils; #[cfg(test)] #[macro_use] extern crate assert_matches; openssh-mux-client-0.17.3/src/request.rs000064400000000000000000000167441046102023000163170ustar 00000000000000#![forbid(unsafe_code)] use super::{constants, default_config, utils::MaybeOwned, NonZeroByteSlice, NonZeroByteVec}; use std::{borrow::Cow, path::Path}; use cfg_if::cfg_if; use serde::{Serialize, Serializer}; use typed_builder::TypedBuilder; #[derive(Copy, Clone, Debug)] pub(crate) enum Request { /// Response with `Response::Hello`. Hello { version: u32 }, /// Server replied with `Response::Alive`. AliveCheck { request_id: u32 }, /// For opening a new multiplexed session in passenger mode, /// send this variant and then sends stdin, stdout and stderr fd. /// /// If successful, the server will reply with `Response::SessionOpened`. /// /// Otherwise it will reply with an error: /// - `Response::PermissionDenied`; /// - `Response::Failure`. /// /// The client now waits for the session to end. When it does, the server /// will send `Response::ExitMessage`. /// /// Two additional cases that the client must cope with are it receiving /// a signal itself and the server disconnecting without sending an exit message. /// /// A master may also send a `Response::TtyAllocFail` before /// `Response::ExitMessage` if remote TTY allocation was unsuccessful. /// /// The client may use this to return its local tty to "cooked" mode. NewSession { request_id: u32, session: SessionZeroCopy, }, /// A server may reply with `Response::Ok`, `Response::RemotePort`, /// `Response::PermissionDenied`, or `Response::Failure`. /// /// For dynamically allocated listen port the server replies with /// `Request::RemotePort`. OpenFwd { request_id: u32, fwd_mode: u32 }, /// A client may request the master to stop accepting new multiplexing requests /// and remove its listener socket. /// /// A server may reply with `Response::Ok`, `Response::PermissionDenied` or /// `Response::Failure`. StopListening { request_id: u32 }, } impl Serialize for Request { fn serialize(&self, serializer: S) -> Result { use constants::*; use Request::*; match self { Hello { version } => { serializer.serialize_newtype_variant("Request", MUX_MSG_HELLO, "Hello", version) } AliveCheck { request_id } => serializer.serialize_newtype_variant( "Request", MUX_C_ALIVE_CHECK, "AliveCheck", request_id, ), NewSession { request_id, session, } => serializer.serialize_newtype_variant( "Request", MUX_C_NEW_SESSION, "NewSession", &(*request_id, "", *session), ), OpenFwd { request_id, fwd_mode, } => serializer.serialize_newtype_variant( "Request", MUX_C_OPEN_FWD, "OpenFwd", &(*request_id, fwd_mode), ), StopListening { request_id } => serializer.serialize_newtype_variant( "Request", MUX_C_STOP_LISTENING, "StopListening", request_id, ), } } } /// Zero copy version of [`Session`] #[derive(Copy, Clone, Debug, Serialize)] pub(crate) struct SessionZeroCopy { pub tty: bool, pub x11_forwarding: bool, pub agent: bool, pub subsystem: bool, pub escape_ch: char, } #[derive(Clone, Debug, Eq, PartialEq, Hash, Serialize, TypedBuilder)] #[builder(doc)] pub struct Session<'a> { #[builder(default = false)] pub tty: bool, #[builder(default = false)] pub x11_forwarding: bool, #[builder(default = false)] pub agent: bool, #[builder(default = false)] pub subsystem: bool, /// Set to `0xffffffff`(`char::MAX`) to disable escape character #[builder(default = char::MAX)] pub escape_ch: char, /// Generally set to `$TERM`. #[builder(default_code = r#"Cow::Borrowed(default_config::get_term())"#)] pub term: Cow<'a, NonZeroByteSlice>, pub cmd: Cow<'a, NonZeroByteSlice>, } #[derive(Copy, Clone, Debug)] pub enum Fwd<'a> { Local { listen_socket: &'a Socket<'a>, connect_socket: &'a Socket<'a>, }, Remote { listen_socket: &'a Socket<'a>, connect_socket: &'a Socket<'a>, }, Dynamic { listen_socket: &'a Socket<'a>, }, } impl<'a> Fwd<'a> { pub(crate) fn as_serializable(&self) -> (u32, &'a Socket<'a>, MaybeOwned<'a, Socket<'a>>) { use Fwd::*; match *self { Local { listen_socket, connect_socket, } => ( constants::MUX_FWD_LOCAL, listen_socket, MaybeOwned::Borrowed(connect_socket), ), Remote { listen_socket, connect_socket, } => ( constants::MUX_FWD_REMOTE, listen_socket, MaybeOwned::Borrowed(connect_socket), ), Dynamic { listen_socket } => ( constants::MUX_FWD_DYNAMIC, listen_socket, MaybeOwned::Owned(Socket::UnixSocket { path: Path::new("").into(), }), ), } } } impl<'a> Serialize for Fwd<'a> { fn serialize(&self, serializer: S) -> Result { self.as_serializable().serialize(serializer) } } trait PathExt { fn to_non_null_bytes(&self) -> Cow<'_, NonZeroByteSlice>; fn to_bytes(&self) -> Cow<'_, [u8]>; fn to_string_lossy_and_as_bytes(&self) -> Cow<'_, [u8]>; } impl PathExt for Path { fn to_non_null_bytes(&self) -> Cow<'_, NonZeroByteSlice> { match self.to_bytes() { Cow::Borrowed(slice) => NonZeroByteVec::from_bytes_slice_lossy(slice), Cow::Owned(bytes) => Cow::Owned(NonZeroByteVec::from_bytes_remove_nul(bytes)), } } fn to_bytes(&self) -> Cow<'_, [u8]> { cfg_if! { if #[cfg(unix)] { use std::os::unix::ffi::OsStrExt; Cow::Borrowed(self.as_os_str().as_bytes()) } else { self.to_string_lossy_and_as_bytes() } } } fn to_string_lossy_and_as_bytes(&self) -> Cow<'_, [u8]> { match self.to_string_lossy() { Cow::Borrowed(s) => Cow::Borrowed(s.as_bytes()), Cow::Owned(s) => Cow::Owned(s.into_bytes()), } } } #[derive(Clone, Debug, Eq, PartialEq, Hash)] pub enum Socket<'a> { UnixSocket { path: Cow<'a, Path> }, TcpSocket { port: u32, host: Cow<'a, str> }, } impl Socket<'_> { pub(crate) fn as_serializable(&self) -> (Cow<'_, NonZeroByteSlice>, u32) { use Socket::*; let unix_socket_port: i32 = -2; match self { // Serialize impl for Path calls to_str and ret err if failed, // so calling to_string_lossy is OK as it does not break backward // compatibility. UnixSocket { path } => (path.to_non_null_bytes(), unix_socket_port as u32), TcpSocket { port, host } => ( NonZeroByteVec::from_bytes_slice_lossy(host.as_bytes()), *port, ), } } } impl<'a> Serialize for Socket<'a> { fn serialize(&self, serializer: S) -> Result { self.as_serializable().serialize(serializer) } } openssh-mux-client-0.17.3/src/response.rs000064400000000000000000000077041046102023000164610ustar 00000000000000#![forbid(unsafe_code)] use serde::{ de::{Deserializer, EnumAccess, Error, VariantAccess, Visitor}, Deserialize, }; use std::{fmt, marker::PhantomData}; use super::constants; /// **WARNING: Response can only be used with ssh_mux_format, which treats /// tuple and struct as the same.** #[derive(Clone, Debug)] pub enum Response { Hello { version: u32 }, Alive { response_id: u32, server_pid: u32 }, Ok { response_id: u32 }, Failure { response_id: u32, reason: Box }, PermissionDenied { response_id: u32, reason: Box }, SessionOpened { response_id: u32, session_id: u32 }, ExitMessage { session_id: u32, exit_value: u32 }, TtyAllocFail { session_id: u32 }, RemotePort { response_id: u32, remote_port: u32 }, } impl<'de> Deserialize<'de> for Response { fn deserialize>(deserializer: D) -> Result { deserializer.deserialize_enum( "Response", &[ "Hello", "Alive", "Ok", "Failure", "PermissionDenied", "SessionOpened", "ExitMessage", "TtyAllocFail", "RemotePort", ], ResponseVisitor, ) } } struct ResponseVisitor; impl<'de> Visitor<'de> for ResponseVisitor { type Value = Response; fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { write!(formatter, "expecting Response") } fn visit_enum(self, data: A) -> Result where A: EnumAccess<'de>, { use constants::*; let result: (u32, _) = data.variant()?; let (index, accessor) = result; match index { MUX_MSG_HELLO => { let version: u32 = accessor.newtype_variant_seed(PhantomData)?; Ok(Response::Hello { version }) } MUX_S_ALIVE => { let tup: (u32, u32) = accessor.newtype_variant_seed(PhantomData)?; Ok(Response::Alive { response_id: tup.0, server_pid: tup.1, }) } MUX_S_OK => { let response_id: u32 = accessor.newtype_variant_seed(PhantomData)?; Ok(Response::Ok { response_id }) } MUX_S_FAILURE => { let tup: (u32, Box) = accessor.newtype_variant_seed(PhantomData)?; Ok(Response::Failure { response_id: tup.0, reason: tup.1, }) } MUX_S_PERMISSION_DENIED => { let tup: (u32, Box) = accessor.newtype_variant_seed(PhantomData)?; Ok(Response::PermissionDenied { response_id: tup.0, reason: tup.1, }) } MUX_S_SESSION_OPENED => { let tup: (u32, u32) = accessor.newtype_variant_seed(PhantomData)?; Ok(Response::SessionOpened { response_id: tup.0, session_id: tup.1, }) } MUX_S_EXIT_MESSAGE => { let tup: (u32, u32) = accessor.newtype_variant_seed(PhantomData)?; Ok(Response::ExitMessage { session_id: tup.0, exit_value: tup.1, }) } MUX_S_TTY_ALLOC_FAIL => { let session_id: u32 = accessor.newtype_variant_seed(PhantomData)?; Ok(Response::TtyAllocFail { session_id }) } MUX_S_REMOTE_PORT => { let tup: (u32, u32) = accessor.newtype_variant_seed(PhantomData)?; Ok(Response::RemotePort { response_id: tup.0, remote_port: tup.1, }) } _ => Err(A::Error::custom("Unexpected packet type")), } } } openssh-mux-client-0.17.3/src/session.rs000064400000000000000000000057771046102023000163160ustar 00000000000000#![forbid(unsafe_code)] use super::{Connection, Error, ErrorExt, Response, Result}; use std::io::ErrorKind; enum EstablishedSessionState { Exited(Option), TtyAllocFail, } /// NOTE that once `EstablishedSession` is dropped, any data written to /// `stdin` will not be send to the remote process and /// `stdout` and `stderr` would eof immediately. /// /// # Cancel safety /// /// All methods of this struct is not cancellation safe. #[derive(Debug)] pub struct EstablishedSession { pub(super) conn: Connection, pub(super) session_id: u32, } impl EstablishedSession { fn check_session_id(&self, session_id: u32) -> Result<()> { if self.session_id != session_id { Err(Error::UnmatchedSessionId) } else { Ok(()) } } /// Return None if TtyAllocFail, Some(...) if the process exited. async fn wait_impl(&mut self) -> Result { use Response::*; let response = match self.conn.read_response().await { Result::Ok(response) => response, Err(err) => match &err { Error::IOError(io_err) if io_err.kind() == ErrorKind::UnexpectedEof => { return Result::Ok(EstablishedSessionState::Exited(None)) } _ => return Err(err), }, }; match response { TtyAllocFail { session_id } => { self.check_session_id(session_id)?; Result::Ok(EstablishedSessionState::TtyAllocFail) } ExitMessage { session_id, exit_value, } => { self.check_session_id(session_id)?; Result::Ok(EstablishedSessionState::Exited(Some(exit_value))) } response => Err(Error::invalid_server_response( &"TtyAllocFail or ExitMessage", &response, )), } } /// Wait for session status to change /// /// Return `Self` on error so that you can handle the error and restart /// the operation. /// /// If the server close the connection without sending anything, /// this function would return `Ok(None)`. pub async fn wait(mut self) -> Result { use EstablishedSessionState::*; match self.wait_impl().await { Ok(Exited(exit_value)) => Ok(SessionStatus::Exited { exit_value }), Ok(TtyAllocFail) => Ok(SessionStatus::TtyAllocFail(self)), Err(err) => Err((err, self)), } } } #[derive(Debug)] pub enum SessionStatus { /// Remote ssh server failed to allocate a tty, you can now return the tty /// to cooked mode. /// /// This arm includes `EstablishedSession` so that you can call `wait` on it /// again and retrieve the exit status and the underlying connection. TtyAllocFail(EstablishedSession), /// The process on the remote machine has exited with `exit_value`. Exited { exit_value: Option }, } openssh-mux-client-0.17.3/src/shutdown_mux_master.rs000064400000000000000000000101401046102023000207260ustar 00000000000000#![forbid(unsafe_code)] use crate::{constants, request::Request, Error, ErrorExt, Response, Result}; use std::{io::Read, io::Write, os::unix::net::UnixStream, path::Path}; use serde::{Deserialize, Serialize}; use ssh_format::{from_bytes, Serializer}; struct Connection { raw_conn: UnixStream, serializer: Serializer, } impl Connection { fn write(&mut self, value: &Request) -> Result<()> { let serializer = &mut self.serializer; serializer.reset_counter(); // Reserve the header serializer.output.resize(4, 0); value.serialize(&mut *serializer)?; let header = serializer.create_header(0)?; // Write the header serializer.output[..4].copy_from_slice(&header); self.raw_conn.write_all(&serializer.output)?; Ok(()) } fn read_and_deserialize<'a, T>(&'a mut self, size: usize) -> Result where T: Deserialize<'a>, { let buffer = &mut self.serializer.output; buffer.resize(size, 0); self.raw_conn.read_exact(buffer)?; // Ignore any trailing bytes to be forward compatible Ok(from_bytes(buffer)?.0) } /// Return size of the response. fn read_header(&mut self) -> Result { self.read_and_deserialize(4) } fn read_response(&mut self) -> Result { let len = self.read_header()?; self.read_and_deserialize(len as usize) } fn check_response_id(request_id: u32, response_id: u32) -> Result<()> { if request_id != response_id { Err(Error::UnmatchedRequestId) } else { Ok(()) } } fn exchange_hello(mut self) -> Result { self.write(&Request::Hello { version: constants::SSHMUX_VER, })?; let response = self.read_response()?; if let Response::Hello { version } = response { if version != constants::SSHMUX_VER { Err(Error::UnsupportedMuxProtocol) } else { Ok(self) } } else { Err(Error::invalid_server_response(&"Hello message", &response)) } } fn connect>(path: P) -> Result { Self::new(UnixStream::connect(path)?).exchange_hello() } fn new(raw_conn: UnixStream) -> Self { Self { raw_conn, serializer: Serializer::new(Vec::with_capacity(20)), } } /// Request the master to stop accepting new multiplexing requests /// and remove its listener socket. fn request_stop_listening(&mut self) -> Result<()> { use Response::*; let request_id = 0; self.write(&Request::StopListening { request_id })?; match self.read_response()? { Ok { response_id } => { Self::check_response_id(request_id, response_id)?; Result::Ok(()) } PermissionDenied { response_id, reason, } => { Self::check_response_id(request_id, response_id)?; Err(Error::PermissionDenied(reason)) } Failure { response_id, reason, } => { Self::check_response_id(request_id, response_id)?; Err(Error::RequestFailure(reason)) } response => Err(Error::invalid_server_response( &"Ok, PermissionDenied or Failure", &response, )), } } } /// Request the master to stop accepting new multiplexing requests /// and remove its listener socket. /// /// **Only suitable to use in `Drop::drop`.** pub fn shutdown_mux_master>(path: P) -> Result<()> { Connection::connect(path)?.request_stop_listening() } pub(crate) fn shutdown_mux_master_from(raw_conn: UnixStream) -> Result<()> { Connection::new(raw_conn).request_stop_listening() } #[cfg(test)] mod tests { use super::shutdown_mux_master; #[test] fn test_sync_request_stop_listening() { shutdown_mux_master("/tmp/openssh-mux-client-test.socket").unwrap(); } } openssh-mux-client-0.17.3/src/utils.rs000064400000000000000000000022731046102023000157570ustar 00000000000000use std::convert::TryInto; use serde::{Serialize, Serializer}; use super::{NonZeroByteSlice, Result}; /// Serialize one `u32` as ssh_format. pub(crate) fn serialize_u32(int: u32) -> [u8; 4] { int.to_be_bytes() } pub(crate) enum MaybeOwned<'a, T> { Owned(T), Borrowed(&'a T), } impl MaybeOwned<'_, T> { pub(crate) fn as_ref(&self) -> &T { use MaybeOwned::*; match self { Owned(val) => val, Borrowed(reference) => reference, } } } impl Serialize for MaybeOwned<'_, T> { fn serialize(&self, serializer: S) -> Result { self.as_ref().serialize(serializer) } } pub(crate) trait SliceExt { fn get_len_as_u32(&self) -> Result; } impl SliceExt for [T] { fn get_len_as_u32(&self) -> Result { self.len() .try_into() .map_err(|_| ssh_format::Error::TooLong.into()) } } impl SliceExt for str { fn get_len_as_u32(&self) -> Result { self.as_bytes().get_len_as_u32() } } impl SliceExt for NonZeroByteSlice { fn get_len_as_u32(&self) -> Result { self.into_inner().get_len_as_u32() } }