1use reqwest::blocking::Client;
2use reqwest::header::{HeaderMap, HeaderValue, AUTHORIZATION, CONTENT_TYPE, CONTENT_LENGTH};
3use std::time::Duration;
4use std::io::Read;
5use base64::{Engine as _, engine::general_purpose};
6use anyhow::Result;
7use crate::error::WinRMError;
8use sspi::{
9 AuthIdentity, CredentialUse, DataRepresentation, Ntlm, Negotiate, Sspi, SspiImpl, SecurityBuffer, BufferType,
10 Username, ClientRequestFlags,
11};
12
13pub struct WinRMClient {
14 endpoint: String,
15 user: String,
16 password: String,
17 client: Client,
18 auth_method: String,
19}
20
21impl WinRMClient {
22 pub fn new(endpoint: &str, user: &str, password: &str, auth_method: &str, insecure: bool, cacert: Option<String>) -> Result<Self> {
23 let mut headers = HeaderMap::new();
24 headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/soap+xml;charset=UTF-8"));
25 headers.insert("User-Agent", HeaderValue::from_static("Rust WinRM Client"));
26
27 let mut builder = Client::builder()
28 .default_headers(headers)
29 .timeout(Duration::from_secs(60))
30 .http1_only() .cookie_store(true); if insecure {
34 builder = builder.danger_accept_invalid_certs(true);
35 }
36
37 if let Some(cert_path) = cacert {
38 let mut buf = Vec::new();
39 std::fs::File::open(cert_path)?.read_to_end(&mut buf)?;
40 let cert = reqwest::Certificate::from_pem(&buf)?;
41 builder = builder.add_root_certificate(cert);
42 }
43
44 let client = builder.build()?;
45
46 Ok(WinRMClient {
47 endpoint: endpoint.to_string(),
48 user: user.to_string(),
49 password: password.to_string(),
50 client,
51 auth_method: auth_method.to_string(),
52 })
53 }
54
55 fn send_message(&mut self, body: &str) -> Result<String> {
56 let mut response = self.client.post(&self.endpoint)
57 .header(CONTENT_TYPE, "application/soap+xml;charset=UTF-8")
58 .body(body.to_string())
59 .send()?;
60
61
62 if response.status() == 401 && self.auth_method == "ntlm" {
63 return self.handle_ntlm_negotiation(body);
64 } else if response.status() == 401 && self.auth_method == "basic" {
65 response = self.client.post(&self.endpoint)
67 .header(AUTHORIZATION, format!("Basic {}", general_purpose::STANDARD.encode(format!("{}:{}", self.user, self.password))))
68 .header(CONTENT_TYPE, "application/soap+xml;charset=UTF-8")
69 .body(body.to_string())
70 .send()?;
71 } else if response.status() == 401 && self.auth_method == "kerberos" {
72 return self.handle_kerberos_negotiation(body);
73 }
74
75 if response.status() != 200 {
76 return Err(WinRMError::ConnectionError {
77 endpoint: self.endpoint.clone(),
78 details: format!("HTTP {}: {}", response.status(), response.text().unwrap_or_default())
79 }.into());
80 }
81
82 Ok(response.text()?)
83 }
84
85 fn handle_ntlm_negotiation(&mut self, original_body: &str) -> Result<String> {
86 use sspi::SecurityStatus;
87
88 let mut ntlm = Ntlm::new();
89
90 let (domain, user) = if let Some((d, u)) = self.user.split_once('\\') {
91 (d, u)
92 } else {
93 ("", self.user.as_str())
94 };
95
96 let username = if domain.is_empty() {
97 Username::new(user, None).unwrap()
98 } else {
99 Username::new(user, Some(domain)).unwrap()
100 };
101
102 let identity = AuthIdentity {
103 username,
104 password: self.password.clone().into(),
105 };
106
107 let mut acq_cred_result = ntlm.acquire_credentials_handle()
109 .with_credential_use(CredentialUse::Outbound)
110 .with_auth_data(&identity)
111 .execute(&mut ntlm)?;
112
113 let mut output_buffer = vec![SecurityBuffer::new(Vec::with_capacity(4096), BufferType::Token)];
114 let target_name = self.endpoint.clone();
115
116 let mut builder = ntlm.initialize_security_context()
118 .with_credentials_handle(&mut acq_cred_result.credentials_handle)
119 .with_context_requirements(ClientRequestFlags::CONFIDENTIALITY | ClientRequestFlags::ALLOCATE_MEMORY)
120 .with_target_data_representation(DataRepresentation::Native)
121 .with_target_name(&target_name)
122 .with_output(&mut output_buffer);
123
124 let mut result = ntlm.initialize_security_context_impl(&mut builder)?
125 .resolve_to_result()?;
126
127 let type1_bytes = output_buffer[0].buffer.clone();
128 let type1_b64 = general_purpose::STANDARD.encode(&type1_bytes);
129
130 let response = self.client.post(&self.endpoint)
134 .header(AUTHORIZATION, format!("Negotiate {}", type1_b64))
135 .header(CONTENT_LENGTH, "0")
136 .body("")
137 .send()?;
138
139 if response.status() != 401 {
142 return Err(WinRMError::AuthenticationFailed {
143 method: "NTLM".to_string(),
144 reason: format!("Expected 401 after Type 1 message, got: {}. Server may not support NTLM.", response.status())
145 }.into());
146 }
147
148 let auth_header_val = response.headers().get("WWW-Authenticate")
150 .ok_or_else(|| WinRMError::InvalidResponse {
151 details: "Missing WWW-Authenticate header in NTLM response. Server may not be configured for NTLM authentication.".to_string()
152 })?
153 .to_str()?;
154
155 if !auth_header_val.starts_with("Negotiate ") {
156 return Err(WinRMError::InvalidResponse {
157 details: format!("Expected 'Negotiate' challenge, got: '{}'. Server authentication scheme mismatch.", auth_header_val)
158 }.into());
159 }
160
161 let challenge_b64 = &auth_header_val[10..];
162 let challenge_bytes = general_purpose::STANDARD.decode(challenge_b64)?;
163
164 let mut input_buffer = vec![SecurityBuffer::new(challenge_bytes, BufferType::Token)];
167
168 loop {
170 output_buffer[0].buffer.clear();
171
172 let mut builder = ntlm.initialize_security_context()
173 .with_credentials_handle(&mut acq_cred_result.credentials_handle)
174 .with_context_requirements(ClientRequestFlags::CONFIDENTIALITY | ClientRequestFlags::ALLOCATE_MEMORY)
175 .with_target_data_representation(DataRepresentation::Native)
176 .with_target_name(&target_name)
177 .with_input(&mut input_buffer)
178 .with_output(&mut output_buffer);
179
180 result = ntlm.initialize_security_context_impl(&mut builder)?
181 .resolve_to_result()?;
182
183 if [SecurityStatus::CompleteAndContinue, SecurityStatus::CompleteNeeded].contains(&result.status) {
185 println!("Completing the token...");
186 ntlm.complete_auth_token(&mut output_buffer)?;
187 }
188
189 let type3_bytes = output_buffer[0].buffer.clone();
191 let type3_b64 = general_purpose::STANDARD.encode(&type3_bytes);
192
193 let response = self.client.post(&self.endpoint)
196 .header(AUTHORIZATION, format!("Negotiate {}", type3_b64))
197 .header(CONTENT_TYPE, "application/soap+xml;charset=UTF-8")
198 .body(original_body.to_string())
199 .send()?;
200
201 let status = response.status();
202 let response_text = response.text().unwrap_or_default();
203
204 if status != 200 {
205 println!("Response body: {}", response_text);
206 return Err(WinRMError::AuthenticationFailed {
207 method: "NTLM".to_string(),
208 reason: format!("Server returned {} after authentication handshake. Check credentials and server configuration.", status)
209 }.into());
210 }
211
212 if ![SecurityStatus::CompleteAndContinue, SecurityStatus::ContinueNeeded].contains(&result.status) {
214 return Ok(response_text);
216 }
217
218 input_buffer[0].buffer.clear();
219 }
220 }
221
222 fn handle_kerberos_negotiation(&mut self, original_body: &str) -> Result<String> {
223 Err(WinRMError::AuthenticationFailed {
226 method: "Kerberos".to_string(),
227 reason: "Kerberos desteği şu an aktif değil. Lütfen --auth ntlm kullanın.".to_string()
228 }.into())
229 }
230
231 pub fn open_shell(&mut self) -> Result<String> {
232 let header = crate::xml::create_header(
233 "http://schemas.xmlsoap.org/ws/2004/09/transfer/Create",
234 "http://schemas.microsoft.com/wbem/wsman/1/windows/shell/cmd",
235 None,
236 &self.endpoint
237 );
238 let body = crate::xml::create_shell_body();
239 let envelope = crate::xml::create_envelope(&header, &body);
240
241 let response = self.send_message(&envelope)?;
242
243 if let Some(start) = response.find("Name=\"ShellId\">") {
244 let rest = &response[start + 15..];
245 if let Some(end) = rest.find('<') {
246 return Ok(rest[..end].to_string());
247 }
248 }
249
250 Err(WinRMError::InvalidResponse {
251 details: "ShellId not found in server response. Server may not support WinRM shell operations or response format is invalid.".to_string()
252 }.into())
253 }
254
255 pub fn run_command(&mut self, shell_id: &str, command: &str) -> Result<String> {
256 let header = crate::xml::create_header(
257 "http://schemas.microsoft.com/wbem/wsman/1/windows/shell/Command",
258 "http://schemas.microsoft.com/wbem/wsman/1/windows/shell/cmd",
259 Some(shell_id),
260 &self.endpoint
261 );
262
263 let header = header.replace("</env:Header>", r#"<w:OptionSet><w:Option Name="WINRS_CONSOLEMODE_STDIN">TRUE</w:Option><w:Option Name="WINRS_SKIP_CMD_SHELL">FALSE</w:Option></w:OptionSet></env:Header>"#);
264
265 let body = crate::xml::create_command_body(command);
266 let envelope = crate::xml::create_envelope(&header, &body);
267
268 let response = self.send_message(&envelope)?;
269
270 if let Some(start) = response.find("CommandId=\"") {
271 let rest = &response[start + 11..];
272 if let Some(end) = rest.find('\"') {
273 return Ok(rest[..end].to_string());
274 }
275 }
276 if let Some(start) = response.find("CommandId>") {
277 let rest = &response[start + 10..];
278 if let Some(end) = rest.find('<') {
279 return Ok(rest[..end].to_string());
280 }
281 }
282
283 Err(WinRMError::InvalidResponse {
284 details: "CommandId not found in server response. Command may have failed to start.".to_string()
285 }.into())
286 }
287
288 pub fn get_command_output(&mut self, shell_id: &str, command_id: &str) -> Result<(String, String, i32)> {
289 let header = crate::xml::create_header(
290 "http://schemas.microsoft.com/wbem/wsman/1/windows/shell/Receive",
291 "http://schemas.microsoft.com/wbem/wsman/1/windows/shell/cmd",
292 Some(shell_id),
293 &self.endpoint
294 );
295 let body = crate::xml::create_receive_body(command_id);
296 let envelope = crate::xml::create_envelope(&header, &body);
297
298 let mut stdout = String::new();
299 let mut stderr = String::new();
300 let mut exit_code = 0;
301 let mut done = false;
302
303 while !done {
304 let response = self.send_message(&envelope)?;
305
306 let mut current_pos = 0;
307 while let Some(start) = response[current_pos..].find("<rsp:Stream") {
308 let stream_start = current_pos + start;
309 let rest = &response[stream_start..];
310
311 if let Some(close_tag) = rest.find("</rsp:Stream>") {
312 let content_start = rest.find('>').unwrap() + 1;
313 let content = &rest[content_start..close_tag];
314
315 let is_stdout = rest.contains("Name=\"stdout\"");
316 let is_stderr = rest.contains("Name=\"stderr\"");
317
318 if let Ok(decoded) = general_purpose::STANDARD.decode(content) {
319 let decoded_str = String::from_utf8_lossy(&decoded);
320 if is_stdout {
321 stdout.push_str(&decoded_str);
322 } else if is_stderr {
323 stderr.push_str(&decoded_str);
324 }
325 }
326
327 current_pos = stream_start + close_tag + 13;
328 } else {
329 break;
330 }
331 }
332
333 if response.contains("State=\"http://schemas.microsoft.com/wbem/wsman/1/windows/shell/CommandState/Done\"") {
334 done = true;
335 if let Some(start) = response.find("<rsp:ExitCode>") {
336 let rest = &response[start + 14..];
337 if let Some(end) = rest.find('<') {
338 exit_code = rest[..end].parse().unwrap_or(0);
339 }
340 }
341 }
342 }
343
344 Ok((stdout, stderr, exit_code))
345 }
346
347 pub fn close_shell(&mut self, shell_id: &str) -> Result<()> {
348 let header = crate::xml::create_header(
349 "http://schemas.xmlsoap.org/ws/2004/09/transfer/Delete",
350 "http://schemas.microsoft.com/wbem/wsman/1/windows/shell/cmd",
351 Some(shell_id),
352 &self.endpoint
353 );
354 let body = crate::xml::create_delete_body();
355 let envelope = crate::xml::create_envelope(&header, &body);
356
357 self.send_message(&envelope)?;
358
359 Ok(())
360 }
361}