From 0ad7cb2cb78fa0a0abcc0800317f4b9c95d16939 Mon Sep 17 00:00:00 2001 From: Bertrand Martin Date: Tue, 29 Sep 2026 19:24:11 +0200 Subject: [PATCH 1/3] Review the documentation before the 2.3.00 release A full pass over src/site/markdown, site.xml and README.md, checked against the code: every finding was verified in the source before being applied. - Correct wrong statements: WQL row values are always strings (a WMI NULL is an empty string), getFaultCode() returns a String, client-side Kerberos failures are a plain WinRMClientException, the legacy API no longer detects the output code page, --https-permissive does not set the insecure system property, PowerShell can be 2.0 on old hosts, and Basic must not be combined with --https-permissive. - Document what was missing: the builder options and their defaults (a new "Client options" table on the overview, which two pages already pointed to), thread sharing, the Kerberos principal and ticket-cache rules, and the 1.x breaking changes the migration page left out. - Tighten the structure: File Transfers covers uploads and how downloads work, Remote Files no longer repeats it; CLI option tables on topic pages become pointers to cli.md; repeated exception tables and implementation rationale are trimmed. - Make it consistent: 2.0.00 version notation, lowercase anchors, the JDK trust store wording. - Fix the --help text (DOMAIN\user printed with two backslashes, the "Connection options" header, the -i line) and stale Javadoc. - README: current feature list and release instructions. Co-Authored-By: Claude Opus 5.5 --- README.md | 42 ++---- .../org/metricshub/winrm/CommandRequest.java | 12 +- .../winrm/WindowsRemoteCommandResult.java | 14 +- .../java/org/metricshub/winrm/WqlResult.java | 2 +- .../org/metricshub/winrm/cli/WinRmCli.java | 6 +- .../winrm/command/WinRMCommandExecutor.java | 14 +- src/site/markdown/authentication.md | 97 +++++------- src/site/markdown/cli.md | 117 +++++++-------- src/site/markdown/commands.md | 142 +++++++++--------- src/site/markdown/file-transfers.md | 88 +++++------ src/site/markdown/files.md | 83 +++++----- src/site/markdown/index.md | 41 ++++- src/site/markdown/installation.md | 15 +- src/site/markdown/legacy.md | 30 ++-- src/site/markdown/migrating-from-1x.md | 77 ++++++---- src/site/markdown/migrating-from-winrm4j.md | 52 +++---- src/site/markdown/preparing-the-host.md | 112 +++++++------- src/site/markdown/timeouts-and-errors.md | 34 ++--- src/site/markdown/tls.md | 24 +-- src/site/markdown/wql.md | 54 +++---- 20 files changed, 526 insertions(+), 530 deletions(-) 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..4ccd8cf 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,10 @@ 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 sends a qualified name unchanged, for +the non-Microsoft WSMan services that accept one.) ```java try (WinRMClient client = WinRMClient.builder("server.example.com") @@ -165,32 +166,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 +197,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..4eb7926 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,13 @@ 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`, each argument becomes one word of the remote command +line, double-quoted when it contains a space: write `command dir 'C:\Program Files'`, not +`command 'dir "C:\Program Files"'`. 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 +57,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 +92,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 +148,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 +165,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 +180,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 +214,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 +288,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 +345,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 +361,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 +404,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..218d7eb 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, and only the transfer directory's 30-day purge +reclaims them. ### 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. From 1c3cbb32b4071cae33814a611778a86ceffbe545 Mon Sep 17 00:00:00 2001 From: Bertrand Martin Date: Tue, 29 Sep 2026 19:37:06 +0200 Subject: [PATCH 2/3] Address Codex review: Basic user name whitespace, upload leftovers - authentication.md: the client strips all whitespace from the user name (WinRMEndpoint), so a qualified Basic name is not sent unchanged. - file-transfers.md: the 30-day purge only covers the transfer-and-run directory; leftovers next to an uploadFile(...) destination stay. Co-Authored-By: Claude Opus 5.5 --- src/site/markdown/authentication.md | 5 +++-- src/site/markdown/file-transfers.md | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/site/markdown/authentication.md b/src/site/markdown/authentication.md index 4ccd8cf..9dfde7a 100644 --- a/src/site/markdown/authentication.md +++ b/src/site/markdown/authentication.md @@ -153,8 +153,9 @@ 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, 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 sends a qualified name unchanged, for -the non-Microsoft WSMan services that accept one.) +`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") diff --git a/src/site/markdown/file-transfers.md b/src/site/markdown/file-transfers.md index 218d7eb..219942a 100644 --- a/src/site/markdown/file-transfers.md +++ b/src/site/markdown/file-transfers.md @@ -127,8 +127,8 @@ 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; -an interrupted transfer can leave them behind, and only the transfer directory's 30-day purge -reclaims them. +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 From 9ce8e6d8e12bc7eea5dc00be8816c21429e0350d Mon Sep 17 00:00:00 2001 From: Bertrand Martin Date: Tue, 29 Sep 2026 19:46:46 +0200 Subject: [PATCH 3/3] Address Codex review: cmd.exe operators in CLI command arguments Only arguments containing whitespace or a double quote are quoted, so the remote cmd.exe interprets &, |, <, >, ^ and % in the others. Say so instead of promising that each argument stays one word. Co-Authored-By: Claude Opus 5.5 --- src/site/markdown/cli.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/site/markdown/cli.md b/src/site/markdown/cli.md index 4eb7926..0924798 100644 --- a/src/site/markdown/cli.md +++ b/src/site/markdown/cli.md @@ -37,9 +37,11 @@ java -jar winrm-java-standalone.jar --help | --version | `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, quoted -by your local shell's rules. For `command`, each argument becomes one word of the remote command -line, double-quoted when it contains a space: write `command dir 'C:\Program Files'`, not -`command 'dir "C:\Program Files"'`. Git Bash rewrites an argument that starts with `/` into a +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.