winrm_rs/config.rs
1// Configuration types for the winrm-rs crate.
2//
3// Contains WinrmConfig, AuthMethod, and WinrmCredentials extracted from client.rs.
4
5use secrecy::SecretString;
6
7/// Configuration for a [`WinrmClient`](crate::WinrmClient) connection.
8///
9/// Controls the transport (HTTP vs HTTPS), timeouts, and authentication
10/// method. Use [`Default::default()`] for sensible defaults (HTTP on port
11/// 5985, NTLM auth, 30 s connect / 60 s operation timeouts).
12#[derive(Debug, Clone)]
13pub struct WinrmConfig {
14 /// TCP port of the WinRM listener (default: 5985 for HTTP, 5986 for HTTPS).
15 pub port: u16,
16 /// Whether to connect over HTTPS. When `true`, the endpoint URL uses `https://`.
17 pub use_tls: bool,
18 /// Accept invalid or self-signed TLS certificates.
19 ///
20 /// # Security
21 ///
22 /// Setting this to `true` disables chain verification on **both** the
23 /// regular HTTPS transport and the CredSSP outer TLS leg, leaving the
24 /// connection vulnerable to MITM. Credentials transmitted over Basic,
25 /// NTLM, or CredSSP can then be intercepted.
26 ///
27 /// Use **only** in test environments. The constructor logs a
28 /// `tracing::warn` when this flag is enabled.
29 pub accept_invalid_certs: bool,
30 /// TCP connect timeout in seconds (default: 30).
31 pub connect_timeout_secs: u64,
32 /// WS-Management `OperationTimeout` in seconds (default: 60). The HTTP
33 /// client timeout is set to this value plus 10 seconds to allow the server
34 /// to respond before the transport gives up.
35 pub operation_timeout_secs: u64,
36 /// Authentication method to use for all requests (default: [`AuthMethod::Ntlm`]).
37 pub auth_method: AuthMethod,
38 /// Maximum SOAP envelope size in bytes (default: 153600).
39 ///
40 /// Controls the `MaxEnvelopeSize` header sent in every WS-Management request.
41 /// Increase this for hosts that return large responses.
42 pub max_envelope_size: u32,
43 /// Maximum number of retries for transient HTTP errors (default: 0 = no retry).
44 ///
45 /// Uses exponential backoff starting at 100 ms (100, 200, 400, ...).
46 /// Only `WinrmError::Http` errors trigger a retry; authentication and
47 /// SOAP faults are returned immediately.
48 pub max_retries: u32,
49 /// Path to client certificate PEM file (for `AuthMethod::Certificate`).
50 pub client_cert_pem: Option<String>,
51 /// Path to client private key PEM file (for `AuthMethod::Certificate`).
52 pub client_key_pem: Option<String>,
53 /// HTTP proxy URL (e.g. `"http://proxy:8080"`).
54 ///
55 /// When set, all WinRM HTTP(S) requests are routed through this proxy.
56 pub proxy: Option<String>,
57 /// Console output code page (default: 65001 = UTF-8).
58 ///
59 /// Controls the `WINRS_CODEPAGE` option in the shell creation envelope.
60 /// Common values: 65001 (UTF-8), 437 (US), 850 (Western European).
61 pub codepage: u32,
62 /// Initial working directory for the remote shell (default: `None`).
63 ///
64 /// When set, the shell starts in this directory. Equivalent to running
65 /// `cd <path>` before any command.
66 pub working_directory: Option<String>,
67 /// Environment variables to set in the remote shell (default: empty).
68 ///
69 /// Each `(key, value)` pair is injected into the shell's environment
70 /// at creation time via `<rsp:Environment>`.
71 pub env_vars: Vec<(String, String)>,
72 /// Message encryption mode for NTLM (default: [`EncryptionMode::Auto`]).
73 ///
74 /// Controls whether NTLM sealing is applied to SOAP message bodies.
75 pub encryption: EncryptionMode,
76 /// Custom HTTP `User-Agent` header (default: `None` = `winrm-rs/<version>`).
77 pub user_agent: Option<String>,
78 /// Shell idle timeout in seconds (default: `None` = server default).
79 ///
80 /// When set, the shell will be automatically closed by the server
81 /// after this many seconds of inactivity.
82 pub idle_timeout_secs: Option<u64>,
83 /// Maximum cumulative bytes accepted from the server for a single command's
84 /// stdout+stderr (default: `Some(64 * 1024 * 1024)` = 64 MiB).
85 ///
86 /// Prevents a malicious or runaway server from exhausting client memory
87 /// by streaming an unbounded base64 chunk sequence. When the cap is
88 /// reached, the operation returns [`crate::error::WinrmError::Transfer`]
89 /// and the shell is best-effort closed. Set to `None` to disable the
90 /// cap (pre-1.1 behaviour).
91 pub max_output_bytes: Option<usize>,
92}
93
94impl Default for WinrmConfig {
95 fn default() -> Self {
96 Self {
97 port: 5985,
98 use_tls: false,
99 accept_invalid_certs: false,
100 connect_timeout_secs: 30,
101 operation_timeout_secs: 60,
102 auth_method: AuthMethod::Ntlm,
103 max_envelope_size: 153_600,
104 max_retries: 0,
105 client_cert_pem: None,
106 client_key_pem: None,
107 proxy: None,
108 encryption: EncryptionMode::Auto,
109 user_agent: None,
110 codepage: 65001,
111 working_directory: None,
112 env_vars: Vec::new(),
113 idle_timeout_secs: None,
114 max_output_bytes: Some(64 * 1024 * 1024),
115 }
116 }
117}
118
119/// Controls whether NTLM message encryption (sealing) is applied to SOAP bodies.
120///
121/// When using HTTP (not HTTPS), NTLM sealing encrypts the SOAP body to prevent
122/// eavesdropping. When using HTTPS, the TLS layer provides encryption, making
123/// sealing redundant.
124#[derive(Debug, Clone, Default, PartialEq)]
125pub enum EncryptionMode {
126 /// Encrypt when using HTTP, skip when using HTTPS (default).
127 #[default]
128 Auto,
129 /// Always encrypt SOAP bodies, even over HTTPS.
130 Always,
131 /// Never encrypt SOAP bodies. **Use only for debugging.**
132 Never,
133}
134
135/// Authentication method for the WinRM HTTP transport.
136#[derive(Debug, Clone)]
137pub enum AuthMethod {
138 /// HTTP Basic authentication. Credentials are sent as a base64-encoded
139 /// `Authorization` header on every request. Only safe over HTTPS.
140 Basic,
141 /// NTLMv2 challenge/response authentication (default). Performs a three-step
142 /// handshake (negotiate, challenge, authenticate) per request.
143 Ntlm,
144 /// Kerberos authentication (requires `kerberos` feature and prior `kinit`).
145 ///
146 /// Uses SPNEGO Negotiate tokens via the system Kerberos library.
147 /// The service principal is derived as `HTTP/<hostname>`.
148 Kerberos,
149 /// TLS client certificate authentication.
150 ///
151 /// The `reqwest::Client` is configured with the client cert at construction
152 /// time. No `Authorization` header is sent; the TLS handshake authenticates.
153 Certificate,
154 /// CredSSP authentication for double-hop credential delegation.
155 ///
156 /// Wraps NTLM or Kerberos inside a TLS channel, allowing credentials
157 /// to be delegated to the remote host for accessing network resources.
158 /// Requires HTTPS (`use_tls = true`).
159 ///
160 /// **Not yet implemented** — will return an error at runtime.
161 CredSsp,
162}
163
164/// Credentials for WinRM authentication.
165///
166/// The password is stored as a [`SecretString`] — zeroized on drop and
167/// redacted in `Debug` output. Use [`WinrmCredentials::new`] to construct.
168///
169/// For NTLM, if [`domain`](Self::domain) is empty the client will use the
170/// domain advertised by the server in its Type 2 challenge message.
171#[derive(Clone)]
172pub struct WinrmCredentials {
173 /// Windows account name (e.g. `"administrator"`).
174 pub username: String,
175 /// Password — stored as SecretString, zeroized on drop, redacted in Debug.
176 pub password: SecretString,
177 /// Optional NetBIOS domain. Leave empty to auto-detect from the NTLM
178 /// challenge.
179 pub domain: String,
180}
181
182impl WinrmCredentials {
183 /// Create new credentials.
184 ///
185 /// Wraps the password in a [`SecretString`] for automatic zeroization.
186 pub fn new(
187 username: impl Into<String>,
188 password: impl Into<String>,
189 domain: impl Into<String>,
190 ) -> Self {
191 Self {
192 username: username.into(),
193 password: SecretString::from(password.into()),
194 domain: domain.into(),
195 }
196 }
197}
198
199impl std::fmt::Debug for WinrmCredentials {
200 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
201 f.debug_struct("WinrmCredentials")
202 .field("username", &self.username)
203 .field("password", &"[REDACTED]")
204 .field("domain", &self.domain)
205 .finish()
206 }
207}
208
209#[cfg(test)]
210mod tests {
211 use super::*;
212 use secrecy::ExposeSecret;
213
214 #[test]
215 fn default_config_uses_http_and_ntlm() {
216 let config = WinrmConfig::default();
217 assert_eq!(config.port, 5985);
218 assert!(!config.use_tls);
219 assert!(matches!(config.auth_method, AuthMethod::Ntlm));
220 }
221
222 #[test]
223 fn default_config_max_envelope_size() {
224 let config = WinrmConfig::default();
225 assert_eq!(config.max_envelope_size, 153_600);
226 }
227
228 #[test]
229 fn custom_max_envelope_size() {
230 let config = WinrmConfig {
231 max_envelope_size: 512_000,
232 ..Default::default()
233 };
234 assert_eq!(config.max_envelope_size, 512_000);
235 }
236
237 #[test]
238 fn default_config_max_retries_is_zero() {
239 let config = WinrmConfig::default();
240 assert_eq!(config.max_retries, 0);
241 }
242
243 #[test]
244 fn default_config_has_no_proxy() {
245 let config = WinrmConfig::default();
246 assert!(config.proxy.is_none());
247 }
248
249 #[test]
250 fn default_config_has_no_cert_paths() {
251 let config = WinrmConfig::default();
252 assert!(config.client_cert_pem.is_none());
253 assert!(config.client_key_pem.is_none());
254 }
255
256 #[test]
257 fn credentials_new_constructor() {
258 let creds = WinrmCredentials::new("admin", "s3cret", "DOMAIN");
259 assert_eq!(creds.username, "admin");
260 assert_eq!(creds.password.expose_secret(), "s3cret");
261 assert_eq!(creds.domain, "DOMAIN");
262 }
263
264 #[test]
265 fn credentials_debug_redacts_password() {
266 let creds = WinrmCredentials::new("admin", "super-secret-password", "DOM");
267 let debug_output = format!("{creds:?}");
268 assert!(debug_output.contains("admin"));
269 assert!(debug_output.contains("DOM"));
270 assert!(debug_output.contains("[REDACTED]"));
271 assert!(!debug_output.contains("super-secret-password"));
272 }
273
274 #[test]
275 fn credentials_expose_secret_works() {
276 let creds = WinrmCredentials::new("user", "my-password", "");
277 let exposed: &str = creds.password.expose_secret();
278 assert_eq!(exposed, "my-password");
279 }
280
281 #[test]
282 fn auth_method_kerberos_variant_exists() {
283 let method = AuthMethod::Kerberos;
284 let debug = format!("{method:?}");
285 assert!(debug.contains("Kerberos"));
286 }
287
288 #[test]
289 fn auth_method_certificate_variant_exists() {
290 let method = AuthMethod::Certificate;
291 let debug = format!("{method:?}");
292 assert!(debug.contains("Certificate"));
293 }
294}