Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
b1a19ef
Add dependency-free "light" WinRM backend behind a runtime toggle
bertysentry Jul 23, 2026
bb266b8
Address Codex review: honor backend toggle for file copy, harden XML …
bertysentry Jul 23, 2026
1da7d38
Harden light backend: reject unencrypted responses, fix timeout/close…
bertysentry Jul 23, 2026
b549182
Harden light backend NTLM/HTTP protocol parsing (P2 review)
bertysentry Jul 23, 2026
dbb0ad4
Make the light backend the default (CXF now opt-in)
bertysentry Jul 23, 2026
df65e97
Harden light default: auth downgrade, hostname parsing, EndOfSequence…
bertysentry Jul 23, 2026
40d5410
Serialize operations on the light backend's NTLM connection (P1 review)
bertysentry Jul 23, 2026
9c8af0b
Reconnect the light backend after a peer closes an idle keep-alive so…
bertysentry Jul 23, 2026
6b61322
Harden light backend: reject unknown backend, clamp timeout, closed-s…
bertysentry Jul 23, 2026
1a93929
Add HTTPS support to the light backend (validate TLS by default)
bertysentry Jul 23, 2026
c6e924a
Extract an AuthScheme seam; move NTLM into NtlmAuthScheme (no behavio…
bertysentry Jul 23, 2026
ab656c7
Add Kerberos (SPNEGO) support to the light backend via JDK JGSS (#105)
bertysentry Jul 23, 2026
e2261c6
Remove unused imports left by the AuthScheme extraction (checkstyle)
bertysentry Jul 23, 2026
324154b
Don't wedge on an auth rejection; fall back on a server-side 401 (rev…
bertysentry Jul 23, 2026
2625fcf
Fall through on active-scheme re-auth failure; dispose auth on close …
bertysentry Jul 23, 2026
782604e
Document the light-default upgrade warning (README, site, CHANGELOG)
bertysentry Jul 23, 2026
38c51ab
Decode command output once, not per chunk, to preserve split multibyt…
bertysentry Jul 23, 2026
ccfeed6
Match the CXF backend's exception surface and fault mapping (#106)
bertysentry Jul 23, 2026
b9b41c9
Add the recorded-exchange protocol test rig and differential harness …
bertysentry Jul 23, 2026
70c2546
Remove the CXF backend; the dependency-free client is the only one (2…
bertysentry Jul 23, 2026
83884ba
Add AGENTS.md and CLAUDE.md agent instructions
bertysentry Jul 23, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Instructions for AI Agents

## Code format

You never need to worry about code formatting at all. Simply run `mvn formatter:format` before committing changes to make sure the new code follows this project's code formatting rules. Make sure not to run `mvn formatter:format` separately before other Maven commands, to avoid concurrency issues.

All files must include the proper license header. When you add a new file, make sure to include the proper license header by running the `mvn license:update-file-header` command before committing (or even before trying the build and test, since the build will fail if a file doesn't include the proper license header).

All public methods must have proper Javadoc. Check the output of Maven to identify issues with Javadoc and fix these issues.

## Build

The project uses Maven to build. A full build is performed with `mvn verify site` (or `mvn clean verify site` when applicable).

@codex, please don't try to use `mvnw` (Maven Wrapper). Maven is already installed and runs perfectly well.

## Test

Whenever required, when you add code or when you modify code that is not covered with unit tests, add the corresponding unit tests. All tests must pass with `mvn test`. Don't use the `-q` (silent) option, as you want to see the result of successful tests. Tests are run with the Maven surefire plugin and results are stored in the ./target/surefire-reports directory.

## Code quality reports

Code quality checks are performed during the build with `mvn verify` (checkstyle, pmd, and spotbugs). Always build the project with `mvn verify` and fix any problem reported in ./target/checkstyle-result.xml, ./target/pmd.xml, and ./target/spotbugsXml.xml before committing and submitting your code!

## Documentation

Any change that affects the end user of this library must be properly documented in README.md.

57 changes: 57 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# Changelog

All notable changes to this project are documented in this file.

## [Unreleased] — 2.0.0

### ⚠️ Breaking — the CXF backend was removed

Version 2.0.0 removes the legacy Apache CXF backend. The dependency-free client introduced in the
previous release is the only implementation; the public API is unchanged, so calling code is
unaffected. Consequences:

- **WinRM over HTTPS with self-signed certificates**: unlike the CXF-based client — which silently
trusted every TLS certificate and skipped hostname verification — this client **validates the
server certificate and verifies the hostname by default**. Connections to hosts with self-signed
or otherwise untrusted certificates **fail** during the TLS handshake unless you:
- install the server certificate (or its issuing CA) into a Java trust store
(`-Djavax.net.ssl.trustStore=...`); or
- disable TLS validation with `-Dorg.metricshub.winrm.tls.insecure=true`
(**insecure — for testing only**).
- Setting `-Dorg.metricshub.winrm.backend=cxf` now fails with a clear error instead of selecting
the removed backend: remove the property (or stay on winrm-java 1.x).
- The jar shrinks dramatically: the Apache CXF / JAX-WS / JAXB stack is gone and the only runtime
dependency left is `smbj` (used for copying files to remote shares).

### Removed

- The Apache CXF-based backend (`WinRMService` and the `service.client` internals), the CXF /
JAX-WS / JAXB / `jaxws-rt` dependencies, and the WSDL/XSD resources and code generation.
- `KerberosCredentialsException` (was thrown only by CXF internals).

### Added

- Dependency-free WinRM client with no Apache CXF / JAX-WS / JAXB stack, immune by construction to
JAXP `ServiceLoader` conflicts (it uses the JDK-default XML factories). Supports NTLM over HTTP
(with message encryption) and HTTPS, and Kerberos (SPNEGO, via the JDK GSS-API) over HTTPS.
- `org.metricshub.winrm.tls.insecure` system property to trust all TLS certificates and skip
hostname verification (insecure — for testing only).
- In-process protocol tests (`WsmanProtocolTest` + `FakeWsmanServer`) covering the full WSMan
path — NTLM handshake, message encryption, multipart framing, Enumerate/Pull paging, shell
lifecycle, and fault mapping — with no Windows host required (they run in `mvn verify`).
- `WinRMLiveTest`: a one-command smoke run against a real host (see README). Before the CXF
removal, its predecessor (`BackendDifferentialTest`) proved result parity between the two
backends on live hosts.

### Changed

- HTTPS connections validate certificates and verify hostnames by default (see the breaking
change above).
- The exception surface matches the pre-2.0.0 CXF backend (feature parity): authentication
rejections raise the same `Authentication error on <endpoint> with user name "<user>"` message,
operations on a closed executor raise the same `IllegalStateException` message, the WSMan
`OperationTimeout` header uses the same `PT#.###S` millisecond-precision format, and the
`EndOfSequence` / `Items` enumeration markers are recognized in both their WS-Enumeration and
WSMan namespace variants. WSMan fault exceptions additionally carry the detailed `WSManFault`
message (including the provider-level detail, e.g. WMI `WBEM_E_*` mnemonics) alongside the SOAP
reason text.
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
@AGENTS.md
48 changes: 48 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,30 @@ The Windows Remote Management (WinRM) Java Client is a library that enables to:
* Connect to a remote Windows server using one of the two authentication types (NTLM, KERBEROS)
* Execute WMI Query Language (WQL) queries which uses HTTP/HTTPS protocols.

> ## ⚠️ Upgrading from 1.x
>
> Version 2.0.0 **removed the legacy Apache CXF backend**: the dependency-free **light** client is
> the only implementation (same public API — calling code is unaffected). Two consequences:
>
> * Unlike the CXF-based client, which silently trusted every TLS certificate, the light client
> **validates the server certificate and verifies the hostname by default**.
> **WinRM-over-HTTPS connections to hosts with self-signed or otherwise untrusted certificates
> will fail** during the TLS handshake unless you install the server certificate (or its issuing
> CA) into a Java trust store (e.g. `-Djavax.net.ssl.trustStore=...`) or disable TLS validation
> with `-Dorg.metricshub.winrm.tls.insecure=true` (**insecure — for testing only**).
> * Setting `-Dorg.metricshub.winrm.backend=cxf` now fails with a clear error instead of selecting
> the removed backend. Remove the property (or stay on winrm-java 1.x).

## The WinRM client

The client is dependency-free (no Apache CXF / JAX-WS / JAXB — the only runtime dependency is
`smbj`, used for copying files to remote shares) and immune by construction to JAXP
`ServiceLoader` conflicts (it uses the JDK-default XML factories). It supports **NTLM over HTTP
(with message encryption) and HTTPS** and **Kerberos (SPNEGO) over HTTPS**. Over HTTPS it
validates the certificate and verifies the hostname by default (see the upgrade warning above);
`-Dorg.metricshub.winrm.tls.insecure=true` trusts all certificates (insecure, testing only).
Kerberos uses the ambient Kerberos configuration (`krb5.conf` / `-Djava.security.krb5.*`).

## Build instructions

This is a simple Maven project. Build with:
Expand All @@ -21,6 +45,30 @@ This is a simple Maven project. Build with:
mvn verify
```

### Protocol tests

The build includes in-process protocol tests (`WsmanProtocolTest`) that exercise the client's
full WSMan path — NTLM handshake, message encryption, `multipart/encrypted` framing, WQL
Enumerate/Pull paging, the command shell lifecycle, and fault mapping — against a fake WSMan
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:

```bash
mvn test -Dtest=WinRMLiveTest \
-Dwinrm.live.host=myhost.example.com \
-Dwinrm.live.protocol=https \
-Dwinrm.live.username='MYDOMAIN\myuser' \
-Dwinrm.live.password-file=/path/to/password.txt
```

Optional properties: `winrm.live.port`, `winrm.live.password` (inline), `winrm.live.namespace`,
`winrm.live.wql`, `winrm.live.command`, and `winrm.live.tls.insecure=true` (skip TLS validation
for hosts with self-signed certificates).

## Release instructions

The artifact is deployed to Sonatype's [Maven Central](https://central.sonatype.com/).
Expand Down
61 changes: 1 addition & 60 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
</parent>

<artifactId>winrm-java</artifactId>
<version>1.1.03-SNAPSHOT</version>
<version>2.0.00-SNAPSHOT</version>

<name>WinRM Java Client</name>
<description>WinRM Java Client</description>
Expand Down Expand Up @@ -103,36 +103,11 @@
<artifactId>junit-jupiter-engine</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>jakarta.xml.bind</groupId>
<artifactId>jakarta.xml.bind-api</artifactId>
<version>4.0.5</version>
</dependency>
<dependency>
<groupId>org.apache.cxf</groupId>
<artifactId>cxf-rt-frontend-jaxws</artifactId>
<version>4.2.1</version>
</dependency>
<dependency>
<groupId>org.apache.cxf</groupId>
<artifactId>cxf-rt-transports-http-hc</artifactId>
<version>4.2.1</version>
</dependency>
<dependency>
<groupId>com.hierynomus</groupId>
<artifactId>smbj</artifactId>
<version>0.14.0</version>
</dependency>
<dependency>
<groupId>jakarta.xml.ws</groupId>
<artifactId>jakarta.xml.ws-api</artifactId>
<version>4.0.2</version>
</dependency>
<dependency>
<groupId>com.sun.xml.ws</groupId>
<artifactId>jaxws-rt</artifactId>
<version>4.0.3</version>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-inline</artifactId>
Expand All @@ -146,40 +121,6 @@
<!-- Actual build plugins -->
<plugins>

<plugin>
<groupId>org.apache.cxf</groupId>
<artifactId>cxf-codegen-plugin</artifactId>
<version>4.2.1</version>
<executions>
<execution>
<id>generate-cxf-stubs</id>
<phase>generate-sources</phase>
<goals>
<goal>wsdl2java</goal>
</goals>
<configuration>
<sourceRoot>${project.build.directory}/generated-sources/cxf</sourceRoot>
<wsdlOptions>
<wsdlOption>
<wsdl>${project.basedir}/src/main/resources/wsdl/WinRM.wsdl</wsdl>

<bindingFiles>
<bindingFile>
${project.basedir}/src/main/resources/jaxws/bindings.xml</bindingFile>
</bindingFiles>

<extraargs>
<extraarg>-validate=basic</extraarg> <!-- enables basic validation -->
<extraarg>-keep</extraarg> <!-- Keeps the generated sources -->
</extraargs>

</wsdlOption>
</wsdlOptions>
</configuration>
</execution>
</executions>
</plugin>

<!-- Prettier -->
<plugin>
<groupId>com.hubspot.maven.plugins</groupId>
Expand Down
10 changes: 9 additions & 1 deletion src/main/java/org/metricshub/winrm/WindowsRemoteExecutor.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲
* WinRM Java Client
* ჻჻჻჻჻჻
* Copyright 2023 - 2024 Metricshub
* Copyright 2023 - 2026 MetricsHub
* ჻჻჻჻჻჻
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -41,7 +41,7 @@
* @throws WqlQuerySyntaxException if WQL query syntax is invalid
* @throws WindowsRemoteException For any problem encountered
*/
public List<Map<String, Object>> executeWql(final String wqlQuery, final long timeout)

Check warning on line 44 in src/main/java/org/metricshub/winrm/WindowsRemoteExecutor.java

View workflow job for this annotation

GitHub Actions / PMD

Code Style UnnecessaryModifier

Unnecessary modifier 'public' on method 'executeWql': the method is declared in an interface type
throws TimeoutException, WqlQuerySyntaxException, WindowsRemoteException;

/**
Expand All @@ -57,7 +57,7 @@
* @throws WindowsRemoteException For any problem encountered
* @throws TimeoutException To notify userName of timeout.
*/
public WindowsRemoteCommandResult executeCommand(

Check warning on line 60 in src/main/java/org/metricshub/winrm/WindowsRemoteExecutor.java

View workflow job for this annotation

GitHub Actions / PMD

Code Style UnnecessaryModifier

Unnecessary modifier 'public' on method 'executeCommand': the method is declared in an interface type
final String command,
final String workingDirectory,
final Charset charset,
Expand All @@ -68,17 +68,25 @@
* Get the hostname.
* @return
*/
public String getHostname();

Check warning on line 71 in src/main/java/org/metricshub/winrm/WindowsRemoteExecutor.java

View workflow job for this annotation

GitHub Actions / PMD

Code Style UnnecessaryModifier

Unnecessary modifier 'public' on method 'getHostname': the method is declared in an interface type

/**
* Get the username.
* @return
*/
public String getUsername();

Check warning on line 77 in src/main/java/org/metricshub/winrm/WindowsRemoteExecutor.java

View workflow job for this annotation

GitHub Actions / PMD

Code Style UnnecessaryModifier

Unnecessary modifier 'public' on method 'getUsername': the method is declared in an interface type

/**
* Get the password.
* @return
*/
public char[] getPassword();

Check warning on line 83 in src/main/java/org/metricshub/winrm/WindowsRemoteExecutor.java

View workflow job for this annotation

GitHub Actions / PMD

Code Style UnnecessaryModifier

Unnecessary modifier 'public' on method 'getPassword': the method is declared in an interface type

/**
* Close the executor and release its resources. Narrows {@link AutoCloseable#close()} so it does
* not declare a checked exception, letting callers use try-with-resources without catching
* {@link Exception}.
*/
@Override
public void close();

Check warning on line 91 in src/main/java/org/metricshub/winrm/WindowsRemoteExecutor.java

View workflow job for this annotation

GitHub Actions / PMD

Code Style UnnecessaryModifier

Unnecessary modifier 'public' on method 'close': the method is declared in an interface type
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲
* WinRM Java Client
* ჻჻჻჻჻჻
* Copyright 2023 - 2024 Metricshub
* Copyright 2023 - 2026 MetricsHub
* ჻჻჻჻჻჻
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -30,11 +30,12 @@
import org.metricshub.winrm.Utils;
import org.metricshub.winrm.WinRMHttpProtocolEnum;
import org.metricshub.winrm.WindowsRemoteCommandResult;
import org.metricshub.winrm.WindowsRemoteExecutor;
import org.metricshub.winrm.WindowsRemoteProcessUtils;
import org.metricshub.winrm.exceptions.WindowsRemoteException;
import org.metricshub.winrm.exceptions.WqlQuerySyntaxException;
import org.metricshub.winrm.service.WinRMEndpoint;
import org.metricshub.winrm.service.WinRMService;
import org.metricshub.winrm.service.WinRMExecutorFactory;
import org.metricshub.winrm.service.client.auth.AuthenticationEnum;
import org.metricshub.winrm.shares.SmbTempShare;

Expand Down Expand Up @@ -103,12 +104,12 @@

if (localFileToCopyList == null || localFileToCopyList.isEmpty()) {
try (
final WinRMService winRMService = WinRMService.createInstance(
final WindowsRemoteExecutor winRMService = WinRMExecutorFactory.createInstance(
Comment thread
bertysentry marked this conversation as resolved.
winRMEndpoint,
timeout,
ticketCache,
authentications
)

Check warning on line 112 in src/main/java/org/metricshub/winrm/command/WinRMCommandExecutor.java

View workflow job for this annotation

GitHub Actions / PMD

Code Style UnnecessaryModifier

Unnecessary modifier 'final' on resource specification 'winRMService': resource specifications are implicitly final
) {
final Charset charset = WindowsRemoteProcessUtils.getWindowsEncodingCharset(
winRMService,
Expand All @@ -122,12 +123,12 @@
}

try (
final SmbTempShare smbTempShare = SmbTempShare.createInstance(
winRMEndpoint,
timeout,
ticketCache,
authentications
)

Check warning on line 131 in src/main/java/org/metricshub/winrm/command/WinRMCommandExecutor.java

View workflow job for this annotation

GitHub Actions / PMD

Code Style UnnecessaryModifier

Unnecessary modifier 'final' on resource specification 'smbTempShare': resource specifications are implicitly final
) {
smbTempShare.checkConnectedFirst();

Expand Down
88 changes: 88 additions & 0 deletions src/main/java/org/metricshub/winrm/light/AuthScheme.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package org.metricshub.winrm.light;

/*-
* ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲
* WinRM Java Client
* ჻჻჻჻჻჻
* Copyright 2023 - 2026 MetricsHub
* ჻჻჻჻჻჻
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* ╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱
*/

/**
* Authentication and message protection for one WSMan connection. A scheme owns its handshake, its
* connection-bound session state, and how it wraps/unwraps the SOAP payload — the two things that
* differ between NTLM and Kerberos. {@link WsmanClient} is otherwise mechanism-agnostic and just
* delegates to the scheme, so a new mechanism is added by implementing this interface rather than
* branching the client.
*
* <p>All methods are called while {@code WsmanClient} holds its operation lock, so implementations
* need no internal synchronization.
*/
interface AuthScheme {
/**
* Run the full authentication handshake over the given transport (which may involve several
* request/response legs), leaving the connection authenticated.
*
* @param transport the connection to authenticate
* @return the {@code Authorization} header value to attach to the first real request, or
* {@code null} if none is needed
* @throws Exception if the handshake fails
*/
String authenticate(HttpTransport transport) throws Exception;

/** @return whether the connection is currently authenticated. */
boolean isAuthenticated();

/**
* Drop the authenticated state so the next request re-runs the handshake. Called when the
* underlying connection was lost, since the session state is bound to the TCP connection.
*/
void reset();

/**
* Encode an outgoing SOAP body for the wire (sealing it over plain HTTP, or passing it through
* over HTTPS where TLS provides confidentiality).
*
* @param soapUtf8 the SOAP envelope, UTF-8 encoded
* @return the bytes to send as the request body
*/
byte[] wrap(byte[] soapUtf8);

/** @return the {@code Content-Type} for the body produced by {@link #wrap(byte[])}. */
String wrapContentType();

/**
* Decode a response body back to plaintext SOAP bytes, verifying integrity where the mechanism
* provides it.
*
* @param response the HTTP response
* @return the plaintext SOAP bytes to parse
* @throws Exception if the body cannot be trusted or decoded
*/
byte[] unwrap(HttpTransport.Response response) throws Exception;

/**
* After the server rejects this scheme on a real request (HTTP 401) — which for Kerberos/NTLM only
* surfaces after {@link #authenticate} has returned, because the token/Type-3 rides the first real
* request — move to the next candidate of an ordered fallback list, if any. A single scheme cannot
* advance.
*
* @return {@code true} if a further scheme is now available so the caller should re-authenticate and
* retry; {@code false} if there is nothing left to try
*/
default boolean advance() {
return false;
}
}
Loading
Loading