Skip to content

Commit cd8f174

Browse files
authored
Merge pull request #66 from openssh-rust/feature/support-subsystem
Feature/support subsystem
2 parents 4978817 + 1687f5d commit cd8f174

File tree

8 files changed

+126
-3
lines changed

8 files changed

+126
-3
lines changed

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ openssh-mux-client = { version = "0.15.0", optional = true }
5555
lazy_static = "1.4.0"
5656
regex = "1"
5757
tokio = { version = "1", features = [ "full" ] }
58+
openssh-sftp-client = "0.10.0"
5859

5960
[[example]]
6061
name = "native-mux_tsp"

src/changelog.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22
use crate::*;
33

44
/// TODO: RENAME THIS INTO THE NEXT VERSION BEFORE RELEASE
5+
///
6+
/// ## Added
7+
/// - [`Session::subsystem`]
58
#[doc(hidden)]
69
pub mod unreleased {}
710

src/lib.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,10 @@
111111
//! [`raw_args`](Command::raw_args), and [`raw_command`](Session::raw_command) to bypass the
112112
//! escaping that `openssh` normally does for you.
113113
//!
114+
//! # Sftp subsystem
115+
//!
116+
//! For sftp and other ssh subsystem, check [`Session::subsystem`] for more information.
117+
//!
114118
//! # Examples
115119
//!
116120
//! ```rust,no_run

src/native_mux_impl/command.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,17 +13,19 @@ use openssh_mux_client::{Connection, NonZeroByteSlice, Session};
1313
pub(crate) struct Command {
1414
cmd: Vec<u8>,
1515
ctl: Box<Path>,
16+
subsystem: bool,
1617

1718
stdin_v: Stdio,
1819
stdout_v: Stdio,
1920
stderr_v: Stdio,
2021
}
2122

