use std::time::Duration;
use tracing::debug;
use crate::client::WinrmClient;
use crate::command::CommandOutput;
use crate::error::WinrmError;
use crate::soap::{self, ReceiveOutput};
pub struct Shell<'a> {
client: &'a WinrmClient,
host: String,
shell_id: String,
closed: bool,
resource_uri: String,
}
impl<'a> Shell<'a> {
pub(crate) fn new(client: &'a WinrmClient, host: String, shell_id: String) -> Self {
Self {
client,
host,
shell_id,
closed: false,
resource_uri: crate::soap::namespaces::RESOURCE_URI_CMD.to_string(),
}
}
pub(crate) fn new_with_resource_uri(
client: &'a WinrmClient,
host: String,
shell_id: String,
resource_uri: String,
) -> Self {
Self {
client,
host,
shell_id,
closed: false,
resource_uri,
}
}
pub fn resource_uri(&self) -> &str {
&self.resource_uri
}
pub async fn run_command(
&self,
command: &str,
args: &[&str],
) -> Result<CommandOutput, WinrmError> {
let command_id = self
.client
.execute_command(&self.host, &self.shell_id, command, args)
.await?;
debug!(command_id = %command_id, "shell command started");
let timeout_duration = Duration::from_secs(self.client.config().operation_timeout_secs * 2);
let result = tokio::time::timeout(timeout_duration, async {
let mut stdout = Vec::new();
let mut stderr = Vec::new();
let mut exit_code: Option<i32> = None;
loop {
let output: ReceiveOutput = self
.client
.receive_output(&self.host, &self.shell_id, &command_id)
.await?;
stdout.extend_from_slice(&output.stdout);
stderr.extend_from_slice(&output.stderr);
exit_code = output.exit_code.or(exit_code);
if output.done {
break;
}
}
self.client
.signal_terminate(&self.host, &self.shell_id, &command_id)
.await
.ok();
Ok(CommandOutput {
stdout,
stderr,
exit_code: exit_code.unwrap_or(-1),
})
})
.await;
match result {
Ok(inner) => inner,
Err(_) => Err(WinrmError::Timeout(
self.client.config().operation_timeout_secs * 2,
)),
}
}
pub async fn run_command_with_cancel(
&self,
command: &str,
args: &[&str],
cancel: tokio_util::sync::CancellationToken,
) -> Result<CommandOutput, WinrmError> {
tokio::select! {
result = self.run_command(command, args) => result,
() = cancel.cancelled() => {
Err(WinrmError::Cancelled)
}
}
}
pub async fn run_powershell(&self, script: &str) -> Result<CommandOutput, WinrmError> {
let encoded = crate::command::encode_powershell_command(script);
self.run_command("powershell.exe", &["-EncodedCommand", &encoded])
.await
}
pub async fn run_powershell_with_cancel(
&self,
script: &str,
cancel: tokio_util::sync::CancellationToken,
) -> Result<CommandOutput, WinrmError> {
let encoded = crate::command::encode_powershell_command(script);
self.run_command_with_cancel("powershell.exe", &["-EncodedCommand", &encoded], cancel)
.await
}
pub async fn send_input(
&self,
command_id: &str,
data: &[u8],
end_of_stream: bool,
) -> Result<(), WinrmError> {
let endpoint = self.client.endpoint(&self.host);
let config = self.client.config();
let envelope = if command_id.is_empty() || command_id == self.shell_id {
soap::send_psrp_request(
&endpoint,
&self.shell_id,
data,
config.operation_timeout_secs,
config.max_envelope_size,
&self.resource_uri,
)
} else {
soap::send_input_request_with_uri(
&endpoint,
&self.shell_id,
command_id,
data,
end_of_stream,
config.operation_timeout_secs,
config.max_envelope_size,
&self.resource_uri,
)
};
self.client.send_soap_raw(&self.host, envelope).await?;
Ok(())
}
pub async fn signal_ctrl_c(&self, command_id: &str) -> Result<(), WinrmError> {
let endpoint = self.client.endpoint(&self.host);
let config = self.client.config();
let envelope = soap::signal_ctrl_c_request(
&endpoint,
&self.shell_id,
command_id,
config.operation_timeout_secs,
config.max_envelope_size,
);
self.client.send_soap_raw(&self.host, envelope).await?;
Ok(())
}
pub async fn start_command_with_id(
&self,
command: &str,
args: &[&str],
command_id: &str,
) -> Result<String, WinrmError> {
let endpoint = self.client.endpoint(&self.host);
let config = self.client.config();
let envelope = soap::execute_command_with_id_request(
&endpoint,
&self.shell_id,
command,
args,
command_id,
config.operation_timeout_secs,
config.max_envelope_size,
&self.resource_uri,
);
let response = self.client.send_soap_raw(&self.host, envelope).await?;
let returned_id = soap::parse_command_id(&response).map_err(WinrmError::Soap)?;
debug!(command_id = %returned_id, "shell command started with specified ID");
Ok(returned_id)
}
pub async fn start_command(&self, command: &str, args: &[&str]) -> Result<String, WinrmError> {
let endpoint = self.client.endpoint(&self.host);
let config = self.client.config();
let envelope = soap::execute_command_request_with_uri(
&endpoint,
&self.shell_id,
command,
args,
config.operation_timeout_secs,
config.max_envelope_size,
&self.resource_uri,
);
let response = self.client.send_soap_raw(&self.host, envelope).await?;
let command_id = soap::parse_command_id(&response).map_err(WinrmError::Soap)?;
debug!(command_id = %command_id, "shell command started (streaming)");
Ok(command_id)
}
pub async fn receive_next(&self, command_id: &str) -> Result<ReceiveOutput, WinrmError> {
let endpoint = self.client.endpoint(&self.host);
let config = self.client.config();
let is_psrp = self.resource_uri.contains("powershell");
let envelope = if is_psrp {
let cid = if command_id.is_empty() || command_id == self.shell_id {
None
} else {
Some(command_id)
};
soap::receive_psrp_request(
&endpoint,
&self.shell_id,
cid,
config.operation_timeout_secs,
config.max_envelope_size,
&self.resource_uri,
)
} else {
soap::receive_output_request_with_uri(
&endpoint,
&self.shell_id,
command_id,
config.operation_timeout_secs,
config.max_envelope_size,
&self.resource_uri,
)
};
let response = self.client.send_soap_raw(&self.host, envelope).await?;
soap::parse_receive_output(&response).map_err(WinrmError::Soap)
}
pub fn shell_id(&self) -> &str {
&self.shell_id
}
pub async fn close(mut self) -> Result<(), WinrmError> {
self.closed = true;
self.client
.delete_shell_raw(&self.host, &self.shell_id)
.await
}
pub async fn disconnect(mut self) -> Result<String, WinrmError> {
self.closed = true;
let endpoint = self.client.endpoint(&self.host);
let config = self.client.config();
let envelope = soap::disconnect_shell_request_with_uri(
&endpoint,
&self.shell_id,
config.operation_timeout_secs,
config.max_envelope_size,
&self.resource_uri,
);
self.client.send_soap_raw(&self.host, envelope).await?;
Ok(std::mem::take(&mut self.shell_id))
}
}
impl Drop for Shell<'_> {
fn drop(&mut self) {
if !self.closed {
tracing::warn!(
shell_id = %self.shell_id,
"shell dropped without close -- resources may leak on server"
);
}
}
}
#[cfg(test)]
mod tests {
use crate::client::WinrmClient;
use crate::config::{AuthMethod, WinrmConfig, WinrmCredentials};
use wiremock::matchers::method;
use wiremock::{Mock, MockServer, ResponseTemplate};
fn test_creds() -> WinrmCredentials {
WinrmCredentials::new("admin", "pass", "")
}
fn basic_config(port: u16) -> WinrmConfig {
WinrmConfig {
port,
auth_method: AuthMethod::Basic,
connect_timeout_secs: 5,
operation_timeout_secs: 10,
..Default::default()
}
}
#[tokio::test]
async fn run_command_polls_until_done() {
let server = MockServer::start().await;
let port = server.address().port();
Mock::given(method("POST"))
.respond_with(ResponseTemplate::new(200).set_body_string(
r"<s:Envelope><s:Body><rsp:Shell><rsp:ShellId>SH-RUN</rsp:ShellId></rsp:Shell></s:Body></s:Envelope>",
))
.up_to_n_times(1)
.mount(&server)
.await;
Mock::given(method("POST"))
.respond_with(ResponseTemplate::new(200).set_body_string(
r"<s:Envelope><s:Body><rsp:CommandResponse><rsp:CommandId>SH-CMD</rsp:CommandId></rsp:CommandResponse></s:Body></s:Envelope>",
))
.up_to_n_times(1)
.mount(&server)
.await;
Mock::given(method("POST"))
.respond_with(ResponseTemplate::new(200).set_body_string(
r#"<s:Envelope><s:Body><rsp:ReceiveResponse>
<rsp:Stream Name="stdout" CommandId="SH-CMD">YWJD</rsp:Stream>
<rsp:CommandState CommandId="SH-CMD" State="http://schemas.microsoft.com/wbem/wsman/1/windows/shell/CommandState/Running"/>
</rsp:ReceiveResponse></s:Body></s:Envelope>"#,
))
.up_to_n_times(1)
.mount(&server)
.await;
Mock::given(method("POST"))
.respond_with(ResponseTemplate::new(200).set_body_string(
r#"<s:Envelope><s:Body><rsp:ReceiveResponse>
<rsp:Stream Name="stdout" CommandId="SH-CMD">REVG</rsp:Stream>
<rsp:CommandState CommandId="SH-CMD" State="http://schemas.microsoft.com/wbem/wsman/1/windows/shell/CommandState/Done">
<rsp:ExitCode>7</rsp:ExitCode>
</rsp:CommandState>
</rsp:ReceiveResponse></s:Body></s:Envelope>"#,
))
.up_to_n_times(1)
.mount(&server)
.await;
Mock::given(method("POST"))
.respond_with(
ResponseTemplate::new(200).set_body_string("<s:Envelope><s:Body/></s:Envelope>"),
)
.mount(&server)
.await;
let client = WinrmClient::new(basic_config(port), test_creds()).unwrap();
let shell = client.open_shell("127.0.0.1").await.unwrap();
let output = shell.run_command("cmd", &["/c", "dir"]).await.unwrap();
assert_eq!(output.exit_code, 7);
assert_eq!(output.stdout, b"abCDEF");
}
#[tokio::test]
async fn send_input_exercises_shell_method() {
let server = MockServer::start().await;
let port = server.address().port();
Mock::given(method("POST"))
.respond_with(ResponseTemplate::new(200).set_body_string(
r"<s:Envelope><s:Body><rsp:Shell><rsp:ShellId>SH-INP</rsp:ShellId></rsp:Shell></s:Body></s:Envelope>",
))
.up_to_n_times(1)
.mount(&server)
.await;
Mock::given(method("POST"))
.respond_with(
ResponseTemplate::new(200).set_body_string("<s:Envelope><s:Body/></s:Envelope>"),
)
.mount(&server)
.await;
let client = WinrmClient::new(basic_config(port), test_creds()).unwrap();
let shell = client.open_shell("127.0.0.1").await.unwrap();
shell.send_input("CMD-X", b"data", false).await.unwrap();
shell.send_input("CMD-X", b"", true).await.unwrap();
}
#[tokio::test]
async fn disconnect_returns_shell_id_and_suppresses_drop_warning() {
let server = MockServer::start().await;
let port = server.address().port();
Mock::given(method("POST"))
.respond_with(ResponseTemplate::new(200).set_body_string(
r"<s:Envelope><s:Body><rsp:Shell><rsp:ShellId>SH-DISC</rsp:ShellId></rsp:Shell></s:Body></s:Envelope>",
))
.up_to_n_times(1)
.mount(&server)
.await;
Mock::given(method("POST"))
.respond_with(
ResponseTemplate::new(200).set_body_string("<s:Envelope><s:Body/></s:Envelope>"),
)
.mount(&server)
.await;
let client = WinrmClient::new(basic_config(port), test_creds()).unwrap();
let shell = client.open_shell("127.0.0.1").await.unwrap();
let id = shell.disconnect().await.unwrap();
assert_eq!(id, "SH-DISC");
}
#[tokio::test]
async fn reconnect_shell_returns_handle_with_existing_id() {
let server = MockServer::start().await;
let port = server.address().port();
Mock::given(method("POST"))
.respond_with(
ResponseTemplate::new(200).set_body_string("<s:Envelope><s:Body/></s:Envelope>"),
)
.mount(&server)
.await;
let client = WinrmClient::new(basic_config(port), test_creds()).unwrap();
let shell = client
.reconnect_shell(
"127.0.0.1",
"SH-EXISTING",
crate::soap::namespaces::RESOURCE_URI_CMD,
)
.await
.unwrap();
assert_eq!(shell.shell_id(), "SH-EXISTING");
shell.close().await.unwrap();
}
#[tokio::test]
async fn signal_ctrl_c_exercises_shell_method() {
let server = MockServer::start().await;
let port = server.address().port();
Mock::given(method("POST"))
.respond_with(ResponseTemplate::new(200).set_body_string(
r"<s:Envelope><s:Body><rsp:Shell><rsp:ShellId>SH-SIG</rsp:ShellId></rsp:Shell></s:Body></s:Envelope>",
))
.up_to_n_times(1)
.mount(&server)
.await;
Mock::given(method("POST"))
.respond_with(
ResponseTemplate::new(200).set_body_string("<s:Envelope><s:Body/></s:Envelope>"),
)
.mount(&server)
.await;
let client = WinrmClient::new(basic_config(port), test_creds()).unwrap();
let shell = client.open_shell("127.0.0.1").await.unwrap();
shell.signal_ctrl_c("CMD-Y").await.unwrap();
}
#[tokio::test]
async fn start_command_returns_id() {
let server = MockServer::start().await;
let port = server.address().port();
Mock::given(method("POST"))
.respond_with(ResponseTemplate::new(200).set_body_string(
r"<s:Envelope><s:Body><rsp:Shell><rsp:ShellId>SH-START</rsp:ShellId></rsp:Shell></s:Body></s:Envelope>",
))
.up_to_n_times(1)
.mount(&server)
.await;
Mock::given(method("POST"))
.respond_with(ResponseTemplate::new(200).set_body_string(
r"<s:Envelope><s:Body><rsp:CommandResponse><rsp:CommandId>SH-START-CMD</rsp:CommandId></rsp:CommandResponse></s:Body></s:Envelope>",
))
.up_to_n_times(1)
.mount(&server)
.await;
Mock::given(method("POST"))
.respond_with(
ResponseTemplate::new(200).set_body_string("<s:Envelope><s:Body/></s:Envelope>"),
)
.mount(&server)
.await;
let client = WinrmClient::new(basic_config(port), test_creds()).unwrap();
let shell = client.open_shell("127.0.0.1").await.unwrap();
let cmd_id = shell.start_command("ping", &["localhost"]).await.unwrap();
assert_eq!(cmd_id, "SH-START-CMD");
}
#[tokio::test]
async fn shell_run_command_missing_exit_code_returns_minus_one() {
let server = MockServer::start().await;
let port = server.address().port();
Mock::given(method("POST"))
.respond_with(ResponseTemplate::new(200).set_body_string(
r"<s:Envelope><s:Body><rsp:Shell><rsp:ShellId>SH-NEG1</rsp:ShellId></rsp:Shell></s:Body></s:Envelope>",
))
.up_to_n_times(1)
.mount(&server)
.await;
Mock::given(method("POST"))
.respond_with(ResponseTemplate::new(200).set_body_string(
r"<s:Envelope><s:Body><rsp:CommandResponse><rsp:CommandId>SH-NEG1-CMD</rsp:CommandId></rsp:CommandResponse></s:Body></s:Envelope>",
))
.up_to_n_times(1)
.mount(&server)
.await;
Mock::given(method("POST"))
.respond_with(ResponseTemplate::new(200).set_body_string(
r#"<s:Envelope><s:Body><rsp:ReceiveResponse>
<rsp:CommandState CommandId="SH-NEG1-CMD" State="http://schemas.microsoft.com/wbem/wsman/1/windows/shell/CommandState/Done"/>
</rsp:ReceiveResponse></s:Body></s:Envelope>"#,
))
.up_to_n_times(1)
.mount(&server)
.await;
Mock::given(method("POST"))
.respond_with(
ResponseTemplate::new(200).set_body_string("<s:Envelope><s:Body/></s:Envelope>"),
)
.mount(&server)
.await;
let client = WinrmClient::new(basic_config(port), test_creds()).unwrap();
let shell = client.open_shell("127.0.0.1").await.unwrap();
let output = shell.run_command("test", &[]).await.unwrap();
assert_eq!(output.exit_code, -1);
}
#[tokio::test]
async fn shell_timeout_duration_kills_plus_mutant() {
let server = MockServer::start().await;
let port = server.address().port();
Mock::given(method("POST"))
.respond_with(ResponseTemplate::new(200).set_body_string(
r"<s:Envelope><s:Body><rsp:Shell><rsp:ShellId>TO2-SHELL</rsp:ShellId></rsp:Shell></s:Body></s:Envelope>",
))
.up_to_n_times(1)
.mount(&server)
.await;
Mock::given(method("POST"))
.respond_with(ResponseTemplate::new(200).set_body_string(
r"<s:Envelope><s:Body><rsp:CommandResponse><rsp:CommandId>TO2-CMD</rsp:CommandId></rsp:CommandResponse></s:Body></s:Envelope>",
))
.up_to_n_times(1)
.mount(&server)
.await;
Mock::given(method("POST"))
.respond_with(
ResponseTemplate::new(200)
.set_body_string(
r#"<s:Envelope><s:Body><rsp:ReceiveResponse>
<rsp:CommandState CommandId="TO2-CMD" State="http://schemas.microsoft.com/wbem/wsman/1/windows/shell/CommandState/Done">
<rsp:ExitCode>0</rsp:ExitCode>
</rsp:CommandState>
</rsp:ReceiveResponse></s:Body></s:Envelope>"#,
)
.set_delay(std::time::Duration::from_millis(5500)),
)
.up_to_n_times(1)
.mount(&server)
.await;
Mock::given(method("POST"))
.respond_with(
ResponseTemplate::new(200).set_body_string("<s:Envelope><s:Body/></s:Envelope>"),
)
.mount(&server)
.await;
let config = WinrmConfig {
port,
auth_method: AuthMethod::Basic,
connect_timeout_secs: 30,
operation_timeout_secs: 3, ..Default::default()
};
let client = WinrmClient::new(config, test_creds()).unwrap();
let shell = client.open_shell("127.0.0.1").await.unwrap();
let result = shell.run_command("slow", &[]).await;
assert!(
result.is_ok(),
"should complete within 6s timeout (* 2), got: {:?}",
result.err()
);
assert_eq!(result.unwrap().exit_code, 0);
}
#[tokio::test]
async fn shell_timeout_duration_kills_div_mutant() {
let server = MockServer::start().await;
let port = server.address().port();
Mock::given(method("POST"))
.respond_with(ResponseTemplate::new(200).set_body_string(
r"<s:Envelope><s:Body><rsp:Shell><rsp:ShellId>TO3-SHELL</rsp:ShellId></rsp:Shell></s:Body></s:Envelope>",
))
.up_to_n_times(1)
.mount(&server)
.await;
Mock::given(method("POST"))
.respond_with(ResponseTemplate::new(200).set_body_string(
r"<s:Envelope><s:Body><rsp:CommandResponse><rsp:CommandId>TO3-CMD</rsp:CommandId></rsp:CommandResponse></s:Body></s:Envelope>",
))
.up_to_n_times(1)
.mount(&server)
.await;
Mock::given(method("POST"))
.respond_with(ResponseTemplate::new(200).set_body_string(
r#"<s:Envelope><s:Body><rsp:ReceiveResponse>
<rsp:CommandState CommandId="TO3-CMD" State="http://schemas.microsoft.com/wbem/wsman/1/windows/shell/CommandState/Done">
<rsp:ExitCode>0</rsp:ExitCode>
</rsp:CommandState>
</rsp:ReceiveResponse></s:Body></s:Envelope>"#,
))
.up_to_n_times(1)
.mount(&server)
.await;
Mock::given(method("POST"))
.respond_with(
ResponseTemplate::new(200).set_body_string("<s:Envelope><s:Body/></s:Envelope>"),
)
.mount(&server)
.await;
let config = WinrmConfig {
port,
auth_method: AuthMethod::Basic,
connect_timeout_secs: 30,
operation_timeout_secs: 1, ..Default::default()
};
let client = WinrmClient::new(config, test_creds()).unwrap();
let shell = client.open_shell("127.0.0.1").await.unwrap();
let result = shell.run_command("fast", &[]).await;
assert!(
result.is_ok(),
"instant response should succeed with 2s timeout, got: {:?}",
result.err()
);
}
#[tracing_test::traced_test]
#[tokio::test]
async fn shell_drop_without_close_emits_warning() {
let server = MockServer::start().await;
let port = server.address().port();
Mock::given(method("POST"))
.respond_with(ResponseTemplate::new(200).set_body_string(
r"<s:Envelope><s:Body><rsp:Shell><rsp:ShellId>DROP-WARN</rsp:ShellId></rsp:Shell></s:Body></s:Envelope>",
))
.up_to_n_times(1)
.mount(&server)
.await;
Mock::given(method("POST"))
.respond_with(
ResponseTemplate::new(200).set_body_string("<s:Envelope><s:Body/></s:Envelope>"),
)
.mount(&server)
.await;
let client = WinrmClient::new(basic_config(port), test_creds()).unwrap();
{
let shell = client.open_shell("127.0.0.1").await.unwrap();
assert_eq!(shell.shell_id(), "DROP-WARN");
}
assert!(logs_contain("shell dropped without close"));
}
#[tracing_test::traced_test]
#[tokio::test]
async fn shell_close_does_not_emit_drop_warning() {
let server = MockServer::start().await;
let port = server.address().port();
Mock::given(method("POST"))
.respond_with(ResponseTemplate::new(200).set_body_string(
r"<s:Envelope><s:Body><rsp:Shell><rsp:ShellId>DROP-OK</rsp:ShellId></rsp:Shell></s:Body></s:Envelope>",
))
.up_to_n_times(1)
.mount(&server)
.await;
Mock::given(method("POST"))
.respond_with(
ResponseTemplate::new(200).set_body_string("<s:Envelope><s:Body/></s:Envelope>"),
)
.mount(&server)
.await;
let client = WinrmClient::new(basic_config(port), test_creds()).unwrap();
let shell = client.open_shell("127.0.0.1").await.unwrap();
shell.close().await.unwrap();
assert!(!logs_contain("shell dropped without close"));
}
#[tokio::test]
async fn resource_uri_matches_default_cmd() {
let server = MockServer::start().await;
let port = server.address().port();
Mock::given(method("POST"))
.respond_with(ResponseTemplate::new(200).set_body_string(
r"<s:Envelope><s:Body><rsp:Shell><rsp:ShellId>SH-URI</rsp:ShellId></rsp:Shell></s:Body></s:Envelope>",
))
.up_to_n_times(1)
.mount(&server)
.await;
Mock::given(method("POST"))
.respond_with(
ResponseTemplate::new(200).set_body_string("<s:Envelope><s:Body/></s:Envelope>"),
)
.mount(&server)
.await;
let client = WinrmClient::new(basic_config(port), test_creds()).unwrap();
let shell = client.open_shell("127.0.0.1").await.unwrap();
assert!(
shell.resource_uri().contains("cmd"),
"resource_uri should contain 'cmd', got: {}",
shell.resource_uri()
);
}
#[tokio::test]
async fn start_command_with_id_returns_server_command_id() {
let server = MockServer::start().await;
let port = server.address().port();
Mock::given(method("POST"))
.respond_with(ResponseTemplate::new(200).set_body_string(
r"<s:Envelope><s:Body><rsp:Shell><rsp:ShellId>SH-WCID</rsp:ShellId></rsp:Shell></s:Body></s:Envelope>",
))
.up_to_n_times(1)
.mount(&server)
.await;
Mock::given(method("POST"))
.respond_with(ResponseTemplate::new(200).set_body_string(
r"<s:Envelope><s:Body><rsp:CommandResponse><rsp:CommandId>MY-CMD-ID</rsp:CommandId></rsp:CommandResponse></s:Body></s:Envelope>",
))
.up_to_n_times(1)
.mount(&server)
.await;
Mock::given(method("POST"))
.respond_with(
ResponseTemplate::new(200).set_body_string("<s:Envelope><s:Body/></s:Envelope>"),
)
.mount(&server)
.await;
let client = WinrmClient::new(basic_config(port), test_creds()).unwrap();
let shell = client.open_shell("127.0.0.1").await.unwrap();
let cmd_id = shell
.start_command_with_id("test", &[], "MY-CMD-ID")
.await
.unwrap();
assert_eq!(cmd_id, "MY-CMD-ID");
}
#[tokio::test]
async fn send_input_psrp_path_when_command_id_is_shell_id() {
let server = MockServer::start().await;
let port = server.address().port();
Mock::given(method("POST"))
.respond_with(
ResponseTemplate::new(200).set_body_string("<s:Envelope><s:Body/></s:Envelope>"),
)
.mount(&server)
.await;
let client = WinrmClient::new(basic_config(port), test_creds()).unwrap();
let shell = client
.reconnect_shell(
"127.0.0.1",
"PSRP-SHELL",
crate::soap::namespaces::RESOURCE_URI_PSRP,
)
.await
.unwrap();
shell
.send_input("PSRP-SHELL", b"data", false)
.await
.unwrap();
shell.send_input("", b"data", false).await.unwrap();
}
#[tokio::test]
async fn receive_next_with_real_command_id() {
let server = MockServer::start().await;
let port = server.address().port();
Mock::given(method("POST"))
.respond_with(ResponseTemplate::new(200).set_body_string(
r"<s:Envelope><s:Body><rsp:Shell><rsp:ShellId>SH-RECV</rsp:ShellId></rsp:Shell></s:Body></s:Envelope>",
))
.up_to_n_times(1)
.mount(&server)
.await;
Mock::given(method("POST"))
.respond_with(ResponseTemplate::new(200).set_body_string(
r#"<s:Envelope><s:Body><rsp:ReceiveResponse>
<rsp:Stream Name="stdout" CommandId="RECV-CMD">YWJD</rsp:Stream>
<rsp:CommandState CommandId="RECV-CMD" State="http://schemas.microsoft.com/wbem/wsman/1/windows/shell/CommandState/Done">
<rsp:ExitCode>0</rsp:ExitCode>
</rsp:CommandState>
</rsp:ReceiveResponse></s:Body></s:Envelope>"#,
))
.up_to_n_times(1)
.mount(&server)
.await;
Mock::given(method("POST"))
.respond_with(
ResponseTemplate::new(200).set_body_string("<s:Envelope><s:Body/></s:Envelope>"),
)
.mount(&server)
.await;
let client = WinrmClient::new(basic_config(port), test_creds()).unwrap();
let shell = client.open_shell("127.0.0.1").await.unwrap();
let output = shell.receive_next("RECV-CMD").await.unwrap();
assert!(output.done);
assert!(!output.stdout.is_empty());
}
}