diff --git a/README.md b/README.md
index a40ab5d..5732565 100644
--- a/README.md
+++ b/README.md
@@ -7,13 +7,14 @@
See **[Project Documentation](https://metricshub.org/winrm-java)** and the [Javadoc](https://metricshub.org/winrm-java/apidocs) for more information on how to use this library in your code.
-The Windows Remote Management (WinRM) Java Client is a library that enables to:
-* Connect to a remote Windows server using one of three authentication types (NTLM, Kerberos, or Basic)
-* Execute WMI Query Language (WQL) queries which uses HTTP/HTTPS protocols.
+The Windows Remote Management (WinRM) Java Client is a library that lets you:
+* Connect to a remote Windows server over HTTP or HTTPS with NTLM, Kerberos, or Basic authentication
+* Run WMI Query Language (WQL) queries and remote commands (`cmd.exe` or PowerShell)
+* Upload, read, list, and download remote files
> ## ⚠️ Upgrading from 1.x
>
-> Version 2.0.0 **removed the legacy Apache CXF backend**: the dependency-free **light** client is
+> Version 2.0.00 **removed the legacy Apache CXF backend**: the dependency-free **light** client is
> the only implementation (same documented entry points; a few CXF/SMB-only public types were
> removed). The main consequence:
>
@@ -101,18 +102,10 @@ try (WinRMClient client = WinRMClient.builder("server01.acme.com")
}
```
-Connection-scoped options on the builder: `https()`, `port(int)`,
-`authentication(AuthScheme.KERBEROS, AuthScheme.NTLM)` (ordered fallback; NTLM is the default —
-Kerberos in the list requires `https()`),
-`ticketCache(Path)`, `namespace(String)`, `trustAllCertificates()` (per-client alternative to the
-`org.metricshub.winrm.tls.insecure` system property; insecure, testing only), and
-`sslContext(SSLContext)` for a dedicated trust store.
-
-Per-operation options: `namespace(...)`, `timeout(...)`, and for WQL enumeration tuning
-`pageSize(int)` (WS-Enumeration `MaxElements`, 32000 by default) and `pullTimeout(Duration)`
-(`MaxTime` per Pull). Commands accept `workingDirectory(String)`, `charset(Charset)` (see
-[Character encoding](#character-encoding)), and `upload(Path...)` to copy local script files and
-rewrite the command to reference the remote copies.
+Every builder and request option (transport, authentication, TLS, timeouts, retries, working
+directory, environment, stdin, uploads, ...) is described on the
+[project site](https://metricshub.org/winrm-java) and in the
+[Javadoc](https://metricshub.org/winrm-java/apidocs).
### Character encoding
@@ -218,7 +211,7 @@ cleanup, and command-line substitution — are documented on the
upgrade warning above); `trustAllCertificates()` on the builder or
`-Dorg.metricshub.winrm.tls.insecure=true` trusts all certificates (insecure, testing only).
Kerberos uses the ambient Kerberos configuration (`krb5.conf` / `-Djava.security.krb5.*`) unless
-the command-line KDC and realm options described below are used.
+the CLI's `--kerberos-kdc` / `--kerberos-realm` options are used.
### Legacy API
@@ -289,7 +282,7 @@ server, so no Windows host is needed in CI.
### Live run against a real host
`WinRMLiveTest` runs a WQL query and a command against a **real** WinRM host (the successor of
-the pre-2.0.0 CXF-vs-light differential harness). It is skipped unless `winrm.live.host` is set:
+the pre-2.0.00 CXF-vs-light differential harness). It is skipped unless `winrm.live.host` is set:
```bash
mvn test -Dtest=WinRMLiveTest \
@@ -305,16 +298,13 @@ for hosts with self-signed certificates).
## Release instructions
-The artifact is deployed to Sonatype's [Maven Central](https://central.sonatype.com/).
-
-The actual repository URL is https://s01.oss.sonatype.org/, with server Id `ossrh` and requires credentials to deploy
-artifacts manually.
-
-But it is strongly recommended to only use [GitHub Actions "Release to Maven Central"](actions/workflows/release.yml) to perform a release:
+The artifact is deployed to [Maven Central](https://central.sonatype.com/) through the Central
+Portal (server id `central`, inherited from `oss-parent`). Release only with the
+["Release to Maven Central"](https://github.com/metricshub/winrm-java/actions/workflows/release.yml)
+GitHub Actions workflow:
-* Manually trigger the "Release" workflow
+* Manually trigger the "Release to Maven Central" workflow
* Specify the version being released and the next version number (SNAPSHOT)
-* Release the corresponding staging repository on [Sonatype's Nexus server](https://s01.oss.sonatype.org/)
* Merge the PR that has been created to prepare the next version
## License
diff --git a/src/main/java/org/metricshub/winrm/CommandRequest.java b/src/main/java/org/metricshub/winrm/CommandRequest.java
index 0163214..604d2e4 100644
--- a/src/main/java/org/metricshub/winrm/CommandRequest.java
+++ b/src/main/java/org/metricshub/winrm/CommandRequest.java
@@ -264,12 +264,12 @@ public CommandRequest charset(final Charset charset) {
* output charset. Default: the output charset (see {@link #charset(Charset)}).
*
* The two directions are not symmetric on Windows. Output follows the shell's console code
- * page, which this client pins to UTF-8. Input written to a command created with
- * console-mode stdin is converted by the WinRM service with the remote machine's
- * ANSI code page instead — so an interactive session (the CLI's {@code shell}
- * subcommand) must encode what it sends with that code page, whatever the console code page
- * is. Input handed to a command created with pipe semantics (any {@code stdin(...)} on this
- * request) reaches the process unconverted and needs no override.
+ * page (UTF-8 unless {@link WinRMClient.Builder#consoleCodePage(int)} says otherwise). Input
+ * handed to a command created with pipe semantics (any {@code stdin(...)} on this request)
+ * reaches the process unconverted and needs no override. Input written to a command created
+ * with console-mode stdin is converted by the WinRM service with a code page that
+ * depends on the Windows version: prefer pipe semantics, or set this to match the console
+ * code page.
*
* @param charset the input charset
* @return this request
diff --git a/src/main/java/org/metricshub/winrm/WindowsRemoteCommandResult.java b/src/main/java/org/metricshub/winrm/WindowsRemoteCommandResult.java
index 1d26338..2d8ddd7 100644
--- a/src/main/java/org/metricshub/winrm/WindowsRemoteCommandResult.java
+++ b/src/main/java/org/metricshub/winrm/WindowsRemoteCommandResult.java
@@ -20,6 +20,10 @@
* ╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱
*/
+/**
+ * The result of a command run by the legacy WinRMCommandExecutor: its output, exit code and
+ * execution time.
+ */
public class WindowsRemoteCommandResult {
private final String stdout;
@@ -32,7 +36,7 @@ public class WindowsRemoteCommandResult {
*
* @param stdout The stdout of the command
* @param stderr The stderr of the command
- * @param executionTime The execution time of the command in milliseconds
+ * @param executionTime The execution time of the command in seconds
* @param statusCode The command return status code
*/
public WindowsRemoteCommandResult(
@@ -50,7 +54,7 @@ public WindowsRemoteCommandResult(
/**
* Get the stdout of the command.
*
- * @return
+ * @return the standard output of the command
*/
public String getStdout() {
return stdout;
@@ -59,7 +63,7 @@ public String getStdout() {
/**
* Get the stderr of the command.
*
- * @return
+ * @return the standard error of the command
*/
public String getStderr() {
return stderr;
@@ -68,7 +72,7 @@ public String getStderr() {
/**
* Get the execution time of the command in seconds.
*
- * @return
+ * @return the execution time, in seconds
*/
public float getExecutionTime() {
return executionTime;
@@ -77,7 +81,7 @@ public float getExecutionTime() {
/**
* Get the return status code of the command
*
- * @return
+ * @return the exit code of the command
*/
public int getStatusCode() {
return statusCode;
diff --git a/src/main/java/org/metricshub/winrm/WqlResult.java b/src/main/java/org/metricshub/winrm/WqlResult.java
index bf001a7..e6a03c2 100644
--- a/src/main/java/org/metricshub/winrm/WqlResult.java
+++ b/src/main/java/org/metricshub/winrm/WqlResult.java
@@ -57,7 +57,7 @@ public final class WqlResult implements Iterable {
/**
* Get the column names, in the order they appear in the WQL query ({@code SELECT *} yields
- * the order the server returned).
+ * them in case-insensitive alphabetical order).
*
* @return an unmodifiable list of column names
*/
diff --git a/src/main/java/org/metricshub/winrm/cli/WinRmCli.java b/src/main/java/org/metricshub/winrm/cli/WinRmCli.java
index 2a9ebeb..541e2f1 100644
--- a/src/main/java/org/metricshub/winrm/cli/WinRmCli.java
+++ b/src/main/java/org/metricshub/winrm/cli/WinRmCli.java
@@ -728,9 +728,9 @@ private static String help() {
" winrm-java [options] cat [cat options]\n" +
" winrm-java [options] get []\n" +
"\n" +
- "Connection options:\n" +
+ "Options (before the subcommand):\n" +
" -h, --hostname Target hostname or IP address (required)\n" +
- " -u, --username User name, optionally DOMAIN\\\\user (required)\n" +
+ " -u, --username User name, optionally DOMAIN\\user (required)\n" +
" -p, --password Password (visible to local processes; avoid in automation)\n" +
" -pf, --password-file Read a UTF-8 password from a file (preferred for automation)\n" +
" -P, --port Target port (default: HTTP 5985, HTTPS 5986)\n" +
@@ -738,7 +738,7 @@ private static String help() {
" -d, --directory Working directory of the remote command or shell\n" +
" --env Environment variable of the remote command or shell (repeatable)\n" +
" --profile Load the user profile in the remote command or shell\n" +
- " -i, --stdin Forward the local standard input to the remote command\n" +
+ " -i, --stdin Always forward the local standard input (automatic when redirected)\n" +
" --https Use HTTPS\n" +
" --https-permissive Trust any HTTPS certificate and hostname (insecure)\n" +
" --ntlm Use NTLM authentication (default)\n" +
diff --git a/src/main/java/org/metricshub/winrm/command/WinRMCommandExecutor.java b/src/main/java/org/metricshub/winrm/command/WinRMCommandExecutor.java
index 9121202..8aeda50 100644
--- a/src/main/java/org/metricshub/winrm/command/WinRMCommandExecutor.java
+++ b/src/main/java/org/metricshub/winrm/command/WinRMCommandExecutor.java
@@ -49,18 +49,18 @@ private WinRMCommandExecutor() {}
* path on the remote system where the files have been copied.
* Example:
*
- * WinRemoteCommandExecutor.execute(
- * "CSCRIPT c:\\MyScript.vbs", null, "remote-srv", null, null, null, 30000, Arrays.asList("c:\\MyScript.vbs"), false);
+ * WinRMCommandExecutor.execute(
+ * "CSCRIPT c:\\MyScript.vbs", null, "remote-srv", null, "DOMAIN\\user", password, null, 30000, List.of("c:\\MyScript.vbs"), null, null);
*
- * This will copy c:\\MyScript.vbs to remote-srv, typically in
- * C:\\Windows\\Temp\\winrm-upload-MYHOST and the command that is executed will therefore
- * become:
- * CSCRIPT "C:\\Windows\\Temp\\winrm-upload-MYHOST\\MyScript.vbs"
+ * This will copy c:\MyScript.vbs to remote-srv, typically in
+ * C:\Windows\Temp\winrm-upload-MYHOST under a content-addressed name, and the command
+ * that is executed will therefore become:
+ * CMD.EXE /C (CSCRIPT C:\Windows\Temp\winrm-upload-MYHOST\MyScript.1a2b3c4d5e6f.vbs)
*
* @param command The command to execute. (Mandatory)
* @param protocol The HTTP protocol (HTTP by default)
* @param hostname Host to connect to. (Mandatory)
- * @param port The port (5985 for HTPP or 5986 for HTTPS by default)
+ * @param port The port (5985 for HTTP or 5986 for HTTPS by default)
* @param username The username name. (Mandatory)
* @param password The password.
* @param workingDirectory Path of the directory for the spawned process on the remote system (can be null)
diff --git a/src/site/markdown/authentication.md b/src/site/markdown/authentication.md
index 481c289..9dfde7a 100644
--- a/src/site/markdown/authentication.md
+++ b/src/site/markdown/authentication.md
@@ -29,9 +29,7 @@ When `authentication(...)` is not called, **NTLM** is used.
Several schemes form an **ordered fallback list**: each is tried in the given order until one
succeeds. `authentication(KERBEROS, NTLM)` attempts Kerberos first and falls back to NTLM — for
-example when the KDC is unreachable or the clock skew is too large. Because the list contains
-Kerberos, it requires an HTTPS transport: Kerberos is rejected over plain HTTP rather than being
-silently dropped (see [Kerberos](#kerberos-spnego) below).
+example when the KDC is unreachable or the clock skew is too large.
## User name and domain
@@ -44,6 +42,10 @@ remember to escape the backslash in a string literal:
"Administrator" // no domain
```
+With Kerberos the `DOMAIN\` part is ignored: the principal is the account name in the default
+realm of the [Kerberos configuration](#kerberos-configuration). Write `user@REALM` (e.g.
+`admin@EXAMPLE.COM`) to name the realm explicitly.
+
The password is a `char[]`, and the builder deliberately does **not** copy it: the client keeps
that same array by reference end-to-end and never converts it to a `String` internally, so after
closing the client you can wipe the single authoritative copy of the secret
@@ -75,8 +77,9 @@ try (WinRMClient client = WinRMClient.builder("server.internal.example.com")
}
```
-Requesting Kerberos on a plain-HTTP client fails at `build()` with a clear message: there is no
-Kerberos message encryption over HTTP.
+Requesting Kerberos on a plain-HTTP client fails at `build()`, even in a fallback list such as
+`(KERBEROS, NTLM)`, rather than being silently dropped: there is no Kerberos message encryption
+over HTTP.
### Kerberos configuration
@@ -90,8 +93,9 @@ java -Djava.security.krb5.realm=EXAMPLE.COM \
-cp ... MyApp
```
-The optional `ticketCache(Path)` builder option points at a Kerberos ticket cache to use for the
-connection; without it, Kerberos logs in with the user name and password.
+The optional `ticketCache(Path)` builder option logs in from a Kerberos ticket cache (filled by
+`kinit`, for example) instead of with the password. `credentials(...)` is still required: its user
+name must be the cached ticket's principal, and the password is not used.
### Credential delegation
@@ -117,8 +121,8 @@ The TGT must be **forwardable**, and the JDK asks the KDC for a forwardable one
set `forwardable = true` in the `[libdefaults]` section of the JDK's `krb5.conf` (see
[Kerberos configuration](#kerberos-configuration)) — or, with `ticketCache(Path)`, get the cached
ticket with `kinit -f`. The `java.security.krb5.realm` and `java.security.krb5.kdc` properties
-cannot say it, but the file is read in addition to them, so next to them a file with only these
-lines is enough:
+cannot request a forwardable ticket, but the JDK still reads `krb5.conf` when they are set, so a
+file with only these lines is enough:
```ini
[libdefaults]
@@ -130,9 +134,9 @@ on the second hop. An account that Active Directory never lets be delegated (*Ac
and cannot be delegated*, or a member of *Protected Users*) fails the same way.
Unlike `winrs`, which delegates only to hosts that Active Directory trusts for delegation, the
-client forwards the ticket to any host it is enabled for (verified with a host that is not trusted
-for delegation). The host then holds a ticket that lets it act as you on the network until the
-ticket expires: enable delegation only for hosts you trust.
+client forwards the ticket whether or not the host is trusted. The host then holds a ticket that
+lets it act as you on the network until the ticket expires: enable delegation only for hosts you
+trust.
`build()` rejects `allowDelegation()` when Kerberos is not among the schemes: NTLM and Basic
credentials cannot be delegated. In an ordered fallback such as `(KERBEROS, NTLM)`, a connection
@@ -147,13 +151,11 @@ transports, but over plain HTTP the credential and the data are sent **in the cl
over HTTPS only, where TLS protects both.
On Windows, WinRM accepts Basic for **local accounts only**, addressed by their **bare user
-name**: a domain account is rejected, and so is a *local* account written with a qualifying
-prefix — `MACHINE\user` or `DOMAIN\user` gets a `401` even when the password is correct
-(verified against a real host). Use `credentials("user", password)`, not
-`credentials("MACHINE\\user", password)`. The client itself does not reject a qualified name —
-some non-Microsoft WSMan services accept one — and sends the account with all whitespace
-removed: a `DOMAIN\user` value is rebuilt as `DOMAIN` + `\` + the account, and a bare name is
-sent as-is.
+name**: a domain account, or a local account written `MACHINE\user` or `DOMAIN\user`, gets a `401`
+even with the correct password. Use `credentials("user", password)`, not
+`credentials("MACHINE\\user", password)`. (The client itself does not reject a qualified name, for
+the non-Microsoft WSMan services that accept one; like every user name, it is sent with all
+whitespace removed: `DOMAIN\John Smith` becomes `DOMAIN\JohnSmith`.)
```java
try (WinRMClient client = WinRMClient.builder("server.example.com")
@@ -165,32 +167,29 @@ try (WinRMClient client = WinRMClient.builder("server.example.com")
}
```
-The server must have Basic authentication enabled on the WinRM service — the `Basic` setting under
-the service's `auth` section, `False` by default:
+The WinRM service must have Basic enabled (it is off by default):
`winrm set winrm/config/service/auth @{Basic=true}`; see
-[Preparing the Windows Host](preparing-the-host.html). Over HTTPS that is all that is needed, since
-TLS provides the confidentiality. Over plain HTTP — which, as noted, should not be used — the
-service would additionally have to set `AllowUnencrypted=true` (otherwise it refuses the unprotected
-SOAP), which is exactly what the HTTPS recommendation exists to avoid.
-
-Note that most server-side refusals above surface as the same `401`: a Basic authentication
-error can mean a wrong password, but also a domain-qualified or domain account, or `Basic`
-disabled on the service — check the configuration before suspecting the credential. Unencrypted
-HTTP with `AllowUnencrypted=false` is refused too, but not always as a `401`: depending on the
-Windows version, the service may authenticate the credential and then reject the unprotected
-SOAP with a WSMan fault (a `401` was observed on Server 2008 R2, a fault is reported on later
-versions), so that misconfiguration can surface as either an authentication error or a fault.
+[Preparing the Windows Host](preparing-the-host.html). Over plain HTTP it must also set
+`AllowUnencrypted=true`, or it refuses the request with a `401` or a WSMan fault, depending on the
+Windows version.
+
+A Basic `401` can therefore mean a wrong password, but also a domain or qualified account name,
+`Basic` disabled on the service, or plain HTTP with `AllowUnencrypted=false`: check the
+configuration before suspecting the credential.
## Authentication failures
-A rejected credential (after every scheme of the fallback list was tried) surfaces as a
+A credential the server rejects (HTTP `401`, after every scheme of the fallback list was tried)
+surfaces as a
[`WinRMAuthenticationException`](apidocs/org/metricshub/winrm/exceptions/WinRMAuthenticationException.html)
whose message has the stable form `Authentication error on with user name ""`.
+When Kerberos is the last scheme tried and fails on the client side (a password the KDC rejects,
+an unreachable KDC, an unknown service principal, a ticket that cannot be delegated), the error is
+a plain `WinRMClientException` carrying the JDK's Kerberos message.
## Choosing the scheme on the command line
-The standalone jar selects the scheme with `--ntlm` (the default), `--kerberos`, or `--basic`. The
-three are mutually exclusive, and `--kerberos` requires `--https`:
+The standalone jar selects the scheme with `--ntlm` (the default), `--kerberos`, or `--basic`:
```bash
java -jar ${project.artifactId}-${project.version}-standalone.jar \
@@ -199,33 +198,12 @@ java -jar ${project.artifactId}-${project.version}-standalone.jar \
command whoami
```
-Instead of relying on the ambient configuration, the CLI can set the JDK Kerberos configuration for
-the current invocation:
-
-| Option | Meaning |
-| --- | --- |
-| `--kerberos-kdc ` | Sets the KDC and, unless `--kerberos-realm` is given, infers the realm from the KDC's DNS suffix. |
-| `--kerberos-realm ` | Overrides the inferred realm. Requires `--kerberos-kdc`. |
-
-```bash
-java -jar ${project.artifactId}-${project.version}-standalone.jar \
- -h server.internal.example.com -u 'DOMAIN\user' -pf password.txt \
- --https --kerberos --kerberos-kdc dc01.internal.example.com \
- command whoami
-```
-
-Here the realm is inferred as `INTERNAL.EXAMPLE.COM` by dropping the KDC's first DNS label and
-upper-casing the rest. This follows a common Active Directory naming convention but is not
-guaranteed by Kerberos — pass `--kerberos-realm` when the realm does not match the KDC's DNS suffix,
-or when the KDC is not a fully qualified DNS name.
-
-`--allow-delegate` turns on [credential delegation](#credential-delegation) for the invocation. It
-requires `--kerberos`, and the ticket must still be forwardable: `--kerberos-kdc` sets the KDC and
-the realm, not `forwardable = true`.
+Its Kerberos options (`--kerberos-kdc`, `--kerberos-realm`, `--allow-delegate`) are described in the
+[Command-Line Client](cli.html#kerberos) manual.
## See also
* [Preparing the Windows Host](preparing-the-host.html) — the privileges the account needs, and why
local administrator accounts are often denied
-* [TLS / HTTPS](tls.html) — required for Kerberos and recommended for NTLM
+* [TLS / HTTPS](tls.html) — required for Kerberos, and the only safe transport for Basic
* [Timeouts and Errors](timeouts-and-errors.html) — how authentication failures surface
diff --git a/src/site/markdown/cli.md b/src/site/markdown/cli.md
index 2ca8397..0924798 100644
--- a/src/site/markdown/cli.md
+++ b/src/site/markdown/cli.md
@@ -6,9 +6,10 @@ description: Manual page of the winrm-java standalone command-line client - subc
Every release ships a self-contained executable jar that bundles the client and a small CLI:
-download `${project.artifactId}-${project.version}-standalone.jar` from the
-[latest release](https://github.com/metricshub/winrm-java/releases/latest) and run it with Java.
-This page is its manual.
+download `${project.artifactId}-${project.version}-standalone.jar` (shortened to
+`winrm-java-standalone.jar` in the synopsis and short examples) from the
+[latest release](https://github.com/metricshub/winrm-java/releases/latest) and run it with Java 11
+or later. This page is its manual.
## Synopsis
@@ -27,7 +28,7 @@ java -jar winrm-java-standalone.jar --help | --version
| Subcommand | Description |
| --- | --- |
-| `wql ` | Run a WQL query and print the rows to stdout as UTF-8 [JSON Lines](https://jsonlines.org/). |
+| `wql ` | Run a WQL query in the `ROOT\CIMV2` namespace and print the rows to stdout as UTF-8 [JSON Lines](https://jsonlines.org/). |
| `command ` | Run a command on the remote host, forwarding its output. `cmd`, `exec`, and `run` are aliases. |
| `shell` | Open an interactive `cmd.exe` session on the remote host (see [Interactive shell](#interactive-shell)). |
| `ls ` | List a remote directory, or a tree, one entry per line (see [Remote files](#remote-files)). |
@@ -35,10 +36,15 @@ java -jar winrm-java-standalone.jar --help | --version
| `cat ` | Copy the bytes of a remote file, or of a byte range, to stdout. |
| `get []` | Download a remote file to a local file, digest-verified. |
-For `wql` and `command`, everything after the subcommand is the query or the command line;
-quoting follows your local shell's rules, and multi-word command lines are reassembled for the
-remote `cmd.exe`. `shell` takes no argument. The file subcommands take a remote path, then their
-own options, in any order.
+For `wql` and `command`, everything after the subcommand is the query or the command line, quoted
+by your local shell's rules. For `command`, the arguments are joined with spaces, and one that
+contains a space is double-quoted: write `command dir 'C:\Program Files'`, not
+`command 'dir "C:\Program Files"'`. The remote `cmd.exe` then parses the line, so its operators
+(`&`, `|`, `<`, `>`, `^`, `%`) keep their meaning outside quotes:
+`command ipconfig '|' findstr IPv4` filters on the host. Git Bash rewrites an argument that starts with `/` into a
+Windows path: double the slash of a switch (`ipconfig //all`). `shell` takes no argument. The file
+subcommands take a remote path and their own [file options](#file-options), in any order. All
+other options go before the subcommand.
## Options
@@ -53,8 +59,8 @@ own options, in any order.
| `-d, --directory ` | Working directory the remote command or interactive shell starts in, like `winrs -d` (only with `command` and `shell`). Default: the user's profile directory, or `C:\Users\Default` when that profile is not loaded (see `--profile`). |
| `--env ` | Environment variable set in the remote shell, like `winrs -env` (only with `command` and `shell`). Repeatable — one occurrence per variable; the value is split on the first `=`, so it may itself contain `=`. |
| `--profile` | Load the user profile in the remote shell (only with `command` and `shell`): `%APPDATA%`, the user's `HKEY_CURRENT_USER` hive. Not loaded by default, the reverse of `winrs`, where `-noprofile` turns it off; see [Loading the user profile](commands.html#loading-the-user-profile). |
-| `-i, --stdin` | Forward the local standard input to the remote command (only with `command`); see below. |
-| `--https` | Connect over HTTPS. |
+| `-i, --stdin` | Always forward the local standard input to the remote command (only with `command`); forwarding is automatic when it is redirected, see below. |
+| `--https` | Connect over HTTPS. The certificate is validated against the Java trust store: for a self-signed or private-CA host, run `java -Djavax.net.ssl.trustStore= -jar ...` (see [Trusting a certificate](tls.html#trusting-a-certificate)). |
| `--https-permissive` | Trust any HTTPS certificate and hostname. Intentionally insecure: testing and isolated hosts only. Requires `--https`. |
| `--ntlm` | Authenticate with NTLM (the default). |
| `--kerberos` | Authenticate with Kerberos. Requires `--https`. |
@@ -88,38 +94,41 @@ The options of `ls`, `stat` and `cat` come **after** the subcommand, before or a
## Passwords
If neither `-p` nor `-pf` is supplied, the CLI securely requests the password from the interactive
-console without echoing it. Non-interactive runs must use `--password-file` (or, less securely,
-`--password`).
+console without echoing it. The prompt needs a console, and there is none when standard input or
+output is redirected or piped (`| jq`, `> out.txt`): such runs must use `--password-file` (or,
+less securely, `--password`).
Password files are decoded as UTF-8. Exactly one final LF, CRLF, or CR is removed; every other
-byte — including whitespace and earlier line endings — is part of the password.
+byte — including whitespace, earlier line endings, and a byte order mark — is part of the password.
## Kerberos
-By default, Kerberos uses the ambient JDK configuration (`krb5.conf` /
-`-Djava.security.krb5.*`). The CLI can instead configure the JDK for the current invocation with
-`--kerberos-kdc `. If no `--kerberos-realm` is supplied, the realm is inferred by removing
-the KDC hostname's first DNS label and uppercasing the remaining suffix — for example, a KDC of
-`camus.internal.example.net` infers the realm `INTERNAL.EXAMPLE.NET`. The inference follows a
-common Active Directory DNS naming convention; it is not guaranteed by Kerberos, so specify
-`--kerberos-realm` when the realm does not match the KDC's DNS suffix or when the KDC is not a
-fully qualified DNS name. Both options are valid only with `--kerberos`, and `--kerberos-realm`
-requires `--kerberos-kdc`.
+With `--kerberos`, pass the host's FQDN to `-h`, not an IP address: the service principal is
+`HTTP/` (see [Kerberos](authentication.html#kerberos-spnego)). By default, Kerberos uses the
+ambient JDK configuration (`krb5.conf` / `-Djava.security.krb5.*`). The CLI can instead configure
+the JDK for the current invocation with `--kerberos-kdc `. If no `--kerberos-realm` is
+supplied, the realm is inferred by removing the KDC hostname's first DNS label and uppercasing the
+remaining suffix — for example, a KDC of `camus.internal.example.net` infers the realm
+`INTERNAL.EXAMPLE.NET`. The inference follows a common Active Directory DNS naming convention; it
+is not guaranteed by Kerberos, so specify `--kerberos-realm` when the realm does not match the
+KDC's DNS suffix or when the KDC is not a fully qualified DNS name. Both options are valid only
+with `--kerberos`, and `--kerberos-realm` requires `--kerberos-kdc`.
`--allow-delegate` forwards your Kerberos ticket to the host, so the remote command can reach a
further host as you — a UNC path, another server — where it would otherwise get *access denied*.
The ticket must be forwardable, which the JDK asks for only when a `krb5.conf` says
-`forwardable = true` in its `[libdefaults]` section (`--kerberos-kdc` does not): otherwise the CLI
-exits with an authentication error (77) saying so. Only delegate to hosts you trust; see
+`forwardable = true` in its `[libdefaults]` section (`--kerberos-kdc` cannot set it): otherwise
+the CLI exits with an authentication error (77) saying so. Only delegate to hosts you trust; see
[Credential delegation](authentication.html#credential-delegation) for the details.
## Basic
`--basic` authenticates with HTTP Basic, sending the credential in the `Authorization` header of
every request. It has no message protection, so the credential and payload are plaintext over HTTP —
-combine it with `--https` (and `--https-permissive` for self-signed hosts) so TLS protects them.
-On Windows, use a **bare local account name** with `-u`: the WinRM service rejects Basic for
-domain accounts and for any `DOMAIN\`-qualified name.
+combine it with `--https` so TLS protects them. Do not combine it with `--https-permissive`: any
+host impersonating the server would receive the password. For a self-signed host, trust its
+certificate instead (see `--https`). On Windows, use a **bare local account name** with `-u`: the
+WinRM service rejects Basic for domain accounts and for any `DOMAIN\`-qualified name.
See [Authentication](authentication.html) for how NTLM, Kerberos, and Basic work on the wire.
@@ -141,8 +150,8 @@ Remote stdout and stderr are forwarded **live** to the corresponding local strea
command runs — each chunk is flushed as it arrives, so a long-running command can be followed in
real time. The output is decoded as UTF-8: the remote shell is created with console code page
65001, so no code-page detection is needed and non-ASCII output survives whatever the remote
-locale. See [Character encoding](commands.html#character-encoding) for the two legacy tools that
-ignore the console code page.
+locale. A few legacy tools (`net.exe`, `chcp.com`) ignore the console code page: their non-ASCII
+characters arrive as `U+FFFD` (see [Character encoding](commands.html#character-encoding)).
When the local standard input is piped or redirected, it is forwarded as the remote command's
standard input, with pipe semantics, so filters just work:
@@ -158,8 +167,8 @@ even when only the *output* is redirected (`... command hostname > result.txt`).
undetectable case is a pipe whose producer has written nothing by the time the CLI starts: pass
`-i`/`--stdin` to force forwarding there. The input is delivered in full before the output is
read: piping a large input into a command that floods its output at the same time can deadlock
-both sides (the classic pipe deadlock), exactly as with `java.lang.Process`. Input the command
-does not read, because it exits first, is discarded, as with a local pipe.
+both sides (the classic pipe deadlock). Input the command does not read, because it exits first,
+is discarded, as with a local pipe.
### `ls`, `stat`, `cat`, `get`
@@ -173,36 +182,29 @@ java -jar winrm-java-standalone.jar -h server.example.net -u 'DOMAIN\user' -pf p
`shell` starts `cmd.exe` on the remote host and bridges it to the local terminal until the remote
shell exits (type `exit`, or send end-of-input — Ctrl+Z then Enter on Windows, Ctrl+D elsewhere —
-which the session turns into an `exit`). The remote exit code is propagated through the usual
-[exit-code contract](#exit-codes).
+which closes the remote shell's input, so `cmd.exe` exits). The remote exit code is propagated
+through the usual [exit-code contract](#exit-codes).
* **Echo is off** — the remote shell runs `cmd.exe /Q`, so the input you forward is never
repeated back: your terminal already shows what you type, and the output stream carries the
prompts and the command output only.
-* **The session runs under a single-byte code page**, the remote machine's ANSI one (queried
- once per session from `Win32_OperatingSystem.CodeSet`, falling back to 1252 when the host
- cannot answer); both what you type and what you see use it. This is deliberate: a remote
- `cmd.exe` decodes the command lines it reads from its standard input **one byte at a time**
- under code page 65001, so every non-ASCII character would be lost. `winrs` has the same
- constraint. The practical limit is that characters outside the host's ANSI code page cannot be
- typed or displayed in an interactive session — `é` on a Western-European host is fine.
- The other subcommands are unaffected: `wql` and `command` keep code page 65001 and full UTF-8
- output, and piped input to `command` transfers bytes unconverted.
+* **The session runs under the host's ANSI code page** (from `Win32_OperatingSystem.CodeSet`,
+ 1252 when the host cannot answer), not UTF-8: under code page 65001, a remote `cmd.exe` reading
+ its input loses every non-ASCII character, as with `winrs`. So characters outside that code
+ page cannot be typed or displayed (`é` on a Western-European host is fine). `wql` and `command`
+ keep full UTF-8 output, and piped input to `command` transfers bytes unconverted.
* **Line-oriented, like `winrs`** — input is line-buffered by the local terminal and forwarded
- when you press Enter. There is no raw-terminal/PTY mode (with zero dependencies there is none in
- pure Java): full-screen programs, cmd.exe line editing, tab completion, and ANSI cursor control
- are not supported. Command output echoes back with sub-second latency; a line typed while the
- session is idle can wait up to the poll cadence (about one second — the WSMan protocol's floor
- for a bounded poll) before it is forwarded.
+ when you press Enter. There is no raw-terminal/PTY mode: full-screen programs, cmd.exe line
+ editing, tab completion, and ANSI cursor control are not supported. Command output echoes back
+ with sub-second latency; a line typed while the session is idle can wait up to the poll cadence
+ (about one second — the WSMan protocol's floor for a bounded poll) before it is forwarded.
* **Ctrl+C interrupts the remote command, not the session** — it is forwarded as the WSMan
`ctrl_c` signal, which stops the running remote child (like a console Ctrl+C) and returns to the
- remote prompt. On a Java runtime without `sun.misc.Signal`, Ctrl+C keeps its default behavior
- and ends the CLI (terminating the remote shell with it).
+ remote prompt.
* **An idle session does not time out** — `-t`/`--timeout` bounds each protocol round trip, and
every round trip of an idle session completes with the protocol's "nothing yet" answer. Only a
server that stops answering altogether trips the timeout. A `--timeout` below 1000 ms is
- rejected for `shell`: one poll round trip cannot complete faster (the WSMan service holds a
- bounded request for at least 500 ms before answering "nothing yet").
+ rejected for `shell`: one poll round trip cannot complete faster.
## Remote files
@@ -214,7 +216,7 @@ PowerShell script does the work on the host, which needs PowerShell 2.0 or later
```bash
# List: long format, machine-readable timestamps and sizes
java -jar winrm-java-standalone.jar -h server -u 'DOMAIN\user' -pf pw.txt \
- ls 'C:\inetpub\logs' --glob '*.log' --recursive --depth 3
+ ls 'C:\inetpub\logs' --glob '*.log' --depth 3
# One path's properties
java -jar winrm-java-standalone.jar ... stat 'C:\Windows\Temp\collect.log'
@@ -288,8 +290,8 @@ created: 2025-12-01T08:00:00.0000000Z
lastAccessed: 2026-01-02T03:04:05.6789012Z
```
-A path that does not exist exits with `66`, any other failure (access denied, for example) with
-`70`: `stat` doubles as an existence test.
+A path that does not exist exits with `66`, a path that cannot be read (access denied, for
+example) with `70`: `stat` doubles as an existence test.
### `cat`
@@ -345,8 +347,7 @@ local shell parses them first:
tolerated silence between two server responses. A large result or file can stream for longer
than the timeout, as long as the server keeps answering; a walking `ls` signals it is alive
every second.
-* For `command`, `stat`, and `get`, it is the **overall deadline** covering the whole operation
- (for `command`, the command itself and any file uploads).
+* For `command`, `stat`, and `get`, it is the **overall deadline** covering the whole operation.
* For `shell`, it bounds **each protocol round trip**; an idle interactive session never trips it
(see [Interactive shell](#interactive-shell)).
@@ -362,8 +363,8 @@ See [Timeouts and Errors](timeouts-and-errors.html) for the underlying semantics
| `64` | Invalid CLI usage. |
| `66` | Remote path not found (`ls`, `stat`, `cat`, `get`). |
| `69` | Connection, DNS, socket, or TLS failure. |
-| `70` | WinRM protocol or other remote failure (including access denied to a remote path, and a remote exit code not representable in 0–255). |
-| `74` | Local I/O failure: stdout closed or not writable, or a file-system error on the local file of `get` (access denied, a missing drive). |
+| `70` | WinRM protocol or other remote failure (including access denied to a remote path, an invalid WQL query, and a remote exit code not representable in 0–255). |
+| `74` | Local I/O failure: stdout closed or not writable (`ls`, `stat`, `cat`), or a file-system error on the local file of `get` (access denied, a missing drive). |
| `77` | Authentication failure. |
| `124` | Operation timeout. |
@@ -405,7 +406,7 @@ java -Djava.security.krb5.conf=krb5.conf -jar ${project.artifactId}-${project.ve
exec dir '\\fileserver\share'
```
-Follow a long-running command live and capture the streamed WQL rows with `jq`:
+Stream WQL rows into `jq` as they arrive:
```bash
java -jar ${project.artifactId}-${project.version}-standalone.jar \
diff --git a/src/site/markdown/commands.md b/src/site/markdown/commands.md
index dbbaeb7..0bb624a 100644
--- a/src/site/markdown/commands.md
+++ b/src/site/markdown/commands.md
@@ -34,18 +34,19 @@ try (WinRMClient client = WinRMClient.builder("server.example.com")
```
The command line is run through `cmd.exe` by the remote shell. One client can run any number of
-commands (and [WQL queries](wql.html)) over the same authenticated connection — see the
-[Overview](index.html) for the builder options.
+commands (and [WQL queries](wql.html)) over the same authenticated connection — see
+[Client options](index.html#client-options).
### Command options
-Everything between `command(...)` and `execute()` is optional:
+Everything between `command(...)` and `execute()` is optional (see
+[`CommandRequest`](apidocs/org/metricshub/winrm/CommandRequest.html)):
| Option | Default | Meaning |
| --- | --- | --- |
| `timeout(Duration)` | the client's timeout | Wall-clock deadline covering file uploads and the command itself with `execute()`; inactivity timeout with `start()`. |
| `charset(Charset)` | `UTF-8` | The charset used to decode the command output (see below). |
-| `workingDirectory(String)` | remote default | Working directory of the remote process. The remote shell is created by the client's **first** command and reused afterward, so this only takes effect on that first command. |
+| `workingDirectory(String)` | remote default | Working directory of the remote process. The remote shell is created by the client's **first** command (remote file operations and `uploadFile(...)` count too) and reused afterward, so this only takes effect on that first command, and never on a request with `upload(...)`. |
| `environment(String, String)` | none | Environment variable set in the remote shell, like `winrs -env` — call it once per variable, insertion order is preserved. Shell-scoped like `workingDirectory`: only takes effect on the client's **first** command. |
| `upload(Path...)` | none | Local files to copy to the host before running (see below). |
| `stdin(String)` / `stdin(Path)` / `stdin(InputStream)` | none | Standard input fed to the command — the remote equivalent of a `< file` redirection (see below). |
@@ -80,12 +81,17 @@ the host: the command then fails with a
[`WinRMFaultException`](apidocs/org/metricshub/winrm/exceptions/WinRMFaultException.html) carrying
the fault code and detail. The CLI's `--profile` option does the same.
+A command that reaches a further host (a UNC path, another server) fails with *access denied*
+unless the client delegates your Kerberos credentials: see
+[Credential delegation](authentication.html#credential-delegation).
+
## Running PowerShell
`powerShell(...)` prepares a PowerShell script execution the same way `command(...)` prepares a
command line. The script travels base64-encoded
-(`powershell.exe -NoProfile -NonInteractive -EncodedCommand …`), so **no quoting or escaping is
-ever needed**: quotes, pipes, newlines, and `$variables` reach PowerShell exactly as written.
+(`powershell.exe -NoProfile -NonInteractive -EncodedCommand …`; `-NoProfile` skips PowerShell's
+profile scripts, not the Windows user profile of `loadUserProfile()`), so **no quoting or escaping
+is ever needed**: quotes, pipes, newlines, and `$variables` reach PowerShell exactly as written.
```java
CommandResult result = client.powerShell(
@@ -101,23 +107,22 @@ Points to know:
* **Exit code** — `powershell.exe` exits with 0 on success and 1 when the script ends with a
terminating error; call `exit ` in the script for a specific code.
+* **Standard error** — `powershell.exe` may write progress records to stderr as CLIXML
+ (`#< CLIXML` followed by ``), even when the script succeeds: judge success by the exit
+ code, not by an empty `stderr()`.
* **Uploads** — `upload(...)` works as with any command: references to the uploaded files in the
script are rewritten to the remote copies *before* the script is encoded.
-* **Script size** — there is none to worry about. A script short enough rides the command line
- encoded; a longer one (roughly 3000 characters and up, where the encoded invocation would no
- longer fit the remote shell's 8191-character command line) is automatically transferred as a
- temporary `.ps1` file — through the WinRM connection itself, exactly like `upload(...)` — and
- its **content** run as a dot-sourced script block (`[ScriptBlock]::Create`), which keeps the
- script behaving like the encoded form: pathless (`$PSScriptRoot` and
- `$MyInvocation.MyCommand.Path` stay empty either way), top-level scope and `param(...)` intact,
- and out of reach of the host's execution policy (which only governs script files). The one
- remaining observable difference is `$MyInvocation`'s own metadata (`InvocationName`, `Line`),
- which reflects the wrapper invocation for a transferred script. The remote copy is
- [content-addressed](file-transfers.html), so re-running an identical script skips the transfer;
- and like any request with uploads, the transfer commands are then what creates the remote
- shell, so the shell-scoped `workingDirectory(...)` does not apply.
-* **Windows PowerShell** — the script runs in `powershell.exe` (Windows PowerShell 5.x, present on
- every supported Windows). To target PowerShell 7+, invoke `pwsh` yourself with `command(...)`.
+* **Script size** — none to worry about. A script too long for the encoded command line (roughly
+ 3000 characters and up; the remote shell's limit is 8191) is transferred automatically as a
+ temporary `.ps1` file, like `upload(...)`, and [content-addressed](file-transfers.html), so an
+ identical re-run skips the transfer. Its content runs as a dot-sourced script block, so it
+ behaves like the encoded form: no script path (`$PSScriptRoot` and
+ `$MyInvocation.MyCommand.Path` stay empty), top-level `param(...)` works, and the execution
+ policy does not apply; only `$MyInvocation.InvocationName` and `Line` differ. As with any
+ upload, the transfer creates the remote shell, so `workingDirectory(...)` does not apply.
+* **Windows PowerShell** — the script runs in `powershell.exe`, whose version depends on the host
+ (5.1 on current Windows, down to 2.0 on Windows Server 2008 R2 and Windows 7): write for the
+ oldest one you target. To target PowerShell 7+, invoke `pwsh` yourself with `command(...)`.
## The result
@@ -130,6 +135,8 @@ Points to know:
| `exitCode()` | `int` | The process exit code (Windows HRESULT codes reported as unsigned 32-bit values are narrowed to the equivalent signed `int`). |
| `elapsed()` | `java.time.Duration` | Wall-clock time of the operation. |
+A nonzero exit code is not an exception: check `exitCode()`.
+
## Streaming the output
`execute()` collects the complete output in memory and returns only when the command has exited.
@@ -140,9 +147,9 @@ For long-running or verbose commands, end the same request with `start()` instea
```java
try (RemoteProcess process = client.command("wevtutil qe System /f:text").start()) {
try (BufferedReader out = process.stdout()) {
- out.lines().forEach(this::process);
+ out.lines().forEach(System.out::println);
}
- int exitCode = process.waitFor(); // or waitFor(Duration) for an overall deadline
+ int exitCode = process.waitFor(); // blocks until the command exits
}
```
@@ -157,12 +164,32 @@ Points to know:
* Output is **decoded incrementally** with the request's charset; a multibyte character split
across protocol chunks is decoded correctly.
* The process **holds the client's serial connection** until completion or close: other operations
- on the same client wait in the meantime.
+ on the same client wait in the meantime, so use a second client for any call made while
+ consuming the process.
* The timeout is an **inactivity** timeout — the longest silence tolerated from the server — not
an overall deadline: a command may run (and stream) far longer than the timeout as long as it
- keeps producing output. Use `waitFor(Duration)` when you need a hard deadline. See
+ keeps producing output. For a hard deadline, use `waitFor(Duration)`: it returns `true` once the
+ command has completed (read the code with `exitCode()`), or `false` when the deadline passes
+ first, with the command still running (`close()` stops it). See
[Timeouts and Errors](timeouts-and-errors.html).
+### Tailing the output of a blocking execution
+
+When you only want to *observe* the output live — logging, progress reporting — but still want the
+blocking call and its complete [`CommandResult`](apidocs/org/metricshub/winrm/CommandResult.html),
+register `onStdout(...)` / `onStderr(...)` callbacks and keep `execute()` as the terminal:
+
+```java
+CommandResult result = client.command("longRunningThing.exe")
+ .onStdout(chunk -> log.info(chunk))
+ .onStderr(chunk -> log.warn(chunk))
+ .execute();
+```
+
+Each callback receives the output chunk by chunk as the server delivers it (not necessarily whole
+lines), on an internal worker thread, never concurrently. The wall-clock timeout of `execute()`
+applies unchanged.
+
## Standard input
Commands that read their standard input can be fed in two ways. Either way, input the command
@@ -181,8 +208,8 @@ CommandResult result = client.command("sort")
.execute();
```
-`stdin(String)` is encoded with the request's charset (see `charset(...)`); the `Path` and
-`InputStream` variants send the bytes exactly as stored. Large input is split into
+`stdin(String)` is encoded with `stdinCharset(...)`, which defaults to `charset(...)`; the `Path`
+and `InputStream` variants send the bytes exactly as stored. Large input is split into
protocol-sized chunks automatically.
Supplying input switches the remote stdin to **pipe semantics**
@@ -230,16 +257,16 @@ Input encoding is not symmetric with output encoding, because Windows treats the
differently:
* **Pipe semantics** (any `stdin(...)`, including the no-argument form) — the bytes reach the
- process **unconverted**. They are encoded with the request's charset (UTF-8 by default), which
- is what a program reading a UTF-8 stream expects, and `stdin(Path)`/`stdin(InputStream)` send
- the bytes verbatim.
+ process **unconverted**. They are encoded with the request's input charset
+ (`stdinCharset(...)`, else `charset(...)`; UTF-8 by default), which is what a program reading a
+ UTF-8 stream expects, and `stdin(Path)`/`stdin(InputStream)` send the bytes verbatim.
* **Console semantics** (no `stdin` declaration — a command started with `start()` and written
to through `RemoteProcess.stdin()`) — the WinRM service converts the bytes to console input
itself, using a code page that depends on the Windows version. Prefer pipe semantics, or set
`stdinCharset(...)` to match the session's console code page.
-Output is unaffected either way: it follows the shell's console code page, which this client
-pins to UTF-8.
+Output is unaffected either way: it follows the shell's console code page, 65001 (UTF-8) unless
+the client is built with `consoleCodePage(...)`.
> A remote `cmd.exe` reading its **command lines** from standard input cannot handle non-ASCII
> at all under console code page 65001 — Windows decodes that input one byte at a time, turning
@@ -250,28 +277,12 @@ pins to UTF-8.
> `findstr`, your own executable) is not affected: it never goes through cmd's parser, so the
> default code page 65001 and UTF-8 are right for it.
-### Tailing the output of a blocking execution
-
-When you only want to *observe* the output live — logging, progress reporting — but still want the
-blocking call and its complete [`CommandResult`](apidocs/org/metricshub/winrm/CommandResult.html),
-register `onStdout(...)` / `onStderr(...)` callbacks and keep `execute()` as the terminal:
-
-```java
-CommandResult result = client.command("longRunningThing.exe")
- .onStdout(chunk -> log.info(chunk))
- .onStderr(chunk -> log.warn(chunk))
- .execute();
-```
-
-Each callback receives the output chunk by chunk as the server delivers it (not necessarily whole
-lines), on an internal worker thread, never concurrently. The wall-clock timeout of `execute()`
-applies unchanged.
-
## Character encoding
-The output character set never needs to be specified. The remote command shell is created with
-console code page **65001**, so its output is UTF-8 whatever the remote machine's locale, and it is
-decoded as such — no detection query, no per-host configuration:
+The output character set never needs to be specified, unless the client is built with
+`consoleCodePage(...)` (see [Input encoding](#input-encoding)). The remote command shell is
+created with console code page **65001**, so its output is UTF-8 whatever the remote machine's
+locale, and it is decoded as such — no detection query, no per-host configuration:
```java
// On a French Windows host:
@@ -293,21 +304,16 @@ client.command("net user Administrateur")
`chcp`) or writes raw bytes in a known encoding to its standard output. WQL results are unaffected
by all of this: they travel as UTF-8 inside the SOAP envelope.
-**Byte order mark.** Under console code page 65001, PowerShell 2.0 (Windows Server 2008 R2,
-Windows 7) writes a UTF-8 byte order mark ahead of its redirected output, which would otherwise
-surface as an invisible `U+FEFF` that `trim()` does not remove (`"\uFEFF2.0"` for
-`$PSVersionTable.PSVersion.ToString()`). When the output is decoded as UTF-8, a single `U+FEFF`
-at the very start of stdout, and of stderr, is dropped — in `execute()`, the `onStdout(...)` /
-`onStderr(...)` callbacks, the `start()` readers, and the legacy `WinRMCommandExecutor`, which
-shares the same decoding. Newer PowerShell versions and `cmd.exe` write no mark, so nothing changes
-for them. A `U+FEFF` anywhere else is kept as data: PowerShell 2.0 emits its mark on the first write
-through its own output writer, so when a direct `[Console]::Out` or `[Console]::Error` write comes
-first, the mark lands mid-stream and stays there (typically on stderr, just before a `#< CLIXML`
-error block). With any other `charset(...)`, the bytes are decoded as they are. File reads
-(`client.file(...)`) return the file's content untouched, including its own byte order mark.
+**Byte order mark.** PowerShell 2.0 (Windows Server 2008 R2, Windows 7) writes a UTF-8 byte
+order mark ahead of its redirected output. When the output is decoded as UTF-8, a `U+FEFF` at the
+very start of stdout and of stderr is dropped by every terminal, callback and reader (and by the
+legacy `WinRMCommandExecutor`). A `U+FEFF` anywhere else is kept as data: PowerShell 2.0 emits it
+mid-stream when a direct `[Console]::Out` or `[Console]::Error` write comes first. With any other
+`charset(...)`, the bytes are decoded as they are. File reads (`client.file(...)`) return the
+file's content untouched, byte order mark included.
> **Changed in 2.0.00** — earlier versions ran a `SELECT CodeSet FROM Win32_OperatingSystem` query
-> before the first command and decoded the output with the code page it reported. That property is
+> before each command and decoded the output with the code page it reported. That property is
> the remote machine's *ANSI* code page, which never matched what the shell emitted, so non-ASCII
> command output was mangled on every non-English host.
@@ -326,7 +332,7 @@ CommandResult result = client.command("CSCRIPT c:\\scripts\\collect.vbs")
copies `c:\scripts\collect.vbs` to the host and runs the equivalent of:
```text
-CSCRIPT "C:\Windows\Temp\...\collect.1a2b3c4d5e6f.vbs"
+CSCRIPT C:\Windows\Temp\winrm-upload-MYHOST\collect.1a2b3c4d5e6f.vbs
```
The client can also copy a file to an explicit destination of your choice, independently of any
@@ -348,14 +354,14 @@ command-line substitution rules.
## Exceptions
-`execute()` reports failures through the unchecked
+`execute()`, `start()` and the `RemoteProcess` methods report failures through the unchecked
[`WinRMClientException`](apidocs/org/metricshub/winrm/exceptions/WinRMClientException.html)
hierarchy:
| Exception | When |
| --- | --- |
| [`WinRMAuthenticationException`](apidocs/org/metricshub/winrm/exceptions/WinRMAuthenticationException.html) | The credentials were rejected. |
-| [`WinRMFaultException`](apidocs/org/metricshub/winrm/exceptions/WinRMFaultException.html) | The remote service answered with a WSMan fault — the fault code and detail are available as fields. |
+| [`WinRMFaultException`](apidocs/org/metricshub/winrm/exceptions/WinRMFaultException.html) | The remote service answered with a WSMan fault — see `getFaultCode()` and `getFaultDetail()`. |
| [`WinRMTimeoutException`](apidocs/org/metricshub/winrm/exceptions/WinRMTimeoutException.html) | The operation did not complete within its timeout. |
| [`WinRMClientException`](apidocs/org/metricshub/winrm/exceptions/WinRMClientException.html) | Any other failure (connection, TLS, protocol, unreadable local file). |
diff --git a/src/site/markdown/file-transfers.md b/src/site/markdown/file-transfers.md
index d614f1d..219942a 100644
--- a/src/site/markdown/file-transfers.md
+++ b/src/site/markdown/file-transfers.md
@@ -7,12 +7,11 @@ description: How the WinRM Java Client copies files to and from the remote host
The client can copy local files to the remote host, and remote files back, **through the WinRM
connection itself** — no SMB, no TCP port 445, no administrative share — so it works from any
-client OS and needs no port beyond the WinRM one. This page explains exactly how the transfers
-work: where files land, which temporary files are created, how integrity is guaranteed, and how
-the command line is rewritten. [Downloading a file](#downloading-a-file) covers the other
-direction.
+client OS and needs no port beyond the WinRM one. This page explains exactly how uploads work:
+where files land, which temporary files are created, how integrity is guaranteed, and how the
+command line is rewritten. [Downloading a file](#downloading-a-file) covers the other direction.
-## The two entry points
+## Uploading a file
**Transfer-and-run** — copy script files and rewrite the command to reference the remote copies
(this is what `upload(...)` on the fluent command builder and `localFileToCopyList` in the legacy
@@ -34,7 +33,8 @@ with a legacy executor):
client.uploadFile(Path.of("collect.ps1"), "C:\\Windows\\Temp\\collect.ps1");
```
-Both use the same transfer engine described below; they differ only in where the file lands.
+Both use the same transfer engine described below; transfer-and-run adds the per-client transfer
+directory (with its housekeeping) and the command-line rewriting.
## Where files land
@@ -49,10 +49,9 @@ remote host:
`SELECT WindowsDirectory FROM Win32_OperatingSystem` — typically `C:\Windows`.
* `` is the name of the machine **running the client** (the `COMPUTERNAME`
environment variable, or the local host name), which usually gives each client machine its own
- directory. Clients that report the same computer name (cloned machines, containers) share one —
- which is safe, because the content-addressed file names below prevent them from ever
- overwriting each other's payloads; they simply also share the cache and the 30-day cleanup.
- Versions before 2.0.0 used `\Temp\SEN_ShareFor_$`, exposed as a
+ directory. Clients that report the same computer name (cloned machines, containers) share one
+ directory, safely: the content-addressed names below keep their payloads apart.
+ Versions before 2.0.00 used `\Temp\SEN_ShareFor_$`, exposed as a
hidden SMB share — no share is created anymore, and a directory left behind by an older
version is neither reused nor cleaned up: it can simply be deleted.
* The directory is created if missing (`IF NOT EXIST ... MKDIR ...`).
@@ -76,14 +75,17 @@ truncated names unique.
With an **explicit destination** (`uploadFile`), the file lands exactly at the path you give —
no content-addressing, no renaming. The destination must be an absolute Windows path, either
-drive-rooted (`C:\...`) or UNC (`\\server\share\...`); relative and drive-relative (`C:x.ps1`)
-paths are rejected, because they would resolve against the remote shell's current directory. The
-destination directory is created if missing.
+drive-rooted (`C:\...`) or UNC (`\\server\share\...`), of at most 229 characters (room for the
+temporary-file suffixes within `MAX_PATH`); relative and drive-relative (`C:x.ps1`) paths are
+rejected, because they would resolve against the remote shell's current directory. The
+destination directory is created if missing. A UNC destination is a second hop from the host:
+without Kerberos [credential delegation](authentication.html#credential-delegation) it typically
+fails with access denied.
## How the bytes travel
-The transfer rides the already-authenticated (and, over HTTP, NTLM-encrypted) WinRM command
-shell — every step below is an ordinary remote command:
+The transfer rides the already-authenticated (and, over HTTPS or with NTLM, encrypted) WinRM
+command shell — every step below is an ordinary remote command:
1. **Skip check.** The destination is hashed on the host
(`certutil -hashfile SHA256`, with a `SHA1` fallback for pre-2012 hosts in the same
@@ -107,12 +109,9 @@ shell — every step below is an ordinary remote command:
(repair).
The same command leg hashes the destination one last time: **the operation only succeeds if
- the destination provably contains the local bytes**. On any failure the temporary files are
- deleted (best effort). A destination that already carried the correct content is never
- touched; but when a *mismatched* destination is being repaired, the replacement happens before
- the final verification — so a failure during a repair can leave the destination already
- replaced (and still unverified). Either way an unverified destination is never silently
- trusted: the failure is reported, and the next transfer detects the mismatch and repairs it.
+ the destination provably contains the local bytes**. A failure during a repair can leave the
+ destination replaced but unverified: the failure is reported, and the next transfer detects the
+ mismatch and repairs it.
An empty local file skips steps 2–4: the destination is created with `TYPE NUL` and verified the
same way.
@@ -127,15 +126,16 @@ Two short-lived artifacts exist next to the destination during a transfer:
```
`` combines a process-wide counter with 64 random bits, so concurrent transfers — same
-JVM or not — never collide. Both files are removed on success and best-effort deleted on failure.
+JVM or not — never collide. Both files are removed on success and best-effort deleted on failure;
+an interrupted transfer can leave them behind. The 30-day purge below reclaims them in the
+transfer directory; next to an `uploadFile(...)` destination, they stay until deleted.
### Housekeeping
Before each transfer-and-run, entries of the transfer directory **not modified for 30 days are
purged** (best effort, via `forfiles`). Content-addressing means every revision of a changing
-script gets a new remote name, so without this lifecycle the directory would grow without bound;
-the purge also reclaims staging files orphaned by an interrupted transfer. The only cost: a
-script unused for 30 days is re-uploaded once.
+script gets a new remote name, so without this lifecycle the directory would grow without bound.
+The only cost: a script unused for 30 days is re-uploaded once.
## Command-line substitution
@@ -151,38 +151,43 @@ executes: CMD.EXE /C (CSCRIPT C:\Windows\Temp\winrm-upload-MYHOST\collect.1a2b3c
Notes:
-* The match is a literal, case-insensitive substring match on the path exactly as you passed it
- to `upload(...)`/`localFileToCopyList` — use the same spelling in the command (`C:\Scripts\X.vbs`
- and `c:\scripts\x.vbs` both match, but `C:\SCRIPTS\..\SCRIPTS\X.VBS` does not).
+* The match is a literal, case-insensitive substring match on the path as you passed it to
+ `upload(...)` (its `toString()`: on a Windows client `scripts/x.vbs` becomes `scripts\x.vbs`) or
+ `localFileToCopyList` — use the same spelling in the command (`C:\Scripts\X.vbs` and
+ `c:\scripts\x.vbs` both match, but `C:\SCRIPTS\..\SCRIPTS\X.VBS` does not).
* A listed file that the command never references is still uploaded; the command is unchanged.
* Because the transfer legs run first, they are what creates the remote shell — so a
`workingDirectory(...)` on the same request does not apply (the shell already exists, with its
default directory, when the actual command runs).
-## Constraints and safety checks
+## Upload constraints and safety checks
* **File names** must be safely embeddable in a quoted cmd.exe argument and creatable on Windows.
- Rejected: names containing `%` or `!` (cmd.exe expands them even between quotes), `"`, control
+ Rejected: names containing `%` or `!` (cmd.exe expands them even between quotes), control
characters, the Windows-forbidden characters `< > : " / \ | ? *`, names ending with a dot or a
space, and reserved device names (`CON`, `NUL`, `COM1`…, with or without extension).
* **Size**: the mechanism is designed for small script files. Base64 over SOAP costs one WinRM
- operation per ~8 kB leg — fine for scripts, wrong for bulk data.
+ operation per ~8 kB leg — fine for scripts, wrong for bulk data. The transfer runs under
+ the command's `timeout(...)` with `upload(...)`, and under the client's timeout with
+ `uploadFile(...)` (no per-call override).
* **Server operation quotas**: old hosts cap concurrent WinRM operations per user very low (15 on
- Windows 2008 R2). Transfer steps are batched to minimize operations, and a command rejected by
- the quota *before it could run* is retried with escalating delays (5/10/15/20 s).
+ Windows Server 2008 R2). Transfer steps are batched to minimize operations, and a command
+ rejected by the quota *before it could run* is retried with escalating delays (5/10/15/20 s).
* **Integrity**: every path through the transfer ends with a digest verification of the actual
destination; the digest is a transfer-integrity check (the channel itself is authenticated and,
- over HTTP, encrypted).
+ over HTTPS or with NTLM, encrypted).
* **Host requirements**: the account must be able to run remote commands and to write to the
destination directory, and the host must provide `certutil` (transfer) and `forfiles`
- (housekeeping). See [Preparing the Windows Host](preparing-the-host.html).
+ (housekeeping); transfer-and-run also needs WMI access, for its WQL query. See
+ [Preparing the Windows Host](preparing-the-host.html).
## Downloading a file
[`WinRMClient.downloadFile(...)`](apidocs/org/metricshub/winrm/WinRMClient.html) copies a remote
file to a local one — the counterpart of `uploadFile(...)`, with the same guarantees: the content
is verified, an identical copy is not transferred again, and a failure never leaves a truncated
-file behind. The same terminal exists on the per-path request, where the timeout can be set:
+file behind. The same terminal exists on
+[`client.file(path)`](files.html#downloading-to-a-local-file), where the timeout can be set:
```java
client.downloadFile("C:\\Windows\\Temp\\collect.log", Path.of("collect.log"));
@@ -203,7 +208,8 @@ destination's directory is created when needed.
1. **Probe.** One PowerShell invocation on the host reports the size and the SHA-256 digest of
the file. It opens the file exactly like the [remote file reads](files.html) do, with a share
mode that tolerates other writers — so a log held open by a running service can be downloaded
- (`certutil -hashfile`, which the uploads use, fails on such a file with a sharing violation).
+ if it is not written to meanwhile (`certutil -hashfile`, which the uploads use, fails on such
+ a file with a sharing violation).
2. **Skip check.** If the local destination already exists with the same size and digest, nothing
is transferred: the download returns 0.
3. **Transfer.** The file is read with [`openStream()`](files.html#streaming-large-files) and
@@ -231,11 +237,9 @@ change confined to bytes already transferred leaves that consistent version in p
copy of a file that changes after it was read.
Downloads are **not resumable**: a download that fails or times out starts over from the first
-byte next time. Resuming would mean tracking verified byte ranges across attempts and proving
-that the remote file did not change in between — more machinery than the speed of this transport
-justifies.
+byte next time.
-The file is read-only on the host: a download writes nothing there, and needs what the remote file
+A download is read-only on the host: it writes nothing there, and needs what the remote file
reads need — PowerShell 2.0 or later in `FullLanguage` mode, and read access to the file (see
[Remote Files](files.html#errors-and-requirements)).
@@ -257,7 +261,7 @@ place lets the move complete, and the download succeeds.
### Download performance
-Measured over HTTP with NTLM encryption, a download runs at about **1.7–1.9 MB/s**: 20 MiB in
+Measured over HTTP with NTLM encryption, a download runs at about **1.6–1.9 MB/s**: 20 MiB in
12 seconds on Windows Server 2022 and 2008 R2 (PowerShell 2.0), 13 seconds on 2019, and 64 MiB in
35 seconds on 2022 — probe included. A small file costs under a second (two PowerShell
invocations: the probe and the read), and skipping an identical copy about 0.4 seconds. The limit
diff --git a/src/site/markdown/files.md b/src/site/markdown/files.md
index ec2272a..a28c7a8 100644
--- a/src/site/markdown/files.md
+++ b/src/site/markdown/files.md
@@ -8,9 +8,10 @@ description: How the WinRM Java Client reads files and lists directories on the
WinRM has no file-access operation of its own (nothing like SFTP's `READ`), so the client reads
remote files and lists directories **through the WinRM command shell**: a small PowerShell script
does the work on the host and writes the result in an encoding-proof form, and the client reads
-it as it arrives. No SMB, no extra port, no share. This is the reverse direction of
-[File Transfers](file-transfers.html). The standalone jar exposes it as the `ls`, `stat`, `cat`,
-and `get` subcommands — see the [Command-Line Client](cli.html#remote-files) manual.
+it as it arrives. No SMB, no extra port, no share. [File Transfers](file-transfers.html) covers
+copying files to the host, and how downloads work. The standalone jar exposes remote file access
+as the `ls`, `stat`, `cat`, and `get` subcommands — see the
+[Command-Line Client](cli.html#remote-files) manual.
## Reading a file
@@ -21,10 +22,11 @@ remote host; nothing is sent until a terminal is called:
// Whole file, bytes (byte-exact: nothing converted, a BOM is kept)
byte[] content = client.file("C:\\Windows\\Temp\\collect.bin").readBytes();
-// Whole file, text — the charset is always explicit, never guessed
+// Whole file, text — the charset is always explicit, never guessed (UTF-8 keeps a BOM as U+FEFF)
String text = client.file("C:\\inetpub\\logs\\u_ex260729.log").readText(StandardCharsets.UTF_8);
-// Integrity: only the digest is transferred (MD5, SHA1, SHA256, SHA384, SHA512)
+// Integrity: the whole file's digest, as lowercase hex (MD5, SHA1, SHA256, SHA384, SHA512);
+// only the digest is transferred, and offset/length do not apply
String sha256 = client.file("C:\\Windows\\Temp\\collect.ps1").digest("SHA256");
```
@@ -71,10 +73,11 @@ try (BufferedReader reader = client.file("D:\\logs\\huge.log").openReader(Standa
```
The stream passes the content on as it arrives: memory stays bounded whatever the file size.
-Like a [`RemoteProcess`](commands.html), **it must be closed** — it holds the client's connection
-until it reaches its end or is closed, and closing it early stops the remote read. The file is
-opened before `openStream()` returns, so a missing file fails there; a failure midway is reported
-by `read()`, never as a silently short read.
+Like a [`RemoteProcess`](commands.html#streaming-the-output), **it must be closed** — it holds the
+client's connection until it reaches its end or is closed (other operations on the same client
+wait meanwhile), and closing it early stops the remote read. The file is opened before
+`openStream()` returns, so a missing file fails there; a failure midway is reported by `read()`,
+never as a silently short read.
## Downloading to a local file
@@ -86,9 +89,8 @@ half-written. `client.downloadFile(remote, local)` does the same with the client
long bytes = client.file("C:\\Windows\\Temp\\collect.log").downloadTo(Path.of("collect.log"));
```
-It streams, so memory stays bounded whatever the size of the file, and its timeout is a
-wall-clock deadline for the whole transfer. The mechanics, the guarantees and the measured speed
-are described in [File Transfers](file-transfers.html#downloading-a-file).
+How it works, its timeout and its speed are described in
+[Downloading a file](file-transfers.html#downloading-a-file).
## Timeouts
@@ -130,21 +132,14 @@ operation. See [Timeouts and Errors](timeouts-and-errors.html).
The mechanism is designed for **configuration files, logs and small data files — not bulk data**.
Measured over HTTP with NTLM encryption, `openStream()` reads about **1.8–2.0 MB/s**: a 20 MiB
file in 10.6–12 seconds, on Windows Server 2008 R2 (PowerShell 2.0), 2019 and 2022
-(PowerShell 5.1) alike. A small file costs about a second, mostly the PowerShell startup. For
+(PowerShell 5.1) alike. A small file costs under a second, mostly the PowerShell startup. For
gigabytes, use SMB.
The limit is on the host, not in the network or the client: the WinRM service reads a command's
-output pipe itself, **at most 32 KiB per read and about 60 reads per second** per output stream —
-about 2 MB/s of output whatever the command. The reader runs at that limit: the host writes the
-file's raw bytes 64 KiB at a time, so every read is a full one and carries nothing but content
-(base64 text would carry only 3 bytes of the file in 4, and measured a third slower). Neither the
-client's `MaxEnvelopeSize` nor the host's `MaxEnvelopeSizekb` changes the limit — raising them
-only makes each response larger and proportionally slower to come.
-
-The same limit applies to any command output: a command that writes its output in small pieces —
-line by line, like `Write-Output` or `type` — gets only what accumulated in the 4 KiB pipe at each
-read, about 0.3 MB/s. Parallel commands, each on its own `WinRMClient`, add up: each command gets
-its own output pipeline.
+output pipe **at most 32 KiB at a time, about 60 times per second** — about 2 MB/s at best. The
+reader's script writes the file's raw bytes 64 KiB at a time, so it runs at that ceiling; a
+command that writes line by line (`Write-Output`, `type`) gets about 0.3 MB/s. Raising the host's
+`MaxEnvelopeSizekb` does not help. Parallel reads, each on its own `WinRMClient`, add up.
## File properties
@@ -174,8 +169,10 @@ invalid path, PowerShell unavailable or constrained.
## Listing a directory
-`list()` prepares the listing of a directory; set its filters, then `execute()` it or
-`stream()` it:
+`list()` prepares the listing of a directory
+([`RemoteDirectoryListing`](apidocs/org/metricshub/winrm/RemoteDirectoryListing.html)); set its
+filters, then `execute()` it into a
+[`RemoteFileList`](apidocs/org/metricshub/winrm/RemoteFileList.html) or `stream()` it:
```java
RemoteFileList logs = client.file("C:\\inetpub\\logs").list()
@@ -215,10 +212,12 @@ discarded locally. The filters select what is *reported*, not where the walk goe
`recursive()`, every subdirectory is traversed, whatever the glob or the type filter.
Like the other streaming terminals, **a `stream()` must be closed** (try-with-resources): it
-holds the client's connection until it is exhausted or closed, and closing it early stops the
-remote walk. Failures are thrown from the stream's operations, when the host reports them. The
-host signals it is alive every second while it walks, so the inactivity timeout only trips when
-the host is unresponsive — or when reading a single directory takes longer than the timeout.
+holds the client's connection until it is exhausted or closed (other operations on the same
+client wait: to read or digest each entry, `execute()` the listing first), and closing it early
+stops the remote walk. Failures are thrown from the stream's operations, when the host reports
+them. The host signals it is alive every second while it walks, so the inactivity timeout only
+trips when the host is unresponsive — or when reading a single directory takes longer than the
+timeout.
### Links and inaccessible directories
@@ -244,20 +243,16 @@ the walk itself counts: a recursive `*.dll` over 10 MB in System32 takes under a
## How the metadata travels
-The listing does not parse `dir` output (locale-dependent columns, dates and decimal separators,
-minute granularity), nor use `Get-ChildItem` (whose `-Depth`, `-File` and `-Directory` need
-PowerShell 3 to 5, and whose object pipeline is slow on big trees). A small script walks the tree
-with the .NET `DirectoryInfo` API and an explicit stack — on every PowerShell version from 2.0 —
-and writes **one ASCII line per entry**: the attributes, size and timestamps as plain integers
-(Windows file times), and only the path base64-encoded (UTF-8). Non-ASCII names therefore
-round-trip exactly, whatever the remote console code page, and a truncated or malformed line
-fails with a clear exception instead of producing a half-populated entry.
-
-WMI could serve metadata too (`CIM_DataFile`, `CIM_Directory` and `ASSOCIATORS OF` queries through
-[`client.wql(...)`](wql.html)), and remains an alternative where PowerShell is constrained — but
-the WMI file provider is notoriously slow (an unindexed `CIM_DataFile` query on a large tree can
-take minutes), a recursive listing needs one query per directory, and timestamps come back as
-DMTF strings.
+A small script walks the tree with the .NET `DirectoryInfo` API — on every PowerShell version from
+2.0 — and writes **one ASCII line per entry**: the attributes, size and timestamps as plain
+integers, and only the path base64-encoded (UTF-8). Non-ASCII names therefore round-trip exactly,
+whatever the remote console code page, and a truncated or malformed line fails with a clear
+exception instead of producing a half-populated entry.
+
+Where PowerShell is constrained, `SELECT` queries on `CIM_DataFile` and `CIM_Directory` through
+[`client.wql(...)`](wql.html) can serve metadata instead, much more slowly: an unindexed
+`CIM_DataFile` query on a large tree can take minutes, and a recursive listing needs one query per
+directory.
## Paths and limitations
diff --git a/src/site/markdown/index.md b/src/site/markdown/index.md
index 69f87b1..32f4004 100644
--- a/src/site/markdown/index.md
+++ b/src/site/markdown/index.md
@@ -1,5 +1,5 @@
keywords: winrm java client, windows remote management, wsman, dependency-free, overview
-description: A dependency-free Java client for Windows Remote Management (WinRM): run WQL queries and remote commands, and read and list remote files, over NTLM or Kerberos.
+description: A dependency-free Java client for Windows Remote Management (WinRM): run WQL queries and remote commands, and read and list remote files, over NTLM, Kerberos, or Basic authentication.
# WinRM Java Client
@@ -24,16 +24,19 @@ All of them can also **stream**: WQL rows are consumed page by page as they arri
returning a `java.lang.Process`-like handle), and file contents and directory listings are
decoded as the host sends them — memory stays bounded regardless of the result size.
+The standalone jar is also a [command-line client](cli.html) for the same operations (`wql`,
+`command`, `shell`, `ls`, `stat`, `cat`, `get`).
+
It supports **NTLM** over HTTP (with message encryption) and HTTPS, **Kerberos (SPNEGO)** over
HTTPS, and **HTTP Basic** over HTTPS ([Authentication](authentication.html)).
-Since 2.0.0 the client has **zero runtime dependencies** (no Apache CXF / JAX-WS / JAXB stack, no SMB
+Since 2.0.00 the client has **zero runtime dependencies** (no Apache CXF / JAX-WS / JAXB stack, no SMB
stack) and is immune by construction to JAXP `ServiceLoader` conflicts, because it uses the
JDK-default XML factories. Problems are reported through exceptions only — the library pulls in no
logging framework.
> [!WARNING]
-> **Upgrading from 1.x?** Version 2.0.0 removed the legacy Apache CXF backend and now
+> **Upgrading from 1.x?** Version 2.0.00 removed the legacy Apache CXF backend and now
> **validates TLS certificates and verifies hostnames by default**. If you connect over HTTPS to
> hosts with self-signed certificates, read [Migrating from 1.x](migrating-from-1x.html) first.
@@ -62,13 +65,13 @@ The library is published on [Maven Central](https://central.sonatype.com/artifac
See [Installation](installation.html) for the coordinates, the supported JDKs, and the standalone
command-line jar.
-## A first WQL query
+## Quick start
> [!NOTE]
> **On the target host**, WinRM must be enabled and the account must have sufficient privileges.
> Windows Server 2012 and later have WinRM enabled by default and an administrator account works
-> with no configuration; Windows 10 / 11, non-administrator accounts, and local (non-domain)
-> administrator accounts all need host-side setup. See
+> with no configuration; Windows 10 / 11, non-administrator accounts, and local administrator
+> accounts other than the built-in `Administrator` need host-side setup. See
> [Preparing the Windows Host](preparing-the-host.html).
Everything starts with the fluent
@@ -128,6 +131,28 @@ hierarchy. The static one-shot helpers that predate `WinRMClient`
[`WinRMCommandExecutor.execute(...)`](apidocs/org/metricshub/winrm/command/WinRMCommandExecutor.html))
remain available and unchanged, with their checked exceptions.
+## Client options
+
+`build()` does not connect: the first operation does. A client may be shared between threads, but
+its connection is a serial channel: operations run one at a time, and an open stream or process
+holds the connection until it is closed. For parallel work, use one client per concurrent task.
+
+Besides the host name passed to `builder(...)`, only `credentials(...)` is mandatory on the
+[builder](apidocs/org/metricshub/winrm/WinRMClient.Builder.html):
+
+| Option | Default | Details |
+| --- | --- | --- |
+| `credentials(String, char[])` | required | [User name and domain](authentication.html#user-name-and-domain) |
+| `https()` / `http()`, `port(int)` | HTTP; port 5985 (5986 with HTTPS) | [TLS / HTTPS](tls.html) |
+| `authentication(AuthScheme...)` | NTLM | [Authentication](authentication.html) |
+| `ticketCache(Path)`, `allowDelegation()` | none, off | [Kerberos](authentication.html#kerberos-spnego), [Credential delegation](authentication.html#credential-delegation) |
+| `sslContext(SSLContext)` / `trustAllCertificates()` | JDK trust store, hostname verified | [TLS / HTTPS](tls.html) |
+| `timeout(Duration)` | 30 s | [Timeouts and Errors](timeouts-and-errors.html) |
+| `retries(int, Duration)` | no retry | [Retrying](timeouts-and-errors.html#retrying-transient-connection-failures) |
+| `namespace(String)` | `ROOT\CIMV2` | [Choosing a namespace](wql.html#choosing-a-namespace) |
+| `loadUserProfile()` | not loaded | [Loading the user profile](commands.html#loading-the-user-profile) |
+| `consoleCodePage(int)` | 65001 (UTF-8) | [Input encoding](commands.html#input-encoding) |
+
## Where to go next
* [Installation](installation.html) — coordinates, supported JDKs, and the standalone CLI jar
@@ -138,9 +163,9 @@ remain available and unchanged, with their checked exceptions.
* [File Transfers](file-transfers.html) — how files are copied to the host and downloaded back through the WinRM channel
* [Remote Files](files.html) — read remote files (whole, byte ranges, tails, streams, digests), get file properties, list directories
* [Command-Line Client](cli.html) — the standalone jar's manual page
-* [Authentication](authentication.html) — NTLM and Kerberos
+* [Authentication](authentication.html) — NTLM, Kerberos (including credential delegation), and Basic
* [TLS / HTTPS](tls.html) — certificate validation and trust stores
* [Timeouts and Errors](timeouts-and-errors.html) — timeout semantics and the exception surface
-* [Migrating from 1.x](migrating-from-1x.html) — the 2.0.0 breaking changes, and moving to the fluent API
+* [Migrating from 1.x](migrating-from-1x.html) — the 2.0.00 breaking changes, and moving to the fluent API
* [Migrating from winrm4j](migrating-from-winrm4j.html) — moving from cloudsoft/winrm4j: option mapping and behavioral differences
* [Legacy API](legacy.html) — the static one-shot helpers that predate `WinRMClient`
diff --git a/src/site/markdown/installation.md b/src/site/markdown/installation.md
index d951977..6f55647 100644
--- a/src/site/markdown/installation.md
+++ b/src/site/markdown/installation.md
@@ -5,18 +5,10 @@ description: Add the WinRM Java Client to your build, or run the standalone comm
-## Coordinates
+## Add it to your build
The library is published on
-[Maven Central](https://central.sonatype.com/artifact/${project.groupId}/${project.artifactId}) under:
-
-| Field | Value |
-| --- | --- |
-| `groupId` | `${project.groupId}` |
-| `artifactId` | `${project.artifactId}` |
-| `version` | `${project.version}` |
-
-## Add it to your build
+[Maven Central](https://central.sonatype.com/artifact/${project.groupId}/${project.artifactId}):
> [!TABS]
> * Maven
@@ -45,7 +37,7 @@ The library targets **Java 11** and runs on any later JDK.
## Runtime dependencies
-Since 2.0.0 the client has **zero runtime dependencies**. There is no longer an Apache CXF /
+Since 2.0.00 the client has **zero runtime dependencies**. There is no longer an Apache CXF /
JAX-WS / JAXB stack, no BouncyCastle, and no SMB stack (`smbj`) on the classpath — the client speaks
WS-Management over the JDK's own HTTP and XML APIs, and copies files through the WinRM channel
itself. If you upgraded from 1.x, see [Migrating from 1.x](migrating-from-1x.html) for the details
@@ -71,5 +63,6 @@ Subcommands, options, password handling, streaming behavior, and exit codes are
* [Preparing the Windows Host](preparing-the-host.html) — enabling WinRM and the required privileges
* [WQL Queries](wql.html)
* [Remote Commands](commands.html)
+* [Remote Files](files.html)
* [Command-Line Client](cli.html)
* [Authentication](authentication.html)
diff --git a/src/site/markdown/legacy.md b/src/site/markdown/legacy.md
index da5fce6..ba49bc2 100644
--- a/src/site/markdown/legacy.md
+++ b/src/site/markdown/legacy.md
@@ -6,10 +6,11 @@ description: The legacy static API that predates WinRMClient — still supported
Before the fluent [`WinRMClient`](apidocs/org/metricshub/winrm/WinRMClient.html), the library was
-used through two static one-shot helpers. They remain **fully supported and unchanged** — existing
-code keeps working — but each call opens a connection, authenticates, runs one operation, and
-closes everything. New code should use the [fluent API](index.html); see
-[Migrating from 1.x](migrating-from-1x.html) for the mapping.
+used through two static one-shot helpers. They remain **fully supported**, with the same signatures
+as in 1.x, but each call opens a connection, authenticates, runs one operation, and closes
+everything. New code should use the [fluent API](index.html); see
+[Migrating from 1.x](migrating-from-1x.html) for the runtime changes (for example, HTTPS
+certificates are now validated) and the mapping.
## WQL queries
@@ -25,7 +26,7 @@ WinRMWqlExecutor result = WinRMWqlExecutor.executeWql(
namespace, // null → ROOT\CIMV2
wqlQuery, // mandatory
timeout, // long, milliseconds, > 0
- ticketCache, // java.nio.file.Path, null for NTLM
+ ticketCache, // java.nio.file.Path, Kerberos ticket cache; null → log in with the password
authentications // List, null/empty → NTLM
);
@@ -51,7 +52,7 @@ WindowsRemoteCommandResult result = WinRMCommandExecutor.execute(
port, // null → protocol default
username, // mandatory
password, // char[]
- workingDirectory, // nullable
+ workingDirectory, // nullable; ignored when localFileToCopyList is not empty
timeout, // long, milliseconds, > 0
localFileToCopyList, // List of local files to copy first, nullable
ticketCache, // nullable
@@ -61,7 +62,7 @@ WindowsRemoteCommandResult result = WinRMCommandExecutor.execute(
result.getStdout();
result.getStderr();
result.getStatusCode(); // process exit code
-result.getExecutionTime();
+result.getExecutionTime(); // float, seconds
```
Files listed in `localFileToCopyList` are copied to the host first, and each reference to them in
@@ -77,14 +78,17 @@ all **checked**.
* **One connection per call**: every invocation performs a full authentication handshake. Code
that polls the same host repeatedly pays that cost on every call — the main reason to move to
the reusable `WinRMClient`.
-* The output character set of a command is detected before each call (one extra WQL query per
- call; the fluent client caches it).
-* TLS configuration is global only: the `org.metricshub.winrm.tls.insecure` system property (see
- [TLS / HTTPS](tls.html)); there is no per-call trust store.
+* Command output is decoded as UTF-8 (the remote shell runs under console code page 65001), as
+ with the fluent client by default; see [Character encoding](commands.html#character-encoding).
+* The static helpers take no TLS settings: trust a certificate through the JVM trust store
+ (`-Djavax.net.ssl.trustStore=...`), or, for testing only, disable validation with
+ `-Dorg.metricshub.winrm.tls.insecure=true` (see [TLS / HTTPS](tls.html#trusting-a-certificate)).
* Authentication schemes come from
[`AuthenticationEnum`](apidocs/org/metricshub/winrm/service/client/auth/AuthenticationEnum.html)
- (`NTLM`, `KERBEROS`, `BASIC`), with the same ordered-fallback semantics as the fluent
- [`AuthScheme`](apidocs/org/metricshub/winrm/AuthScheme.html).
+ (`NTLM`, `KERBEROS`, `BASIC`), with the same
+ [ordered-fallback semantics](authentication.html#ordered-fallback) as the fluent
+ [`AuthScheme`](apidocs/org/metricshub/winrm/AuthScheme.html): a list that contains `KERBEROS`
+ requires HTTPS (over HTTP the call fails, even when `NTLM` comes first).
* For advanced use, the underlying reusable executor is also public:
[`WinRMExecutorFactory.createInstance(...)`](apidocs/org/metricshub/winrm/service/WinRMExecutorFactory.html)
returns a [`WindowsRemoteExecutor`](apidocs/org/metricshub/winrm/WindowsRemoteExecutor.html) —
diff --git a/src/site/markdown/migrating-from-1x.md b/src/site/markdown/migrating-from-1x.md
index 6a314e9..e5c3ff9 100644
--- a/src/site/markdown/migrating-from-1x.md
+++ b/src/site/markdown/migrating-from-1x.md
@@ -1,16 +1,16 @@
keywords: migration, upgrade, 1.x, 2.0, cxf, tls, smb, breaking changes
-description: What changed in WinRM Java Client 2.0.0 and how to upgrade from the 1.x releases.
+description: What changed in WinRM Java Client 2.0.00 and how to upgrade from the 1.x releases.
# Migrating from 1.x
-Version 2.0.0 is a major cleanup: the legacy Apache CXF backend and the SMB-based file copy are
-gone, leaving a **dependency-free** client. The **1.x entry points are unchanged**, so typical
-calling code is unaffected — but a few CXF/SMB-only public types were removed (see the *Removed types*
-section below), and two runtime behaviors changed. Read this page before upgrading.
+Version 2.0.00 is a major cleanup: the legacy Apache CXF backend and the SMB-based file copy are
+gone, leaving a **dependency-free** client. The **1.x entry points keep their signatures**, but a few
+CXF/SMB-only public types were removed and several runtime behaviors changed (summarized below).
+Read this page before upgrading.
-Version 2.0.0 also introduces the **fluent [`WinRMClient`](apidocs/org/metricshub/winrm/WinRMClient.html)
+Version 2.0.00 also introduces the **fluent [`WinRMClient`](apidocs/org/metricshub/winrm/WinRMClient.html)
API**, which the documentation is now written around. Upgrading does not require adopting it —
the [legacy API](legacy.html) keeps working — but moving is straightforward and worthwhile; see
[Moving to the fluent API](#moving-to-the-fluent-api) below.
@@ -21,6 +21,8 @@ the [legacy API](legacy.html) keeps working — but moving is straightforward an
* HTTPS now **validates the certificate and verifies the hostname by default** (1.x trusted every
certificate). Self-signed hosts that used to work will now fail the TLS handshake until you trust
the certificate or opt out.
+* **Kerberos over plain HTTP is rejected**, even as a fallback: see
+ [Other behavior changes](#other-behavior-changes).
* File copy for `localFileToCopyList` now goes **through the WinRM channel** instead of SMB.
* A few CXF/SMB-only classes were removed.
* A new **fluent API** (`WinRMClient`) is the recommended way to use the library; the 1.x static
@@ -29,7 +31,7 @@ the [legacy API](legacy.html) keeps working — but moving is straightforward an
## TLS is validated by default
The 1.x CXF client silently trusted every TLS certificate and skipped hostname verification. The
-2.0.0 client uses the JDK's default validating socket factory instead, so **HTTPS connections to
+2.0.00 client uses the JDK's default validating socket factory instead, so **HTTPS connections to
hosts with self-signed or otherwise untrusted certificates now fail** during the handshake.
To restore connectivity, either:
@@ -42,46 +44,66 @@ See [TLS / HTTPS](tls.html) for details.
## The CXF backend was removed
-The dependency-free client introduced in the previous release is now the only implementation. There
-is no switch to fall back to the Apache CXF backend — if you still need it, stay on winrm-java 1.x.
+The dependency-free client is now the only implementation. There is no switch to fall back to the
+Apache CXF backend — if you still need it, stay on winrm-java 1.x.
## File copy no longer uses SMB
Files listed in `localFileToCopyList` for
-[`WinRMCommandExecutor.execute(...)`](commands.html) are no longer copied over SMB. They are
-transferred **through the WinRM command shell** (chunked base64, decoded on the host with `certutil`
-and verified with a digest). The consequences:
+[`WinRMCommandExecutor.execute(...)`](legacy.html#remote-commands) are no longer copied over SMB.
+They are transferred **through the WinRM command shell** (chunked base64, decoded on the host with
+`certutil` and verified with a digest). The consequences:
* **No SMB requirement** — TCP port 445 no longer needs to be reachable, no administrative or
temporary share is created on the host, and the copy now works from **any client OS** (1.x wrote
through a Windows UNC path and only worked from a Windows client).
+* Files land in `\Temp\winrm-upload-` instead of 1.x's
+ `SEN_ShareFor_$`. Where ACLs are hardened, grant write access on the new
+ path. Directories left by 1.x are not reused and can be deleted
+ ([Where files land](file-transfers.html#where-files-land)).
* The remote copy is **content-addressed**: a fragment of the content digest is inserted before the
- file extension (for example `script.1a2b3c4d.vbs`). Same-named files with different content can no
- longer overwrite each other, and a script that reads its own name (`WScript.ScriptName`) will see
- the digest fragment.
+ file extension (for example `script.1a2b3c4d5e6f.vbs`). Same-named files with different content
+ can no longer overwrite each other, and a script that reads its own name (`WScript.ScriptName`)
+ will see the digest fragment.
* A file already present on the host with an identical digest is not transferred again.
* The transport is meant for **small script files**, not bulk data.
+## Other behavior changes
+
+* **Kerberos requires HTTPS.** On an HTTP endpoint, any `authentication` list that contains
+ `KERBEROS` fails before connecting (`Kerberos over WinRM requires HTTPS ...`), even
+ `[NTLM, KERBEROS]`, which 1.x served with NTLM. Use HTTPS, or remove `KERBEROS` from lists used
+ over HTTP ([Authentication](authentication.html#kerberos-spnego)).
+* **Command output is decoded as UTF-8.** The shell runs under console code page 65001 (1.x decoded
+ the output with the host's ANSI code page), so non-ASCII characters now come through intact.
+ `WindowsRemoteProcessUtils.getWindowsEncodingCharset()` is deprecated. See
+ [Character encoding](commands.html#character-encoding).
+* **Result collections are unmodifiable.** `WinRMWqlExecutor.getHeaders()`/`getRows()` and
+ `WqlQuery.getSelectedProperties()`/`getSubPropertiesMap()` return unmodifiable collections: copy
+ them before modifying.
+
## Fewer dependencies
Removing CXF and SMB leaves the library with **zero runtime dependencies**: the Apache CXF /
JAX-WS / JAXB stack is gone, and so are `smbj`, BouncyCastle, SLF4J, `mbassador`, and `asn-one`. The
-standalone CLI jar shrinks from around 9 MB to a few hundred kB, and the library no longer references
-any logging API — problems are reported through [exceptions](timeouts-and-errors.html) only.
+library is a single jar of a few hundred kB, and it no longer references any logging API — problems
+are reported through [exceptions](timeouts-and-errors.html) only.
## Removed types
-These types and members were public in 1.x but are **removed** in 2.0.0. Code that referenced them
-will not compile against 2.0.0 (all were CXF- or SMB-specific):
+These types and members were public in 1.x but are **removed** in 2.0.00. Code that referenced them
+will not compile against 2.0.00 (all were CXF- or SMB-specific):
* `KerberosCredentialsException` — was thrown only by CXF internals.
* `SmbTempShare` and `WindowsRemoteProcessUtils.copyLocalFilesToShare(...)` — replaced by the
- WinRM-channel file transfer. `WindowsTempShare` is unchanged.
-* The Apache CXF-based `WinRMService` and its `service.client` internals, along with the generated
- WSDL/XSD resources.
+ WinRM-channel file transfer. `WindowsTempShare` keeps its API, but the share it creates is now
+ `winrm-upload-`: no trailing `$`, so it is no longer hidden.
+ `WindowsRemoteProcessUtils.buildNewOutputFileName()` now starts names with `winrm-`, not `SEN_`.
+* The Apache CXF-based `WinRMService` and its `service.client` internals (`AuthenticationEnum`
+ stays), along with the generated WSDL/XSD resources.
The 1.x entry points — `WinRMWqlExecutor`, `WinRMCommandExecutor`, `WinRMEndpoint`,
-`WindowsRemoteCommandResult`, the enums, and the exception types — are unchanged.
+`WindowsRemoteCommandResult`, the enums, and the exception types — keep their signatures.
## Moving to the fluent API
@@ -146,6 +168,8 @@ try (WinRMClient client = WinRMClient.builder("server").https()
| `ticketCache` argument | `ticketCache(Path)` |
| `List` | `authentication(AuthScheme...)` — same ordered-fallback semantics |
| `localFileToCopyList` | `upload(Path...)` on the command |
+| `workingDirectory` argument | `workingDirectory(String)` on the command (applies to the client's first command only) |
+| `getExecutionTime()` | `elapsed()` (a `Duration`) |
| `-Dorg.metricshub.winrm.tls.insecure=true` | `trustAllCertificates()` per client (or `sslContext(...)` for a dedicated trust store) |
| `getHeaders()` / `getRows()` (parallel lists) | `WqlResult.columns()` / iterable `WqlRow` with lookup by property name |
| `getStatusCode()` | `CommandResult.exitCode()` |
@@ -155,7 +179,7 @@ try (WinRMClient client = WinRMClient.builder("server").https()
The fluent API reports failures through the unchecked
[`WinRMClientException`](apidocs/org/metricshub/winrm/exceptions/WinRMClientException.html)
hierarchy instead of the 1.x checked exceptions — no more mandatory `try`/`catch` around every
-call, and WSMan faults expose their code and detail as fields:
+call, and WSMan faults expose their code and detail through getters:
| 1.x checked exception | Fluent unchecked exception |
| --- | --- |
@@ -164,5 +188,6 @@ call, and WSMan faults expose their code and detail as fields:
| `java.util.concurrent.TimeoutException` | [`WinRMTimeoutException`](apidocs/org/metricshub/winrm/exceptions/WinRMTimeoutException.html) |
| `WqlQuerySyntaxException` | [`WqlSyntaxException`](apidocs/org/metricshub/winrm/exceptions/WqlSyntaxException.html) |
-The exception **messages are unchanged**, so code that matches on message text keeps working after
-the switch. See [Timeouts and Errors](timeouts-and-errors.html) for the complete picture.
+Except for timeouts, the exception **messages are unchanged**, so code that matches on message text
+keeps working after the switch. See [Timeouts and Errors](timeouts-and-errors.html) for the complete
+picture.
diff --git a/src/site/markdown/migrating-from-winrm4j.md b/src/site/markdown/migrating-from-winrm4j.md
index d918a2c..0f87d40 100644
--- a/src/site/markdown/migrating-from-winrm4j.md
+++ b/src/site/markdown/migrating-from-winrm4j.md
@@ -6,7 +6,7 @@ description: Move from cloudsoft/winrm4j to the WinRM Java Client: why, a one-fo
[winrm4j](https://github.com/cloudsoft/winrm4j) has served Java projects well, but it is dormant —
-the last release (0.12.3) dates back to August 2021 and the last commit to March 2023 — and its
+the last release (0.12.3) dates back to August 2021 and the last commit to March 2022 — and its
Apache CXF / Java 8-era stack has aged poorly: on modern JDKs it needs manually added JAXB and
JAX-WS dependencies, and its `ServiceLoader`-based XML factory lookup is a known source of
classpath conflicts. This page maps the winrm4j API to the **WinRM Java Client** so an existing
@@ -23,11 +23,8 @@ code base can switch in one sitting.
so another library's `ServiceLoader`-registered XML implementation cannot break it — a classic
winrm4j/CXF failure mode.
* **Actively maintained**, with releases published on Maven Central.
-* **Features winrm4j never had:** [WQL / WMI queries](wql.html), [file transfers](file-transfers.html)
- and [remote file reads and directory listings](files.html) through the WinRM channel,
- [standard input](commands.html#standard-input),
- [`Process`-style streaming](commands.html#streaming-the-output) of live output, and a
- full-featured [command-line client](cli.html).
+* **Features winrm4j never had** — WQL queries, file transfers, remote file reads and listings,
+ standard input, `Process`-style streaming, and a full CLI: see [What you gain](#what-you-gain).
## The five-minute version
@@ -56,7 +53,7 @@ try (WinRMClient client = WinRMClient.builder("server.example.com")
}
```
-NTLM over HTTP on port 5985 is the default on both sides, so nothing needs to be spelled out. Two
+NTLM over HTTP on port 5985 is this client's default, so nothing needs to be spelled out. Two
shape differences are visible immediately:
* The client is **`AutoCloseable`** and meant for try-with-resources — it authenticates once and
@@ -75,20 +72,21 @@ with largely overlapping options. Both map to the single
| winrm4j | WinRM Java Client |
| --- | --- |
-| `WinRmTool.Builder.builder(address, username, password)` | `WinRMClient.builder(hostname).credentials(username, password)` — password as `char[]` |
+| `WinRmTool.Builder.builder(address, username, password)` | `WinRMClient.builder(hostname).credentials(username, password)` — a bare host name or IP, not a URL or `host:port` (use `https()` and `port(int)`); password as `char[]` |
| `builder(address, domain, username, password)` | `credentials("DOMAIN\\user", password)` — the domain rides in the user name |
| `useHttps(true)` | `https()` |
| `port(int)` | `port(int)` |
| `authenticationScheme(AuthSchemes.NTLM)` | `authentication(AuthScheme.NTLM)` — the default; several schemes form an ordered fallback list ([Authentication](authentication.html)) |
| `authenticationScheme(AuthSchemes.KERBEROS)` | `authentication(AuthScheme.KERBEROS)` — requires `https()` (see [behavioral differences](#behavioral-differences)) |
+| `authenticationScheme(AuthSchemes.SPNEGO)` | `authentication(AuthScheme.KERBEROS)` — always SPNEGO (`Negotiate`) here, and requires `https()`; credential delegation, automatic in winrm4j's SPNEGO, is opt-in with `allowDelegation()` ([Credential delegation](authentication.html#credential-delegation)) |
| `authenticationScheme(AuthSchemes.BASIC)` | `authentication(AuthScheme.BASIC)` — over HTTPS (see [behavioral differences](#behavioral-differences)) |
| `disableCertificateChecks(true)` | `trustAllCertificates()` |
| `sslContext(SSLContext)` | `sslContext(SSLContext)` — hostname verification stays on |
| `hostnameVerifier(...)`, `sslSocketFactory(...)` | none — hostname verification is all or nothing: on with `sslContext(...)`, off (together with certificate validation) with `trustAllCertificates()`. There is no custom-verifier hook, so the certificate must identify the hostname you connect by ([TLS / HTTPS](tls.html)) |
| `operationTimeout(long)` (milliseconds) | `timeout(Duration)` — different semantics, see [behavioral differences](#behavioral-differences) |
| `connectionTimeout(long)`, `connectionRequestTimeout(long)`, `receiveTimeout(Long)` | none — the single `timeout(Duration)` is a wall-clock deadline covering all of it |
-| `retriesForConnectionFailures(int)` | `retries(int, Duration)` — **opt-in**; see [behavioral differences](#behavioral-differences) |
-| `failureRetryPolicy(...)`, `retryReceiveAfterOperationTimeout(...)` | none — see [behavioral differences](#behavioral-differences) |
+| `retriesForConnectionFailures(int)` | `retries(int, Duration)` — **opt-in**; `retries(1, Duration.ofSeconds(5))` is the closest equivalent of winrm4j's default (one retry, 5-second pause); see [behavioral differences](#behavioral-differences) |
+| a custom `failureRetryPolicy(...)`, `retryReceiveAfterOperationTimeout(...)` | none — see [behavioral differences](#behavioral-differences) |
| `workingDirectory(String)` | `workingDirectory(String)` — on the **command**, not the client ([command options](commands.html#command-options)) |
| `environment(Map)` | `environment(String, String)` — on the **command**, once per variable |
| `requestNewKerberosTicket(boolean)` | none — Kerberos logs in with the password by default; `ticketCache(Path)` points at an existing ticket cache instead |
@@ -117,7 +115,7 @@ with largely overlapping options. Both map to the single
Failures surface through the unchecked
[`WinRMClientException`](apidocs/org/metricshub/winrm/exceptions/WinRMClientException.html)
hierarchy instead of winrm4j's `SOAPFaultException` / `RuntimeException` mix — authentication
-rejections, WSMan faults (with the fault code and detail as fields), and timeouts each have their
+rejections, WSMan faults (with getters for the fault code and detail), and timeouts each have their
own type. See [Timeouts and Errors](timeouts-and-errors.html).
Users of the lower-level `WinRmClient` / `createShell()` / `ShellCommand` API map the same way:
@@ -130,28 +128,19 @@ becomes `command(cmd).onStdout(...).onStderr(...).execute()`, and the shell reus
Beyond the API shapes, a few runtime behaviors differ deliberately. Worth reading before flipping
the switch:
-* **Payload encryption is always on over HTTP.** In winrm4j, NTLM message encryption is a
+* **NTLM payload encryption is always on over HTTP.** In winrm4j, NTLM message encryption is a
configurable `PayloadEncryptionMode` (`OFF` / `OPTIONAL` / `REQUIRED`) that only appeared in its
- final 0.12.x releases. Here, HTTP always uses NTLM message encryption — there is no unencrypted
- mode and nothing to configure, and hosts that require encryption (`AllowUnencrypted=false`, the
- Windows default) work out of the box.
-* **Basic needs HTTPS, but the client does not enforce it.** winrm4j offers `AuthSchemes.BASIC`,
- which here maps to `authentication(AuthScheme.BASIC)`. This client has no Basic message
- protection: it accepts the scheme over both transports, so **use `https()` with it** — without
- TLS, the credential and payload travel in the clear and the client will not stop you. The host
- must have the `Basic` setting enabled on the WinRM service and be reachable over HTTPS
- ([Preparing the Windows Host](preparing-the-host.html)), and the credential must be a **bare
- local account name** — Windows rejects Basic for domain accounts and for any
- `DOMAIN\`/`MACHINE\`-qualified name ([Authentication](authentication.html)). NTLM remains the
- recommended scheme.
+ final 0.12.x releases. Here, NTLM over HTTP always uses message encryption — nothing to
+ configure — and hosts that require encryption (`AllowUnencrypted=false`, the Windows default)
+ work out of the box.
+* **Basic: use it with `https()` and a bare local account name.** The client accepts Basic over
+ plain HTTP too, where the credential and payload travel in the clear, and Windows accepts Basic
+ only for local accounts named without a `DOMAIN\`/`MACHINE\` prefix
+ ([Authentication](authentication.html#basic)). NTLM remains the recommended scheme.
* **Kerberos requires HTTPS.** winrm4j runs Kerberos over plain HTTP; this client refuses at
`build()`, because it does not implement Kerberos message encryption — without TLS the payload
would travel unprotected. Connect with `https()` and by the FQDN the KDC knows
([Authentication](authentication.html)).
-* **TLS certificates are validated by default**, including hostname verification — like winrm4j
- (`disableCertificateChecks` exists on both sides), but worth re-checking if your winrm4j setup
- disabled checks and you want to stop doing that: point `sslContext(...)` at a trust store
- containing the host certificate instead ([TLS / HTTPS](tls.html)).
* **Command output is UTF-8, not code page 437.** winrm4j hardcodes `WINRS_CODEPAGE=437` (US-OEM),
which mangles any non-ASCII output on non-English hosts. This client creates the remote shell
with code page **65001 (UTF-8)**, so accented and non-Latin output decodes correctly whatever
@@ -192,12 +181,13 @@ Once on the fluent API, features winrm4j never offered are one call away:
* **Live streaming** — `start()` returns a `java.lang.Process`-shaped
[`RemoteProcess`](apidocs/org/metricshub/winrm/RemoteProcess.html) whose output is consumed
while the command runs, with bounded memory ([Streaming the output](commands.html#streaming-the-output)).
-* **A real CLI** — the standalone jar runs commands, PowerShell, WQL queries, and an interactive
- remote shell from the terminal ([Command-Line Client](cli.html)).
+* **A real CLI** — the standalone jar runs commands and WQL queries, opens an interactive remote
+ shell, and lists, reads and downloads remote files (`ls`, `stat`, `cat`, `get`) from the terminal
+ ([Command-Line Client](cli.html)).
## See also
* [Installation](installation.html) — coordinates and supported JDKs
* [Remote Commands](commands.html) — the complete command API
-* [Authentication](authentication.html) — NTLM and Kerberos details
+* [Authentication](authentication.html) — NTLM, Kerberos, and Basic details
* [Timeouts and Errors](timeouts-and-errors.html) — timeout semantics and the exception surface
diff --git a/src/site/markdown/preparing-the-host.md b/src/site/markdown/preparing-the-host.md
index 8c5b65b..b56afac 100644
--- a/src/site/markdown/preparing-the-host.md
+++ b/src/site/markdown/preparing-the-host.md
@@ -20,26 +20,24 @@ when it is already on, how to turn it on, and how to get the privileges right.
| Requirement | Detail |
| --- | --- |
| WinRM service running | Startup type is automatic (or delayed automatic) on Windows Server 2008 and later. |
-| A listener | HTTP on port **5985**, or HTTPS on port **5986**. See [TLS / HTTPS](tls.html). |
+| A listener | HTTP on port **5985**, or HTTPS on port **5986** (required for Kerberos). See [TLS / HTTPS](tls.html). |
| Firewall open on that port | Inbound, from the machine running the client. |
-| `Negotiate` authentication enabled on the service | **`True` by default.** This is what carries NTLM; `Kerberos` (also `True` by default) carries Kerberos. |
+| `Negotiate` authentication enabled on the service | **`True` by default.** The client sends both NTLM and Kerberos (SPNEGO) under this scheme; leave `Kerberos` (also `True` by default) as is. |
| The `Basic` setting enabled on the service (`winrm/config/service/auth`) | **Only for HTTP Basic** (not for NTLM or Kerberos). See below. |
-| An account with the right privileges | See [Privileges](#Privileges) below. |
+| An account with the right privileges | See [Privileges](#privileges) below. |
Just as important, a few settings that other WinRM guides tell you to change are **not** needed
here:
-* **`AllowUnencrypted` stays `False` for NTLM and Kerberos.** Over plain HTTP the client protects
- the payload with **NTLM message encryption**, so the service's default refusal of unencrypted
- traffic is satisfied. (Exception: HTTP Basic has no message protection, so it belongs on HTTPS,
- where TLS provides the confidentiality and no extra setting is needed. If — contrary to that —
- you run Basic over plain HTTP, the service must also set `AllowUnencrypted=true`, since otherwise
- it refuses the unprotected SOAP — see [Authentication](authentication.html).)
-* **The service's `Basic` and `CredSSP` settings stay `False` unless you use HTTP Basic.** NTLM
- and Kerberos need neither. To use HTTP Basic, enable the `Basic` setting under the service's
- `auth` section — `winrm set winrm/config/service/auth @{Basic=true}` — connect over HTTPS, and
- authenticate with a **bare local account name**: Windows rejects Basic for domain accounts and
- for any `DOMAIN\`-qualified name ([Authentication](authentication.html)).
+* **`AllowUnencrypted` stays `False`.** Over plain HTTP the client protects the payload with
+ **NTLM message encryption**; Kerberos runs over HTTPS only, where TLS protects it. (HTTP Basic
+ over plain HTTP would also need `AllowUnencrypted=true`; see
+ [Authentication](authentication.html#basic).)
+* **The service's `CredSSP` setting stays `False`** (this client never uses CredSSP), **and so
+ does `Basic` unless you use HTTP Basic.** To use it, enable the `Basic` setting under the
+ service's `auth` section — `winrm set winrm/config/service/auth @{Basic=true}` — connect over
+ HTTPS, and authenticate with a **bare local account name**: Windows rejects Basic for domain
+ accounts and for any `DOMAIN\`-qualified name ([Authentication](authentication.html#basic)).
* **`TrustedHosts` is irrelevant.** That is a setting on the *Windows* WinRM **client**, consulted
by the `winrs` command-line tool. A Java client never reads it, so you do not need to add anything
to it on either machine.
@@ -60,8 +58,9 @@ installation therefore already has:
* the firewall open on 5985, and
* `Kerberos` and `Negotiate` authentication enabled.
-That is exactly what this client needs, which is why connecting to a freshly installed Windows
-Server with an administrator account normally works with no host-side preparation at all.
+That is all NTLM needs, which is why connecting to a freshly installed Windows Server with an
+administrator account normally works with no host-side preparation at all; Kerberos also needs an
+[HTTPS listener](#https-port-5986).
It can still have been turned off afterwards — by Group Policy, by a hardening baseline, or by an
unattended-install answer file — so verify rather than assume.
@@ -69,7 +68,7 @@ unattended-install answer file — so verify rather than assume.
### Windows 10 / 11 and other client versions — no
Client versions of Windows do **not** enable WinRM by default. You must enable it explicitly, and
-the network profile matters (see [Enabling WinRM](#Enabling_WinRM)).
+the [network profile](#on-client-versions-of-windows-the-network-profile) matters.
### Domain membership is not what enables WinRM
@@ -83,12 +82,12 @@ Domain membership does change three things that matter here:
* **Kerberos becomes possible.** Kerberos needs a KDC, so it is only available in a domain;
workgroup hosts are limited to NTLM. See [Authentication](authentication.html).
-* **Group Policy becomes the practical way to enable WinRM** across many hosts at once — which is
- why WinRM *is* in fact enabled on the client machines of many domains. That is the GPO's doing,
- not the domain's.
+* **Group Policy becomes the practical way to enable WinRM** across many hosts at once
+ ([At scale: Group Policy](#at-scale-group-policy)), which is why many domains' workstations do
+ have it on.
* **Domain accounts escape UAC remote token filtering**, unlike local accounts. This is the single
most common cause of "my local admin account gets access denied" — see
- [Local administrators and UAC](#Local_administrators_and_UAC).
+ [Local administrators and UAC](#local-administrators-and-uac).
### Checking on the host
@@ -96,11 +95,12 @@ Domain membership does change three things that matter here:
Get-Service WinRM # is the service running?
winrm enumerate winrm/config/listener # is there a listener, on which port and address?
winrm get winrm/config/service # Negotiate/Kerberos enabled? AllowUnencrypted? RootSDDL?
-Get-NetFirewallRule -Name 'WINRM*' | Select-Object Name, Enabled, Profile
+Get-NetFirewallPortFilter | Where-Object LocalPort -in 5985, 5986 |
+ Get-NetFirewallRule | Select-Object DisplayName, Enabled, Profile # which rules open the ports?
```
`winrm get winrm/config/service` needs an elevated prompt; it is also the quickest way to read the
-listener's security descriptor (`RootSDDL`) discussed under [Privileges](#Privileges).
+listener's security descriptor (`RootSDDL`) discussed under [Privileges](#privileges).
### Checking from the client
@@ -126,15 +126,15 @@ needs WMI access, the second needs remote-shell access.
### The quick way (HTTP, port 5985)
-This client needs three things on the host: the **service running**, a **listener**, and the
-**firewall open**. One command sets up all three, from an **elevated** prompt on the target host:
+From an **elevated** prompt on the target host, one command starts the service, creates the HTTP
+listener and opens the firewall:
```console
winrm quickconfig
```
-Answer `y` when it asks. Add `-quiet` to skip the prompt. You can also use PowerShell's
-`Enable-PSRemoting -Force`, which also enables WinRM.
+Answer `y` when it asks. Add `-quiet` to skip the prompt. PowerShell's `Enable-PSRemoting -Force`
+does the same.
> [!NOTE]
> `winrm quickconfig` creates the firewall exception **only for the current firewall profile**. If
@@ -188,8 +188,8 @@ New-WSManInstance -ResourceURI winrm/config/Listener `
New-NetFirewallRule -DisplayName 'WinRM HTTPS' -Direction Inbound -Protocol TCP -LocalPort 5986 -Action Allow
```
-On the client side, a self-signed certificate is not trusted by default since 2.0.0: import it into
-a Java trust store, or opt out for testing. See [TLS / HTTPS](tls.html).
+On the client side, a self-signed certificate is not trusted by default: import it into a Java
+trust store, or opt out for testing. See [TLS / HTTPS](tls.html).
### At scale: Group Policy
@@ -225,7 +225,7 @@ a frequent reason a Windows Server that "should" work does not.
| **Domain administrator** (or any domain account in the host's local `Administrators`) | **Yes.** Nothing to configure. |
| **Built-in local `Administrator`** | **Yes** (it is exempt from UAC token filtering by default). |
| **Any other local account in `Administrators`** | **No** — access denied until `LocalAccountTokenFilterPolicy` is set. See below. |
-| **Non-administrator account** | **No** — needs an explicit grant on the listener, plus WMI grants *if it runs WQL queries*. See [Configuring a non-administrator account](#Configuring_a_non-administrator_account). |
+| **Non-administrator account** | **No** — needs an explicit grant on the listener, plus WMI grants *if it runs WQL queries*. See [Configuring a non-administrator account](#configuring-a-non-administrator-account). |
Administrator rights are what make WinRM work with zero host configuration, because the default
security descriptor on the WinRM listener grants full access to `BUILTIN\Administrators` and to
@@ -234,10 +234,6 @@ nobody else who connects over the network.
### Local administrators and UAC
This is the trap that catches most people connecting to a workgroup host or with a local account.
-Microsoft states it plainly:
-
-> Local administrator accounts other than the built-in Administrator account may not have rights to
-> manage a server remotely, even if remote management is enabled.
Under UAC, a local account that is a member of `Administrators` receives a **filtered token** on
network logon, stripped of its administrative privileges — so WinRM denies it. The built-in
@@ -250,6 +246,10 @@ New-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies
-Name LocalAccountTokenFilterPolicy -PropertyType DWord -Value 1 -Force
```
+On Windows 10 / 11, `winrm quickconfig` sets this value itself (its prompt lists *Configure
+LocalAccountTokenFilterPolicy to grant administrative rights remotely to local users*), so the
+warning below applies there too.
+
> [!WARNING]
> `LocalAccountTokenFilterPolicy = 1` disables UAC remote restrictions for **all** local
> administrator accounts, which materially weakens the host's resistance to lateral movement with a
@@ -270,21 +270,21 @@ separately:
| --- | --- |
| **WQL queries** (`client.wql(...)`) | Remote access to the listener, access to the **WMI plug-in**, and rights on the target **WMI namespace** (`ROOT\CIMV2` by default) — plus whatever the queried class itself demands. |
| **Remote commands** (`client.command(...)`) | Remote access to the listener, remote shell access on the host (`AllowRemoteShellAccess`, `True` by default), and whatever rights **the command itself** needs once it runs. |
-| **Transfer-and-run** (`upload(...)`) | Both of the above, plus write access to `\Temp\winrm-upload-`, and `certutil` and `forfiles` present on the host. See [File Transfers](file-transfers.html). |
+| **Transfer-and-run** (`upload(...)`, and `powerShell(...)` scripts over ~3,000 characters) | Both of the above, plus write access to `\Temp\winrm-upload-`, and `certutil` and `forfiles` present on the host. See [File Transfers](file-transfers.html). |
| **`uploadFile(...)`** to an explicit path | Remote shell access, write access to the destination directory, and `certutil` on the host (the same transfer engine, minus the transfer directory and its `forfiles` housekeeping). |
| **Remote file access** (`client.file(...)`: reads, downloads, `info()`, `list()`) | Remote shell access, **PowerShell 2.0 or later in `FullLanguage` mode** (not constrained by AppLocker or WDAC), and read access to the files and directories themselves. Nothing is written on the host. See [Remote Files](files.html). |
So an account can perfectly well run WQL queries and fail to run commands, or the reverse. When
-diagnosing, test the two independently — as in [Checking from the client](#Checking_from_the_client)
+diagnosing, test the two independently — as in [Checking from the client](#checking-from-the-client)
above.
### Configuring a non-administrator account
Non-administrative access is possible, and is the right choice for a monitoring account that only
needs to read WMI. Step 1 below is always required. **Step 2 is only needed for WQL queries** (and
-therefore for transfer-and-run, which discovers the remote Windows directory with one): an account
-that only runs commands, calls `uploadFile(...)` or accesses remote files never reaches WMI, so
-grant it nothing there.
+therefore for transfer-and-run, which discovers the remote Windows directory with one, long
+`powerShell(...)` scripts included): an account that only runs commands, calls `uploadFile(...)` or
+accesses remote files never reaches WMI, so grant it nothing there.
**1. Grant remote access to the WinRM listener.** The default listener security descriptor
(`RootSDDL`) grants full access to `BUILTIN\Administrators` and read access to interactive users
@@ -322,14 +322,9 @@ whereas assigning the value wholesale risks dropping the entries already there.
```
> [!NOTE]
- > `WinRMRemoteWMIUsers__` and `Remote Management Users` carry the *same* description in Windows
- > ("can access WMI resources over management protocols"), which makes them easy to confuse.
- > `WinRMRemoteWMIUsers__` is the one Microsoft's WinRM documentation names for the WMI plug-in on
- > Windows 8 / Server 2012 **and later** — it is current, not a legacy name — and it is created when
- > WinRM is configured, so a host where WinRM was never enabled may not have it yet.
- >
- > Either way, **membership in neither group is sufficient on its own**: the namespace rights below
- > and the `RootSDDL` entry from step 1 are what actually grant access.
+ > `WinRMRemoteWMIUsers__` and `Remote Management Users` carry the *same* description in Windows,
+ > which makes them easy to confuse. `WinRMRemoteWMIUsers__` is the current group for the WMI
+ > plug-in (Windows 8 / Server 2012 and later), created when WinRM is configured.
* Then grant the account rights on the WMI namespace itself, since group membership alone does not
confer them. Run `wmimgmt.msc` → **WMI Control** → *Properties* → *Security*, select the
@@ -337,13 +332,9 @@ whereas assigning the value wholesale risks dropping the entries already there.
**Enable Account** and **Remote Enable**, with *Applies to* set to
**This namespace and subnamespaces**.
- Those two are all a `SELECT` query needs. In particular **`Execute Methods` is not required** —
- it authorizes invoking WMI class methods, which this client never does (it only runs `SELECT`
- queries; anything else is rejected locally as a `WqlSyntaxException`). Many monitoring guides
- grant it anyway; leave it off unless something else on the account's behalf needs it.
-
- Granting these on `Root` with *This namespace and subnamespaces* covers every namespace at once;
- granting them on `Root\CIMV2` alone is tighter and usually sufficient.
+ Those two are all a query needs: **`Execute Methods` is not required**, since this client never
+ invokes WMI methods. Granting them on `Root` covers every namespace; `Root\CIMV2` alone is tighter
+ and usually sufficient.
**3. Remote commands need more than this.** Listener access lets a non-administrator open a remote
shell, but the commands you then run are ordinary Windows processes subject to ordinary Windows
@@ -379,8 +370,8 @@ the host, the tighter they are:
| `MaxMemoryPerShellMB` | Memory per shell, including child processes | Historically **150 MB**; 1024 MB on modern hosts. A command whose output is large can hit it. |
| `MaxShellsPerUser` | Concurrent shells per user | 5 on older hosts, 30 on modern ones. Close clients you no longer need. |
| `MaxConcurrentOperationsPerUser` | Concurrent operations per user | 15 on Windows Server 2008 R2, 1500 later. File transfers are batched specifically to stay under low limits. |
-| `MaxEnvelopeSizekb` | SOAP envelope size | 150 KB on older hosts, **500 KB** on modern ones; caps how much a single response can carry. |
-| `IdleTimeout` | How long an idle shell survives | 180000 ms (3 min) on older hosts, **7200000 ms** (2 h) on modern ones; 60000 ms minimum. |
+| `MaxEnvelopeSizekb` | SOAP envelope size | 150 KB on older hosts, **500 KB** on modern ones. This client always uses 150 KB envelopes, so the default never needs raising. |
+| `IdleTimeout` | How long an idle shell survives | 180000 ms (3 min) on older hosts, **7200000 ms** (2 h) on modern ones; 60000 ms minimum. The client transparently recreates a shell the host reaped. |
Read them with `winrm get winrm/config`. Raising a quota is a considered decision, not a reflex —
prefer narrowing the query or splitting the command.
@@ -390,12 +381,11 @@ prefer narrowing the query or splitting the command.
| What you see | Likely cause on the host |
| --- | --- |
| Connection refused / connection timed out on 5985 or 5986 | Service not running, no listener, or the firewall is closed. Check all three, in that order. |
-| Connection succeeds but every request is refused, with correct credentials | The account is denied by `RootSDDL`, or it is a local administrator hitting UAC token filtering. |
-| `WinRMAuthenticationException` for one local admin but not for the built-in `Administrator` | UAC token filtering — see [Local administrators and UAC](#Local_administrators_and_UAC). |
-| `WinRMAuthenticationException` with a Kerberos scheme, NTLM working | Connect by the FQDN the KDC knows, check clock skew, or fall back to NTLM. See [Authentication](authentication.html). |
+| `WinRMAuthenticationException` although the password is right | A non-administrator not granted in `RootSDDL` ([step 1](#configuring-a-non-administrator-account)), or a local administrator other than the built-in `Administrator` hitting UAC token filtering ([Local administrators and UAC](#local-administrators-and-uac)). |
+| Kerberos fails while NTLM works: a `WinRMClientException` carrying the JDK's Kerberos message (*Server not found in Kerberos database*, *Clock skew too great*), or a `WinRMAuthenticationException` | Connect by the FQDN the KDC knows, check clock skew, or fall back to NTLM. See [Authentication](authentication.html#kerberos-spnego). |
| `WinRMFaultException` whose detail is `WBEM_E_ACCESS_DENIED` | The account reached WMI but lacks namespace rights — step 2 above. |
| `WinRMFaultException` whose detail is `WBEM_E_INVALID_CLASS` or `WBEM_E_INVALID_NAMESPACE` | The query is wrong, not the permissions. See [WQL Queries](wql.html). |
-| WQL works, commands do not | Remote shell access, `AllowRemoteShellAccess`, or a per-user shell quota. |
+| WQL works, commands do not | `AllowRemoteShellAccess` is disabled, or the `MaxShellsPerUser` quota is reached (see [Host quotas](#host-quotas-worth-knowing-about)). |
| Commands work, WQL does not | WMI plug-in or namespace rights — step 2 above. |
| A TLS handshake failure over HTTPS | The certificate is not trusted by the JVM, or its name does not match. See [TLS / HTTPS](tls.html). |
@@ -404,7 +394,7 @@ The full exception surface, including how to read a WSMan fault code, is describ
## See also
-* [Authentication](authentication.html) — NTLM and Kerberos, and what each needs from the host
+* [Authentication](authentication.html) — NTLM, Kerberos and Basic, and what each needs from the host
* [TLS / HTTPS](tls.html) — trusting the listener's certificate
* [File Transfers](file-transfers.html) — what transfers need on the host
* [Remote Files](files.html) — reading and listing remote files, and what that needs on the host
diff --git a/src/site/markdown/timeouts-and-errors.md b/src/site/markdown/timeouts-and-errors.md
index fdfac69..539b8bb 100644
--- a/src/site/markdown/timeouts-and-errors.md
+++ b/src/site/markdown/timeouts-and-errors.md
@@ -8,8 +8,8 @@ description: Timeout semantics and the exception surface of the WinRM Java Clien
## Timeouts
Timeouts are `java.time.Duration` values and must be **at least one millisecond**. The builder's
-`timeout(...)` sets the default for every operation (30 seconds when unset), and each operation
-can override it:
+`timeout(...)` sets the default for every operation (30 seconds when unset), and each request
+can override it (`uploadFile(...)` and `downloadFile(...)` always use the client's):
```java
try (WinRMClient client = WinRMClient.builder("server.example.com")
@@ -28,7 +28,7 @@ first operation), every WSMan round trip, and — for commands — the file uplo
download: the whole transfer), all budgeted against the same deadline. When it elapses, the operation
fails with
[`WinRMTimeoutException`](apidocs/org/metricshub/winrm/exceptions/WinRMTimeoutException.html) and
-no part of it (in particular: the command itself) runs afterward.
+stops: a command not started yet is never started, and one still running is terminated.
The timeout also drives the wire-level behavior: the WSMan `OperationTimeout` header and the
socket timeouts follow each operation's own deadline.
@@ -44,8 +44,8 @@ it is an **inactivity timeout**, the longest silence tolerated from the server b
responses. A query result can be consumed, or a command can keep streaming output, for arbitrarily
long — but as soon as the server stays silent for a whole timeout, the operation fails with
[`WinRMTimeoutException`](apidocs/org/metricshub/winrm/exceptions/WinRMTimeoutException.html).
-For commands, `RemoteProcess.waitFor(Duration)` provides an overall deadline on top when one is
-needed.
+For commands, `RemoteProcess.waitFor(Duration)` adds an overall deadline: it returns `false` when
+the deadline passes, and the command keeps running.
## Retrying transient connection failures
@@ -97,13 +97,13 @@ subtypes for the cases worth catching specifically:
| Exception | Meaning |
| --- | --- |
-| [`WinRMAuthenticationException`](apidocs/org/metricshub/winrm/exceptions/WinRMAuthenticationException.html) | The credentials were rejected (after every scheme of an ordered fallback list). |
+| [`WinRMAuthenticationException`](apidocs/org/metricshub/winrm/exceptions/WinRMAuthenticationException.html) | The server rejected the credentials (after every scheme of an ordered fallback list); the message reads `Authentication error on with user name ""`. A Kerberos failure on the client side (login refused by the KDC, unknown SPN) is a plain `WinRMClientException` caused by a `LoginException` or `GSSException`. |
| [`WinRMFaultException`](apidocs/org/metricshub/winrm/exceptions/WinRMFaultException.html) | The remote service answered with a WSMan fault. |
| [`WinRMTimeoutException`](apidocs/org/metricshub/winrm/exceptions/WinRMTimeoutException.html) | The operation exceeded its timeout. |
| [`WqlSyntaxException`](apidocs/org/metricshub/winrm/exceptions/WqlSyntaxException.html) | The WQL query does not match the supported `SELECT` syntax. |
-| [`WinRMClientException`](apidocs/org/metricshub/winrm/exceptions/WinRMClientException.html) | Base type: any other failure (connection, DNS, TLS, protocol, local I/O). |
+| [`WinRMClientException`](apidocs/org/metricshub/winrm/exceptions/WinRMClientException.html) | Base type: any other failure (connection, DNS, TLS, protocol, local I/O); the underlying exception, e.g. `java.net.ConnectException` or `javax.net.ssl.SSLHandshakeException`, is in its cause chain. |
-`IllegalArgumentException` (invalid option values) and `IllegalStateException` (missing
+`IllegalArgumentException` (invalid option values) and `IllegalStateException` (e.g. missing
credentials at `build()`, or an operation on a closed client) report programming errors
immediately, before anything touches the network.
@@ -114,30 +114,24 @@ the fault **programmatically**, so no message parsing is needed:
| Method | Returns |
| --- | --- |
-| `getFaultCode()` | The numeric WSManFault code, e.g. `2150858778`. |
-| `getFaultReason()` | The SOAP fault reason text. |
-| `getFaultDetail()` | The provider-level detail — where WMI puts mnemonics such as `WBEM_E_INVALID_CLASS` or `WBEM_E_INVALID_NAMESPACE`. |
+| `getFaultCode()` | The WSManFault code as a decimal `String`, e.g. `"2150858778"`, or `null` when the response carried none. |
+| `getFaultReason()` | The SOAP fault reason text, or `null`. |
+| `getFaultDetail()` | The provider-level detail — where WMI puts mnemonics such as `WBEM_E_INVALID_CLASS` or `WBEM_E_INVALID_NAMESPACE` — or `null`. |
| `getHttpStatus()` | The HTTP status of the faulting response (typically 500). |
The exception message still carries the same text as the legacy API, so message-based matching
keeps working.
-### Authentication failures
-
-A rejected credential surfaces as a
-[`WinRMAuthenticationException`](apidocs/org/metricshub/winrm/exceptions/WinRMAuthenticationException.html)
-whose message has the stable form `Authentication error on with user name ""`.
-
### The legacy API
The [legacy static helpers](legacy.html) keep their historical **checked** exceptions
-(`WinRMException`, `WqlQuerySyntaxException`, `TimeoutException`, `IOException`); they are
-unaffected by the unchecked hierarchy above.
+(`WindowsRemoteException` and its subclass `WinRMException`, `WqlQuerySyntaxException`,
+`TimeoutException`, `IOException`); they are unaffected by the unchecked hierarchy above.
## Command-line exit codes
The standalone jar maps these outcomes to stable process exit codes — see the
-[Command-Line Client](cli.html) manual.
+[Command-Line Client](cli.html#exit-codes) manual.
## See also
diff --git a/src/site/markdown/tls.md b/src/site/markdown/tls.md
index 2a4b8f4..e38f484 100644
--- a/src/site/markdown/tls.md
+++ b/src/site/markdown/tls.md
@@ -17,9 +17,10 @@ WinRMClient.builder("server.example.com")
## Validation is on by default
-Since 2.0.0 the client uses the JDK's default, **validating** `SSLSocketFactory`: it checks the
-server certificate against the platform trust store and **verifies the server hostname** during the
-handshake.
+Since 2.0.00 the client uses the JDK's default, **validating** `SSLSocketFactory`: it checks the
+server certificate against the JDK's trust store (its `cacerts`, not the Windows certificate store)
+and **verifies** during the handshake that the certificate matches the host name passed to
+`builder(...)`.
> [!WARNING]
> This is a change from the 1.x CXF-based client, which silently trusted every certificate and
@@ -27,6 +28,11 @@ handshake.
> untrusted certificates now fail** during the TLS handshake unless you trust the certificate or
> explicitly opt out (below). See [Migrating from 1.x](migrating-from-1x.html).
+Current JDKs also disable TLS 1.0 and 1.1 (`jdk.tls.disabledAlgorithms`), so the handshake fails
+with a host that offers nothing newer (Windows Server 2008 R2 by default), whatever the trust
+settings. Enable TLS 1.2 on the host, or, for a test host, re-enable the old protocol in a file
+passed with `-Djava.security.properties=`.
+
## Trusting a certificate
The recommended fix for a self-signed or private-CA host is to add the server certificate (or its
@@ -39,7 +45,7 @@ java -Djavax.net.ssl.trustStore=/path/to/truststore.jks \
```
Because the client uses the JDK default socket factory, any trust store configured this way (or the
-platform's default trust store) applies automatically.
+JDK's default `cacerts`) applies automatically.
### A dedicated trust store for one client
@@ -93,12 +99,8 @@ precedence over the system property for that client.
## On the command line
-The standalone jar mirrors this behavior:
-
-| Option | Meaning |
-| --- | --- |
-| `--https` | Use HTTPS (port 5986 by default). |
-| `--https-permissive` | Trust any certificate and hostname. Intentionally insecure; testing only. Requires `--https`. |
+The standalone jar's `--https` and `--https-permissive` options are `https()` and
+`trustAllCertificates()`; see the [Command-Line Client](cli.html#options) manual:
```bash
java -jar ${project.artifactId}-${project.version}-standalone.jar \
@@ -107,8 +109,6 @@ java -jar ${project.artifactId}-${project.version}-standalone.jar \
wql 'SELECT Name FROM Win32_ComputerSystem'
```
-`--https-permissive` sets `org.metricshub.winrm.tls.insecure=true` for that invocation.
-
## See also
* [Authentication](authentication.html) — Kerberos requires HTTPS
diff --git a/src/site/markdown/wql.md b/src/site/markdown/wql.md
index 6c8830e..7aa531c 100644
--- a/src/site/markdown/wql.md
+++ b/src/site/markdown/wql.md
@@ -33,7 +33,7 @@ try (WinRMClient client = WinRMClient.builder("server.example.com")
```
One client can run any number of queries (and [commands](commands.html)) over the same
-authenticated connection — see the [Overview](index.html) for the builder options.
+authenticated connection — see the [client options](index.html#client-options).
### Query options
@@ -86,7 +86,7 @@ Points to know:
an exhausted stream cleans up on its own.
* The stream **holds the client's serial connection** while open: other operations on the same
client wait until it is closed or exhausted (the same contract as a JDBC `ResultSet` on its
- connection).
+ connection). Never call the same client while consuming the stream; use a second client.
* The timeout is an **inactivity** timeout — the longest silence tolerated from the server between
two responses — not an overall deadline: consuming a huge result can take arbitrarily long as
long as the server keeps answering. See [Timeouts and Errors](timeouts-and-errors.html).
@@ -99,7 +99,7 @@ Points to know:
| Method | Returns | Description |
| --- | --- | --- |
-| `columns()` | `List` | The property (column) names, in query order. |
+| `columns()` | `List` | The property (column) names (see [Column order and case](#column-order-and-case)). |
| `rows()` | `List<`[`WqlRow`](apidocs/org/metricshub/winrm/WqlRow.html)`>` | The result rows. |
| `size()` / `isEmpty()` | `int` / `boolean` | Row count. |
| `elapsed()` | `java.time.Duration` | Wall-clock time of the query. |
@@ -108,20 +108,26 @@ Each [`WqlRow`](apidocs/org/metricshub/winrm/WqlRow.html) exposes the instance p
| Method | Returns | Description |
| --- | --- | --- |
-| `string(String)` | `String` | The property value as a string, or `null`. |
-| `get(String)` | `Object` | The raw property value, or `null`. |
+| `string(String)` | `String` | The property value, or `null` when the row has no such property. |
+| `get(String)` | `Object` | The same value (currently always a `String`). |
| `asMap()` | `Map` | All properties, in server order (unmodifiable). |
Property lookup is **case-insensitive**, matching WMI semantics: `row.string("name")` and
`row.string("Name")` return the same value.
-### Property order and case
+Values are the text WinRM sends, unconverted: parse numbers, booleans and dates yourself. A WMI
+`NULL` comes back as an empty string.
+
+### Column order and case
+
+These rules apply to `WqlResult.columns()`; each row's `asMap()` keeps the server's order, and
+lookups in it are case-sensitive.
* When you select explicit properties (`SELECT Name, State FROM ...`), the columns keep the **order
of the query** and the **exact case reported by WMI**.
-* With `SELECT * FROM ...`, the properties are returned in **alphabetical order** (case-insensitive).
-* If the query returns no rows, the columns fall back to the property names exactly as written in
- the query (WMI's own casing cannot be recovered from an empty result set).
+* With `SELECT * FROM ...`, the columns are in **alphabetical order** (case-insensitive).
+* If the query returns no rows, the columns are the selected property names in lower case (WMI's
+ own casing cannot be recovered from an empty result set), or an empty list for `SELECT *`.
## Supported WQL syntax
@@ -133,9 +139,9 @@ SELECT Name, State, StartMode FROM Win32_Service
SELECT Name FROM Win32_Process WHERE Name = 'explorer.exe'
```
-The grammar is a single `SELECT` of either `*` or a comma-separated property list, a `FROM` clause,
-and an optional `WHERE` clause. Joins, sub-selects, and other advanced constructs are not part of
-the supported syntax. An invalid query raises a
+The grammar is a single `SELECT` of either `*` or a comma-separated property list, a `FROM` clause
+naming one class, and an optional `WHERE` clause. Anything else, such as `ASSOCIATORS OF`,
+`REFERENCES OF` or event queries (`WITHIN`), is rejected: an invalid query raises a
[`WqlSyntaxException`](apidocs/org/metricshub/winrm/exceptions/WqlSyntaxException.html) before
anything is sent to the host.
@@ -148,21 +154,15 @@ accepted.
## Exceptions
-`execute()` reports failures through the unchecked
-[`WinRMClientException`](apidocs/org/metricshub/winrm/exceptions/WinRMClientException.html)
-hierarchy:
-
-| Exception | When |
-| --- | --- |
-| [`WqlSyntaxException`](apidocs/org/metricshub/winrm/exceptions/WqlSyntaxException.html) | The query does not match the supported `SELECT` syntax. |
-| [`WinRMAuthenticationException`](apidocs/org/metricshub/winrm/exceptions/WinRMAuthenticationException.html) | The credentials were rejected. |
-| [`WinRMFaultException`](apidocs/org/metricshub/winrm/exceptions/WinRMFaultException.html) | The remote service answered with a WSMan fault (e.g. an unknown class or namespace) — the fault code and detail are available as fields. |
-| [`WinRMTimeoutException`](apidocs/org/metricshub/winrm/exceptions/WinRMTimeoutException.html) | The query did not complete within its timeout. |
-| [`WinRMClientException`](apidocs/org/metricshub/winrm/exceptions/WinRMClientException.html) | Any other failure (connection, TLS, protocol). |
-
-See [Timeouts and Errors](timeouts-and-errors.html) for the full exception surface.
+`execute()` and `stream()` report failures as unchecked
+[`WinRMClientException`](apidocs/org/metricshub/winrm/exceptions/WinRMClientException.html)s; see
+[Timeouts and Errors](timeouts-and-errors.html#the-exception-surface) for the full list. An
+unknown class or namespace raises a
+[`WinRMFaultException`](apidocs/org/metricshub/winrm/exceptions/WinRMFaultException.html) whose
+`getFaultDetail()` carries the WMI error, e.g. `WBEM_E_INVALID_CLASS` or
+`WBEM_E_INVALID_NAMESPACE`.
## From the command line
-The standalone jar exposes the same capability through its `wql` subcommand, streaming the rows
-to stdout as JSON Lines — see the [Command-Line Client](cli.html) manual.
+The standalone jar runs queries in the default `ROOT\CIMV2` namespace through its `wql` subcommand,
+streaming the rows to stdout as JSON Lines — see the [Command-Line Client](cli.html) manual.