librust_winrm/
client.rs

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()  // WinRM requires HTTP/1.1
31            .cookie_store(true); // Important for session persistence
32
33        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             // Basic Auth
66             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        // Acquire credentials
108        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        // Step 1: Initialize security context (Type 1 message)
117        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        // Removed: log_verbose(&format!("Sending Type 1: Negotiate {}...", &type1_b64[..std::cmp::min(20, type1_b64.len())]));
131
132        // Send Type 1 message
133        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        // Removed: log_verbose(&format!("Response Status: {}", response.status()));
140
141        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        // Parse Type 2 challenge
149        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        // Removed: log_verbose("Received Type 2 challenge");
165
166        let mut input_buffer = vec![SecurityBuffer::new(challenge_bytes, BufferType::Token)];
167
168        // Loop for completing NTLM handshake
169        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            // Check if we need to complete the token
184            if [SecurityStatus::CompleteAndContinue, SecurityStatus::CompleteNeeded].contains(&result.status) {
185                println!("Completing the token...");
186                ntlm.complete_auth_token(&mut output_buffer)?;
187            }
188
189            // This is our Type 3 message - send it with the actual SOAP body
190            let type3_bytes = output_buffer[0].buffer.clone();
191            let type3_b64 = general_purpose::STANDARD.encode(&type3_bytes);
192            
193            // Removed: log_verbose("Sending Type 3 with actual SOAP request");
194            
195            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            // Break if we don't need to continue
213            if ![SecurityStatus::CompleteAndContinue, SecurityStatus::ContinueNeeded].contains(&result.status) {
214                // Removed: log_verbose("NTLM handshake complete!");
215                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        // NOT: Negotiate için sspi crate örnek kod eksik
224        // Şimdilik basit stub - gerçek implementation daha sonra
225        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}