1use std::time::Duration;
7
8use tracing::debug;
9
10use crate::client::WinrmClient;
11use crate::command::CommandOutput;
12use crate::error::WinrmError;
13use crate::soap::{self, ReceiveOutput};
14
15pub struct Shell<'a> {
24 client: &'a WinrmClient,
25 host: String,
26 shell_id: String,
27 closed: bool,
28 resource_uri: String,
32}
33
34impl<'a> Shell<'a> {
35 pub(crate) fn new(client: &'a WinrmClient, host: String, shell_id: String) -> Self {
36 Self {
37 client,
38 host,
39 shell_id,
40 closed: false,
41 resource_uri: crate::soap::namespaces::RESOURCE_URI_CMD.to_string(),
42 }
43 }
44
45 pub(crate) fn new_with_resource_uri(
46 client: &'a WinrmClient,
47 host: String,
48 shell_id: String,
49 resource_uri: String,
50 ) -> Self {
51 Self {
52 client,
53 host,
54 shell_id,
55 closed: false,
56 resource_uri,
57 }
58 }
59
60 pub fn resource_uri(&self) -> &str {
62 &self.resource_uri
63 }
64
65 pub async fn run_command(
70 &self,
71 command: &str,
72 args: &[&str],
73 ) -> Result<CommandOutput, WinrmError> {
74 let command_id = self
75 .client
76 .execute_command(&self.host, &self.shell_id, command, args)
77 .await?;
78 debug!(command_id = %command_id, "shell command started");
79
80 let timeout_duration = Duration::from_secs(self.client.config().operation_timeout_secs * 2);
81
82 let result = tokio::time::timeout(timeout_duration, async {
83 let mut stdout = Vec::new();
84 let mut stderr = Vec::new();
85 let mut exit_code: Option<i32> = None;
86
87 loop {
88 let output: ReceiveOutput = self
89 .client
90 .receive_output(&self.host, &self.shell_id, &command_id)
91 .await?;
92 stdout.extend_from_slice(&output.stdout);
93 stderr.extend_from_slice(&output.stderr);
94
95 exit_code = output.exit_code.or(exit_code);
96
97 if output.done {
98 break;
99 }
100 }
101
102 self.client
104 .signal_terminate(&self.host, &self.shell_id, &command_id)
105 .await
106 .ok();
107
108 Ok(CommandOutput {
109 stdout,
110 stderr,
111 exit_code: exit_code.unwrap_or(-1),
112 })
113 })
114 .await;
115
116 match result {
117 Ok(inner) => inner,
118 Err(_) => Err(WinrmError::Timeout(
119 self.client.config().operation_timeout_secs * 2,
120 )),
121 }
122 }
123
124 pub async fn run_command_with_cancel(
130 &self,
131 command: &str,
132 args: &[&str],
133 cancel: tokio_util::sync::CancellationToken,
134 ) -> Result<CommandOutput, WinrmError> {
135 tokio::select! {
136 result = self.run_command(command, args) => result,
137 () = cancel.cancelled() => {
138 Err(WinrmError::Cancelled)
139 }
140 }
141 }
142
143 pub async fn run_powershell(&self, script: &str) -> Result<CommandOutput, WinrmError> {
148 let encoded = crate::command::encode_powershell_command(script);
149 self.run_command("powershell.exe", &["-EncodedCommand", &encoded])
150 .await
151 }
152
153 pub async fn run_powershell_with_cancel(
158 &self,
159 script: &str,
160 cancel: tokio_util::sync::CancellationToken,
161 ) -> Result<CommandOutput, WinrmError> {
162 let encoded = crate::command::encode_powershell_command(script);
163 self.run_command_with_cancel("powershell.exe", &["-EncodedCommand", &encoded], cancel)
164 .await
165 }
166
167 pub async fn send_input(
172 &self,
173 command_id: &str,
174 data: &[u8],
175 end_of_stream: bool,
176 ) -> Result<(), WinrmError> {
177 let endpoint = self.client.endpoint(&self.host);
182 let config = self.client.config();
183 let envelope = if command_id.is_empty() || command_id == self.shell_id {
184 soap::send_psrp_request(
186 &endpoint,
187 &self.shell_id,
188 data,
189 config.operation_timeout_secs,
190 config.max_envelope_size,
191 &self.resource_uri,
192 )
193 } else {
194 soap::send_input_request_with_uri(
195 &endpoint,
196 &self.shell_id,
197 command_id,
198 data,
199 end_of_stream,
200 config.operation_timeout_secs,
201 config.max_envelope_size,
202 &self.resource_uri,
203 )
204 };
205 self.client.send_soap_raw(&self.host, envelope).await?;
206 Ok(())
207 }
208
209 pub async fn signal_ctrl_c(&self, command_id: &str) -> Result<(), WinrmError> {
213 let endpoint = self.client.endpoint(&self.host);
214 let config = self.client.config();
215 let envelope = soap::signal_ctrl_c_request(
216 &endpoint,
217 &self.shell_id,
218 command_id,
219 config.operation_timeout_secs,
220 config.max_envelope_size,
221 );
222 self.client.send_soap_raw(&self.host, envelope).await?;
223 Ok(())
224 }
225
226 pub async fn start_command_with_id(
250 &self,
251 command: &str,
252 args: &[&str],
253 command_id: &str,
254 ) -> Result<String, WinrmError> {
255 let endpoint = self.client.endpoint(&self.host);
256 let config = self.client.config();
257 let envelope = soap::execute_command_with_id_request(
258 &endpoint,
259 &self.shell_id,
260 command,
261 args,
262 command_id,
263 config.operation_timeout_secs,
264 config.max_envelope_size,
265 &self.resource_uri,
266 );
267 let response = self.client.send_soap_raw(&self.host, envelope).await?;
268 let returned_id = soap::parse_command_id(&response).map_err(WinrmError::Soap)?;
269 debug!(command_id = %returned_id, "shell command started with specified ID");
270 Ok(returned_id)
271 }
272
273 pub async fn start_command(&self, command: &str, args: &[&str]) -> Result<String, WinrmError> {
274 let endpoint = self.client.endpoint(&self.host);
275 let config = self.client.config();
276 let envelope = soap::execute_command_request_with_uri(
277 &endpoint,
278 &self.shell_id,
279 command,
280 args,
281 config.operation_timeout_secs,
282 config.max_envelope_size,
283 &self.resource_uri,
284 );
285 let response = self.client.send_soap_raw(&self.host, envelope).await?;
286 let command_id = soap::parse_command_id(&response).map_err(WinrmError::Soap)?;
287 debug!(command_id = %command_id, "shell command started (streaming)");
288 Ok(command_id)
289 }
290
291 pub async fn receive_next(&self, command_id: &str) -> Result<ReceiveOutput, WinrmError> {
297 let endpoint = self.client.endpoint(&self.host);
298 let config = self.client.config();
299 let is_psrp = self.resource_uri.contains("powershell");
300 let envelope = if is_psrp {
301 let cid = if command_id.is_empty() || command_id == self.shell_id {
303 None
304 } else {
305 Some(command_id)
306 };
307 soap::receive_psrp_request(
308 &endpoint,
309 &self.shell_id,
310 cid,
311 config.operation_timeout_secs,
312 config.max_envelope_size,
313 &self.resource_uri,
314 )
315 } else {
316 soap::receive_output_request_with_uri(
317 &endpoint,
318 &self.shell_id,
319 command_id,
320 config.operation_timeout_secs,
321 config.max_envelope_size,
322 &self.resource_uri,
323 )
324 };
325 let response = self.client.send_soap_raw(&self.host, envelope).await?;
326 soap::parse_receive_output(&response).map_err(WinrmError::Soap)
327 }
328
329 pub fn shell_id(&self) -> &str {
331 &self.shell_id
332 }
333
334 pub async fn close(mut self) -> Result<(), WinrmError> {
336 self.closed = true;
337 self.client
338 .delete_shell_raw(&self.host, &self.shell_id)
339 .await
340 }
341
342 pub async fn disconnect(mut self) -> Result<String, WinrmError> {
350 self.closed = true;
351 let endpoint = self.client.endpoint(&self.host);
352 let config = self.client.config();
353 let envelope = soap::disconnect_shell_request_with_uri(
354 &endpoint,
355 &self.shell_id,
356 config.operation_timeout_secs,
357 config.max_envelope_size,
358 &self.resource_uri,
359 );
360 self.client.send_soap_raw(&self.host, envelope).await?;
361 Ok(std::mem::take(&mut self.shell_id))
362 }
363}
364
365impl Drop for Shell<'_> {
366 fn drop(&mut self) {
367 if !self.closed {
368 tracing::warn!(
369 shell_id = %self.shell_id,
370 "shell dropped without close -- resources may leak on server"
371 );
372 }
373 }
374}
375
376#[cfg(test)]
377mod tests {
378 use crate::client::WinrmClient;
379 use crate::config::{AuthMethod, WinrmConfig, WinrmCredentials};
380 use wiremock::matchers::method;
381 use wiremock::{Mock, MockServer, ResponseTemplate};
382
383 fn test_creds() -> WinrmCredentials {
384 WinrmCredentials::new("admin", "pass", "")
385 }
386
387 fn basic_config(port: u16) -> WinrmConfig {
388 WinrmConfig {
389 port,
390 auth_method: AuthMethod::Basic,
391 connect_timeout_secs: 5,
392 operation_timeout_secs: 10,
393 ..Default::default()
394 }
395 }
396
397 #[tokio::test]
398 async fn run_command_polls_until_done() {
399 let server = MockServer::start().await;
400 let port = server.address().port();
401
402 Mock::given(method("POST"))
404 .respond_with(ResponseTemplate::new(200).set_body_string(
405 r"<s:Envelope><s:Body><rsp:Shell><rsp:ShellId>SH-RUN</rsp:ShellId></rsp:Shell></s:Body></s:Envelope>",
406 ))
407 .up_to_n_times(1)
408 .mount(&server)
409 .await;
410
411 Mock::given(method("POST"))
413 .respond_with(ResponseTemplate::new(200).set_body_string(
414 r"<s:Envelope><s:Body><rsp:CommandResponse><rsp:CommandId>SH-CMD</rsp:CommandId></rsp:CommandResponse></s:Body></s:Envelope>",
415 ))
416 .up_to_n_times(1)
417 .mount(&server)
418 .await;
419
420 Mock::given(method("POST"))
422 .respond_with(ResponseTemplate::new(200).set_body_string(
423 r#"<s:Envelope><s:Body><rsp:ReceiveResponse>
424 <rsp:Stream Name="stdout" CommandId="SH-CMD">YWJD</rsp:Stream>
425 <rsp:CommandState CommandId="SH-CMD" State="http://schemas.microsoft.com/wbem/wsman/1/windows/shell/CommandState/Running"/>
426 </rsp:ReceiveResponse></s:Body></s:Envelope>"#,
427 ))
428 .up_to_n_times(1)
429 .mount(&server)
430 .await;
431
432 Mock::given(method("POST"))
434 .respond_with(ResponseTemplate::new(200).set_body_string(
435 r#"<s:Envelope><s:Body><rsp:ReceiveResponse>
436 <rsp:Stream Name="stdout" CommandId="SH-CMD">REVG</rsp:Stream>
437 <rsp:CommandState CommandId="SH-CMD" State="http://schemas.microsoft.com/wbem/wsman/1/windows/shell/CommandState/Done">
438 <rsp:ExitCode>7</rsp:ExitCode>
439 </rsp:CommandState>
440 </rsp:ReceiveResponse></s:Body></s:Envelope>"#,
441 ))
442 .up_to_n_times(1)
443 .mount(&server)
444 .await;
445
446 Mock::given(method("POST"))
448 .respond_with(
449 ResponseTemplate::new(200).set_body_string("<s:Envelope><s:Body/></s:Envelope>"),
450 )
451 .mount(&server)
452 .await;
453
454 let client = WinrmClient::new(basic_config(port), test_creds()).unwrap();
455 let shell = client.open_shell("127.0.0.1").await.unwrap();
456 let output = shell.run_command("cmd", &["/c", "dir"]).await.unwrap();
457 assert_eq!(output.exit_code, 7);
458 assert_eq!(output.stdout, b"abCDEF");
459 }
460
461 #[tokio::test]
462 async fn send_input_exercises_shell_method() {
463 let server = MockServer::start().await;
464 let port = server.address().port();
465
466 Mock::given(method("POST"))
468 .respond_with(ResponseTemplate::new(200).set_body_string(
469 r"<s:Envelope><s:Body><rsp:Shell><rsp:ShellId>SH-INP</rsp:ShellId></rsp:Shell></s:Body></s:Envelope>",
470 ))
471 .up_to_n_times(1)
472 .mount(&server)
473 .await;
474
475 Mock::given(method("POST"))
477 .respond_with(
478 ResponseTemplate::new(200).set_body_string("<s:Envelope><s:Body/></s:Envelope>"),
479 )
480 .mount(&server)
481 .await;
482
483 let client = WinrmClient::new(basic_config(port), test_creds()).unwrap();
484 let shell = client.open_shell("127.0.0.1").await.unwrap();
485 shell.send_input("CMD-X", b"data", false).await.unwrap();
486 shell.send_input("CMD-X", b"", true).await.unwrap();
487 }
488
489 #[tokio::test]
490 async fn disconnect_returns_shell_id_and_suppresses_drop_warning() {
491 let server = MockServer::start().await;
492 let port = server.address().port();
493
494 Mock::given(method("POST"))
496 .respond_with(ResponseTemplate::new(200).set_body_string(
497 r"<s:Envelope><s:Body><rsp:Shell><rsp:ShellId>SH-DISC</rsp:ShellId></rsp:Shell></s:Body></s:Envelope>",
498 ))
499 .up_to_n_times(1)
500 .mount(&server)
501 .await;
502
503 Mock::given(method("POST"))
505 .respond_with(
506 ResponseTemplate::new(200).set_body_string("<s:Envelope><s:Body/></s:Envelope>"),
507 )
508 .mount(&server)
509 .await;
510
511 let client = WinrmClient::new(basic_config(port), test_creds()).unwrap();
512 let shell = client.open_shell("127.0.0.1").await.unwrap();
513 let id = shell.disconnect().await.unwrap();
514 assert_eq!(id, "SH-DISC");
515 }
516
517 #[tokio::test]
518 async fn reconnect_shell_returns_handle_with_existing_id() {
519 let server = MockServer::start().await;
520 let port = server.address().port();
521
522 Mock::given(method("POST"))
524 .respond_with(
525 ResponseTemplate::new(200).set_body_string("<s:Envelope><s:Body/></s:Envelope>"),
526 )
527 .mount(&server)
528 .await;
529
530 let client = WinrmClient::new(basic_config(port), test_creds()).unwrap();
531 let shell = client
532 .reconnect_shell(
533 "127.0.0.1",
534 "SH-EXISTING",
535 crate::soap::namespaces::RESOURCE_URI_CMD,
536 )
537 .await
538 .unwrap();
539 assert_eq!(shell.shell_id(), "SH-EXISTING");
540 shell.close().await.unwrap();
542 }
543
544 #[tokio::test]
545 async fn signal_ctrl_c_exercises_shell_method() {
546 let server = MockServer::start().await;
547 let port = server.address().port();
548
549 Mock::given(method("POST"))
551 .respond_with(ResponseTemplate::new(200).set_body_string(
552 r"<s:Envelope><s:Body><rsp:Shell><rsp:ShellId>SH-SIG</rsp:ShellId></rsp:Shell></s:Body></s:Envelope>",
553 ))
554 .up_to_n_times(1)
555 .mount(&server)
556 .await;
557
558 Mock::given(method("POST"))
560 .respond_with(
561 ResponseTemplate::new(200).set_body_string("<s:Envelope><s:Body/></s:Envelope>"),
562 )
563 .mount(&server)
564 .await;
565
566 let client = WinrmClient::new(basic_config(port), test_creds()).unwrap();
567 let shell = client.open_shell("127.0.0.1").await.unwrap();
568 shell.signal_ctrl_c("CMD-Y").await.unwrap();
569 }
570
571 #[tokio::test]
572 async fn start_command_returns_id() {
573 let server = MockServer::start().await;
574 let port = server.address().port();
575
576 Mock::given(method("POST"))
578 .respond_with(ResponseTemplate::new(200).set_body_string(
579 r"<s:Envelope><s:Body><rsp:Shell><rsp:ShellId>SH-START</rsp:ShellId></rsp:Shell></s:Body></s:Envelope>",
580 ))
581 .up_to_n_times(1)
582 .mount(&server)
583 .await;
584
585 Mock::given(method("POST"))
587 .respond_with(ResponseTemplate::new(200).set_body_string(
588 r"<s:Envelope><s:Body><rsp:CommandResponse><rsp:CommandId>SH-START-CMD</rsp:CommandId></rsp:CommandResponse></s:Body></s:Envelope>",
589 ))
590 .up_to_n_times(1)
591 .mount(&server)
592 .await;
593
594 Mock::given(method("POST"))
596 .respond_with(
597 ResponseTemplate::new(200).set_body_string("<s:Envelope><s:Body/></s:Envelope>"),
598 )
599 .mount(&server)
600 .await;
601
602 let client = WinrmClient::new(basic_config(port), test_creds()).unwrap();
603 let shell = client.open_shell("127.0.0.1").await.unwrap();
604 let cmd_id = shell.start_command("ping", &["localhost"]).await.unwrap();
605 assert_eq!(cmd_id, "SH-START-CMD");
606 }
607
608 #[tokio::test]
613 async fn shell_run_command_missing_exit_code_returns_minus_one() {
614 let server = MockServer::start().await;
615 let port = server.address().port();
616
617 Mock::given(method("POST"))
619 .respond_with(ResponseTemplate::new(200).set_body_string(
620 r"<s:Envelope><s:Body><rsp:Shell><rsp:ShellId>SH-NEG1</rsp:ShellId></rsp:Shell></s:Body></s:Envelope>",
621 ))
622 .up_to_n_times(1)
623 .mount(&server)
624 .await;
625
626 Mock::given(method("POST"))
628 .respond_with(ResponseTemplate::new(200).set_body_string(
629 r"<s:Envelope><s:Body><rsp:CommandResponse><rsp:CommandId>SH-NEG1-CMD</rsp:CommandId></rsp:CommandResponse></s:Body></s:Envelope>",
630 ))
631 .up_to_n_times(1)
632 .mount(&server)
633 .await;
634
635 Mock::given(method("POST"))
637 .respond_with(ResponseTemplate::new(200).set_body_string(
638 r#"<s:Envelope><s:Body><rsp:ReceiveResponse>
639 <rsp:CommandState CommandId="SH-NEG1-CMD" State="http://schemas.microsoft.com/wbem/wsman/1/windows/shell/CommandState/Done"/>
640 </rsp:ReceiveResponse></s:Body></s:Envelope>"#,
641 ))
642 .up_to_n_times(1)
643 .mount(&server)
644 .await;
645
646 Mock::given(method("POST"))
648 .respond_with(
649 ResponseTemplate::new(200).set_body_string("<s:Envelope><s:Body/></s:Envelope>"),
650 )
651 .mount(&server)
652 .await;
653
654 let client = WinrmClient::new(basic_config(port), test_creds()).unwrap();
655 let shell = client.open_shell("127.0.0.1").await.unwrap();
656 let output = shell.run_command("test", &[]).await.unwrap();
657 assert_eq!(output.exit_code, -1);
659 }
660
661 #[tokio::test]
666 async fn shell_timeout_duration_kills_plus_mutant() {
667 let server = MockServer::start().await;
668 let port = server.address().port();
669
670 Mock::given(method("POST"))
672 .respond_with(ResponseTemplate::new(200).set_body_string(
673 r"<s:Envelope><s:Body><rsp:Shell><rsp:ShellId>TO2-SHELL</rsp:ShellId></rsp:Shell></s:Body></s:Envelope>",
674 ))
675 .up_to_n_times(1)
676 .mount(&server)
677 .await;
678
679 Mock::given(method("POST"))
681 .respond_with(ResponseTemplate::new(200).set_body_string(
682 r"<s:Envelope><s:Body><rsp:CommandResponse><rsp:CommandId>TO2-CMD</rsp:CommandId></rsp:CommandResponse></s:Body></s:Envelope>",
683 ))
684 .up_to_n_times(1)
685 .mount(&server)
686 .await;
687
688 Mock::given(method("POST"))
690 .respond_with(
691 ResponseTemplate::new(200)
692 .set_body_string(
693 r#"<s:Envelope><s:Body><rsp:ReceiveResponse>
694 <rsp:CommandState CommandId="TO2-CMD" State="http://schemas.microsoft.com/wbem/wsman/1/windows/shell/CommandState/Done">
695 <rsp:ExitCode>0</rsp:ExitCode>
696 </rsp:CommandState>
697 </rsp:ReceiveResponse></s:Body></s:Envelope>"#,
698 )
699 .set_delay(std::time::Duration::from_millis(5500)),
700 )
701 .up_to_n_times(1)
702 .mount(&server)
703 .await;
704
705 Mock::given(method("POST"))
707 .respond_with(
708 ResponseTemplate::new(200).set_body_string("<s:Envelope><s:Body/></s:Envelope>"),
709 )
710 .mount(&server)
711 .await;
712
713 let config = WinrmConfig {
714 port,
715 auth_method: AuthMethod::Basic,
716 connect_timeout_secs: 30,
717 operation_timeout_secs: 3, ..Default::default()
719 };
720 let client = WinrmClient::new(config, test_creds()).unwrap();
721 let shell = client.open_shell("127.0.0.1").await.unwrap();
722 let result = shell.run_command("slow", &[]).await;
723
724 assert!(
727 result.is_ok(),
728 "should complete within 6s timeout (* 2), got: {:?}",
729 result.err()
730 );
731 assert_eq!(result.unwrap().exit_code, 0);
732 }
733
734 #[tokio::test]
742 async fn shell_timeout_duration_kills_div_mutant() {
743 let server = MockServer::start().await;
744 let port = server.address().port();
745
746 Mock::given(method("POST"))
748 .respond_with(ResponseTemplate::new(200).set_body_string(
749 r"<s:Envelope><s:Body><rsp:Shell><rsp:ShellId>TO3-SHELL</rsp:ShellId></rsp:Shell></s:Body></s:Envelope>",
750 ))
751 .up_to_n_times(1)
752 .mount(&server)
753 .await;
754
755 Mock::given(method("POST"))
757 .respond_with(ResponseTemplate::new(200).set_body_string(
758 r"<s:Envelope><s:Body><rsp:CommandResponse><rsp:CommandId>TO3-CMD</rsp:CommandId></rsp:CommandResponse></s:Body></s:Envelope>",
759 ))
760 .up_to_n_times(1)
761 .mount(&server)
762 .await;
763
764 Mock::given(method("POST"))
766 .respond_with(ResponseTemplate::new(200).set_body_string(
767 r#"<s:Envelope><s:Body><rsp:ReceiveResponse>
768 <rsp:CommandState CommandId="TO3-CMD" State="http://schemas.microsoft.com/wbem/wsman/1/windows/shell/CommandState/Done">
769 <rsp:ExitCode>0</rsp:ExitCode>
770 </rsp:CommandState>
771 </rsp:ReceiveResponse></s:Body></s:Envelope>"#,
772 ))
773 .up_to_n_times(1)
774 .mount(&server)
775 .await;
776
777 Mock::given(method("POST"))
779 .respond_with(
780 ResponseTemplate::new(200).set_body_string("<s:Envelope><s:Body/></s:Envelope>"),
781 )
782 .mount(&server)
783 .await;
784
785 let config = WinrmConfig {
786 port,
787 auth_method: AuthMethod::Basic,
788 connect_timeout_secs: 30,
789 operation_timeout_secs: 1, ..Default::default()
791 };
792 let client = WinrmClient::new(config, test_creds()).unwrap();
793 let shell = client.open_shell("127.0.0.1").await.unwrap();
794 let result = shell.run_command("fast", &[]).await;
795
796 assert!(
799 result.is_ok(),
800 "instant response should succeed with 2s timeout, got: {:?}",
801 result.err()
802 );
803 }
804
805 #[tracing_test::traced_test]
809 #[tokio::test]
810 async fn shell_drop_without_close_emits_warning() {
811 let server = MockServer::start().await;
812 let port = server.address().port();
813
814 Mock::given(method("POST"))
816 .respond_with(ResponseTemplate::new(200).set_body_string(
817 r"<s:Envelope><s:Body><rsp:Shell><rsp:ShellId>DROP-WARN</rsp:ShellId></rsp:Shell></s:Body></s:Envelope>",
818 ))
819 .up_to_n_times(1)
820 .mount(&server)
821 .await;
822
823 Mock::given(method("POST"))
825 .respond_with(
826 ResponseTemplate::new(200).set_body_string("<s:Envelope><s:Body/></s:Envelope>"),
827 )
828 .mount(&server)
829 .await;
830
831 let client = WinrmClient::new(basic_config(port), test_creds()).unwrap();
832
833 {
834 let shell = client.open_shell("127.0.0.1").await.unwrap();
835 assert_eq!(shell.shell_id(), "DROP-WARN");
836 }
838
839 assert!(logs_contain("shell dropped without close"));
843 }
844
845 #[tracing_test::traced_test]
849 #[tokio::test]
850 async fn shell_close_does_not_emit_drop_warning() {
851 let server = MockServer::start().await;
852 let port = server.address().port();
853
854 Mock::given(method("POST"))
856 .respond_with(ResponseTemplate::new(200).set_body_string(
857 r"<s:Envelope><s:Body><rsp:Shell><rsp:ShellId>DROP-OK</rsp:ShellId></rsp:Shell></s:Body></s:Envelope>",
858 ))
859 .up_to_n_times(1)
860 .mount(&server)
861 .await;
862
863 Mock::given(method("POST"))
865 .respond_with(
866 ResponseTemplate::new(200).set_body_string("<s:Envelope><s:Body/></s:Envelope>"),
867 )
868 .mount(&server)
869 .await;
870
871 let client = WinrmClient::new(basic_config(port), test_creds()).unwrap();
872 let shell = client.open_shell("127.0.0.1").await.unwrap();
873 shell.close().await.unwrap();
874
875 assert!(!logs_contain("shell dropped without close"));
878 }
879
880 #[tokio::test]
882 async fn resource_uri_matches_default_cmd() {
883 let server = MockServer::start().await;
884 let port = server.address().port();
885
886 Mock::given(method("POST"))
887 .respond_with(ResponseTemplate::new(200).set_body_string(
888 r"<s:Envelope><s:Body><rsp:Shell><rsp:ShellId>SH-URI</rsp:ShellId></rsp:Shell></s:Body></s:Envelope>",
889 ))
890 .up_to_n_times(1)
891 .mount(&server)
892 .await;
893
894 Mock::given(method("POST"))
896 .respond_with(
897 ResponseTemplate::new(200).set_body_string("<s:Envelope><s:Body/></s:Envelope>"),
898 )
899 .mount(&server)
900 .await;
901
902 let client = WinrmClient::new(basic_config(port), test_creds()).unwrap();
903 let shell = client.open_shell("127.0.0.1").await.unwrap();
904 assert!(
905 shell.resource_uri().contains("cmd"),
906 "resource_uri should contain 'cmd', got: {}",
907 shell.resource_uri()
908 );
909 }
910
911 #[tokio::test]
913 async fn start_command_with_id_returns_server_command_id() {
914 let server = MockServer::start().await;
915 let port = server.address().port();
916
917 Mock::given(method("POST"))
919 .respond_with(ResponseTemplate::new(200).set_body_string(
920 r"<s:Envelope><s:Body><rsp:Shell><rsp:ShellId>SH-WCID</rsp:ShellId></rsp:Shell></s:Body></s:Envelope>",
921 ))
922 .up_to_n_times(1)
923 .mount(&server)
924 .await;
925
926 Mock::given(method("POST"))
928 .respond_with(ResponseTemplate::new(200).set_body_string(
929 r"<s:Envelope><s:Body><rsp:CommandResponse><rsp:CommandId>MY-CMD-ID</rsp:CommandId></rsp:CommandResponse></s:Body></s:Envelope>",
930 ))
931 .up_to_n_times(1)
932 .mount(&server)
933 .await;
934
935 Mock::given(method("POST"))
937 .respond_with(
938 ResponseTemplate::new(200).set_body_string("<s:Envelope><s:Body/></s:Envelope>"),
939 )
940 .mount(&server)
941 .await;
942
943 let client = WinrmClient::new(basic_config(port), test_creds()).unwrap();
944 let shell = client.open_shell("127.0.0.1").await.unwrap();
945 let cmd_id = shell
946 .start_command_with_id("test", &[], "MY-CMD-ID")
947 .await
948 .unwrap();
949 assert_eq!(cmd_id, "MY-CMD-ID");
950 }
951
952 #[tokio::test]
955 async fn send_input_psrp_path_when_command_id_is_shell_id() {
956 let server = MockServer::start().await;
957 let port = server.address().port();
958
959 Mock::given(method("POST"))
961 .respond_with(
962 ResponseTemplate::new(200).set_body_string("<s:Envelope><s:Body/></s:Envelope>"),
963 )
964 .mount(&server)
965 .await;
966
967 let client = WinrmClient::new(basic_config(port), test_creds()).unwrap();
968 let shell = client
969 .reconnect_shell(
970 "127.0.0.1",
971 "PSRP-SHELL",
972 crate::soap::namespaces::RESOURCE_URI_PSRP,
973 )
974 .await
975 .unwrap();
976 shell
978 .send_input("PSRP-SHELL", b"data", false)
979 .await
980 .unwrap();
981 shell.send_input("", b"data", false).await.unwrap();
983 }
984
985 #[tokio::test]
987 async fn receive_next_with_real_command_id() {
988 let server = MockServer::start().await;
989 let port = server.address().port();
990
991 Mock::given(method("POST"))
993 .respond_with(ResponseTemplate::new(200).set_body_string(
994 r"<s:Envelope><s:Body><rsp:Shell><rsp:ShellId>SH-RECV</rsp:ShellId></rsp:Shell></s:Body></s:Envelope>",
995 ))
996 .up_to_n_times(1)
997 .mount(&server)
998 .await;
999
1000 Mock::given(method("POST"))
1002 .respond_with(ResponseTemplate::new(200).set_body_string(
1003 r#"<s:Envelope><s:Body><rsp:ReceiveResponse>
1004 <rsp:Stream Name="stdout" CommandId="RECV-CMD">YWJD</rsp:Stream>
1005 <rsp:CommandState CommandId="RECV-CMD" State="http://schemas.microsoft.com/wbem/wsman/1/windows/shell/CommandState/Done">
1006 <rsp:ExitCode>0</rsp:ExitCode>
1007 </rsp:CommandState>
1008 </rsp:ReceiveResponse></s:Body></s:Envelope>"#,
1009 ))
1010 .up_to_n_times(1)
1011 .mount(&server)
1012 .await;
1013
1014 Mock::given(method("POST"))
1016 .respond_with(
1017 ResponseTemplate::new(200).set_body_string("<s:Envelope><s:Body/></s:Envelope>"),
1018 )
1019 .mount(&server)
1020 .await;
1021
1022 let client = WinrmClient::new(basic_config(port), test_creds()).unwrap();
1023 let shell = client.open_shell("127.0.0.1").await.unwrap();
1024 let output = shell.receive_next("RECV-CMD").await.unwrap();
1025 assert!(output.done);
1026 assert!(!output.stdout.is_empty());
1027 }
1028}