Skip to content
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

Allow conversion of std::process::Command into a openssh::Command given a openssh::Session. #112

Merged
merged 27 commits into from
Jul 28, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
e314422
Add OverSsh trait to allow conversion of std::process::Command into a…
aalekhpatel07 Jan 31, 2023
d9ba8d3
Add a doc comment and example usage.
aalekhpatel07 Jan 31, 2023
dd64633
Add tests for OverSsh's with_session
aalekhpatel07 Jan 31, 2023
9743a5e
Use raw_command and raw_args. Refactor the name to 'over_ssh'.
aalekhpatel07 Jan 31, 2023
0a69ed1
Remove a dangling test import. Use better name for test.
aalekhpatel07 Jan 31, 2023
974cd52
Clean up doc comment.
aalekhpatel07 Jan 31, 2023
cca22b1
Apply suggestions of code review.
aalekhpatel07 Jan 31, 2023
66cecba
Update src/command.rs
aalekhpatel07 Jan 31, 2023
511c62c
Apply more suggestions from the code review.
aalekhpatel07 Jan 31, 2023
d14a4c2
Apply more suggestions from the code review.
aalekhpatel07 Jan 31, 2023
32a8960
Update src/command.rs
aalekhpatel07 Jan 31, 2023
08b77a5
Update src/lib.rs
aalekhpatel07 Jan 31, 2023
5d9a112
Update src/escape.rs
aalekhpatel07 Jan 31, 2023
7d84871
Apply more suggestions. Add a test for non-utf8 chars for escape.
aalekhpatel07 Jan 31, 2023
c4baafd
Slight refactor of escape.
CdnCentreForChildProtection Jan 31, 2023
7e84beb
Run cargo clippy --fix and persist all modifications to escape.rs
CdnCentreForChildProtection Jan 31, 2023
7fcfaf2
Apply rustfmt to command.rs and escape.rs
CdnCentreForChildProtection Jan 31, 2023
13d865a
Lint over_session test
CdnCentreForChildProtection Jan 31, 2023
c4ded38
Update src/escape.rs
aalekhpatel07 Feb 1, 2023
67d0885
Update src/escape.rs
aalekhpatel07 Feb 1, 2023
6970753
Update src/escape.rs
aalekhpatel07 Feb 1, 2023
8207907
Apply more suggestions.
aalekhpatel07 Feb 6, 2023
10687fb
Rustfmt fix.
aalekhpatel07 Feb 6, 2023
020366a
Fix a doctest for overssh.
aalekhpatel07 Feb 11, 2023
6842e5a
Add a test for overssh that requires escaping the argument passed to …
aalekhpatel07 Feb 11, 2023
e09d2fd
Update overssh require-escaping-arguments test to contain a space.
aalekhpatel07 Feb 11, 2023
f30d34e
Apply rustfmt to tests/openssh.rs
aalekhpatel07 Feb 11, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Apply more suggestions from the code review.
Signed-off-by: Aalekh Patel <aalekh.gwpeck.7998@icloud.com>
  • Loading branch information
aalekhpatel07 authored and NobodyXu committed Jul 28, 2023
commit 511c62c7193feb50d937f67b31019cc2bbb1df79
67 changes: 20 additions & 47 deletions src/command.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
use crate::escape;

use super::stdio::TryFromChildIo;
use super::RemoteChild;
use super::Stdio;
use super::{Error, Session};

use std::borrow::Cow;
use std::ffi::OsStr;
use std::os::unix::prelude::OsStrExt;
use std::process;

#[derive(Debug)]
Expand Down Expand Up @@ -57,15 +60,10 @@ pub trait OverSsh {
/// **Note**: The command to be executed on the remote machine does not include
/// any environment variables or the current directory. They must be
/// set explicitly, if desired.
fn over_session<'session>(&self, session: &'session Session) -> crate::Command<'session>;
}

impl OverSsh for std::process::Command {
/// Given a session, convert a `std::process::Command` into an `openssh::Command`
/// that can be executed over that session.
/// **Note**: The command to be executed on the remote machine does not include
/// any environment variables or the current directory. They must be
/// set explicitly, if desired.
///
/// # Example
/// Consider the implementation of `OverSsh` for `std::process::Command`:
///
/// ```no_run
/// # #[tokio::main(flavor = "current_thread")]
/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
Expand All @@ -87,42 +85,26 @@ impl OverSsh for std::process::Command {
/// }
///
/// ```
fn over_session<'session>(&self, session: &'session Session) -> crate::Command<'session>;
}

