-
Notifications
You must be signed in to change notification settings - Fork 38
Allow conversion of std::process::Command
into a openssh::Command
given a openssh::Session
.
#112
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
e314422
d9ba8d3
dd64633
9743a5e
0a69ed1
974cd52
cca22b1
66cecba
511c62c
d14a4c2
32a8960
08b77a5
5d9a112
7d84871
c4baafd
7e84beb
7fcfaf2
13d865a
c4ded38
67d0885
6970753
8207907
10687fb
020366a
6842e5a
e09d2fd
f30d34e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,5 @@ | ||
use crate::escape::escape; | ||
|
||
use super::stdio::TryFromChildIo; | ||
use super::RemoteChild; | ||
use super::Stdio; | ||
|
@@ -49,6 +51,130 @@ macro_rules! delegate { | |
}}; | ||
} | ||
|
||
/// If a command is `OverSsh` then it can be executed over an SSH session. | ||
/// | ||
/// Primarily a way to allow `std::process::Command` to be turned directly into an `openssh::Command`. | ||
pub trait OverSsh { | ||
/// Given an ssh session, return a command that can be executed over that ssh session. | ||
/// | ||
/// ### Notes | ||
/// | ||
/// The command to be executed on the remote machine should not explicitly | ||
/// set environment variables or the current working directory. It errors if the source command | ||
/// has environment variables or a current working directory set, since `openssh` doesn't (yet) have | ||
/// a method to set environment variables and `ssh` doesn't support setting a current working directory | ||
/// outside of `bash/dash/zsh` (which is not always available). | ||
/// | ||
/// ### Examples | ||
/// | ||
/// 1. Consider the implementation of `OverSsh` for `std::process::Command`. Let's build a | ||
/// `ls -l -a -h` command and execute it over an SSH session. | ||
/// | ||
/// ```no_run | ||
/// # #[tokio::main(flavor = "current_thread")] | ||
/// async fn main() -> Result<(), Box<dyn std::error::Error>> { | ||
/// use std::process::Command; | ||
/// use openssh::{Session, KnownHosts, OverSsh}; | ||
/// | ||
/// let session = Session::connect_mux("[email protected]", KnownHosts::Strict).await?; | ||
/// let ls = | ||
/// Command::new("ls") | ||
/// .arg("-l") | ||
/// .arg("-a") | ||
/// .arg("-h") | ||
/// .over_ssh(&session)? | ||
/// .output() | ||
/// .await?; | ||
/// | ||
/// assert!(String::from_utf8(ls.stdout).unwrap().contains("total")); | ||
/// # Ok(()) | ||
/// } | ||
/// | ||
/// ``` | ||
/// 2. Building a command with environment variables or a current working directory set will | ||
/// results in an error. | ||
/// | ||
/// ```no_run | ||
/// # #[tokio::main(flavor = "current_thread")] | ||
/// async fn main() -> Result<(), Box<dyn std::error::Error>> { | ||
/// use std::process::Command; | ||
/// use openssh::{Session, KnownHosts, OverSsh}; | ||
/// | ||
/// let session = Session::connect_mux("[email protected]", KnownHosts::Strict).await?; | ||
/// let echo = | ||
/// Command::new("echo") | ||
/// .env("MY_ENV_VAR", "foo") | ||
/// .arg("$MY_ENV_VAR") | ||
/// .over_ssh(&session); | ||
/// assert!(matches!(echo, Err(openssh::Error::CommandHasEnv))); | ||
/// | ||
/// # Ok(()) | ||
/// } | ||
/// | ||
/// ``` | ||
fn over_ssh<'session>( | ||
&self, | ||
session: &'session Session, | ||
) -> Result<crate::Command<'session>, crate::Error>; | ||
} | ||
|
||
NobodyXu marked this conversation as resolved.
Show resolved
Hide resolved
|
||
impl OverSsh for std::process::Command { | ||
NobodyXu marked this conversation as resolved.
Show resolved
Hide resolved
|
||
fn over_ssh<'session>( | ||
&self, | ||
session: &'session Session, | ||
) -> Result<Command<'session>, crate::Error> { | ||
// I'd really like `!self.get_envs().is_empty()` here, but that's | ||
// behind a `exact_size_is_empty` feature flag. | ||
if self.get_envs().len() > 0 { | ||
return Err(crate::Error::CommandHasEnv); | ||
} | ||
|
||
if self.get_current_dir().is_some() { | ||
return Err(crate::Error::CommandHasCwd); | ||
} | ||
|
||
let program_escaped: Cow<'_, OsStr> = escape(self.get_program()); | ||
let mut command = session.raw_command(program_escaped); | ||
|
||
let args = self.get_args().map(escape); | ||
command.raw_args(args); | ||
jonhoo marked this conversation as resolved.
Show resolved
Hide resolved
|
||
Ok(command) | ||
} | ||
} | ||
|
||
impl OverSsh for tokio::process::Command { | ||
fn over_ssh<'session>( | ||
&self, | ||
session: &'session Session, | ||
) -> Result<Command<'session>, crate::Error> { | ||
self.as_std().over_ssh(session) | ||
} | ||
} | ||
|
||
impl<S> OverSsh for &S | ||
where | ||
S: OverSsh, | ||
{ | ||
fn over_ssh<'session>( | ||
&self, | ||
session: &'session Session, | ||
) -> Result<Command<'session>, crate::Error> { | ||
<S as OverSsh>::over_ssh(self, session) | ||
} | ||
} | ||
|
||
impl<S> OverSsh for &mut S | ||
where | ||
S: OverSsh, | ||
{ | ||
fn over_ssh<'session>( | ||
&self, | ||
session: &'session Session, | ||
) -> Result<Command<'session>, crate::Error> { | ||
<S as OverSsh>::over_ssh(self, session) | ||
} | ||
} | ||
|
||
/// A remote process builder, providing fine-grained control over how a new remote process should | ||
/// be spawned. | ||
/// | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,91 @@ | ||
//! Escape characters that may have special meaning in a shell, including spaces. | ||
//! This is a modified version of the [`shell-escape::unix`] module of [`shell-escape`] crate. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we submit a PR to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Great suggestion! I've initiated a PR to |
||
//! | ||
//! [`shell-escape`]: https://crates.io/crates/shell-escape | ||
//! [`shell-escape::unix`]: https://docs.rs/shell-escape/latest/src/shell_escape/lib.rs.html#101 | ||
|
||
use std::{ | ||
borrow::Cow, | ||
ffi::{OsStr, OsString}, | ||
os::unix::ffi::OsStrExt, | ||
os::unix::ffi::OsStringExt, | ||
}; | ||
|
||
fn allowed(byte: u8) -> bool { | ||
matches!(byte, b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9' | b'-' | b'_' | b'=' | b'/' | b',' | b'.' | b'+') | ||
} | ||
|
||
/// Escape characters that may have special meaning in a shell, including spaces. | ||
/// | ||
/// **Note**: This function is an adaptation of [`shell-escape::unix::escape`]. | ||
/// This function exists only for type compatibility and the implementation is | ||
/// almost exactly the same as [`shell-escape::unix::escape`]. | ||
/// | ||
/// [`shell-escape::unix::escape`]: https://docs.rs/shell-escape/latest/src/shell_escape/lib.rs.html#101 | ||
/// | ||
pub(crate) fn escape(s: &OsStr) -> Cow<'_, OsStr> { | ||
let as_bytes = s.as_bytes(); | ||
let all_allowed = as_bytes.iter().copied().all(allowed); | ||
|
||
if !as_bytes.is_empty() && all_allowed { | ||
return Cow::Borrowed(s); | ||
} | ||
|
||
let mut escaped = Vec::with_capacity(as_bytes.len() + 2); | ||
escaped.push(b'\''); | ||
|
||
for &b in as_bytes { | ||
match b { | ||
b'\'' | b'!' => { | ||
escaped.reserve(4); | ||
escaped.push(b'\''); | ||
NobodyXu marked this conversation as resolved.
Show resolved
Hide resolved
aalekhpatel07 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
escaped.push(b'\\'); | ||
escaped.push(b); | ||
escaped.push(b'\''); | ||
} | ||
_ => escaped.push(b), | ||
} | ||
} | ||
escaped.push(b'\''); | ||
OsString::from_vec(escaped).into() | ||
} | ||
|
||
#[cfg(test)] | ||
mod tests { | ||
use super::*; | ||
|
||
fn test_escape_case(input: &str, expected: &str) { | ||
NobodyXu marked this conversation as resolved.
Show resolved
Hide resolved
|
||
test_escape_from_bytes(input.as_bytes(), expected.as_bytes()) | ||
} | ||
|
||
fn test_escape_from_bytes(input: &[u8], expected: &[u8]) { | ||
let input_os_str = OsStr::from_bytes(input); | ||
let observed_os_str = escape(input_os_str); | ||
let expected_os_str = OsStr::from_bytes(expected); | ||
assert_eq!(observed_os_str, expected_os_str); | ||
} | ||
|
||
// These tests are courtesy of the `shell-escape` crate. | ||
#[test] | ||
fn test_escape() { | ||
test_escape_case( | ||
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_=/,.+", | ||
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_=/,.+", | ||
); | ||
test_escape_case("--aaa=bbb-ccc", "--aaa=bbb-ccc"); | ||
test_escape_case( | ||
"linker=gcc -L/foo -Wl,bar", | ||
r#"'linker=gcc -L/foo -Wl,bar'"#, | ||
); | ||
test_escape_case(r#"--features="default""#, r#"'--features="default"'"#); | ||
test_escape_case(r#"'!\$`\\\n "#, r#"''\'''\!'\$`\\\n '"#); | ||
test_escape_case("", r#"''"#); | ||
test_escape_case(" ", r#"' '"#); | ||
test_escape_case("*", r#"'*'"#); | ||
|
||
test_escape_from_bytes( | ||
&[0x66, 0x6f, 0x80, 0x6f], | ||
&[b'\'', 0x66, 0x6f, 0x80, 0x6f, b'\''], | ||
); | ||
} | ||
} |
Uh oh!
There was an error while loading. Please reload this page.