2223
impl Command {
23-
pub(crate) fn new(ctl: Box<Path>, cmd: Vec<u8>) -> Self {
24+
pub(crate) fn new(ctl: Box<Path>, cmd: Vec<u8>, subsystem: bool) -> Self {
2425
Self {
2526
cmd,
2627
ctl,
28+
subsystem,
2729

2830
stdin_v: Stdio::inherit(),
2931
stdout_v: Stdio::inherit(),
@@ -71,7 +73,10 @@ impl Command {
7173

7274
let cmd = NonZeroByteSlice::new(&self.cmd).ok_or(Error::InvalidCommand)?;
7375

74-
let session = Session::builder().cmd(Cow::Borrowed(cmd)).build();
76+
let session = Session::builder()
77+
.cmd(Cow::Borrowed(cmd))
78+
.subsystem(self.subsystem)
79+
.build();
7580

7681
let established_session = Connection::connect(&self.ctl)
7782
.await?

src/native_mux_impl/session.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,11 @@ impl Session {
3838
}
3939

4040
pub(crate) fn raw_command<S: AsRef<OsStr>>(&self, program: S) -> Command {
41-
Command::new(self.ctl.clone(), program.as_ref().as_bytes().into())
41+
Command::new(self.ctl.clone(), program.as_ref().as_bytes().into(), false)
42+
}
43+
44+
pub(crate) fn subsystem<S: AsRef<OsStr>>(&self, program: S) -> Command {
45+
Command::new(self.ctl.clone(), program.as_ref().as_bytes().into(), true)
4246
}
4347

4448
pub(crate) async fn request_port_forward(

src/process_impl/session.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,18 @@ impl Session {
8181
Command::new(cmd)
8282
}
8383

84+
pub(crate) fn subsystem<S: AsRef<OsStr>>(&self, program: S) -> Command {
85+
// XXX: Should we do a self.check() here first?
86+
87+
// NOTE: we pass -p 9 nine here (the "discard" port) to ensure that ssh does not
88+
// succeed in establishing a _new_ connection if the master connection has failed.
89+
90+
let mut cmd = self.new_cmd(&["-T", "-p", "9", "-s"]);
91+
cmd.arg("--").arg(program);
92+
93+
Command::new(cmd)
94+
}
95+
8496
pub(crate) async fn request_port_forward(
8597
&self,
8698
forward_type: impl Into<ForwardType>,

src/session.rs

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,63 @@ impl Session {
166166
)
167167
}
168168

169+
/// Constructs a new [`Command`] for launching subsystem `program` on the remote
170+
/// host.
171+
///
172+
/// Unlike [`command`](Session::command), this method does not shell-escape `program`, so it may be evaluated in
173+
/// unforeseen ways by the remote shell.
174+
///
175+
/// The returned `Command` is a builder, with the following default configuration:
176+
///
177+
/// * No arguments to the program
178+
/// * Empty stdin and dsicard stdout/stderr for `spawn` or `status`, but create output pipes for
179+
/// `output`
180+
///
181+
/// Builder methods are provided to change these defaults and otherwise configure the process.
182+
///
183+
/// ## Sftp subsystem
184+
///
185+
/// To use the sftp subsystem, you'll want to use [`openssh-sftp-client`],
186+
/// then use the following code to construct a sftp instance:
187+
///
188+
/// [`openssh-sftp-client`]: https://crates.io/crates/openssh-sftp-client
189+
///
190+
/// ```rust,no_run
191+
/// # use std::error::Error;
192+
/// # #[cfg(feature = "native-mux")]
193+
/// # #[tokio::main]
194+
/// # async fn main() -> Result<(), Box<dyn Error>> {
195+
///
196+
/// use openssh::{Session, KnownHosts, Stdio};
197+
/// use openssh_sftp_client::highlevel::Sftp;
198+
///
199+
/// let session = Session::connect_mux("me@ssh.example.com", KnownHosts::Strict).await?;
200+
///
201+
/// let mut child = session
202+
/// .subsystem("sftp")
203+
/// .stdin(Stdio::piped())
204+
/// .stdout(Stdio::piped())
205+
/// .spawn()
206+
/// .await?;
207+
///
208+
/// Sftp::new(
209+
/// child.stdin().take().unwrap(),
210+
/// child.stdout().take().unwrap(),
211+
/// Default::default(),
212+
/// )
213+
/// .await?
214+
/// .close()
215+
/// .await?;
216+
///
217+
/// # Ok(()) }
218+
/// ```
219+
pub fn subsystem<S: AsRef<OsStr>>(&self, program: S) -> Command<'_> {
220+
Command::new(
221+
self,
222+
delegate!(&self.0, imp, { imp.subsystem(program).into() }),
223+
)
224+
}
225+
169226
/// Constructs a new [`Command`] that runs the provided shell command on the remote host.
170227
///
171228
/// The provided command is passed as a single, escaped argument to `sh -c`, and from that

tests/openssh.rs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -828,3 +828,40 @@ async fn local_socket_forward() {
828828
assert!(output.status.success());
829829
}
830830
}
831+
832+
#[tokio::test]
833+
#[cfg_attr(not(ci), ignore)]
834+
async fn test_sftp_subsystem() {
835+
use openssh_sftp_client::highlevel::Sftp;
836+
837+
let content = b"This is a test case for the openssh-rust/openssh crate.\n";
838+
839+
for session in connects().await {
840+
let mut child = session
841+
.subsystem("sftp")
842+
.stdin(Stdio::piped())
843+
.stdout(Stdio::piped())
844+
.spawn()
845+
.await
846+
.unwrap();
847+
848+
let sftp = Sftp::new(
849+
child.stdin().take().unwrap(),
850+
child.stdout().take().unwrap(),
851+
Default::default(),
852+
)
853+
.await
854+
.unwrap();
855+
856+
let file_path = "/tmp/openssh-rust-test-sftp-subsystem";
857+
858+
{
859+
let mut fs = sftp.fs();
860+
861+
fs.write(file_path, content).await.unwrap();
862+
assert_eq!(&*sftp.fs().read(file_path).await.unwrap(), content);
863+
}
864+
865+
sftp.close().await.unwrap();
866+
}
867+
}

0 commit comments

Comments
 (0)