impl OverSsh for std::process::Command {
fn over_session<'session>(&self, session: &'session Session) -> Command<'session> {
let mut command = session.command(self.get_program().to_string_lossy());
command.raw_args(self.get_args());
let program_escaped = escape(self.get_program().as_bytes());
let mut command = session.command(Cow::Borrowed(program_escaped.as_str()));

self
.get_args()
.for_each(|arg| {
let arg_escaped = escape(arg.as_bytes());
command.arg(Cow::Borrowed(arg_escaped.as_str()));
});
command
}
}


impl OverSsh for tokio::process::Command {
/// Given a session, convert a `tokio::process::Command` into an `openssh::Command`
/// that can be executed over that session.
///
/// **Note**: The command to be executed on the remote machine does not include
/// any environment variables or the current directory. They must be
/// set explicitly, if desired.
/// ```no_run
/// # #[tokio::main(flavor = "current_thread")]
/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
/// use tokio::process::Command;
/// use openssh::{Session, KnownHosts, OverSsh};

/// let session = Session::connect_mux("me@ssh.example.com", KnownHosts::Strict).await?;
/// let ls =
/// Command::new("ls")
/// .arg("-l")
/// .arg("-a")
/// .arg("-h")
/// .over_session(&session)
/// .output()
/// .await?;
///
/// assert!(String::from_utf8(ls.stdout).unwrap().contains("total"));
/// # Ok(())
/// # }
///
/// ```
fn over_session<'session>(&self, session: &'session Session) -> Command<'session> {
self.as_std().over_session(session)
}
Expand All @@ -138,15 +120,6 @@ where
}
}

impl<S> OverSsh for &mut S
where
S: OverSsh
{
fn over_session<'session>(&self, session: &'session Session) -> Command<'session> {
<S as OverSsh>::over_session(self, session)
}
}

impl<S> OverSsh for Box<S>
where
S: OverSsh
Expand Down
81 changes: 81 additions & 0 deletions src/escape.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
//! 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.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we submit a PR to shell-escape to add a version supporting OsStr there directly? I'm okay with us landing our own version initially, but it'd be way better if we could just use a version from them in the future.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great suggestion! I've initiated a PR to shell-escape and if it gets merged, we can avoid having a src/escape.rs altogether.

//!
//! [`shell-escape`]: https://crates.io/crates/shell-escape
//! [`shell-escape::unix`]: https://docs.rs/shell-escape/latest/src/shell_escape/lib.rs.html#101


fn whitelisted(ch: char) -> bool {
match ch {
'a'..='z' | 'A'..='Z' | '0'..='9' | '-' | '_' | '=' | '/' | ',' | '.' | '+' => true,
_ => false,
}
}

/// 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 fn escape(s: &[u8]) -> String {
let all_whitelisted = s.iter().all(|x| whitelisted(*x as char));

if !s.is_empty() && all_whitelisted {
// All bytes are whitelisted and valid single-byte UTF-8,
// so we can build the original string and return as is.
return String::from_utf8(s.to_vec()).unwrap();
}

let mut escaped = String::with_capacity(s.len() + 2);
escaped.push('\'');

for &b in s {
match b {
b'\'' | b'!' => {
escaped.push_str("'\\");
escaped.push(b as char);
escaped.push('\'');
}
_ => escaped.push(b as char),
}
}
escaped.push('\'');
escaped
}


#[cfg(test)]
mod tests {
use super::*;

// These tests are courtesy of the `shell-escape` crate.
#[test]
fn test_escape() {
assert_eq!(
escape(b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_=/,.+"),
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_=/,.+"
);
assert_eq!(
escape(b"--aaa=bbb-ccc"),
"--aaa=bbb-ccc"
);
assert_eq!(
escape(b"linker=gcc -L/foo -Wl,bar"),
r#"'linker=gcc -L/foo -Wl,bar'"#
);
assert_eq!(
escape(br#"--features="default""#),
r#"'--features="default"'"#
);
assert_eq!(
escape(br#"'!\$`\\\n "#),
r#"''\'''\!'\$`\\\n '"#
);
assert_eq!(escape(b""), r#"''"#);
assert_eq!(escape(b" "), r#"' '"#);
assert_eq!(escape(b"\xC4b"), r#"'Äb'"#);
}
}
3 changes: 3 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,9 @@ pub use builder::{KnownHosts, SessionBuilder};
mod command;
pub use command::{Command, OverSsh};

mod escape;
pub use escape::escape;

mod child;
pub use child::RemoteChild;

Expand Down