diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..6b97797 --- /dev/null +++ b/AGENTS.md @@ -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. + diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..675af42 --- /dev/null +++ b/CHANGELOG.md @@ -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 with user name ""` 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. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..43c994c --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +@AGENTS.md diff --git a/README.md b/README.md index dbbdb42..4ef5373 100644 --- a/README.md +++ b/README.md @@ -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: @@ -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/). diff --git a/pom.xml b/pom.xml index 36510ec..456a252 100644 --- a/pom.xml +++ b/pom.xml @@ -8,7 +8,7 @@ winrm-java - 1.1.03-SNAPSHOT + 2.0.00-SNAPSHOT WinRM Java Client WinRM Java Client @@ -103,36 +103,11 @@ junit-jupiter-engine test - - jakarta.xml.bind - jakarta.xml.bind-api - 4.0.5 - - - org.apache.cxf - cxf-rt-frontend-jaxws - 4.2.1 - - - org.apache.cxf - cxf-rt-transports-http-hc - 4.2.1 - com.hierynomus smbj 0.14.0 - - jakarta.xml.ws - jakarta.xml.ws-api - 4.0.2 - - - com.sun.xml.ws - jaxws-rt - 4.0.3 - org.mockito mockito-inline @@ -146,40 +121,6 @@ - - org.apache.cxf - cxf-codegen-plugin - 4.2.1 - - - generate-cxf-stubs - generate-sources - - wsdl2java - - - ${project.build.directory}/generated-sources/cxf - - - ${project.basedir}/src/main/resources/wsdl/WinRM.wsdl - - - - ${project.basedir}/src/main/resources/jaxws/bindings.xml - - - - -validate=basic - -keep - - - - - - - - - com.hubspot.maven.plugins diff --git a/src/main/java/org/metricshub/winrm/WindowsRemoteExecutor.java b/src/main/java/org/metricshub/winrm/WindowsRemoteExecutor.java index c9cbfae..ce87c62 100644 --- a/src/main/java/org/metricshub/winrm/WindowsRemoteExecutor.java +++ b/src/main/java/org/metricshub/winrm/WindowsRemoteExecutor.java @@ -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. @@ -81,4 +81,12 @@ public WindowsRemoteCommandResult executeCommand( * @return */ public char[] getPassword(); + + /** + * 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(); } diff --git a/src/main/java/org/metricshub/winrm/command/WinRMCommandExecutor.java b/src/main/java/org/metricshub/winrm/command/WinRMCommandExecutor.java index 9e21c79..25deae4 100644 --- a/src/main/java/org/metricshub/winrm/command/WinRMCommandExecutor.java +++ b/src/main/java/org/metricshub/winrm/command/WinRMCommandExecutor.java @@ -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. @@ -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; @@ -103,7 +104,7 @@ public static WindowsRemoteCommandResult execute( if (localFileToCopyList == null || localFileToCopyList.isEmpty()) { try ( - final WinRMService winRMService = WinRMService.createInstance( + final WindowsRemoteExecutor winRMService = WinRMExecutorFactory.createInstance( winRMEndpoint, timeout, ticketCache, diff --git a/src/main/java/org/metricshub/winrm/light/AuthScheme.java b/src/main/java/org/metricshub/winrm/light/AuthScheme.java new file mode 100644 index 0000000..6562785 --- /dev/null +++ b/src/main/java/org/metricshub/winrm/light/AuthScheme.java @@ -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. + * + *

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; + } +} diff --git a/src/main/java/org/metricshub/winrm/service/client/encryption/ByteArrayUtils.java b/src/main/java/org/metricshub/winrm/light/ByteArrayUtils.java similarity index 91% rename from src/main/java/org/metricshub/winrm/service/client/encryption/ByteArrayUtils.java rename to src/main/java/org/metricshub/winrm/light/ByteArrayUtils.java index 8b8d620..d468a14 100644 --- a/src/main/java/org/metricshub/winrm/service/client/encryption/ByteArrayUtils.java +++ b/src/main/java/org/metricshub/winrm/light/ByteArrayUtils.java @@ -1,104 +1,103 @@ -package org.metricshub.winrm.service.client.encryption; - -/*- - * ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲ - * WinRM Java Client - * ჻჻჻჻჻჻ - * Copyright 2023 - 2024 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. - * ╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱ - */ - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.nio.ByteBuffer; -import java.nio.ByteOrder; -import org.metricshub.winrm.Utils; - -/** - * Code from io.cloudsoft.winrm4j.client.encryption.ByteArrayUtils - * release 0.12.3 @link https://github.com/cloudsoft/winrm4j - */ -public class ByteArrayUtils { - - private ByteArrayUtils() {} - - private static final int WIDTH = 32; - - public static String formatHexDump(final byte[] array) { - if (array == null) { - return "null"; - } - - // from https://gist.github.com/jen20/906db194bd97c14d91df - - final StringBuilder builder = new StringBuilder(); - - for (int rowOffset = 0; rowOffset < array.length; rowOffset += WIDTH) { - builder.append(String.format("%06d: ", rowOffset)); - - for (int index = 0; index < WIDTH; index++) { - if (rowOffset + index < array.length) { - builder.append(String.format("%02x", array[rowOffset + index])); - } else { - builder.append(" "); - } - - if (index % 4 == 3) { - builder.append(" "); - } - } - - if (rowOffset < array.length) { - builder.append(" | "); - for (int index = 0; index < WIDTH; index++) { - if (rowOffset + index < array.length) { - final byte c = array[rowOffset + index]; - builder.append((c >= 20 && c < 127) ? (char) c : '.'); - - if (index % 8 == 7) builder.append(" "); - } - } - } - - builder.append(Utils.NEW_LINE); - } - - return builder.toString(); - } - - public static byte[] getLittleEndianUnsignedInt(final long x) { - final ByteBuffer byteBuffer = ByteBuffer.allocate(4); - byteBuffer.order(ByteOrder.LITTLE_ENDIAN); - byteBuffer.putInt((int) (x & 0xFFFFFFFF)); - return byteBuffer.array(); - } - - public static long readLittleEndianUnsignedInt(final byte[] input, final int offset) { - final ByteBuffer byteBuffer = ByteBuffer.wrap(input); - byteBuffer.order(ByteOrder.LITTLE_ENDIAN); - return Integer.toUnsignedLong(byteBuffer.getInt(offset)); - } - - public static byte[] concat(final byte[]... sequences) { - try (final ByteArrayOutputStream out = new ByteArrayOutputStream()) { - for (byte[] s : sequences) { - out.write(s); - } - return out.toByteArray(); - } catch (final IOException e) { - throw new IllegalStateException(e); - } - } -} +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. + * ╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱ + */ + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +/** + * Code from io.cloudsoft.winrm4j.client.encryption.ByteArrayUtils + * release 0.12.3 @link https://github.com/cloudsoft/winrm4j + */ +public class ByteArrayUtils { + + private ByteArrayUtils() {} + + private static final int WIDTH = 32; + + public static String formatHexDump(final byte[] array) { + if (array == null) { + return "null"; + } + + // from https://gist.github.com/jen20/906db194bd97c14d91df + + final StringBuilder builder = new StringBuilder(); + + for (int rowOffset = 0; rowOffset < array.length; rowOffset += WIDTH) { + builder.append(String.format("%06d: ", rowOffset)); + + for (int index = 0; index < WIDTH; index++) { + if (rowOffset + index < array.length) { + builder.append(String.format("%02x", array[rowOffset + index])); + } else { + builder.append(" "); + } + + if (index % 4 == 3) { + builder.append(" "); + } + } + + if (rowOffset < array.length) { + builder.append(" | "); + for (int index = 0; index < WIDTH; index++) { + if (rowOffset + index < array.length) { + final byte c = array[rowOffset + index]; + builder.append((c >= 20 && c < 127) ? (char) c : '.'); + + if (index % 8 == 7) builder.append(" "); + } + } + } + + builder.append("\n"); + } + + return builder.toString(); + } + + public static byte[] getLittleEndianUnsignedInt(final long x) { + final ByteBuffer byteBuffer = ByteBuffer.allocate(4); + byteBuffer.order(ByteOrder.LITTLE_ENDIAN); + byteBuffer.putInt((int) (x & 0xFFFFFFFF)); + return byteBuffer.array(); + } + + public static long readLittleEndianUnsignedInt(final byte[] input, final int offset) { + final ByteBuffer byteBuffer = ByteBuffer.wrap(input); + byteBuffer.order(ByteOrder.LITTLE_ENDIAN); + return Integer.toUnsignedLong(byteBuffer.getInt(offset)); + } + + public static byte[] concat(final byte[]... sequences) { + try (final ByteArrayOutputStream out = new ByteArrayOutputStream()) { + for (byte[] s : sequences) { + out.write(s); + } + return out.toByteArray(); + } catch (final IOException e) { + throw new IllegalStateException(e); + } + } +} diff --git a/src/main/java/org/metricshub/winrm/service/client/encryption/CipherGen.java b/src/main/java/org/metricshub/winrm/light/CipherGen.java similarity index 87% rename from src/main/java/org/metricshub/winrm/service/client/encryption/CipherGen.java rename to src/main/java/org/metricshub/winrm/light/CipherGen.java index 2afbec5..378670a 100644 --- a/src/main/java/org/metricshub/winrm/service/client/encryption/CipherGen.java +++ b/src/main/java/org/metricshub/winrm/light/CipherGen.java @@ -1,593 +1,589 @@ -package org.metricshub.winrm.service.client.encryption; - -/*- - * ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲ - * WinRM Java Client - * ჻჻჻჻჻჻ - * Copyright 2023 - 2024 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. - * ╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱ - */ - -import java.security.Key; -import java.security.MessageDigest; -import java.util.Arrays; -import java.util.Locale; -import java.util.Random; -import javax.crypto.Cipher; -import javax.crypto.spec.SecretKeySpec; -import org.apache.http.impl.auth.NTLMEngineException; -import org.metricshub.winrm.service.client.auth.ntlm.NTLMEngineUtils; - -/** - * Code from io.cloudsoft.winrm4j.client.ntlm.forks.httpclient.NTLMEngineImpl - * release 0.12.3 @link https://github.com/cloudsoft/winrm4j - * io.cloudsoft.winrm4j.client.ntlm.forks.httpclient is a fork of apache-httpclient 4.5.13 - */ -public class CipherGen { - - private final Random random; - private final long currentTime; - - private final String domain; - private final String user; - private final String password; - private final byte[] challenge; - private final byte[] targetInformation; - - // Information we can generate but may be passed in (for testing) - private byte[] clientChallenge; - private byte[] clientChallenge2; - private byte[] secondaryKey; - private byte[] timestamp; - - // Stuff we always generate - private byte[] lmHash = null; - private byte[] lmResponse = null; - private byte[] ntlmHash = null; - private byte[] ntlmResponse = null; - private byte[] ntlmv2Hash = null; - private byte[] lmv2Hash = null; - private byte[] lmv2Response = null; - private byte[] ntlmv2Blob = null; - private byte[] ntlmv2Response = null; - private byte[] ntlm2SessionResponse = null; - private byte[] lm2SessionResponse = null; - private byte[] lmUserSessionKey = null; - private byte[] ntlmUserSessionKey = null; - private byte[] ntlmv2UserSessionKey = null; - private byte[] ntlm2SessionResponseUserSessionKey = null; - private byte[] lanManagerSessionKey = null; - - public CipherGen( - final Random random, - final long currentTime, - final String domain, - final String user, - final String password, - final byte[] challenge, - final String target, - final byte[] targetInformation - ) { - this.random = random; - this.currentTime = currentTime; - - this.domain = domain; - this.user = user; - this.password = password; - this.challenge = challenge; - this.targetInformation = targetInformation; - } - - /** Calculate and return client challenge */ - private byte[] getClientChallenge() { - if (clientChallenge == null) { - clientChallenge = makeRandomChallenge(random); - } - return clientChallenge; - } - - /** Calculate and return second client challenge */ - private byte[] getClientChallenge2() { - if (clientChallenge2 == null) { - clientChallenge2 = makeRandomChallenge(random); - } - return clientChallenge2; - } - - /** Calculate and return random secondary key */ - public byte[] getSecondaryKey() { - if (secondaryKey == null) { - secondaryKey = makeSecondaryKey(random); - } - return secondaryKey; - } - - /** Calculate and return the LMHash */ - private byte[] getLMHash() throws NTLMEngineException { - if (lmHash == null) { - lmHash = lmHash(password); - } - return lmHash; - } - - /** Calculate and return the LMResponse */ - public byte[] getLMResponse() throws NTLMEngineException { - if (lmResponse == null) { - lmResponse = lmResponse(getLMHash(), challenge); - } - return lmResponse; - } - - /** Calculate and return the NTLMHash */ - private byte[] getNTLMHash() throws NTLMEngineException { - if (ntlmHash == null) { - ntlmHash = ntlmHash(password); - } - return ntlmHash; - } - - /** Calculate and return the NTLMResponse */ - public byte[] getNTLMResponse() throws NTLMEngineException { - if (ntlmResponse == null) { - ntlmResponse = lmResponse(getNTLMHash(), challenge); - } - return ntlmResponse; - } - - /** Calculate the LMv2 hash */ - private byte[] getLMv2Hash() throws NTLMEngineException { - if (lmv2Hash == null) { - lmv2Hash = lmv2Hash(domain, user, getNTLMHash()); - } - return lmv2Hash; - } - - /** Calculate the NTLMv2 hash */ - private byte[] getNTLMv2Hash() throws NTLMEngineException { - if (ntlmv2Hash == null) { - ntlmv2Hash = ntlmv2Hash(domain, user, getNTLMHash()); - } - return ntlmv2Hash; - } - - /** Calculate a timestamp */ - private byte[] getTimestamp() { - if (timestamp == null) { - long time = this.currentTime; - time += 11644473600000l; // milliseconds from January 1, 1601 -> epoch. - time *= 10000; // tenths of a microsecond. - // convert to little-endian byte array. - timestamp = new byte[8]; - for (int i = 0; i < 8; i++) { - timestamp[i] = (byte) time; - time >>>= 8; - } - } - return timestamp; - } - - /** Calculate the NTLMv2Blob */ - private byte[] getNTLMv2Blob() { - if (ntlmv2Blob == null) { - ntlmv2Blob = createBlob(getClientChallenge2(), targetInformation, getTimestamp()); - } - return ntlmv2Blob; - } - - /** - * Creates the NTLMv2 blob from the given target information block and - * client challenge. - * - * @param targetInformation - * The target information block from the Type 2 message. - * @param clientChallenge - * The random 8-byte client challenge. - * - * @return The blob, used in the calculation of the NTLMv2 Response. - */ - private static byte[] createBlob( - final byte[] clientChallenge, - final byte[] targetInformation, - final byte[] timestamp - ) { - final byte[] blobSignature = new byte[] { (byte) 0x01, (byte) 0x01, (byte) 0x00, (byte) 0x00 }; - final byte[] reserved = new byte[] { (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00 }; - final byte[] unknown1 = new byte[] { (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00 }; - final byte[] unknown2 = new byte[] { (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00 }; - final byte[] blob = new byte[blobSignature.length + - reserved.length + - timestamp.length + - 8 + - unknown1.length + - targetInformation.length + - unknown2.length]; - int offset = 0; - System.arraycopy(blobSignature, 0, blob, offset, blobSignature.length); - offset += blobSignature.length; - System.arraycopy(reserved, 0, blob, offset, reserved.length); - offset += reserved.length; - System.arraycopy(timestamp, 0, blob, offset, timestamp.length); - offset += timestamp.length; - System.arraycopy(clientChallenge, 0, blob, offset, 8); - offset += 8; - System.arraycopy(unknown1, 0, blob, offset, unknown1.length); - offset += unknown1.length; - System.arraycopy(targetInformation, 0, blob, offset, targetInformation.length); - offset += targetInformation.length; - System.arraycopy(unknown2, 0, blob, offset, unknown2.length); - offset += unknown2.length; - return blob; - } - - /** Calculate the NTLMv2Response */ - public byte[] getNTLMv2Response() throws NTLMEngineException { - if (ntlmv2Response == null) { - ntlmv2Response = lmv2Response(getNTLMv2Hash(), challenge, getNTLMv2Blob()); - } - return ntlmv2Response; - } - - /** Calculate the LMv2Response */ - public byte[] getLMv2Response() throws NTLMEngineException { - if (lmv2Response == null) { - lmv2Response = lmv2Response(getLMv2Hash(), challenge, getClientChallenge()); - } - return lmv2Response; - } - - /** Get NTLM2SessionResponse */ - public byte[] getNTLM2SessionResponse() throws NTLMEngineException { - if (ntlm2SessionResponse == null) { - ntlm2SessionResponse = ntlm2SessionResponse(getNTLMHash(), challenge, getClientChallenge()); - } - return ntlm2SessionResponse; - } - - /** - * Calculates the NTLM2 Session Response for the given challenge, using the - * specified password and client challenge. - * - * @param ntlmHash - * @param challenge - * @param clientChallenge - * @return The NTLM2 Session Response. This is placed in the NTLM response - * field of the Type 3 message; the LM response field contains the - * client challenge, null-padded to 24 bytes. - */ - private static byte[] ntlm2SessionResponse( - final byte[] ntlmHash, - final byte[] challenge, - final byte[] clientChallenge - ) throws NTLMEngineException { - try { - final MessageDigest md5 = EncryptionUtils.getMD5(); - md5.update(challenge); - md5.update(clientChallenge); - final byte[] digest = md5.digest(); - - final byte[] sessionHash = new byte[8]; - System.arraycopy(digest, 0, sessionHash, 0, 8); - return lmResponse(ntlmHash, sessionHash); - } catch (final NTLMEngineException e) { - throw (NTLMEngineException) e; - } catch (final Exception e) { - throw new NTLMEngineException(e.getMessage(), e); - } - } - - /** - * Creates the LM Response from the given hash and Type 2 challenge. - * - * @param hash - * The LM or NTLM Hash. - * @param challenge - * The server challenge from the Type 2 message. - * - * @return The response (either LM or NTLM, depending on the provided hash). - */ - private static byte[] lmResponse(final byte[] hash, final byte[] challenge) throws NTLMEngineException { - try { - final byte[] keyBytes = new byte[21]; - System.arraycopy(hash, 0, keyBytes, 0, 16); - final Key lowKey = createDESKey(keyBytes, 0); - final Key middleKey = createDESKey(keyBytes, 7); - final Key highKey = createDESKey(keyBytes, 14); - final Cipher des = Cipher.getInstance("DES/ECB/NoPadding"); - des.init(Cipher.ENCRYPT_MODE, lowKey); - final byte[] lowResponse = des.doFinal(challenge); - des.init(Cipher.ENCRYPT_MODE, middleKey); - final byte[] middleResponse = des.doFinal(challenge); - des.init(Cipher.ENCRYPT_MODE, highKey); - final byte[] highResponse = des.doFinal(challenge); - final byte[] lmResponse = new byte[24]; - System.arraycopy(lowResponse, 0, lmResponse, 0, 8); - System.arraycopy(middleResponse, 0, lmResponse, 8, 8); - System.arraycopy(highResponse, 0, lmResponse, 16, 8); - return lmResponse; - } catch (final Exception e) { - throw new NTLMEngineException(e.getMessage(), e); - } - } - - /** - * Creates the LM Hash of the user's password. - * - * @param password - * The password. - * - * @return The LM Hash of the given password, used in the calculation of the - * LM Response. - */ - private static byte[] lmHash(final String password) throws NTLMEngineException { - try { - final byte[] oemPassword = password.toUpperCase(Locale.ROOT).getBytes(NTLMEngineUtils.DEFAULT_CHARSET); - - final int length = Math.min(oemPassword.length, 14); - final byte[] keyBytes = new byte[14]; - System.arraycopy(oemPassword, 0, keyBytes, 0, length); - final Key lowKey = createDESKey(keyBytes, 0); - final Key highKey = createDESKey(keyBytes, 7); - final byte[] magicConstant = "KGS!@#$%".getBytes(NTLMEngineUtils.DEFAULT_CHARSET); - final Cipher des = Cipher.getInstance("DES/ECB/NoPadding"); - des.init(Cipher.ENCRYPT_MODE, lowKey); - final byte[] lowHash = des.doFinal(magicConstant); - des.init(Cipher.ENCRYPT_MODE, highKey); - final byte[] highHash = des.doFinal(magicConstant); - final byte[] lmHash = new byte[16]; - System.arraycopy(lowHash, 0, lmHash, 0, 8); - System.arraycopy(highHash, 0, lmHash, 8, 8); - return lmHash; - } catch (final Exception e) { - throw new NTLMEngineException(e.getMessage(), e); - } - } - - /** - * Creates a DES encryption key from the given key material. - * - * @param bytes - * A byte array containing the DES key material. - * @param offset - * The offset in the given byte array at which the 7-byte key - * material starts. - * - * @return A DES encryption key created from the key material starting at - * the specified offset in the given byte array. - */ - private static Key createDESKey(final byte[] bytes, final int offset) { - final byte[] keyBytes = new byte[7]; - System.arraycopy(bytes, offset, keyBytes, 0, 7); - final byte[] material = new byte[8]; - material[0] = keyBytes[0]; - material[1] = (byte) ((keyBytes[0] << 7) | ((keyBytes[1] & 0xff) >>> 1)); - material[2] = (byte) ((keyBytes[1] << 6) | ((keyBytes[2] & 0xff) >>> 2)); - material[3] = (byte) ((keyBytes[2] << 5) | ((keyBytes[3] & 0xff) >>> 3)); - material[4] = (byte) ((keyBytes[3] << 4) | ((keyBytes[4] & 0xff) >>> 4)); - material[5] = (byte) ((keyBytes[4] << 3) | ((keyBytes[5] & 0xff) >>> 5)); - material[6] = (byte) ((keyBytes[5] << 2) | ((keyBytes[6] & 0xff) >>> 6)); - material[7] = (byte) (keyBytes[6] << 1); - oddParity(material); - return new SecretKeySpec(material, "DES"); - } - - /** - * Applies odd parity to the given byte array. - * - * @param bytes - * The data whose parity bits are to be adjusted for odd parity. - */ - private static void oddParity(final byte[] bytes) { - for (int i = 0; i < bytes.length; i++) { - final byte b = bytes[i]; - final boolean needsParity = - (((b >>> 7) ^ (b >>> 6) ^ (b >>> 5) ^ (b >>> 4) ^ (b >>> 3) ^ (b >>> 2) ^ (b >>> 1)) & 0x01) == 0; - if (needsParity) { - bytes[i] |= (byte) 0x01; - } else { - bytes[i] &= (byte) 0xfe; - } - } - } - - /** Calculate and return LM2 session response */ - public byte[] getLM2SessionResponse() { - if (lm2SessionResponse == null) { - final byte[] clntChallenge = getClientChallenge(); - lm2SessionResponse = new byte[24]; - System.arraycopy(clntChallenge, 0, lm2SessionResponse, 0, clntChallenge.length); - Arrays.fill(lm2SessionResponse, clntChallenge.length, lm2SessionResponse.length, (byte) 0x00); - } - return lm2SessionResponse; - } - - /** Get LMUserSessionKey */ - public byte[] getLMUserSessionKey() throws NTLMEngineException { - if (lmUserSessionKey == null) { - lmUserSessionKey = new byte[16]; - System.arraycopy(getLMHash(), 0, lmUserSessionKey, 0, 8); - Arrays.fill(lmUserSessionKey, 8, 16, (byte) 0x00); - } - return lmUserSessionKey; - } - - /** Get NTLMUserSessionKey */ - public byte[] getNTLMUserSessionKey() throws NTLMEngineException { - if (ntlmUserSessionKey == null) { - final MD4 md4 = new MD4(); - md4.update(getNTLMHash()); - ntlmUserSessionKey = md4.getOutput(); - } - return ntlmUserSessionKey; - } - - /** GetNTLMv2UserSessionKey */ - public byte[] getNTLMv2UserSessionKey() throws NTLMEngineException { - if (ntlmv2UserSessionKey == null) { - final byte[] ntlmv2hash = getNTLMv2Hash(); - final byte[] truncatedResponse = new byte[16]; - System.arraycopy(getNTLMv2Response(), 0, truncatedResponse, 0, 16); - ntlmv2UserSessionKey = hmacMD5(truncatedResponse, ntlmv2hash); - } - return ntlmv2UserSessionKey; - } - - /** Get NTLM2SessionResponseUserSessionKey */ - public byte[] getNTLM2SessionResponseUserSessionKey() throws NTLMEngineException { - if (ntlm2SessionResponseUserSessionKey == null) { - final byte[] ntlm2SessionResponseNonce = getLM2SessionResponse(); - final byte[] sessionNonce = new byte[challenge.length + ntlm2SessionResponseNonce.length]; - System.arraycopy(challenge, 0, sessionNonce, 0, challenge.length); - System.arraycopy(ntlm2SessionResponseNonce, 0, sessionNonce, challenge.length, ntlm2SessionResponseNonce.length); - ntlm2SessionResponseUserSessionKey = hmacMD5(sessionNonce, getNTLMUserSessionKey()); - } - return ntlm2SessionResponseUserSessionKey; - } - - /** Get LAN Manager session key */ - public byte[] getLanManagerSessionKey() throws NTLMEngineException { - if (lanManagerSessionKey == null) { - try { - final byte[] keyBytes = new byte[14]; - System.arraycopy(getLMHash(), 0, keyBytes, 0, 8); - Arrays.fill(keyBytes, 8, keyBytes.length, (byte) 0xbd); - final Key lowKey = createDESKey(keyBytes, 0); - final Key highKey = createDESKey(keyBytes, 7); - final byte[] truncatedResponse = new byte[8]; - System.arraycopy(getLMResponse(), 0, truncatedResponse, 0, truncatedResponse.length); - Cipher des = Cipher.getInstance("DES/ECB/NoPadding"); - des.init(Cipher.ENCRYPT_MODE, lowKey); - final byte[] lowPart = des.doFinal(truncatedResponse); - des = Cipher.getInstance("DES/ECB/NoPadding"); - des.init(Cipher.ENCRYPT_MODE, highKey); - final byte[] highPart = des.doFinal(truncatedResponse); - lanManagerSessionKey = new byte[16]; - System.arraycopy(lowPart, 0, lanManagerSessionKey, 0, lowPart.length); - System.arraycopy(highPart, 0, lanManagerSessionKey, lowPart.length, highPart.length); - } catch (final Exception e) { - throw new NTLMEngineException(e.getMessage(), e); - } - } - return lanManagerSessionKey; - } - - /** - * Creates the NTLM Hash of the user's password. - * - * @param password - * The password. - * - * @return The NTLM Hash of the given password, used in the calculation of - * the NTLM Response and the NTLMv2 and LMv2 Hashes. - */ - private static byte[] ntlmHash(final String password) throws NTLMEngineException { - if (NTLMEngineUtils.UNICODE_LITTLE_UNMARKED == null) { - throw new NTLMEngineException("Unicode not supported"); - } - final byte[] unicodePassword = password.getBytes(NTLMEngineUtils.UNICODE_LITTLE_UNMARKED); - final MD4 md4 = new MD4(); - md4.update(unicodePassword); - return md4.getOutput(); - } - - /** - * Creates the LMv2 Hash of the user's password. - * - * @return The LMv2 Hash, used in the calculation of the NTLMv2 and LMv2 - * Responses. - */ - private static byte[] lmv2Hash(final String domain, final String user, final byte[] ntlmHash) - throws NTLMEngineException { - if (NTLMEngineUtils.UNICODE_LITTLE_UNMARKED == null) { - throw new NTLMEngineException("Unicode not supported"); - } - final HMACMD5 hmacMD5 = new HMACMD5(ntlmHash); - // Upper case username, upper case domain! - hmacMD5.update(user.toUpperCase(Locale.ROOT).getBytes(NTLMEngineUtils.UNICODE_LITTLE_UNMARKED)); - if (domain != null) { - hmacMD5.update(domain.toUpperCase(Locale.ROOT).getBytes(NTLMEngineUtils.UNICODE_LITTLE_UNMARKED)); - } - return hmacMD5.getOutput(); - } - - /** - * Creates the NTLMv2 Hash of the user's password. - * - * @return The NTLMv2 Hash, used in the calculation of the NTLMv2 and LMv2 - * Responses. - */ - private static byte[] ntlmv2Hash(final String domain, final String user, final byte[] ntlmHash) - throws NTLMEngineException { - if (NTLMEngineUtils.UNICODE_LITTLE_UNMARKED == null) { - throw new NTLMEngineException("Unicode not supported"); - } - final HMACMD5 hmacMD5 = new HMACMD5(ntlmHash); - // Upper case username, mixed case target!! - hmacMD5.update(user.toUpperCase(Locale.ROOT).getBytes(NTLMEngineUtils.UNICODE_LITTLE_UNMARKED)); - if (domain != null) { - hmacMD5.update(domain.getBytes(NTLMEngineUtils.UNICODE_LITTLE_UNMARKED)); - } - return hmacMD5.getOutput(); - } - - /** - * Creates the LMv2 Response from the given hash, client data, and Type 2 - * challenge. - * - * @param hash - * The NTLMv2 Hash. - * @param clientData - * The client data (blob or client challenge). - * @param challenge - * The server challenge from the Type 2 message. - * - * @return The response (either NTLMv2 or LMv2, depending on the client - * data). - */ - private static byte[] lmv2Response(final byte[] hash, final byte[] challenge, final byte[] clientData) { - final HMACMD5 hmacMD5 = new HMACMD5(hash); - hmacMD5.update(challenge); - hmacMD5.update(clientData); - final byte[] mac = hmacMD5.getOutput(); - final byte[] lmv2Response = new byte[mac.length + clientData.length]; - System.arraycopy(mac, 0, lmv2Response, 0, mac.length); - System.arraycopy(clientData, 0, lmv2Response, mac.length, clientData.length); - return lmv2Response; - } - - /** Calculate a challenge block */ - private static byte[] makeRandomChallenge(final Random random) { - final byte[] rval = new byte[8]; - synchronized (random) { - random.nextBytes(rval); - } - return rval; - } - - /** Calculate a 16-byte secondary key */ - private static byte[] makeSecondaryKey(final Random random) { - final byte[] rval = new byte[16]; - synchronized (random) { - random.nextBytes(rval); - } - return rval; - } - - /** Calculates HMAC-MD5 */ - private static byte[] hmacMD5(final byte[] value, final byte[] key) { - final HMACMD5 hmacMD5 = new HMACMD5(key); - hmacMD5.update(value); - return hmacMD5.getOutput(); - } -} +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. + * ╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱ + */ + +import java.security.Key; +import java.security.MessageDigest; +import java.util.Arrays; +import java.util.Locale; +import java.util.Random; +import javax.crypto.Cipher; +import javax.crypto.spec.SecretKeySpec; + +/** + * Code from io.cloudsoft.winrm4j.client.ntlm.forks.httpclient.NTLMEngineImpl + * release 0.12.3 @link https://github.com/cloudsoft/winrm4j + * io.cloudsoft.winrm4j.client.ntlm.forks.httpclient is a fork of apache-httpclient 4.5.13 + */ +public class CipherGen { + + private final Random random; + private final long currentTime; + + private final String domain; + private final String user; + private final String password; + private final byte[] challenge; + private final byte[] targetInformation; + + // Information we can generate but may be passed in (for testing) + private byte[] clientChallenge; + private byte[] clientChallenge2; + private byte[] secondaryKey; + private byte[] timestamp; + + // Stuff we always generate + private byte[] lmHash = null; + private byte[] lmResponse = null; + private byte[] ntlmHash = null; + private byte[] ntlmResponse = null; + private byte[] ntlmv2Hash = null; + private byte[] lmv2Hash = null; + private byte[] lmv2Response = null; + private byte[] ntlmv2Blob = null; + private byte[] ntlmv2Response = null; + private byte[] ntlm2SessionResponse = null; + private byte[] lm2SessionResponse = null; + private byte[] lmUserSessionKey = null; + private byte[] ntlmUserSessionKey = null; + private byte[] ntlmv2UserSessionKey = null; + private byte[] ntlm2SessionResponseUserSessionKey = null; + private byte[] lanManagerSessionKey = null; + + public CipherGen( + final Random random, + final long currentTime, + final String domain, + final String user, + final String password, + final byte[] challenge, + final String target, + final byte[] targetInformation + ) { + this.random = random; + this.currentTime = currentTime; + + this.domain = domain; + this.user = user; + this.password = password; + this.challenge = challenge; + this.targetInformation = targetInformation; + } + + /** Calculate and return client challenge */ + private byte[] getClientChallenge() { + if (clientChallenge == null) { + clientChallenge = makeRandomChallenge(random); + } + return clientChallenge; + } + + /** Calculate and return second client challenge */ + private byte[] getClientChallenge2() { + if (clientChallenge2 == null) { + clientChallenge2 = makeRandomChallenge(random); + } + return clientChallenge2; + } + + /** Calculate and return random secondary key */ + public byte[] getSecondaryKey() { + if (secondaryKey == null) { + secondaryKey = makeSecondaryKey(random); + } + return secondaryKey; + } + + /** Calculate and return the LMHash */ + private byte[] getLMHash() throws NtlmException { + if (lmHash == null) { + lmHash = lmHash(password); + } + return lmHash; + } + + /** Calculate and return the LMResponse */ + public byte[] getLMResponse() throws NtlmException { + if (lmResponse == null) { + lmResponse = lmResponse(getLMHash(), challenge); + } + return lmResponse; + } + + /** Calculate and return the NTLMHash */ + private byte[] getNTLMHash() throws NtlmException { + if (ntlmHash == null) { + ntlmHash = ntlmHash(password); + } + return ntlmHash; + } + + /** Calculate and return the NTLMResponse */ + public byte[] getNTLMResponse() throws NtlmException { + if (ntlmResponse == null) { + ntlmResponse = lmResponse(getNTLMHash(), challenge); + } + return ntlmResponse; + } + + /** Calculate the LMv2 hash */ + private byte[] getLMv2Hash() throws NtlmException { + if (lmv2Hash == null) { + lmv2Hash = lmv2Hash(domain, user, getNTLMHash()); + } + return lmv2Hash; + } + + /** Calculate the NTLMv2 hash */ + private byte[] getNTLMv2Hash() throws NtlmException { + if (ntlmv2Hash == null) { + ntlmv2Hash = ntlmv2Hash(domain, user, getNTLMHash()); + } + return ntlmv2Hash; + } + + /** Calculate a timestamp */ + private byte[] getTimestamp() { + if (timestamp == null) { + long time = this.currentTime; + time += 11644473600000l; // milliseconds from January 1, 1601 -> epoch. + time *= 10000; // tenths of a microsecond. + // convert to little-endian byte array. + timestamp = new byte[8]; + for (int i = 0; i < 8; i++) { + timestamp[i] = (byte) time; + time >>>= 8; + } + } + return timestamp; + } + + /** Calculate the NTLMv2Blob */ + private byte[] getNTLMv2Blob() { + if (ntlmv2Blob == null) { + ntlmv2Blob = createBlob(getClientChallenge2(), targetInformation, getTimestamp()); + } + return ntlmv2Blob; + } + + /** + * Creates the NTLMv2 blob from the given target information block and + * client challenge. + * + * @param targetInformation + * The target information block from the Type 2 message. + * @param clientChallenge + * The random 8-byte client challenge. + * + * @return The blob, used in the calculation of the NTLMv2 Response. + */ + private static byte[] createBlob( + final byte[] clientChallenge, + final byte[] targetInformation, + final byte[] timestamp + ) { + final byte[] blobSignature = new byte[] { (byte) 0x01, (byte) 0x01, (byte) 0x00, (byte) 0x00 }; + final byte[] reserved = new byte[] { (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00 }; + final byte[] unknown1 = new byte[] { (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00 }; + final byte[] unknown2 = new byte[] { (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00 }; + final byte[] blob = new byte[blobSignature.length + + reserved.length + + timestamp.length + + 8 + + unknown1.length + + targetInformation.length + + unknown2.length]; + int offset = 0; + System.arraycopy(blobSignature, 0, blob, offset, blobSignature.length); + offset += blobSignature.length; + System.arraycopy(reserved, 0, blob, offset, reserved.length); + offset += reserved.length; + System.arraycopy(timestamp, 0, blob, offset, timestamp.length); + offset += timestamp.length; + System.arraycopy(clientChallenge, 0, blob, offset, 8); + offset += 8; + System.arraycopy(unknown1, 0, blob, offset, unknown1.length); + offset += unknown1.length; + System.arraycopy(targetInformation, 0, blob, offset, targetInformation.length); + offset += targetInformation.length; + System.arraycopy(unknown2, 0, blob, offset, unknown2.length); + offset += unknown2.length; + return blob; + } + + /** Calculate the NTLMv2Response */ + public byte[] getNTLMv2Response() throws NtlmException { + if (ntlmv2Response == null) { + ntlmv2Response = lmv2Response(getNTLMv2Hash(), challenge, getNTLMv2Blob()); + } + return ntlmv2Response; + } + + /** Calculate the LMv2Response */ + public byte[] getLMv2Response() throws NtlmException { + if (lmv2Response == null) { + lmv2Response = lmv2Response(getLMv2Hash(), challenge, getClientChallenge()); + } + return lmv2Response; + } + + /** Get NTLM2SessionResponse */ + public byte[] getNTLM2SessionResponse() throws NtlmException { + if (ntlm2SessionResponse == null) { + ntlm2SessionResponse = ntlm2SessionResponse(getNTLMHash(), challenge, getClientChallenge()); + } + return ntlm2SessionResponse; + } + + /** + * Calculates the NTLM2 Session Response for the given challenge, using the + * specified password and client challenge. + * + * @param ntlmHash + * @param challenge + * @param clientChallenge + * @return The NTLM2 Session Response. This is placed in the NTLM response + * field of the Type 3 message; the LM response field contains the + * client challenge, null-padded to 24 bytes. + */ + private static byte[] ntlm2SessionResponse( + final byte[] ntlmHash, + final byte[] challenge, + final byte[] clientChallenge + ) throws NtlmException { + try { + final MessageDigest md5 = EncryptionUtils.getMD5(); + md5.update(challenge); + md5.update(clientChallenge); + final byte[] digest = md5.digest(); + + final byte[] sessionHash = new byte[8]; + System.arraycopy(digest, 0, sessionHash, 0, 8); + return lmResponse(ntlmHash, sessionHash); + } catch (final NtlmException e) { + throw (NtlmException) e; + } catch (final Exception e) { + throw new NtlmException(e.getMessage(), e); + } + } + + /** + * Creates the LM Response from the given hash and Type 2 challenge. + * + * @param hash + * The LM or NTLM Hash. + * @param challenge + * The server challenge from the Type 2 message. + * + * @return The response (either LM or NTLM, depending on the provided hash). + */ + private static byte[] lmResponse(final byte[] hash, final byte[] challenge) throws NtlmException { + try { + final byte[] keyBytes = new byte[21]; + System.arraycopy(hash, 0, keyBytes, 0, 16); + final Key lowKey = createDESKey(keyBytes, 0); + final Key middleKey = createDESKey(keyBytes, 7); + final Key highKey = createDESKey(keyBytes, 14); + final Cipher des = Cipher.getInstance("DES/ECB/NoPadding"); + des.init(Cipher.ENCRYPT_MODE, lowKey); + final byte[] lowResponse = des.doFinal(challenge); + des.init(Cipher.ENCRYPT_MODE, middleKey); + final byte[] middleResponse = des.doFinal(challenge); + des.init(Cipher.ENCRYPT_MODE, highKey); + final byte[] highResponse = des.doFinal(challenge); + final byte[] lmResponse = new byte[24]; + System.arraycopy(lowResponse, 0, lmResponse, 0, 8); + System.arraycopy(middleResponse, 0, lmResponse, 8, 8); + System.arraycopy(highResponse, 0, lmResponse, 16, 8); + return lmResponse; + } catch (final Exception e) { + throw new NtlmException(e.getMessage(), e); + } + } + + /** + * Creates the LM Hash of the user's password. + * + * @param password + * The password. + * + * @return The LM Hash of the given password, used in the calculation of the + * LM Response. + */ + private static byte[] lmHash(final String password) throws NtlmException { + try { + final byte[] oemPassword = password.toUpperCase(Locale.ROOT).getBytes(NTLMEngineUtils.DEFAULT_CHARSET); + + final int length = Math.min(oemPassword.length, 14); + final byte[] keyBytes = new byte[14]; + System.arraycopy(oemPassword, 0, keyBytes, 0, length); + final Key lowKey = createDESKey(keyBytes, 0); + final Key highKey = createDESKey(keyBytes, 7); + final byte[] magicConstant = "KGS!@#$%".getBytes(NTLMEngineUtils.DEFAULT_CHARSET); + final Cipher des = Cipher.getInstance("DES/ECB/NoPadding"); + des.init(Cipher.ENCRYPT_MODE, lowKey); + final byte[] lowHash = des.doFinal(magicConstant); + des.init(Cipher.ENCRYPT_MODE, highKey); + final byte[] highHash = des.doFinal(magicConstant); + final byte[] lmHash = new byte[16]; + System.arraycopy(lowHash, 0, lmHash, 0, 8); + System.arraycopy(highHash, 0, lmHash, 8, 8); + return lmHash; + } catch (final Exception e) { + throw new NtlmException(e.getMessage(), e); + } + } + + /** + * Creates a DES encryption key from the given key material. + * + * @param bytes + * A byte array containing the DES key material. + * @param offset + * The offset in the given byte array at which the 7-byte key + * material starts. + * + * @return A DES encryption key created from the key material starting at + * the specified offset in the given byte array. + */ + private static Key createDESKey(final byte[] bytes, final int offset) { + final byte[] keyBytes = new byte[7]; + System.arraycopy(bytes, offset, keyBytes, 0, 7); + final byte[] material = new byte[8]; + material[0] = keyBytes[0]; + material[1] = (byte) ((keyBytes[0] << 7) | ((keyBytes[1] & 0xff) >>> 1)); + material[2] = (byte) ((keyBytes[1] << 6) | ((keyBytes[2] & 0xff) >>> 2)); + material[3] = (byte) ((keyBytes[2] << 5) | ((keyBytes[3] & 0xff) >>> 3)); + material[4] = (byte) ((keyBytes[3] << 4) | ((keyBytes[4] & 0xff) >>> 4)); + material[5] = (byte) ((keyBytes[4] << 3) | ((keyBytes[5] & 0xff) >>> 5)); + material[6] = (byte) ((keyBytes[5] << 2) | ((keyBytes[6] & 0xff) >>> 6)); + material[7] = (byte) (keyBytes[6] << 1); + oddParity(material); + return new SecretKeySpec(material, "DES"); + } + + /** + * Applies odd parity to the given byte array. + * + * @param bytes + * The data whose parity bits are to be adjusted for odd parity. + */ + private static void oddParity(final byte[] bytes) { + for (int i = 0; i < bytes.length; i++) { + final byte b = bytes[i]; + final boolean needsParity = + (((b >>> 7) ^ (b >>> 6) ^ (b >>> 5) ^ (b >>> 4) ^ (b >>> 3) ^ (b >>> 2) ^ (b >>> 1)) & 0x01) == 0; + if (needsParity) { + bytes[i] |= (byte) 0x01; + } else { + bytes[i] &= (byte) 0xfe; + } + } + } + + /** Calculate and return LM2 session response */ + public byte[] getLM2SessionResponse() { + if (lm2SessionResponse == null) { + final byte[] clntChallenge = getClientChallenge(); + lm2SessionResponse = new byte[24]; + System.arraycopy(clntChallenge, 0, lm2SessionResponse, 0, clntChallenge.length); + Arrays.fill(lm2SessionResponse, clntChallenge.length, lm2SessionResponse.length, (byte) 0x00); + } + return lm2SessionResponse; + } + + /** Get LMUserSessionKey */ + public byte[] getLMUserSessionKey() throws NtlmException { + if (lmUserSessionKey == null) { + lmUserSessionKey = new byte[16]; + System.arraycopy(getLMHash(), 0, lmUserSessionKey, 0, 8); + Arrays.fill(lmUserSessionKey, 8, 16, (byte) 0x00); + } + return lmUserSessionKey; + } + + /** Get NTLMUserSessionKey */ + public byte[] getNTLMUserSessionKey() throws NtlmException { + if (ntlmUserSessionKey == null) { + final MD4 md4 = new MD4(); + md4.update(getNTLMHash()); + ntlmUserSessionKey = md4.getOutput(); + } + return ntlmUserSessionKey; + } + + /** GetNTLMv2UserSessionKey */ + public byte[] getNTLMv2UserSessionKey() throws NtlmException { + if (ntlmv2UserSessionKey == null) { + final byte[] ntlmv2hash = getNTLMv2Hash(); + final byte[] truncatedResponse = new byte[16]; + System.arraycopy(getNTLMv2Response(), 0, truncatedResponse, 0, 16); + ntlmv2UserSessionKey = hmacMD5(truncatedResponse, ntlmv2hash); + } + return ntlmv2UserSessionKey; + } + + /** Get NTLM2SessionResponseUserSessionKey */ + public byte[] getNTLM2SessionResponseUserSessionKey() throws NtlmException { + if (ntlm2SessionResponseUserSessionKey == null) { + final byte[] ntlm2SessionResponseNonce = getLM2SessionResponse(); + final byte[] sessionNonce = new byte[challenge.length + ntlm2SessionResponseNonce.length]; + System.arraycopy(challenge, 0, sessionNonce, 0, challenge.length); + System.arraycopy(ntlm2SessionResponseNonce, 0, sessionNonce, challenge.length, ntlm2SessionResponseNonce.length); + ntlm2SessionResponseUserSessionKey = hmacMD5(sessionNonce, getNTLMUserSessionKey()); + } + return ntlm2SessionResponseUserSessionKey; + } + + /** Get LAN Manager session key */ + public byte[] getLanManagerSessionKey() throws NtlmException { + if (lanManagerSessionKey == null) { + try { + final byte[] keyBytes = new byte[14]; + System.arraycopy(getLMHash(), 0, keyBytes, 0, 8); + Arrays.fill(keyBytes, 8, keyBytes.length, (byte) 0xbd); + final Key lowKey = createDESKey(keyBytes, 0); + final Key highKey = createDESKey(keyBytes, 7); + final byte[] truncatedResponse = new byte[8]; + System.arraycopy(getLMResponse(), 0, truncatedResponse, 0, truncatedResponse.length); + Cipher des = Cipher.getInstance("DES/ECB/NoPadding"); + des.init(Cipher.ENCRYPT_MODE, lowKey); + final byte[] lowPart = des.doFinal(truncatedResponse); + des = Cipher.getInstance("DES/ECB/NoPadding"); + des.init(Cipher.ENCRYPT_MODE, highKey); + final byte[] highPart = des.doFinal(truncatedResponse); + lanManagerSessionKey = new byte[16]; + System.arraycopy(lowPart, 0, lanManagerSessionKey, 0, lowPart.length); + System.arraycopy(highPart, 0, lanManagerSessionKey, lowPart.length, highPart.length); + } catch (final Exception e) { + throw new NtlmException(e.getMessage(), e); + } + } + return lanManagerSessionKey; + } + + /** + * Creates the NTLM Hash of the user's password. + * + * @param password + * The password. + * + * @return The NTLM Hash of the given password, used in the calculation of + * the NTLM Response and the NTLMv2 and LMv2 Hashes. + */ + private static byte[] ntlmHash(final String password) throws NtlmException { + if (NTLMEngineUtils.UNICODE_LITTLE_UNMARKED == null) { + throw new NtlmException("Unicode not supported"); + } + final byte[] unicodePassword = password.getBytes(NTLMEngineUtils.UNICODE_LITTLE_UNMARKED); + final MD4 md4 = new MD4(); + md4.update(unicodePassword); + return md4.getOutput(); + } + + /** + * Creates the LMv2 Hash of the user's password. + * + * @return The LMv2 Hash, used in the calculation of the NTLMv2 and LMv2 + * Responses. + */ + private static byte[] lmv2Hash(final String domain, final String user, final byte[] ntlmHash) throws NtlmException { + if (NTLMEngineUtils.UNICODE_LITTLE_UNMARKED == null) { + throw new NtlmException("Unicode not supported"); + } + final HMACMD5 hmacMD5 = new HMACMD5(ntlmHash); + // Upper case username, upper case domain! + hmacMD5.update(user.toUpperCase(Locale.ROOT).getBytes(NTLMEngineUtils.UNICODE_LITTLE_UNMARKED)); + if (domain != null) { + hmacMD5.update(domain.toUpperCase(Locale.ROOT).getBytes(NTLMEngineUtils.UNICODE_LITTLE_UNMARKED)); + } + return hmacMD5.getOutput(); + } + + /** + * Creates the NTLMv2 Hash of the user's password. + * + * @return The NTLMv2 Hash, used in the calculation of the NTLMv2 and LMv2 + * Responses. + */ + private static byte[] ntlmv2Hash(final String domain, final String user, final byte[] ntlmHash) throws NtlmException { + if (NTLMEngineUtils.UNICODE_LITTLE_UNMARKED == null) { + throw new NtlmException("Unicode not supported"); + } + final HMACMD5 hmacMD5 = new HMACMD5(ntlmHash); + // Upper case username, mixed case target!! + hmacMD5.update(user.toUpperCase(Locale.ROOT).getBytes(NTLMEngineUtils.UNICODE_LITTLE_UNMARKED)); + if (domain != null) { + hmacMD5.update(domain.getBytes(NTLMEngineUtils.UNICODE_LITTLE_UNMARKED)); + } + return hmacMD5.getOutput(); + } + + /** + * Creates the LMv2 Response from the given hash, client data, and Type 2 + * challenge. + * + * @param hash + * The NTLMv2 Hash. + * @param clientData + * The client data (blob or client challenge). + * @param challenge + * The server challenge from the Type 2 message. + * + * @return The response (either NTLMv2 or LMv2, depending on the client + * data). + */ + private static byte[] lmv2Response(final byte[] hash, final byte[] challenge, final byte[] clientData) { + final HMACMD5 hmacMD5 = new HMACMD5(hash); + hmacMD5.update(challenge); + hmacMD5.update(clientData); + final byte[] mac = hmacMD5.getOutput(); + final byte[] lmv2Response = new byte[mac.length + clientData.length]; + System.arraycopy(mac, 0, lmv2Response, 0, mac.length); + System.arraycopy(clientData, 0, lmv2Response, mac.length, clientData.length); + return lmv2Response; + } + + /** Calculate a challenge block */ + private static byte[] makeRandomChallenge(final Random random) { + final byte[] rval = new byte[8]; + synchronized (random) { + random.nextBytes(rval); + } + return rval; + } + + /** Calculate a 16-byte secondary key */ + private static byte[] makeSecondaryKey(final Random random) { + final byte[] rval = new byte[16]; + synchronized (random) { + random.nextBytes(rval); + } + return rval; + } + + /** Calculates HMAC-MD5 */ + private static byte[] hmacMD5(final byte[] value, final byte[] key) { + final HMACMD5 hmacMD5 = new HMACMD5(key); + hmacMD5.update(value); + return hmacMD5.getOutput(); + } +} diff --git a/src/main/java/org/metricshub/winrm/service/client/encryption/EncryptionUtils.java b/src/main/java/org/metricshub/winrm/light/EncryptionUtils.java similarity index 93% rename from src/main/java/org/metricshub/winrm/service/client/encryption/EncryptionUtils.java rename to src/main/java/org/metricshub/winrm/light/EncryptionUtils.java index 4716e8c..9108c14 100644 --- a/src/main/java/org/metricshub/winrm/service/client/encryption/EncryptionUtils.java +++ b/src/main/java/org/metricshub/winrm/light/EncryptionUtils.java @@ -1,85 +1,85 @@ -package org.metricshub.winrm.service.client.encryption; - -/*- - * ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲ - * WinRM Java Client - * ჻჻჻჻჻჻ - * Copyright 2023 - 2024 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. - * ╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱ - */ - -import java.security.InvalidKeyException; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import javax.crypto.Cipher; -import javax.crypto.Mac; -import javax.crypto.spec.SecretKeySpec; - -/** - * Code from io.cloudsoft.winrm4j.client.encryption.WinrmEncryptionUtils - * release 0.12.3 @link https://github.com/cloudsoft/winrm4j - */ -public class EncryptionUtils { - - private EncryptionUtils() {} - - private static final String HMAC_MD5 = "HmacMD5"; - private static final String RC4 = "RC4"; - - public static MessageDigest getMD5() { - try { - return MessageDigest.getInstance("MD5"); - } catch (final NoSuchAlgorithmException ex) { - throw new IllegalStateException("MD5 message digest doesn't seem to exist - fatal error: " + ex.getMessage(), ex); - } - } - - public static byte[] md5digest(byte[] bytes) { - final MessageDigest handle = getMD5(); - handle.update(bytes); - return handle.digest(); - } - - public static Cipher arc4(byte[] key) { - // engine needs to be stateful - try { - final Cipher rc4 = Cipher.getInstance(RC4); - rc4.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(key, RC4)); - return rc4; - } catch (final Exception e) { - throw new IllegalStateException(e); - } - } - - /** Calculates RC4 */ - public static byte[] calculateRC4(final byte[] value, final byte[] key) { - try { - return arc4(key).doFinal(value); - } catch (final Exception e) { - throw new IllegalStateException(e); - } - } - - public static byte[] hmacMd5(byte[] key, byte[] body) { - try { - final SecretKeySpec keySpec = new SecretKeySpec(key, HMAC_MD5); - final Mac mac = Mac.getInstance(HMAC_MD5); - mac.init(keySpec); - return mac.doFinal(body); - } catch (final NoSuchAlgorithmException | InvalidKeyException e) { - throw new IllegalStateException(e); - } - } -} +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. + * ╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱ + */ + +import java.security.InvalidKeyException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import javax.crypto.Cipher; +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; + +/** + * Code from io.cloudsoft.winrm4j.client.encryption.WinrmEncryptionUtils + * release 0.12.3 @link https://github.com/cloudsoft/winrm4j + */ +public class EncryptionUtils { + + private EncryptionUtils() {} + + private static final String HMAC_MD5 = "HmacMD5"; + private static final String RC4 = "RC4"; + + public static MessageDigest getMD5() { + try { + return MessageDigest.getInstance("MD5"); + } catch (final NoSuchAlgorithmException ex) { + throw new IllegalStateException("MD5 message digest doesn't seem to exist - fatal error: " + ex.getMessage(), ex); + } + } + + public static byte[] md5digest(byte[] bytes) { + final MessageDigest handle = getMD5(); + handle.update(bytes); + return handle.digest(); + } + + public static Cipher arc4(byte[] key) { + // engine needs to be stateful + try { + final Cipher rc4 = Cipher.getInstance(RC4); + rc4.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(key, RC4)); + return rc4; + } catch (final Exception e) { + throw new IllegalStateException(e); + } + } + + /** Calculates RC4 */ + public static byte[] calculateRC4(final byte[] value, final byte[] key) { + try { + return arc4(key).doFinal(value); + } catch (final Exception e) { + throw new IllegalStateException(e); + } + } + + public static byte[] hmacMd5(byte[] key, byte[] body) { + try { + final SecretKeySpec keySpec = new SecretKeySpec(key, HMAC_MD5); + final Mac mac = Mac.getInstance(HMAC_MD5); + mac.init(keySpec); + return mac.doFinal(body); + } catch (final NoSuchAlgorithmException | InvalidKeyException e) { + throw new IllegalStateException(e); + } + } +} diff --git a/src/main/java/org/metricshub/winrm/light/Envelopes.java b/src/main/java/org/metricshub/winrm/light/Envelopes.java new file mode 100644 index 0000000..001e924 --- /dev/null +++ b/src/main/java/org/metricshub/winrm/light/Envelopes.java @@ -0,0 +1,234 @@ +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. + * ╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱ + */ + +import java.math.BigDecimal; +import java.text.DecimalFormat; +import java.text.DecimalFormatSymbols; +import java.util.Locale; +import java.util.UUID; + +/** + * WS-Management SOAP envelope templates — the only "WSDL" the light client needs. + * Covers Identify, WQL Enumerate/Pull, and the command shell lifecycle + * (Create / Command / Receive / Signal / Delete). + */ +final class Envelopes { + + private static final String SOAP = "http://www.w3.org/2003/05/soap-envelope"; + private static final String WSA = "http://schemas.xmlsoap.org/ws/2004/08/addressing"; + private static final String WSMAN = "http://schemas.dmtf.org/wbem/wsman/1/wsman.xsd"; + private static final String WSEN = "http://schemas.xmlsoap.org/ws/2004/09/enumeration"; + private static final String RSP = "http://schemas.microsoft.com/wbem/wsman/1/windows/shell"; + private static final String ANONYMOUS = "http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous"; + + private static final String ACTION_ENUMERATE = "http://schemas.xmlsoap.org/ws/2004/09/enumeration/Enumerate"; + private static final String ACTION_PULL = "http://schemas.xmlsoap.org/ws/2004/09/enumeration/Pull"; + private static final String ACTION_CREATE = "http://schemas.xmlsoap.org/ws/2004/09/transfer/Create"; + private static final String ACTION_DELETE = "http://schemas.xmlsoap.org/ws/2004/09/transfer/Delete"; + private static final String ACTION_COMMAND = RSP + "/Command"; + private static final String ACTION_RECEIVE = RSP + "/Receive"; + private static final String ACTION_SIGNAL = RSP + "/Signal"; + + static final String SHELL_RESOURCE_URI = RSP + "/cmd"; + static final String TERMINATE_CODE = RSP + "/signal/terminate"; + static final String COMMAND_STATE_DONE = RSP + "/CommandState/Done"; + + private static final int MAX_ENVELOPE_SIZE = 153600; + + private Envelopes() {} + + // --- WQL --------------------------------------------------------------- + + static String enumerateWql(final String url, final String namespace, final String wql, final long timeoutMs) { + return ( + envelopeOpen(false) + + header(url, wmiResourceUri(namespace), ACTION_ENUMERATE, timeoutMs, null, null) + + "" + + "" + + "32000" + + "" + + escape(wql) + + "" + + "" + ); + } + + static String pull(final String url, final String namespace, final String context, final long timeoutMs) { + return ( + envelopeOpen(false) + + header(url, wmiResourceUri(namespace), ACTION_PULL, timeoutMs, null, null) + + "" + + "" + + escape(context) + + "" + + "32000" + + "" + ); + } + + // --- Command shell ----------------------------------------------------- + + static String createShell(final String url, final String workingDirectory, final long timeoutMs) { + final String optionSet = + "" + + "TRUE" + + "437" + + ""; + final String workingDir = (workingDirectory == null || workingDirectory.trim().isEmpty()) + ? "" + : "" + escape(workingDirectory) + ""; + return ( + envelopeOpen(true) + + header(url, SHELL_RESOURCE_URI, ACTION_CREATE, timeoutMs, null, optionSet) + + "" + + "stdin" + + "stdout stderr" + + workingDir + + "" + ); + } + + static String command(final String url, final String shellId, final String commandLine, final long timeoutMs) { + final String optionSet = + "" + + "TRUE" + + "FALSE" + + ""; + return ( + envelopeOpen(true) + + header(url, SHELL_RESOURCE_URI, ACTION_COMMAND, timeoutMs, shellSelector(shellId), optionSet) + + "" + + escape(commandLine) + + "" + ); + } + + static String receive(final String url, final String shellId, final String commandId, final long timeoutMs) { + return ( + envelopeOpen(true) + + header(url, SHELL_RESOURCE_URI, ACTION_RECEIVE, timeoutMs, shellSelector(shellId), null) + + "stdout stderr" + ); + } + + static String signal(final String url, final String shellId, final String commandId, final long timeoutMs) { + return ( + envelopeOpen(true) + + header(url, SHELL_RESOURCE_URI, ACTION_SIGNAL, timeoutMs, shellSelector(shellId), null) + + "" + + TERMINATE_CODE + + "" + ); + } + + static String deleteShell(final String url, final String shellId, final long timeoutMs) { + return ( + envelopeOpen(true) + + header(url, SHELL_RESOURCE_URI, ACTION_DELETE, timeoutMs, shellSelector(shellId), null) + + "" + ); + } + + // --- helpers ----------------------------------------------------------- + + private static String wmiResourceUri(final String namespace) { + return "http://schemas.microsoft.com/wbem/wsman/1/wmi/" + namespace + "/*"; + } + + private static String shellSelector(final String shellId) { + return ( + "" + escape(shellId) + "" + ); + } + + /** + * Format the WSMan OperationTimeout exactly like the CXF backend: {@code PT#.###S} with ROOT + * locale symbols (always a {@code .} decimal separator, up to millisecond precision, no trailing + * zeros) — e.g. 30000 ms → {@code PT30S}, 1500 ms → {@code PT1.5S}, 1234 ms → {@code PT1.234S}. + */ + private static String operationTimeout(final long timeoutMs) { + final BigDecimal seconds = BigDecimal.valueOf(timeoutMs).divide(BigDecimal.valueOf(1000)); + return new DecimalFormat("PT#.###S", new DecimalFormatSymbols(Locale.ROOT)).format(seconds); + } + + private static String header( + final String url, + final String resourceUri, + final String action, + final long timeoutMs, + final String selectorSet, + final String optionSet + ) { + return ( + "" + + "" + + url + + "" + + "" + + resourceUri + + "" + + "" + + ANONYMOUS + + "" + + "" + + action + + "" + + "" + + MAX_ENVELOPE_SIZE + + "" + + "uuid:" + + UUID.randomUUID().toString().toUpperCase() + + "" + + "" + + (selectorSet == null ? "" : selectorSet) + + (optionSet == null ? "" : optionSet) + + "" + + operationTimeout(timeoutMs) + + "" + + "" + ); + } + + private static String envelopeOpen(final boolean shell) { + return ( + "" + ); + } + + private static String escape(final String s) { + return s.replace("&", "&").replace("<", "<").replace(">", ">").replace("\"", """); + } +} diff --git a/src/main/java/org/metricshub/winrm/light/FallbackAuthScheme.java b/src/main/java/org/metricshub/winrm/light/FallbackAuthScheme.java new file mode 100644 index 0000000..efe7039 --- /dev/null +++ b/src/main/java/org/metricshub/winrm/light/FallbackAuthScheme.java @@ -0,0 +1,119 @@ +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. + * ╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱ + */ + +import java.util.List; + +/** + * Tries several {@link AuthScheme}s in the caller's order, using the first whose handshake succeeds + * (e.g. {@code [KERBEROS, NTLM]}: attempt Kerberos, fall back to NTLM). Once a scheme authenticates + * it becomes the active one for the rest of the connection; on reconnect the fallback runs again. + * + *

Fallback triggers on a failed handshake — the common case being Kerberos unavailable (no TGT, + * no reachable KDC, unconfigured realm), which fails client-side before any SOAP is sent. + */ +final class FallbackAuthScheme implements AuthScheme { + + private final List candidates; + // The next candidate to try when (re)running the fallback; advanced past a server-rejected scheme. + private int startIndex; + private AuthScheme active; + + FallbackAuthScheme(final List candidates) { + if (candidates == null || candidates.isEmpty()) { + throw new IllegalArgumentException("At least one authentication scheme is required"); + } + this.candidates = candidates; + } + + @Override + public String authenticate(final HttpTransport transport) throws Exception { + // Run the candidates from startIndex. After a dropped connection startIndex still points at the + // scheme that last succeeded, so it is retried first; but if that re-authentication now fails + // (e.g. an expired TGT or a briefly unavailable KDC) we fall through to the remaining candidates + // rather than abandoning the whole fallback list. + Exception lastFailure = null; + for (int i = startIndex; i < candidates.size(); i++) { + final AuthScheme candidate = candidates.get(i); + try { + final String authorization = candidate.authenticate(transport); + active = candidate; + startIndex = i; + return authorization; + } catch (final Exception e) { + lastFailure = e; + candidate.reset(); + // Give the next scheme a clean connection — a partial handshake may have left the socket + // mid-stream or the server may have closed it. + if (i < candidates.size() - 1) { + transport.close(); + } + } + } + throw new IllegalStateException( + "All requested authentication schemes failed" + + (lastFailure == null ? "" : " (last: " + lastFailure.getMessage() + ")"), + lastFailure + ); + } + + @Override + public boolean isAuthenticated() { + return active != null && active.isAuthenticated(); + } + + @Override + public void reset() { + // A dropped connection: clear the active scheme's session but keep it selected so the next + // authenticate() re-handshakes with the same (already-accepted) scheme. + if (active != null) { + active.reset(); + } + } + + @Override + public boolean advance() { + // The server rejected the active scheme (401 on a real request). Drop it and move to the next + // candidate so the next authenticate() runs the remaining schemes. + if (active != null && startIndex + 1 < candidates.size()) { + active.reset(); + active = null; + startIndex++; + return true; + } + return false; + } + + @Override + public byte[] wrap(final byte[] soapUtf8) { + return active.wrap(soapUtf8); + } + + @Override + public String wrapContentType() { + return active.wrapContentType(); + } + + @Override + public byte[] unwrap(final HttpTransport.Response response) throws Exception { + return active.unwrap(response); + } +} diff --git a/src/main/java/org/metricshub/winrm/service/client/encryption/HMACMD5.java b/src/main/java/org/metricshub/winrm/light/HMACMD5.java similarity index 93% rename from src/main/java/org/metricshub/winrm/service/client/encryption/HMACMD5.java rename to src/main/java/org/metricshub/winrm/light/HMACMD5.java index 41ffbe3..319dbf4 100644 --- a/src/main/java/org/metricshub/winrm/service/client/encryption/HMACMD5.java +++ b/src/main/java/org/metricshub/winrm/light/HMACMD5.java @@ -1,82 +1,82 @@ -package org.metricshub.winrm.service.client.encryption; - -/*- - * ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲ - * WinRM Java Client - * ჻჻჻჻჻჻ - * Copyright 2023 - 2024 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. - * ╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱ - */ - -import java.security.MessageDigest; - -/** - * Cryptography support - HMACMD5 - algorithmically based on various web - * resources by Karl Wright - * - * Code from io.cloudsoft.winrm4j.client.ntlm.forks.httpclient.NTLMEngineImpl - * release 0.12.3 @link https://github.com/cloudsoft/winrm4j - * io.cloudsoft.winrm4j.client.ntlm.forks.httpclient is a fork of apache-httpclient 4.5.13 - */ -public class HMACMD5 { - - private final byte[] ipad; - private final byte[] opad; - private final MessageDigest md5; - - HMACMD5(final byte[] input) { - byte[] key = input; - md5 = EncryptionUtils.getMD5(); - - // Initialize the pad buffers with the key - ipad = new byte[64]; - opad = new byte[64]; - - int keyLength = key.length; - if (keyLength > 64) { - // Use MD5 of the key instead, as described in RFC 2104 - md5.update(key); - key = md5.digest(); - keyLength = key.length; - } - int i = 0; - while (i < keyLength) { - ipad[i] = (byte) (key[i] ^ (byte) 0x36); - opad[i] = (byte) (key[i] ^ (byte) 0x5c); - i++; - } - while (i < 64) { - ipad[i] = (byte) 0x36; - opad[i] = (byte) 0x5c; - i++; - } - - // Very important: processChallenge the digest with the ipad buffer - md5.reset(); - md5.update(ipad); - } - - /** Grab the current digest. This is the "answer". */ - byte[] getOutput() { - final byte[] digest = md5.digest(); - md5.update(opad); - return md5.digest(digest); - } - - /** Update by adding a complete array */ - void update(final byte[] input) { - md5.update(input); - } -} +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. + * ╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱ + */ + +import java.security.MessageDigest; + +/** + * Cryptography support - HMACMD5 - algorithmically based on various web + * resources by Karl Wright + * + * Code from io.cloudsoft.winrm4j.client.ntlm.forks.httpclient.NTLMEngineImpl + * release 0.12.3 @link https://github.com/cloudsoft/winrm4j + * io.cloudsoft.winrm4j.client.ntlm.forks.httpclient is a fork of apache-httpclient 4.5.13 + */ +public class HMACMD5 { + + private final byte[] ipad; + private final byte[] opad; + private final MessageDigest md5; + + HMACMD5(final byte[] input) { + byte[] key = input; + md5 = EncryptionUtils.getMD5(); + + // Initialize the pad buffers with the key + ipad = new byte[64]; + opad = new byte[64]; + + int keyLength = key.length; + if (keyLength > 64) { + // Use MD5 of the key instead, as described in RFC 2104 + md5.update(key); + key = md5.digest(); + keyLength = key.length; + } + int i = 0; + while (i < keyLength) { + ipad[i] = (byte) (key[i] ^ (byte) 0x36); + opad[i] = (byte) (key[i] ^ (byte) 0x5c); + i++; + } + while (i < 64) { + ipad[i] = (byte) 0x36; + opad[i] = (byte) 0x5c; + i++; + } + + // Very important: processChallenge the digest with the ipad buffer + md5.reset(); + md5.update(ipad); + } + + /** Grab the current digest. This is the "answer". */ + byte[] getOutput() { + final byte[] digest = md5.digest(); + md5.update(opad); + return md5.digest(digest); + } + + /** Update by adding a complete array */ + void update(final byte[] input) { + md5.update(input); + } +} diff --git a/src/main/java/org/metricshub/winrm/light/HttpTransport.java b/src/main/java/org/metricshub/winrm/light/HttpTransport.java new file mode 100644 index 0000000..8cfd8d4 --- /dev/null +++ b/src/main/java/org/metricshub/winrm/light/HttpTransport.java @@ -0,0 +1,387 @@ +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. + * ╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱ + */ + +import java.io.BufferedInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.net.SocketTimeoutException; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import javax.net.ssl.SSLParameters; +import javax.net.ssl.SSLSocket; +import javax.net.ssl.SSLSocketFactory; +import org.metricshub.winrm.Utils; + +/** + * Minimal HTTP/1.1 client over a single kept-alive TCP socket. WinRM's NTLM authentication is + * bound to the transport connection (not the request), so every request in a session must ride + * the same socket — which is exactly why this does not use a pooling client. + */ +final class HttpTransport implements AutoCloseable { + + // After a connection has been idle at least this long, validate it before reuse: a peer may have + // silently dropped an idle keep-alive connection, which Socket.isClosed() cannot detect. Kept small + // so it never fires between the back-to-back requests of a single operation, only across idle gaps + // (e.g. a cached executor sitting between polling cycles). + private static final long VALIDATE_AFTER_INACTIVITY_MS = 1000; + + private final String host; + private final int port; + private final int timeoutMillis; + // Non-null => HTTPS: the socket is wrapped in TLS. Null => plain HTTP. + private final SSLSocketFactory sslSocketFactory; + private final boolean verifyHostname; + private Socket socket; + private OutputStream out; + private BufferedInputStream in; + private long lastActivityMillis; + + HttpTransport(final String host, final int port, final int timeoutMillis) { + this(host, port, timeoutMillis, null, false); + } + + HttpTransport( + final String host, + final int port, + final int timeoutMillis, + final SSLSocketFactory sslSocketFactory, + final boolean verifyHostname + ) { + this.host = host; + this.port = port; + this.timeoutMillis = timeoutMillis; + this.sslSocketFactory = sslSocketFactory; + this.verifyHostname = verifyHostname; + } + + static final class Response { + + final int status; + final List headers; // {name, value}, name lower-cased + final byte[] body; + + Response(final int status, final List headers, final byte[] body) { + this.status = status; + this.headers = headers; + this.body = body; + } + + String firstHeader(final String name) { + final String n = name.toLowerCase(Locale.ROOT); + for (final String[] h : headers) { + if (h[0].equals(n)) { + return h[1]; + } + } + return null; + } + + List allHeaders(final String name) { + final String n = name.toLowerCase(Locale.ROOT); + final List values = new ArrayList<>(); + for (final String[] h : headers) { + if (h[0].equals(n)) { + values.add(h[1]); + } + } + return values; + } + + /** + * Extract the Negotiate challenge token from the 401 response, scanning every + * {@code WWW-Authenticate} header (order-independent) and tolerating combined challenges. Both + * NTLM (masqueraded) and Kerberos ride under the {@code Negotiate} scheme, so both schemes use this. + * + * @return the base64 token, or {@code null} if no Negotiate challenge carries one + */ + String negotiateToken() { + for (final String value : allHeaders("www-authenticate")) { + final Matcher matcher = NEGOTIATE_TOKEN.matcher(value); + if (matcher.find()) { + return matcher.group(1); + } + } + return null; + } + } + + // A WWW-Authenticate value may list several challenges ("Negotiate , NTLM ...") and a server + // or proxy may split them across multiple header lines. Match the Negotiate scheme only at a + // challenge boundary (start of value or right after a comma) and capture just its base64 token. + private static final Pattern NEGOTIATE_TOKEN = Pattern.compile("(?i)(?:^|,)\\s*Negotiate\\s+([A-Za-z0-9+/=]+)"); + + /** Whether a live connection is currently held. */ + boolean isConnected() { + if (socket == null || socket.isClosed() || !socket.isConnected()) { + return false; + } + // Socket.isClosed() only reflects LOCAL closure: a server (or intermediary) that dropped an idle + // keep-alive connection is invisible until the next write/read fails, which would silently lose + // the first operation after the idle gap. Once the connection has been idle a while, probe it so + // we treat it as dead here — request() then resets the NTLM session and reconnects before sending. + if (Utils.getCurrentTimeMillis() - lastActivityMillis >= VALIDATE_AFTER_INACTIVITY_MS && isStalePeerClosed()) { + close(); + return false; + } + return true; + } + + /** + * Probe whether the peer has closed the connection, using a very short blocking read. A healthy idle + * keep-alive connection has no readable bytes, so the read times out (returns {@code false}); any + * byte or EOF means the connection is unusable (returns {@code true}). The read only ever consumes + * data on the unusable path, where the socket is discarded anyway, so a live stream is never disturbed. + */ + private boolean isStalePeerClosed() { + final int previousTimeout; + try { + previousTimeout = socket.getSoTimeout(); + } catch (final IOException e) { + return true; + } + try { + socket.setSoTimeout(1); + // A healthy idle keep-alive has nothing to read, so this blocks and times out (caught below). + // Any return — EOF (peer closed) or an unexpected byte (protocol desync) — means it is unusable. + in.read(); + return true; + } catch (final SocketTimeoutException e) { + return false; + } catch (final IOException e) { + return true; + } finally { + try { + socket.setSoTimeout(previousTimeout); + } catch (final IOException ignore) { + // socket is being discarded on the stale path anyway + } + } + } + + private void ensureConnected() throws IOException { + if (isConnected()) { + return; + } + final Socket newSocket = sslSocketFactory == null ? new Socket() : sslSocketFactory.createSocket(); + try { + newSocket.setTcpNoDelay(true); + if (newSocket instanceof SSLSocket && verifyHostname) { + // Turn on hostname verification against the server certificate during the handshake + // (raw SSLSockets do not do this by default). + final SSLSocket sslSocket = (SSLSocket) newSocket; + final SSLParameters params = sslSocket.getSSLParameters(); + params.setEndpointIdentificationAlgorithm("HTTPS"); + sslSocket.setSSLParameters(params); + } + newSocket.connect(new InetSocketAddress(host, port), timeoutMillis); + // Read timeout slightly above the caller's timeout so the WSMan OperationTimeout fault + // (which the Receive loop retries) reliably arrives before a socket read times out. + newSocket.setSoTimeout(timeoutMillis + 10_000); + if (newSocket instanceof SSLSocket) { + // Force the TLS handshake now so certificate/hostname failures surface here, not on + // the first read after we have already sent the request. + ((SSLSocket) newSocket).startHandshake(); + } + socket = newSocket; + out = socket.getOutputStream(); + in = new BufferedInputStream(socket.getInputStream()); + lastActivityMillis = Utils.getCurrentTimeMillis(); + } catch (final IOException | RuntimeException e) { + // Never leave a half-open socket in the field, or ensureConnected would skip reconnecting. + // Catch RuntimeException too so an unexpected failure during TLS setup (e.g. setSSLParameters) + // cannot leak the freshly created SSLSocket. + try { + newSocket.close(); + } catch (final IOException ignore) { + // best effort + } + socket = null; + out = null; + in = null; + throw e; + } + } + + Response post(final String path, final byte[] body, final String contentType, final String authorization) + throws IOException { + ensureConnected(); + try { + final StringBuilder head = new StringBuilder(); + head.append("POST ").append(path).append(" HTTP/1.1\r\n"); + head.append("Accept: */*\r\n"); + head.append("User-Agent: winrm-java-light\r\n"); + head.append("Content-Length: ").append(body == null ? 0 : body.length).append("\r\n"); + if (contentType != null) { + head.append("Content-Type: ").append(contentType).append("\r\n"); + } + head.append("Host: ").append(host).append(':').append(port).append("\r\n"); + head.append("Connection: Keep-Alive\r\n"); + if (authorization != null) { + head.append("Authorization: ").append(authorization).append("\r\n"); + } + head.append("\r\n"); + + // Send head and body in a single write (one TCP segment), matching the reference client. + final byte[] headBytes = head.toString().getBytes(StandardCharsets.ISO_8859_1); + final ByteArrayOutputStream request = new ByteArrayOutputStream( + headBytes.length + (body == null ? 0 : body.length) + ); + request.write(headBytes); + if (body != null && body.length > 0) { + request.write(body); + } + out.write(request.toByteArray()); + out.flush(); + + final Response response = readResponse(); + lastActivityMillis = Utils.getCurrentTimeMillis(); + return response; + } catch (final IOException | RuntimeException e) { + // A broken write/read leaves the socket in an unknown state and its read position possibly + // corrupted; close it so the next request establishes a fresh (re-authenticated) connection. + close(); + throw e; + } + } + + private Response readResponse() throws IOException { + final String statusLine = readLine(); + if (statusLine == null) { + throw new IOException("Connection closed by server before response"); + } + final String[] statusParts = statusLine.split(" ", 3); + final int status = Integer.parseInt(statusParts[1]); + + final List headers = new ArrayList<>(); + int contentLength = -1; + boolean chunked = false; + boolean close = false; + String line; + while ((line = readLine()) != null && !line.isEmpty()) { + final int colon = line.indexOf(':'); + if (colon < 0) { + continue; + } + final String name = line.substring(0, colon).trim().toLowerCase(Locale.ROOT); + final String value = line.substring(colon + 1).trim(); + headers.add(new String[] { name, value }); + if ("content-length".equals(name)) { + contentLength = Integer.parseInt(value); + } else if ("transfer-encoding".equals(name) && value.toLowerCase(Locale.ROOT).contains("chunked")) { + chunked = true; + } else if ("connection".equals(name) && value.toLowerCase(Locale.ROOT).contains("close")) { + close = true; + } + } + + final byte[] body; + if (chunked) { + body = readChunked(); + } else if (contentLength >= 0) { + body = readFixed(contentLength); + } else { + body = new byte[0]; + } + + if (close) { + close(); + } + return new Response(status, headers, body); + } + + private String readLine() throws IOException { + final ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + int b; + int prev = -1; + while ((b = in.read()) != -1) { + if (prev == '\r' && b == '\n') { + final byte[] raw = buffer.toByteArray(); + return new String(raw, 0, raw.length - 1, StandardCharsets.ISO_8859_1); + } + buffer.write(b); + prev = b; + } + return buffer.size() == 0 ? null : buffer.toString("ISO-8859-1"); + } + + private byte[] readFixed(final int length) throws IOException { + final byte[] buffer = new byte[length]; + int read = 0; + while (read < length) { + final int n = in.read(buffer, read, length - read); + if (n < 0) { + throw new IOException("Unexpected EOF: got " + read + " of " + length + " body bytes"); + } + read += n; + } + return buffer; + } + + private byte[] readChunked() throws IOException { + final ByteArrayOutputStream body = new ByteArrayOutputStream(); + while (true) { + final String sizeLine = readLine(); + if (sizeLine == null) { + throw new IOException("Unexpected EOF in chunked body"); + } + final int semicolon = sizeLine.indexOf(';'); + final int size = Integer.parseInt((semicolon < 0 ? sizeLine : sizeLine.substring(0, semicolon)).trim(), 16); + if (size == 0) { + // After the terminating chunk come zero or more optional trailer fields, then a final + // empty line. Consume them all, or leftover bytes desync the kept-alive NTLM socket. + String trailer; + while ((trailer = readLine()) != null && !trailer.isEmpty()) { + // discard trailer field + } + break; + } + body.write(readFixed(size)); + readLine(); // CRLF after each chunk + } + return body.toByteArray(); + } + + @Override + public void close() { + // Read the field into a local before closing so a concurrent close() (the main thread closing + // the socket to unblock a worker blocked in a socket read) cannot NPE on a check-then-use race. + // Closing an already-closed Socket is a no-op; closing an open one unblocks any pending read. + final Socket doomed = socket; + socket = null; + out = null; + in = null; + if (doomed != null) { + try { + doomed.close(); + } catch (final IOException ignore) { + // best effort + } + } + } +} diff --git a/src/main/java/org/metricshub/winrm/light/KerberosAuthScheme.java b/src/main/java/org/metricshub/winrm/light/KerberosAuthScheme.java new file mode 100644 index 0000000..ac3f30f --- /dev/null +++ b/src/main/java/org/metricshub/winrm/light/KerberosAuthScheme.java @@ -0,0 +1,194 @@ +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. + * ╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱ + */ + +import java.nio.file.Path; +import java.security.PrivilegedExceptionAction; +import java.util.Base64; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import javax.security.auth.Subject; +import javax.security.auth.callback.Callback; +import javax.security.auth.callback.CallbackHandler; +import javax.security.auth.callback.NameCallback; +import javax.security.auth.callback.PasswordCallback; +import javax.security.auth.login.AppConfigurationEntry; +import javax.security.auth.login.Configuration; +import javax.security.auth.login.LoginContext; +import org.ietf.jgss.GSSContext; +import org.ietf.jgss.GSSException; +import org.ietf.jgss.GSSManager; +import org.ietf.jgss.GSSName; +import org.ietf.jgss.Oid; + +/** + * Kerberos (SPNEGO) authentication scheme using the JDK's built-in GSS-API — no Apache/CXF. It + * obtains a TGT via JAAS ({@code Krb5LoginModule}) from a username+password (or a ticket cache), + * then a service ticket for {@code HTTP/} and emits the AP-REQ under the {@code Negotiate} + * header. + * + *

HTTPS only. Like the CXF backend (which never implemented Kerberos message encryption over + * HTTP), the SOAP travels plaintext inside TLS, so {@link #wrap}/{@link #unwrap} are pass-throughs. + * + *

Realm and KDC resolution is left to the ambient Kerberos configuration (a {@code krb5.conf} or + * the {@code java.security.krb5.*} system properties), exactly as the CXF path did — the library + * sets none itself. + */ +final class KerberosAuthScheme implements AuthScheme { + + private static final String SOAP_CONTENT_TYPE = "application/soap+xml;charset=UTF-8"; + // SPNEGO mechanism OID — the "Negotiate" scheme Windows http.sys expects. + private static final String SPNEGO_OID = "1.3.6.1.5.5.2"; + + private final String servicePrincipalHost; + private final String username; + private final String password; + private final Path ticketCache; + + private GSSContext context; + private boolean authenticated; + + /** + * @param servicePrincipalHost the host whose {@code HTTP/} SPN to target — must be the FQDN + * the KDC knows (never an IP) + * @param username the account name (without any {@code DOMAIN\} prefix) + * @param password the account password (unused when {@code ticketCache} is set) + * @param ticketCache a Kerberos credential cache to reuse, or {@code null} to log in with + * the password + */ + KerberosAuthScheme( + final String servicePrincipalHost, + final String username, + final String password, + final Path ticketCache + ) { + this.servicePrincipalHost = servicePrincipalHost; + this.username = username; + this.password = password; + this.ticketCache = ticketCache; + } + + @Override + public String authenticate(final HttpTransport transport) throws Exception { + final Subject subject = login(); + final byte[] apReq = Subject.doAs( + subject, + (PrivilegedExceptionAction) () -> { + final GSSManager manager = GSSManager.getInstance(); + final Oid spnego = new Oid(SPNEGO_OID); + // NT_HOSTBASED_SERVICE "HTTP@host" maps to the SPN HTTP/host. + final GSSName serverName = manager.createName("HTTP@" + servicePrincipalHost, GSSName.NT_HOSTBASED_SERVICE); + context = manager.createContext(serverName, spnego, null, GSSContext.DEFAULT_LIFETIME); + context.requestMutualAuth(true); + context.requestCredDeleg(false); + // The AP-REQ is complete after the first call; the KDC issued the service ticket using the + // Subject's TGT. The server validates it on the first real request (and, over HTTPS, TLS + // already authenticates the server, so we do not need to process a mutual-auth reply token). + return context.initSecContext(new byte[0], 0, 0); + } + ); + authenticated = true; + return "Negotiate " + Base64.getEncoder().encodeToString(apReq); + } + + @Override + public boolean isAuthenticated() { + return authenticated; + } + + @Override + public void reset() { + if (context != null) { + try { + context.dispose(); + } catch (final GSSException ignore) { + // disposing a dead context is best-effort + } + context = null; + } + authenticated = false; + } + + @Override + public byte[] wrap(final byte[] soapUtf8) { + // HTTPS only: TLS provides confidentiality, so the SOAP travels plaintext. + return soapUtf8; + } + + @Override + public String wrapContentType() { + return SOAP_CONTENT_TYPE; + } + + @Override + public byte[] unwrap(final HttpTransport.Response response) { + return response.body; + } + + /** Obtain a Kerberos {@link Subject} (holding the TGT) via a programmatic JAAS login. */ + private Subject login() throws Exception { + final LoginContext loginContext = new LoginContext("", null, callbackHandler(), krb5Configuration()); + loginContext.login(); + return loginContext.getSubject(); + } + + private CallbackHandler callbackHandler() { + return callbacks -> { + for (final Callback callback : callbacks) { + if (callback instanceof NameCallback) { + ((NameCallback) callback).setName(username); + } else if (callback instanceof PasswordCallback) { + ((PasswordCallback) callback).setPassword(password == null ? null : password.toCharArray()); + } + } + }; + } + + private Configuration krb5Configuration() { + return new Configuration() { + @Override + public AppConfigurationEntry[] getAppConfigurationEntry(final String name) { + final Map options = new HashMap<>(); + options.put("isInitiator", "true"); + options.put("refreshKrb5Config", "true"); + // Uppercase the whole principal: a lowercase UPN domain with an uppercase realm triggers a + // "Message stream modified" KrbException; uppercasing avoids it (as the CXF path does). + options.put("principal", username.toUpperCase(Locale.ROOT)); + if (ticketCache != null) { + options.put("useTicketCache", "true"); + options.put("ticketCache", ticketCache.toString()); + options.put("doNotPrompt", "true"); + } else { + options.put("useTicketCache", "false"); + options.put("doNotPrompt", "false"); + } + return new AppConfigurationEntry[] { + new AppConfigurationEntry( + "com.sun.security.auth.module.Krb5LoginModule", + AppConfigurationEntry.LoginModuleControlFlag.REQUIRED, + options + ) + }; + } + }; + } +} diff --git a/src/main/java/org/metricshub/winrm/light/LightTls.java b/src/main/java/org/metricshub/winrm/light/LightTls.java new file mode 100644 index 0000000..a42413e --- /dev/null +++ b/src/main/java/org/metricshub/winrm/light/LightTls.java @@ -0,0 +1,94 @@ +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. + * ╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱ + */ + +import java.security.GeneralSecurityException; +import java.security.cert.X509Certificate; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509TrustManager; + +/** + * TLS setup for the light backend's HTTPS transport. + * + *

Unlike the legacy CXF path (which trusts every certificate), the light backend validates by + * default: it uses the JDK default {@link SSLSocketFactory}, so the platform trust store (and any + * {@code -Djavax.net.ssl.trustStore}) applies and the server hostname is verified during the + * handshake. Setting the system property {@value #INSECURE_PROPERTY} to {@code true} opts out — + * trusting all certificates and skipping hostname verification — for self-signed test hosts. That + * is insecure and must not be used in production. + */ +final class LightTls { + + /** System property that disables TLS certificate and hostname validation (insecure; testing only). */ + static final String INSECURE_PROPERTY = "org.metricshub.winrm.tls.insecure"; + + private LightTls() {} + + /** Whether TLS validation has been disabled via {@value #INSECURE_PROPERTY}. */ + static boolean isInsecure() { + return Boolean.getBoolean(INSECURE_PROPERTY); + } + + /** Whether the server hostname should be verified during the TLS handshake (true unless insecure). */ + static boolean verifyHostname() { + return !isInsecure(); + } + + /** + * The socket factory for HTTPS connections: the JDK default (validating) factory, or a + * trust-all factory when {@value #INSECURE_PROPERTY} is set. + * + * @return an {@link SSLSocketFactory} + */ + static SSLSocketFactory socketFactory() { + if (!isInsecure()) { + return (SSLSocketFactory) SSLSocketFactory.getDefault(); + } + try { + final SSLContext context = SSLContext.getInstance("TLS"); + context.init(null, new TrustManager[] { trustAllManager() }, null); + return context.getSocketFactory(); + } catch (final GeneralSecurityException e) { + throw new IllegalStateException("Cannot build an insecure (trust-all) TLS context", e); + } + } + + private static X509TrustManager trustAllManager() { + return new X509TrustManager() { + @Override + public void checkClientTrusted(final X509Certificate[] chain, final String authType) { + // insecure mode: accept any client certificate + } + + @Override + public void checkServerTrusted(final X509Certificate[] chain, final String authType) { + // insecure mode: accept any server certificate + } + + @Override + public X509Certificate[] getAcceptedIssuers() { + return new X509Certificate[0]; + } + }; + } +} diff --git a/src/main/java/org/metricshub/winrm/light/LightWinRMService.java b/src/main/java/org/metricshub/winrm/light/LightWinRMService.java new file mode 100644 index 0000000..7ced0fb --- /dev/null +++ b/src/main/java/org/metricshub/winrm/light/LightWinRMService.java @@ -0,0 +1,251 @@ +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. + * ╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱ + */ + +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicBoolean; +import javax.net.ssl.SSLSocketFactory; +import org.metricshub.winrm.Utils; +import org.metricshub.winrm.WinRMHttpProtocolEnum; +import org.metricshub.winrm.WindowsRemoteCommandResult; +import org.metricshub.winrm.WindowsRemoteExecutor; +import org.metricshub.winrm.WmiHelper; +import org.metricshub.winrm.exceptions.WinRMException; +import org.metricshub.winrm.exceptions.WindowsRemoteException; +import org.metricshub.winrm.exceptions.WqlQuerySyntaxException; +import org.metricshub.winrm.service.WinRMEndpoint; +import org.metricshub.winrm.service.client.auth.AuthenticationEnum; + +/** + * Dependency-free {@link WindowsRemoteExecutor} backed by {@link WsmanClient}. A drop-in + * replacement for the CXF-based {@code WinRMService} that shipped before 2.0.0: same public + * behaviour, no Apache CXF / JAX-WS / JAXB stack, and immune by construction to JAXP + * {@code ServiceLoader} poisoning (it uses the JDK-default XML factories). + * + *

Supports NTLM over HTTP (with message encryption) and over HTTPS (plaintext SOAP inside TLS, + * validating the server certificate by default; see {@link LightTls}), and Kerberos over HTTPS + * (SPNEGO via the JDK GSS-API; see {@link KerberosAuthScheme}). A multi-scheme request such as + * {@code [KERBEROS, NTLM]} is tried in order with fallback. + */ +public final class LightWinRMService implements WindowsRemoteExecutor { + + private final WinRMEndpoint winRMEndpoint; + private final WsmanClient client; + private final AtomicBoolean closed = new AtomicBoolean(false); + + private LightWinRMService(final WinRMEndpoint winRMEndpoint, final WsmanClient client) { + this.winRMEndpoint = winRMEndpoint; + this.client = client; + } + + /** + * Create a light WinRM executor. + * + * @param winRMEndpoint endpoint with credentials (mandatory) + * @param timeout timeout in milliseconds (must be > 0) + * @param ticketCache Kerberos ticket cache path (used by the Kerberos scheme; {@code null} logs + * in with the password) + * @param authentications requested authentication schemes, tried in order (NTLM and/or Kerberos); + * {@code null}/empty means NTLM only + * @return a new {@code LightWinRMService} + * @throws WinRMException on invalid arguments or an unsupported authentication request + */ + public static LightWinRMService createInstance( + final WinRMEndpoint winRMEndpoint, + final long timeout, + final java.nio.file.Path ticketCache, + final List authentications + ) throws WinRMException { + Utils.checkNonNull(winRMEndpoint, "winRMEndpoint"); + Utils.checkArgumentNotZeroOrNegative(timeout, "timeout"); + + // HTTPS wraps the transport in TLS and exchanges plaintext SOAP; HTTP uses NTLM message sealing. + // TLS validates by default (platform trust store + hostname verification); see LightTls. + final boolean https = winRMEndpoint.getProtocol() == WinRMHttpProtocolEnum.HTTPS; + final SSLSocketFactory sslSocketFactory = https ? LightTls.socketFactory() : null; + + final AuthScheme authScheme = resolveAuthScheme(winRMEndpoint, authentications, https, ticketCache); + + // Use the endpoint's own validated host/port rather than re-parsing the URL: URI.getHost()/getPort() + // return null/-1 for names URI cannot classify (underscores, Unicode) that WinRMEndpoint accepts, + // which would otherwise make the default backend unable to reach hosts the CXF backend could. + final WsmanClient client = new WsmanClient( + winRMEndpoint.getHostname(), + winRMEndpoint.getPort(), + timeout, + sslSocketFactory, + https && LightTls.verifyHostname(), + authScheme, + winRMEndpoint.getRawUsername() + ); + return new LightWinRMService(winRMEndpoint, client); + } + + /** + * Resolve the requested authentication schemes into a single {@link AuthScheme}, honoring the + * caller's order. {@code null}/empty means NTLM only. A single scheme is used directly; several + * become an ordered {@link FallbackAuthScheme} (e.g. Kerberos then NTLM). Kerberos requires HTTPS + * (no message encryption over plain HTTP, matching the CXF backend), so it is dropped from the + * candidate list over HTTP — a fallback list then uses its remaining schemes, and a Kerberos-only + * request over HTTP fails toward the escape hatch. + */ + private static AuthScheme resolveAuthScheme( + final WinRMEndpoint winRMEndpoint, + final List authentications, + final boolean https, + final java.nio.file.Path ticketCache + ) throws WinRMException { + final List requested = authentications == null || authentications.isEmpty() + ? List.of(AuthenticationEnum.NTLM) + : authentications; + + final String domain = winRMEndpoint.getDomain(); + final String username = winRMEndpoint.getUsername(); + final String password = new String(winRMEndpoint.getPassword()); + + final List schemes = new ArrayList<>(); + for (final AuthenticationEnum auth : requested) { + if (auth == AuthenticationEnum.NTLM) { + schemes.add(new NtlmAuthScheme(domain, username, password, https)); + } else if (auth == AuthenticationEnum.KERBEROS) { + if (https) { + // The SPN is HTTP/, so the caller must connect by the FQDN the KDC knows. + schemes.add(new KerberosAuthScheme(winRMEndpoint.getHostname(), username, password, ticketCache)); + } + // else: Kerberos is unavailable over plain HTTP — leave it out of the candidate list. + } else { + throw new WinRMException( + "The light WinRM backend supports only NTLM and Kerberos (requested: " + requested + ")." + ); + } + } + + if (schemes.isEmpty()) { + // e.g. Kerberos requested over plain HTTP with no other scheme to fall back to. + throw new WinRMException( + "Kerberos over WinRM requires HTTPS (endpoint was " + + winRMEndpoint.getEndpoint() + + "): there is no Kerberos message encryption over plain HTTP. Use HTTPS, or add NTLM to the authentication list." + ); + } + return schemes.size() == 1 ? schemes.get(0) : new FallbackAuthScheme(schemes); + } + + @Override + public List> executeWql(final String wqlQuery, final long timeout) + throws TimeoutException, WqlQuerySyntaxException, WindowsRemoteException { + checkNotClosed(); + Utils.checkNonNull(wqlQuery, "wqlQuery"); + if (!WmiHelper.isValidWql(wqlQuery)) { + throw new WqlQuerySyntaxException(wqlQuery); + } + Utils.checkArgumentNotZeroOrNegative(timeout, "timeout"); + + // Enforce the caller's timeout as a wall-clock deadline (throwing TimeoutException), matching + // the CXF WinRMService and bounding the WSMan Pull loop. + try { + return Utils.execute( + () -> { + final List> rows = client.wql(winRMEndpoint.getNamespace(), wqlQuery); + final List> result = new ArrayList<>(rows.size()); + for (final Map row : rows) { + result.add(new LinkedHashMap<>(row)); + } + return result; + }, + timeout + ); + } catch (final InterruptedException | ExecutionException e) { + if (e.getCause() != null) { + throw new WinRMException(e.getCause(), e.getCause().getMessage()); + } + throw new WinRMException(e); + } + } + + @Override + public WindowsRemoteCommandResult executeCommand( + final String command, + final String workingDirectory, + final Charset charset, + final long timeout + ) throws WindowsRemoteException, TimeoutException { + checkNotClosed(); + Utils.checkNonNull(command, "command"); + Utils.checkArgumentNotZeroOrNegative(timeout, "timeout"); + + // Enforce the caller's timeout as a wall-clock deadline (throwing TimeoutException), matching + // the CXF WinRMService and bounding the WSMan Receive loop. + try { + return Utils.execute( + () -> { + final long start = Utils.getCurrentTimeMillis(); + final WsmanClient.CommandOutput output = client.executeCommand(command, workingDirectory, charset); + final float executionTime = (Utils.getCurrentTimeMillis() - start) / 1000.0f; + return new WindowsRemoteCommandResult(output.stdout, output.stderr, executionTime, output.exitCode); + }, + timeout + ); + } catch (final InterruptedException | ExecutionException e) { + if (e.getCause() != null) { + throw new WinRMException(e.getCause(), e.getCause().getMessage()); + } + throw new WinRMException(e); + } + } + + @Override + public String getHostname() { + return winRMEndpoint.getHostname(); + } + + @Override + public String getUsername() { + return winRMEndpoint.getRawUsername(); + } + + @Override + public char[] getPassword() { + return winRMEndpoint.getPassword(); + } + + @Override + public void close() { + // Idempotent: releases the underlying connection exactly once, and marks the executor closed so + // a later operation is rejected rather than silently reviving it with a fresh handshake. + if (closed.compareAndSet(false, true)) { + client.close(); + } + } + + private void checkNotClosed() { + // Same message as the CXF backend's checkConnectedFirst() — part of the exception surface. + if (closed.get()) { + throw new IllegalStateException("This instance has been closed and a new one must be created."); + } + } +} diff --git a/src/main/java/org/metricshub/winrm/service/client/encryption/MD4.java b/src/main/java/org/metricshub/winrm/light/MD4.java similarity index 96% rename from src/main/java/org/metricshub/winrm/service/client/encryption/MD4.java rename to src/main/java/org/metricshub/winrm/light/MD4.java index aa0eed3..e323247 100644 --- a/src/main/java/org/metricshub/winrm/service/client/encryption/MD4.java +++ b/src/main/java/org/metricshub/winrm/light/MD4.java @@ -1,211 +1,211 @@ -package org.metricshub.winrm.service.client.encryption; - -/*- - * ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲ - * WinRM Java Client - * ჻჻჻჻჻჻ - * Copyright 2023 - 2024 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. - * ╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱ - */ - -/** - * Cryptography support - MD4. The following class was based loosely on the - * RFC and on code found at http://www.cs.umd.edu/~harry/jotp/src/md.java. - * Code correctness was verified by looking at MD4.java from the jcifs - * library (http://jcifs.samba.org). It was massaged extensively to the - * final form found here by Karl Wright (kwright@metacarta.com). - * - * Code from io.cloudsoft.winrm4j.client.ntlm.forks.httpclient.NTLMEngineImpl - * release 0.12.3 @link https://github.com/cloudsoft/winrm4j - * io.cloudsoft.winrm4j.client.ntlm.forks.httpclient is a fork of apache-httpclient 4.5.13 - */ -public class MD4 { - - private int a = 0x67452301; - private int b = 0xefcdab89; - private int c = 0x98badcfe; - private int d = 0x10325476; - private long count = 0L; - private final byte[] dataBuffer = new byte[64]; - - void update(final byte[] input) { - // We always deal with 512 bits at a time. Correspondingly, there is - // a buffer 64 bytes long that we write data into until it gets - // full. - int curBufferPos = (int) (count & 63L); - int inputIndex = 0; - while (input.length - inputIndex + curBufferPos >= dataBuffer.length) { - // We have enough data to do the next step. Do a partial copy - // and a transform, updating inputIndex and curBufferPos - // accordingly - final int transferAmt = dataBuffer.length - curBufferPos; - System.arraycopy(input, inputIndex, dataBuffer, curBufferPos, transferAmt); - count += transferAmt; - curBufferPos = 0; - inputIndex += transferAmt; - processBuffer(); - } - - // If there's anything left, copy it into the buffer and leave it. - // We know there's not enough left to process. - if (inputIndex < input.length) { - final int transferAmt = input.length - inputIndex; - System.arraycopy(input, inputIndex, dataBuffer, curBufferPos, transferAmt); - count += transferAmt; - curBufferPos += transferAmt; - } - } - - byte[] getOutput() { - // Feed pad/length data into engine. This must round out the input - // to a multiple of 512 bits. - final int bufferIndex = (int) (count & 63L); - final int padLen = (bufferIndex < 56) ? (56 - bufferIndex) : (120 - bufferIndex); - final byte[] postBytes = new byte[padLen + 8]; - // Leading 0x80, specified amount of zero padding, then length in - // bits. - postBytes[0] = (byte) 0x80; - // Fill out the last 8 bytes with the length - for (int i = 0; i < 8; i++) { - postBytes[padLen + i] = (byte) ((count * 8) >>> (8 * i)); - } - - // Update the engine - update(postBytes); - - // Calculate final result - final byte[] result = new byte[16]; - writeULong(result, a, 0); - writeULong(result, b, 4); - writeULong(result, c, 8); - writeULong(result, d, 12); - return result; - } - - private static void writeULong(final byte[] buffer, final int value, final int offset) { - buffer[offset] = (byte) (value & 0xff); - buffer[offset + 1] = (byte) ((value >> 8) & 0xff); - buffer[offset + 2] = (byte) ((value >> 16) & 0xff); - buffer[offset + 3] = (byte) ((value >> 24) & 0xff); - } - - private void processBuffer() { - // Convert current buffer to 16 ulongs - final int[] d = new int[16]; - - for (int i = 0; i < 16; i++) { - d[i] = - (dataBuffer[i * 4] & 0xff) + - ((dataBuffer[i * 4 + 1] & 0xff) << 8) + - ((dataBuffer[i * 4 + 2] & 0xff) << 16) + - ((dataBuffer[i * 4 + 3] & 0xff) << 24); - } - - // Do a round of processing - final int aa = a; - final int bb = b; - final int cc = c; - final int dd = this.d; - round1(d); - round2(d); - round3(d); - a += aa; - b += bb; - c += cc; - this.d += dd; - } - - private void round1(final int[] d) { - a = rotintlft((a + f(b, c, this.d) + d[0]), 3); - this.d = rotintlft((this.d + f(a, b, c) + d[1]), 7); - c = rotintlft((c + f(this.d, a, b) + d[2]), 11); - b = rotintlft((b + f(c, this.d, a) + d[3]), 19); - - a = rotintlft((a + f(b, c, this.d) + d[4]), 3); - this.d = rotintlft((this.d + f(a, b, c) + d[5]), 7); - c = rotintlft((c + f(this.d, a, b) + d[6]), 11); - b = rotintlft((b + f(c, this.d, a) + d[7]), 19); - - a = rotintlft((a + f(b, c, this.d) + d[8]), 3); - this.d = rotintlft((this.d + f(a, b, c) + d[9]), 7); - c = rotintlft((c + f(this.d, a, b) + d[10]), 11); - b = rotintlft((b + f(c, this.d, a) + d[11]), 19); - - a = rotintlft((a + f(b, c, this.d) + d[12]), 3); - this.d = rotintlft((this.d + f(a, b, c) + d[13]), 7); - c = rotintlft((c + f(this.d, a, b) + d[14]), 11); - b = rotintlft((b + f(c, this.d, a) + d[15]), 19); - } - - private void round2(final int[] d) { - a = rotintlft((a + g(b, c, this.d) + d[0] + 0x5a827999), 3); - this.d = rotintlft((this.d + g(a, b, c) + d[4] + 0x5a827999), 5); - c = rotintlft((c + g(this.d, a, b) + d[8] + 0x5a827999), 9); - b = rotintlft((b + g(c, this.d, a) + d[12] + 0x5a827999), 13); - - a = rotintlft((a + g(b, c, this.d) + d[1] + 0x5a827999), 3); - this.d = rotintlft((this.d + g(a, b, c) + d[5] + 0x5a827999), 5); - c = rotintlft((c + g(this.d, a, b) + d[9] + 0x5a827999), 9); - b = rotintlft((b + g(c, this.d, a) + d[13] + 0x5a827999), 13); - - a = rotintlft((a + g(b, c, this.d) + d[2] + 0x5a827999), 3); - this.d = rotintlft((this.d + g(a, b, c) + d[6] + 0x5a827999), 5); - c = rotintlft((c + g(this.d, a, b) + d[10] + 0x5a827999), 9); - b = rotintlft((b + g(c, this.d, a) + d[14] + 0x5a827999), 13); - - a = rotintlft((a + g(b, c, this.d) + d[3] + 0x5a827999), 3); - this.d = rotintlft((this.d + g(a, b, c) + d[7] + 0x5a827999), 5); - c = rotintlft((c + g(this.d, a, b) + d[11] + 0x5a827999), 9); - b = rotintlft((b + g(c, this.d, a) + d[15] + 0x5a827999), 13); - } - - private void round3(final int[] d) { - a = rotintlft((a + h(b, c, this.d) + d[0] + 0x6ed9eba1), 3); - this.d = rotintlft((this.d + h(a, b, c) + d[8] + 0x6ed9eba1), 9); - c = rotintlft((c + h(this.d, a, b) + d[4] + 0x6ed9eba1), 11); - b = rotintlft((b + h(c, this.d, a) + d[12] + 0x6ed9eba1), 15); - - a = rotintlft((a + h(b, c, this.d) + d[2] + 0x6ed9eba1), 3); - this.d = rotintlft((this.d + h(a, b, c) + d[10] + 0x6ed9eba1), 9); - c = rotintlft((c + h(this.d, a, b) + d[6] + 0x6ed9eba1), 11); - b = rotintlft((b + h(c, this.d, a) + d[14] + 0x6ed9eba1), 15); - - a = rotintlft((a + h(b, c, this.d) + d[1] + 0x6ed9eba1), 3); - this.d = rotintlft((this.d + h(a, b, c) + d[9] + 0x6ed9eba1), 9); - c = rotintlft((c + h(this.d, a, b) + d[5] + 0x6ed9eba1), 11); - b = rotintlft((b + h(c, this.d, a) + d[13] + 0x6ed9eba1), 15); - - a = rotintlft((a + h(b, c, this.d) + d[3] + 0x6ed9eba1), 3); - this.d = rotintlft((this.d + h(a, b, c) + d[11] + 0x6ed9eba1), 9); - c = rotintlft((c + h(this.d, a, b) + d[7] + 0x6ed9eba1), 11); - b = rotintlft((b + h(c, this.d, a) + d[15] + 0x6ed9eba1), 15); - } - - private static int f(final int x, final int y, final int z) { - return ((x & y) | (~x & z)); - } - - private static int g(final int x, final int y, final int z) { - return ((x & y) | (x & z) | (y & z)); - } - - private static int h(final int x, final int y, final int z) { - return (x ^ y ^ z); - } - - private static int rotintlft(final int val, final int numbits) { - return ((val << numbits) | (val >>> (32 - numbits))); - } -} +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. + * ╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱ + */ + +/** + * Cryptography support - MD4. The following class was based loosely on the + * RFC and on code found at http://www.cs.umd.edu/~harry/jotp/src/md.java. + * Code correctness was verified by looking at MD4.java from the jcifs + * library (http://jcifs.samba.org). It was massaged extensively to the + * final form found here by Karl Wright (kwright@metacarta.com). + * + * Code from io.cloudsoft.winrm4j.client.ntlm.forks.httpclient.NTLMEngineImpl + * release 0.12.3 @link https://github.com/cloudsoft/winrm4j + * io.cloudsoft.winrm4j.client.ntlm.forks.httpclient is a fork of apache-httpclient 4.5.13 + */ +public class MD4 { + + private int a = 0x67452301; + private int b = 0xefcdab89; + private int c = 0x98badcfe; + private int d = 0x10325476; + private long count = 0L; + private final byte[] dataBuffer = new byte[64]; + + void update(final byte[] input) { + // We always deal with 512 bits at a time. Correspondingly, there is + // a buffer 64 bytes long that we write data into until it gets + // full. + int curBufferPos = (int) (count & 63L); + int inputIndex = 0; + while (input.length - inputIndex + curBufferPos >= dataBuffer.length) { + // We have enough data to do the next step. Do a partial copy + // and a transform, updating inputIndex and curBufferPos + // accordingly + final int transferAmt = dataBuffer.length - curBufferPos; + System.arraycopy(input, inputIndex, dataBuffer, curBufferPos, transferAmt); + count += transferAmt; + curBufferPos = 0; + inputIndex += transferAmt; + processBuffer(); + } + + // If there's anything left, copy it into the buffer and leave it. + // We know there's not enough left to process. + if (inputIndex < input.length) { + final int transferAmt = input.length - inputIndex; + System.arraycopy(input, inputIndex, dataBuffer, curBufferPos, transferAmt); + count += transferAmt; + curBufferPos += transferAmt; + } + } + + byte[] getOutput() { + // Feed pad/length data into engine. This must round out the input + // to a multiple of 512 bits. + final int bufferIndex = (int) (count & 63L); + final int padLen = (bufferIndex < 56) ? (56 - bufferIndex) : (120 - bufferIndex); + final byte[] postBytes = new byte[padLen + 8]; + // Leading 0x80, specified amount of zero padding, then length in + // bits. + postBytes[0] = (byte) 0x80; + // Fill out the last 8 bytes with the length + for (int i = 0; i < 8; i++) { + postBytes[padLen + i] = (byte) ((count * 8) >>> (8 * i)); + } + + // Update the engine + update(postBytes); + + // Calculate final result + final byte[] result = new byte[16]; + writeULong(result, a, 0); + writeULong(result, b, 4); + writeULong(result, c, 8); + writeULong(result, d, 12); + return result; + } + + private static void writeULong(final byte[] buffer, final int value, final int offset) { + buffer[offset] = (byte) (value & 0xff); + buffer[offset + 1] = (byte) ((value >> 8) & 0xff); + buffer[offset + 2] = (byte) ((value >> 16) & 0xff); + buffer[offset + 3] = (byte) ((value >> 24) & 0xff); + } + + private void processBuffer() { + // Convert current buffer to 16 ulongs + final int[] d = new int[16]; + + for (int i = 0; i < 16; i++) { + d[i] = + (dataBuffer[i * 4] & 0xff) + + ((dataBuffer[i * 4 + 1] & 0xff) << 8) + + ((dataBuffer[i * 4 + 2] & 0xff) << 16) + + ((dataBuffer[i * 4 + 3] & 0xff) << 24); + } + + // Do a round of processing + final int aa = a; + final int bb = b; + final int cc = c; + final int dd = this.d; + round1(d); + round2(d); + round3(d); + a += aa; + b += bb; + c += cc; + this.d += dd; + } + + private void round1(final int[] d) { + a = rotintlft((a + f(b, c, this.d) + d[0]), 3); + this.d = rotintlft((this.d + f(a, b, c) + d[1]), 7); + c = rotintlft((c + f(this.d, a, b) + d[2]), 11); + b = rotintlft((b + f(c, this.d, a) + d[3]), 19); + + a = rotintlft((a + f(b, c, this.d) + d[4]), 3); + this.d = rotintlft((this.d + f(a, b, c) + d[5]), 7); + c = rotintlft((c + f(this.d, a, b) + d[6]), 11); + b = rotintlft((b + f(c, this.d, a) + d[7]), 19); + + a = rotintlft((a + f(b, c, this.d) + d[8]), 3); + this.d = rotintlft((this.d + f(a, b, c) + d[9]), 7); + c = rotintlft((c + f(this.d, a, b) + d[10]), 11); + b = rotintlft((b + f(c, this.d, a) + d[11]), 19); + + a = rotintlft((a + f(b, c, this.d) + d[12]), 3); + this.d = rotintlft((this.d + f(a, b, c) + d[13]), 7); + c = rotintlft((c + f(this.d, a, b) + d[14]), 11); + b = rotintlft((b + f(c, this.d, a) + d[15]), 19); + } + + private void round2(final int[] d) { + a = rotintlft((a + g(b, c, this.d) + d[0] + 0x5a827999), 3); + this.d = rotintlft((this.d + g(a, b, c) + d[4] + 0x5a827999), 5); + c = rotintlft((c + g(this.d, a, b) + d[8] + 0x5a827999), 9); + b = rotintlft((b + g(c, this.d, a) + d[12] + 0x5a827999), 13); + + a = rotintlft((a + g(b, c, this.d) + d[1] + 0x5a827999), 3); + this.d = rotintlft((this.d + g(a, b, c) + d[5] + 0x5a827999), 5); + c = rotintlft((c + g(this.d, a, b) + d[9] + 0x5a827999), 9); + b = rotintlft((b + g(c, this.d, a) + d[13] + 0x5a827999), 13); + + a = rotintlft((a + g(b, c, this.d) + d[2] + 0x5a827999), 3); + this.d = rotintlft((this.d + g(a, b, c) + d[6] + 0x5a827999), 5); + c = rotintlft((c + g(this.d, a, b) + d[10] + 0x5a827999), 9); + b = rotintlft((b + g(c, this.d, a) + d[14] + 0x5a827999), 13); + + a = rotintlft((a + g(b, c, this.d) + d[3] + 0x5a827999), 3); + this.d = rotintlft((this.d + g(a, b, c) + d[7] + 0x5a827999), 5); + c = rotintlft((c + g(this.d, a, b) + d[11] + 0x5a827999), 9); + b = rotintlft((b + g(c, this.d, a) + d[15] + 0x5a827999), 13); + } + + private void round3(final int[] d) { + a = rotintlft((a + h(b, c, this.d) + d[0] + 0x6ed9eba1), 3); + this.d = rotintlft((this.d + h(a, b, c) + d[8] + 0x6ed9eba1), 9); + c = rotintlft((c + h(this.d, a, b) + d[4] + 0x6ed9eba1), 11); + b = rotintlft((b + h(c, this.d, a) + d[12] + 0x6ed9eba1), 15); + + a = rotintlft((a + h(b, c, this.d) + d[2] + 0x6ed9eba1), 3); + this.d = rotintlft((this.d + h(a, b, c) + d[10] + 0x6ed9eba1), 9); + c = rotintlft((c + h(this.d, a, b) + d[6] + 0x6ed9eba1), 11); + b = rotintlft((b + h(c, this.d, a) + d[14] + 0x6ed9eba1), 15); + + a = rotintlft((a + h(b, c, this.d) + d[1] + 0x6ed9eba1), 3); + this.d = rotintlft((this.d + h(a, b, c) + d[9] + 0x6ed9eba1), 9); + c = rotintlft((c + h(this.d, a, b) + d[5] + 0x6ed9eba1), 11); + b = rotintlft((b + h(c, this.d, a) + d[13] + 0x6ed9eba1), 15); + + a = rotintlft((a + h(b, c, this.d) + d[3] + 0x6ed9eba1), 3); + this.d = rotintlft((this.d + h(a, b, c) + d[11] + 0x6ed9eba1), 9); + c = rotintlft((c + h(this.d, a, b) + d[7] + 0x6ed9eba1), 11); + b = rotintlft((b + h(c, this.d, a) + d[15] + 0x6ed9eba1), 15); + } + + private static int f(final int x, final int y, final int z) { + return ((x & y) | (~x & z)); + } + + private static int g(final int x, final int y, final int z) { + return ((x & y) | (x & z) | (y & z)); + } + + private static int h(final int x, final int y, final int z) { + return (x ^ y ^ z); + } + + private static int rotintlft(final int val, final int numbits) { + return ((val << numbits) | (val >>> (32 - numbits))); + } +} diff --git a/src/main/java/org/metricshub/winrm/service/client/auth/ntlm/NTLMEngineUtils.java b/src/main/java/org/metricshub/winrm/light/NTLMEngineUtils.java similarity index 83% rename from src/main/java/org/metricshub/winrm/service/client/auth/ntlm/NTLMEngineUtils.java rename to src/main/java/org/metricshub/winrm/light/NTLMEngineUtils.java index f62b475..3eb5b5e 100644 --- a/src/main/java/org/metricshub/winrm/service/client/auth/ntlm/NTLMEngineUtils.java +++ b/src/main/java/org/metricshub/winrm/light/NTLMEngineUtils.java @@ -1,85 +1,103 @@ -package org.metricshub.winrm.service.client.auth.ntlm; - -/*- - * ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲ - * WinRM Java Client - * ჻჻჻჻჻჻ - * Copyright 2023 - 2024 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. - * ╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱ - */ - -import java.nio.charset.Charset; -import java.nio.charset.StandardCharsets; -import org.apache.http.impl.auth.NTLMEngineException; - -/** - * Code from io.cloudsoft.winrm4j.client.ntlm.forks.httpclient.NTLMEngineImpl - * release 0.12.3 @link https://github.com/cloudsoft/winrm4j - * io.cloudsoft.winrm4j.client.ntlm.forks.httpclient is a fork of apache-httpclient 4.5.13 - */ -public class NTLMEngineUtils { - - private NTLMEngineUtils() {} - - /** Unicode encoding */ - public static final Charset UNICODE_LITTLE_UNMARKED = StandardCharsets.UTF_16LE; - /** Character encoding */ - public static final Charset DEFAULT_CHARSET = StandardCharsets.US_ASCII; - - // Flags we use; descriptions according to: - // http://davenport.sourceforge.net/ntlm.html - // and - // http://msdn.microsoft.com/en-us/library/cc236650%28v=prot.20%29.aspx - // [MS-NLMP] section 2.2.2.5 - static final int FLAG_REQUEST_UNICODE_ENCODING = 0x00000001; // Unicode string encoding requested - static final int FLAG_REQUEST_SIGN = 0x00000010; // Requests all messages have a signature attached, in NEGOTIATE message. - static final int FLAG_REQUEST_LAN_MANAGER_KEY = 0x00000080; // Request Lan Manager key instead of user session key - static final int FLAG_REQUEST_NTLM_V1 = 0x00000200; // Request NTLMv1 security. MUST be set in NEGOTIATE and CHALLENGE both - static final int FLAG_REQUEST_ALWAYS_SIGN = 0x00008000; // Requests a signature block on all messages. Overridden by REQUEST_SIGN and REQUEST_SEAL. - static final int FLAG_REQUEST_NTLM2_SESSION = 0x00080000; // From server in challenge, requesting NTLM2 session security - static final int FLAG_REQUEST_VERSION = 0x02000000; // Request protocol version - static final int FLAG_TARGETINFO_PRESENT = 0x00800000; // From server in challenge message, indicating targetinfo is present - static final int FLAG_REQUEST_128BIT_KEY_EXCH = 0x20000000; // Request explicit 128-bit key exchange - static final int FLAG_REQUEST_EXPLICIT_KEY_EXCH = 0x40000000; // Request explicit key exchange - static final int FLAG_REQUEST_56BIT_ENCRYPTION = 0x80000000; // Must be used in conjunction with SEAL - - // Code from io.cloudsoft.winrm4j.client.ntlm.NtlmKeys.NegotiateFlags - // release 0.12.3 @link https://github.com/cloudsoft/winrm4j - // expanded set of what is in NTLMEngineImpl - // 0b 10100010_10001010_10000010_00000101 - // 0b 10100010_00001000_10000010_00110001 - public static final long NTLMSSP_NEGOTIATE_EXTENDED_SESSIONSECURITY = 0x00080000L; - public static final long NTLMSSP_NEGOTIATE_KEY_EXCH = 0x40000000L; - static final long NTLMSSP_NEGOTIATE_SEAL = 0x00000020L; - static final long NTLMSSP_NEGOTIATE_SIGN = 0x00000010L; - static final long NTLMSSP_NEGOTIATE_56 = 0x80000000L; - static final long NTLMSSP_NEGOTIATE_128 = 0x20000000L; - static final long NTLMSSP_NEGOTIATE_LM_KEY = 0x00000080L; - - /** - * Find the character set based on the flags. - * @param flags is the flags. - * @return the character set. - */ - static Charset getCharset(final int flags) throws NTLMEngineException { - if ((flags & FLAG_REQUEST_UNICODE_ENCODING) == 0) { - return DEFAULT_CHARSET; - } - if (UNICODE_LITTLE_UNMARKED == null) { - throw new NTLMEngineException("Unicode not supported"); - } - return UNICODE_LITTLE_UNMARKED; - } -} +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. + * ╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱ + */ + +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; + +/** + * Code from io.cloudsoft.winrm4j.client.ntlm.forks.httpclient.NTLMEngineImpl + * release 0.12.3 @link https://github.com/cloudsoft/winrm4j + * io.cloudsoft.winrm4j.client.ntlm.forks.httpclient is a fork of apache-httpclient 4.5.13 + */ +public class NTLMEngineUtils { + + private NTLMEngineUtils() {} + + /** Strip everything from the first dot onward (FQDN host/domain -> short name). */ + static String stripDotSuffix(final String value) { + if (value == null) { + return null; + } + final int index = value.indexOf('.'); + return index != -1 ? value.substring(0, index) : value; + } + + /** Convert a host name to its unqualified (short) form. */ + static String convertHost(final String host) { + return stripDotSuffix(host); + } + + /** Convert a domain name to its unqualified (short) form. */ + static String convertDomain(final String domain) { + return stripDotSuffix(domain); + } + + /** Unicode encoding */ + public static final Charset UNICODE_LITTLE_UNMARKED = StandardCharsets.UTF_16LE; + /** Character encoding */ + public static final Charset DEFAULT_CHARSET = StandardCharsets.US_ASCII; + + // Flags we use; descriptions according to: + // http://davenport.sourceforge.net/ntlm.html + // and + // http://msdn.microsoft.com/en-us/library/cc236650%28v=prot.20%29.aspx + // [MS-NLMP] section 2.2.2.5 + static final int FLAG_REQUEST_UNICODE_ENCODING = 0x00000001; // Unicode string encoding requested + static final int FLAG_REQUEST_SIGN = 0x00000010; // Requests all messages have a signature attached, in NEGOTIATE message. + static final int FLAG_REQUEST_LAN_MANAGER_KEY = 0x00000080; // Request Lan Manager key instead of user session key + static final int FLAG_REQUEST_NTLM_V1 = 0x00000200; // Request NTLMv1 security. MUST be set in NEGOTIATE and CHALLENGE both + static final int FLAG_REQUEST_ALWAYS_SIGN = 0x00008000; // Requests a signature block on all messages. Overridden by REQUEST_SIGN and REQUEST_SEAL. + static final int FLAG_REQUEST_NTLM2_SESSION = 0x00080000; // From server in challenge, requesting NTLM2 session security + static final int FLAG_REQUEST_VERSION = 0x02000000; // Request protocol version + static final int FLAG_TARGETINFO_PRESENT = 0x00800000; // From server in challenge message, indicating targetinfo is present + static final int FLAG_REQUEST_128BIT_KEY_EXCH = 0x20000000; // Request explicit 128-bit key exchange + static final int FLAG_REQUEST_EXPLICIT_KEY_EXCH = 0x40000000; // Request explicit key exchange + static final int FLAG_REQUEST_56BIT_ENCRYPTION = 0x80000000; // Must be used in conjunction with SEAL + + // Code from io.cloudsoft.winrm4j.client.ntlm.NtlmKeys.NegotiateFlags + // release 0.12.3 @link https://github.com/cloudsoft/winrm4j + // expanded set of what is in NTLMEngineImpl + // 0b 10100010_10001010_10000010_00000101 + // 0b 10100010_00001000_10000010_00110001 + public static final long NTLMSSP_NEGOTIATE_EXTENDED_SESSIONSECURITY = 0x00080000L; + public static final long NTLMSSP_NEGOTIATE_KEY_EXCH = 0x40000000L; + static final long NTLMSSP_NEGOTIATE_SEAL = 0x00000020L; + static final long NTLMSSP_NEGOTIATE_SIGN = 0x00000010L; + static final long NTLMSSP_NEGOTIATE_56 = 0x80000000L; + static final long NTLMSSP_NEGOTIATE_128 = 0x20000000L; + static final long NTLMSSP_NEGOTIATE_LM_KEY = 0x00000080L; + + /** + * Find the character set based on the flags. + * @param flags is the flags. + * @return the character set. + */ + static Charset getCharset(final int flags) throws NtlmException { + if ((flags & FLAG_REQUEST_UNICODE_ENCODING) == 0) { + return DEFAULT_CHARSET; + } + if (UNICODE_LITTLE_UNMARKED == null) { + throw new NtlmException("Unicode not supported"); + } + return UNICODE_LITTLE_UNMARKED; + } +} diff --git a/src/main/java/org/metricshub/winrm/service/client/auth/ntlm/NTLMMessage.java b/src/main/java/org/metricshub/winrm/light/NTLMMessage.java similarity index 85% rename from src/main/java/org/metricshub/winrm/service/client/auth/ntlm/NTLMMessage.java rename to src/main/java/org/metricshub/winrm/light/NTLMMessage.java index 9a87f7a..10bdd54 100644 --- a/src/main/java/org/metricshub/winrm/service/client/auth/ntlm/NTLMMessage.java +++ b/src/main/java/org/metricshub/winrm/light/NTLMMessage.java @@ -1,181 +1,176 @@ -package org.metricshub.winrm.service.client.auth.ntlm; - -/*- - * ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲ - * WinRM Java Client - * ჻჻჻჻჻჻ - * Copyright 2023 - 2024 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. - * ╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱ - */ - -import org.apache.commons.codec.binary.Base64; -import org.apache.http.impl.auth.NTLMEngineException; - -/** - * NTLM message generation, base class - * - * Code from io.cloudsoft.winrm4j.client.ntlm.forks.httpclient.NTLMEngineImpl - * release 0.12.3 @link https://github.com/cloudsoft/winrm4j - * io.cloudsoft.winrm4j.client.ntlm.forks.httpclient is a fork of apache-httpclient 4.5.13 - */ -class NTLMMessage { - - /** The signature string as bytes in the default encoding */ - private static final byte[] SIGNATURE; - - static { - final byte[] bytesWithoutNull = "NTLMSSP".getBytes(NTLMEngineUtils.DEFAULT_CHARSET); - final byte[] target = new byte[bytesWithoutNull.length + 1]; - System.arraycopy(bytesWithoutNull, 0, target, 0, bytesWithoutNull.length); - target[bytesWithoutNull.length] = (byte) 0x00; - - SIGNATURE = target; - } - - /** The current response */ - protected byte[] messageContents = null; - - /** The current output position */ - protected int currentOutputPosition = 0; - - /** Constructor to use when message contents are not yet known */ - NTLMMessage() {} - - /** Constructor to use when message bytes are known */ - NTLMMessage(final byte[] message, final int expectedType) throws NTLMEngineException { - messageContents = message; - // Look for NTLM message - if (messageContents.length < SIGNATURE.length) { - throw new NTLMEngineException("NTLM message decoding error - packet too short"); - } - int i = 0; - while (i < SIGNATURE.length) { - if (messageContents[i] != SIGNATURE[i]) { - throw new NTLMEngineException("NTLM message expected - instead got unrecognized bytes"); - } - i++; - } - - // Check to be sure there's a type 2 message indicator next - final int type = readULong(SIGNATURE.length); - if (type != expectedType) { - throw new NTLMEngineException( - String.format("NTLM type %d message expected - instead got type %d", expectedType, type) - ); - } - - currentOutputPosition = messageContents.length; - } - - /** Read a ulong from a position within the message buffer */ - int readULong(final int position) { - return readULong(messageContents, position); - } - - static int readULong(final byte[] src, final int index) { - if (src.length < index + 4) { - return 0; - } - return ( - (src[index] & 0xff) | - ((src[index + 1] & 0xff) << 8) | - ((src[index + 2] & 0xff) << 16) | - ((src[index + 3] & 0xff) << 24) - ); - } - - /** - * Prepares the object to create a response of the given length. - * - * @param maxlength - * the maximum length of the response to prepare, - * including the type and the signature (which this method - * adds). - */ - void prepareResponse(final int maxlength, final int messageType) { - messageContents = new byte[maxlength]; - currentOutputPosition = 0; - addBytes(SIGNATURE); - addULong(messageType); - } - - /** - * Adds the given byte to the response. - * - * @param b - * the byte to add. - */ - private void addByte(final byte b) { - messageContents[currentOutputPosition] = b; - currentOutputPosition++; - } - - /** - * Adds the given bytes to the response. - * - * @param bytes - * the bytes to add. - */ - void addBytes(final byte[] bytes) { - if (bytes == null) { - return; - } - for (final byte b : bytes) { - messageContents[currentOutputPosition] = b; - currentOutputPosition++; - } - } - - /** Adds a USHORT to the response */ - void addUShort(final int value) { - addByte((byte) (value & 0xff)); - addByte((byte) ((value >> 8) & 0xff)); - } - - /** Adds a ULong to the response */ - void addULong(final int value) { - addByte((byte) (value & 0xff)); - addByte((byte) ((value >> 8) & 0xff)); - addByte((byte) ((value >> 16) & 0xff)); - addByte((byte) ((value >> 24) & 0xff)); - } - - /** - * Returns the response that has been generated after shrinking the - * array if required and base64 encodes the response. - * - * @return The response as above. - */ - String getResponse() { - return new String(Base64.encodeBase64(getBytes()), NTLMEngineUtils.DEFAULT_CHARSET); - } - - private byte[] getBytes() { - if (messageContents == null) { - buildMessage(); - } - - if (messageContents.length > currentOutputPosition) { - final byte[] tmp = new byte[currentOutputPosition]; - System.arraycopy(messageContents, 0, tmp, 0, currentOutputPosition); - messageContents = tmp; - } - return messageContents; - } - - protected void buildMessage() { - throw new RuntimeException("Message builder not implemented for " + getClass().getName()); - } -} +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. + * ╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱ + */ + +/** + * NTLM message generation, base class + * + * Code from io.cloudsoft.winrm4j.client.ntlm.forks.httpclient.NTLMEngineImpl + * release 0.12.3 @link https://github.com/cloudsoft/winrm4j + * io.cloudsoft.winrm4j.client.ntlm.forks.httpclient is a fork of apache-httpclient 4.5.13 + */ +class NTLMMessage { + + /** The signature string as bytes in the default encoding */ + private static final byte[] SIGNATURE; + + static { + final byte[] bytesWithoutNull = "NTLMSSP".getBytes(NTLMEngineUtils.DEFAULT_CHARSET); + final byte[] target = new byte[bytesWithoutNull.length + 1]; + System.arraycopy(bytesWithoutNull, 0, target, 0, bytesWithoutNull.length); + target[bytesWithoutNull.length] = (byte) 0x00; + + SIGNATURE = target; + } + + /** The current response */ + protected byte[] messageContents = null; + + /** The current output position */ + protected int currentOutputPosition = 0; + + /** Constructor to use when message contents are not yet known */ + NTLMMessage() {} + + /** Constructor to use when message bytes are known */ + NTLMMessage(final byte[] message, final int expectedType) throws NtlmException { + messageContents = message; + // Look for NTLM message + if (messageContents.length < SIGNATURE.length) { + throw new NtlmException("NTLM message decoding error - packet too short"); + } + int i = 0; + while (i < SIGNATURE.length) { + if (messageContents[i] != SIGNATURE[i]) { + throw new NtlmException("NTLM message expected - instead got unrecognized bytes"); + } + i++; + } + + // Check to be sure there's a type 2 message indicator next + final int type = readULong(SIGNATURE.length); + if (type != expectedType) { + throw new NtlmException(String.format("NTLM type %d message expected - instead got type %d", expectedType, type)); + } + + currentOutputPosition = messageContents.length; + } + + /** Read a ulong from a position within the message buffer */ + int readULong(final int position) { + return readULong(messageContents, position); + } + + static int readULong(final byte[] src, final int index) { + if (src.length < index + 4) { + return 0; + } + return ( + (src[index] & 0xff) | + ((src[index + 1] & 0xff) << 8) | + ((src[index + 2] & 0xff) << 16) | + ((src[index + 3] & 0xff) << 24) + ); + } + + /** + * Prepares the object to create a response of the given length. + * + * @param maxlength + * the maximum length of the response to prepare, + * including the type and the signature (which this method + * adds). + */ + void prepareResponse(final int maxlength, final int messageType) { + messageContents = new byte[maxlength]; + currentOutputPosition = 0; + addBytes(SIGNATURE); + addULong(messageType); + } + + /** + * Adds the given byte to the response. + * + * @param b + * the byte to add. + */ + private void addByte(final byte b) { + messageContents[currentOutputPosition] = b; + currentOutputPosition++; + } + + /** + * Adds the given bytes to the response. + * + * @param bytes + * the bytes to add. + */ + void addBytes(final byte[] bytes) { + if (bytes == null) { + return; + } + for (final byte b : bytes) { + messageContents[currentOutputPosition] = b; + currentOutputPosition++; + } + } + + /** Adds a USHORT to the response */ + void addUShort(final int value) { + addByte((byte) (value & 0xff)); + addByte((byte) ((value >> 8) & 0xff)); + } + + /** Adds a ULong to the response */ + void addULong(final int value) { + addByte((byte) (value & 0xff)); + addByte((byte) ((value >> 8) & 0xff)); + addByte((byte) ((value >> 16) & 0xff)); + addByte((byte) ((value >> 24) & 0xff)); + } + + /** + * Returns the response that has been generated after shrinking the + * array if required and base64 encodes the response. + * + * @return The response as above. + */ + String getResponse() { + return new String(java.util.Base64.getEncoder().encode(getBytes()), NTLMEngineUtils.DEFAULT_CHARSET); + } + + private byte[] getBytes() { + if (messageContents == null) { + buildMessage(); + } + + if (messageContents.length > currentOutputPosition) { + final byte[] tmp = new byte[currentOutputPosition]; + System.arraycopy(messageContents, 0, tmp, 0, currentOutputPosition); + messageContents = tmp; + } + return messageContents; + } + + protected void buildMessage() { + throw new RuntimeException("Message builder not implemented for " + getClass().getName()); + } +} diff --git a/src/main/java/org/metricshub/winrm/light/NtlmAuthScheme.java b/src/main/java/org/metricshub/winrm/light/NtlmAuthScheme.java new file mode 100644 index 0000000..a81ca80 --- /dev/null +++ b/src/main/java/org/metricshub/winrm/light/NtlmAuthScheme.java @@ -0,0 +1,146 @@ +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. + * ╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱ + */ + +import java.nio.charset.StandardCharsets; +import java.util.Locale; + +/** + * NTLM (masqueraded as Negotiate) authentication scheme. Over plain HTTP it seals the SOAP with the + * NTLM session keys ({@code multipart/encrypted}); over HTTPS it authenticates only and sends + * plaintext SOAP inside TLS. + */ +final class NtlmAuthScheme implements AuthScheme { + + // Type 1 flags over plain HTTP: engine defaults + SIGN | SEAL | KEY_EXCH (matches + // NtlmMasqAsSpnegoScheme). Message sealing is what protects the SOAP over an unencrypted transport. + private static final int TYPE1_FLAGS_ENCRYPTED = (int) (Type1Message.getDefaultFlags() | + NTLMEngineUtils.NTLMSSP_NEGOTIATE_SIGN | + NTLMEngineUtils.NTLMSSP_NEGOTIATE_SEAL | + NTLMEngineUtils.NTLMSSP_NEGOTIATE_KEY_EXCH); + + // Type 1 flags over HTTPS: engine defaults only. TLS already provides confidentiality/integrity, so + // we authenticate WITHOUT negotiating sealing and exchange plaintext SOAP — claiming SEAL but then + // sending plaintext would make the server reject the message. + private static final int TYPE1_FLAGS_PLAIN = (int) Type1Message.getDefaultFlags(); + + private static final String SOAP_CONTENT_TYPE = "application/soap+xml;charset=UTF-8"; + private static final byte[] PRE_AUTH_BOGUS = "AWAITING_ENCRYPTION_KEYS".getBytes(StandardCharsets.US_ASCII); + + private final boolean https; + private final WinRMSession session; + + NtlmAuthScheme(final String domain, final String username, final String password, final boolean https) { + this.https = https; + // Uppercase the domain: NTOWFv2 (and thus the NTLM session key) is computed over it, the + // Type 3 DomainName field goes on the wire uppercased, and the server derives its session + // key from the uppercased value. A lowercase domain here passes authentication but fails + // message integrity (server-side seal mismatch → HTTP 400). + // Workstation is left empty in the Type 3 message, matching the reference client. + final String upperDomain = domain == null ? null : domain.toUpperCase(Locale.ROOT); + this.session = new WinRMSession(upperDomain, null, username, password); + } + + @Override + public String authenticate(final HttpTransport transport) throws Exception { + // Request 0: unauthenticated probe (bogus body), mirroring the reference client. + transport.post("/wsman", PRE_AUTH_BOGUS, SOAP_CONTENT_TYPE, null); + + // Request A: Type 1 under the Negotiate header. No keys yet, so send the bogus placeholder. + final String type1 = new Type1Message(null, null, https ? TYPE1_FLAGS_PLAIN : TYPE1_FLAGS_ENCRYPTED).getResponse(); + final HttpTransport.Response challenge = transport.post( + "/wsman", + PRE_AUTH_BOGUS, + SOAP_CONTENT_TYPE, + "Negotiate " + type1 + ); + if (challenge.status != 401) { + throw new IllegalStateException("Expected HTTP 401 with an NTLM challenge, got HTTP " + challenge.status); + } + final String type2 = challenge.negotiateToken(); + if (type2 == null) { + throw new IllegalStateException( + "No Negotiate challenge token in response: " + challenge.allHeaders("www-authenticate") + ); + } + + final Type2Message challengeMessage = new Type2Message(type2); + final Type3Message type3Message = new Type3Message( + session.getDomain(), + session.getWorkstation(), + session.getUsername(), + session.getPassword(), + challengeMessage.getChallenge(), + challengeMessage.getFlags(), + challengeMessage.getTarget(), + challengeMessage.getTargetInfo() + ); + final String type3 = type3Message.getResponse(); + if (https) { + // No sealing over TLS: authenticate the connection but derive no RC4 keys. + session.markAuthenticated(); + } else { + session.applyKeys(type3Message); + } + return "Negotiate " + type3; + } + + @Override + public boolean isAuthenticated() { + return session.isAuthenticated(); + } + + @Override + public void reset() { + session.reset(); + } + + @Override + public byte[] wrap(final byte[] soapUtf8) { + return https ? soapUtf8 : NtlmCrypto.encryptAndSign(session, soapUtf8); + } + + @Override + public String wrapContentType() { + return https ? SOAP_CONTENT_TYPE : NtlmCrypto.ENCRYPTED_CONTENT_TYPE; + } + + @Override + public byte[] unwrap(final HttpTransport.Response response) { + if (https) { + // Over TLS the response body is plaintext application/soap+xml; TLS already guarantees + // confidentiality and integrity, so there is no multipart/encrypted envelope to unseal. + return response.body; + } + final String contentType = response.firstHeader("content-type"); + // Once the NTLM session is authenticated, the seal is the ONLY thing protecting response + // integrity over plaintext HTTP. A non-encrypted body (from a proxy, a misconfigured server, + // or an on-path attacker returning a forged HTTP 200/500) has not passed the HMAC check, so it + // must never be parsed as a trusted WSMan response. This is only reached after the handshake, + // so an encrypted content type is always required. + if (contentType == null || !contentType.startsWith("multipart/encrypted")) { + throw new IllegalStateException( + "Refusing to parse an unencrypted WSMan response after authentication (Content-Type: " + contentType + ")" + ); + } + return NtlmCrypto.decrypt(session, response.body); + } +} diff --git a/src/main/java/org/metricshub/winrm/light/NtlmCrypto.java b/src/main/java/org/metricshub/winrm/light/NtlmCrypto.java new file mode 100644 index 0000000..f32c5ee --- /dev/null +++ b/src/main/java/org/metricshub/winrm/light/NtlmCrypto.java @@ -0,0 +1,200 @@ +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. + * ╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱ + */ + +import java.io.ByteArrayOutputStream; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.zip.CRC32; + +/** + * NTLM message sealing (encrypt+sign) and unsealing (decrypt+verify) for WinRM's + * {@code multipart/encrypted} framing. Ported verbatim in wire behavior from + * NtlmEncryptionUtils + Decryptor, but operating on byte[] and WinRMSession instead of + * CXF Message + apache NTCredentials. + */ +final class NtlmCrypto { + + static final String ENCRYPTED_CONTENT_TYPE = + "multipart/encrypted;protocol=\"application/HTTP-SPNEGO-session-encrypted\";boundary=\"Encrypted Boundary\""; + + private static final String BOUNDARY_CR = "--Encrypted Boundary\r\n"; + private static final String BOUNDARY_END = "--Encrypted Boundary--\r\n"; + + private NtlmCrypto() {} + + static byte[] encryptAndSign(final WinRMSession session, final byte[] messageBody) { + try (final ByteArrayOutputStream out = new ByteArrayOutputStream()) { + out.write(BOUNDARY_CR.getBytes(StandardCharsets.US_ASCII)); + out.write("\tContent-Type: application/HTTP-SPNEGO-session-encrypted\r\n".getBytes(StandardCharsets.US_ASCII)); + out.write( + String + .format("\tOriginalContent: type=application/soap+xml;charset=UTF-8;Length=%d\r\n", messageBody.length) + .getBytes(StandardCharsets.US_ASCII) + ); + out.write(BOUNDARY_CR.getBytes(StandardCharsets.US_ASCII)); + out.write("\tContent-Type: application/octet-stream\r\n".getBytes(StandardCharsets.US_ASCII)); + + final long seqNum = session.getSequenceNumberOutgoing().incrementAndGet(); + // Seal the body FIRST (advances the stateful cipher), even though the signature is written before it. + final byte[] sealed = session.seal(messageBody); + final ByteArrayOutputStream signature = new ByteArrayOutputStream(); + calculateSignature(session, messageBody, seqNum, signature, true); + + out.write(ByteArrayUtils.getLittleEndianUnsignedInt(signature.size())); + out.write(signature.toByteArray()); + out.write(sealed); + + out.write(BOUNDARY_END.getBytes(StandardCharsets.US_ASCII)); + return out.toByteArray(); + } catch (final Exception e) { + throw new IllegalStateException("Cannot encrypt WinRM message", e); + } + } + + static byte[] decrypt(final WinRMSession session, final byte[] rawBytes) { + final byte[] payload = unwrap(rawBytes); + final int signatureLength = (int) ByteArrayUtils.readLittleEndianUnsignedInt(payload, 0); + final byte[] signatureBytes = Arrays.copyOfRange(payload, 4, 4 + signatureLength); + final byte[] sealedBytes = Arrays.copyOfRange(payload, 4 + signatureLength, payload.length); + + final byte[] unsealed = session.unseal(sealedBytes); + verify(session, unsealed, signatureBytes); + return unsealed; + } + + private static void verify(final WinRMSession session, final byte[] unsealed, final byte[] signatureBytes) { + final long seqNum = ByteArrayUtils.readLittleEndianUnsignedInt(signatureBytes, 12); + final int checkSumOffset = session.hasNegotiateFlag(NTLMEngineUtils.NTLMSSP_NEGOTIATE_EXTENDED_SESSIONSECURITY) + ? 4 + : 8; + final byte[] checksum = Arrays.copyOfRange(signatureBytes, checkSumOffset, 12); + final ByteArrayOutputStream expected = new ByteArrayOutputStream(); + calculateSignature(session, unsealed, seqNum, expected, false); + final byte[] expectedChecksum = Arrays.copyOfRange(expected.toByteArray(), checkSumOffset, 12); + final long expectedSeqNum = ByteArrayUtils.readLittleEndianUnsignedInt(expected.toByteArray(), 12); + if (!Arrays.equals(checksum, expectedChecksum)) { + throw new IllegalStateException( + "Checksum mismatch\n" + + ByteArrayUtils.formatHexDump(checksum) + + "--\n" + + ByteArrayUtils.formatHexDump(expectedChecksum) + ); + } + if (expectedSeqNum != seqNum) { + throw new IllegalStateException(String.format("Sequence number mismatch: %d != %d", seqNum, expectedSeqNum)); + } + session.getSequenceNumberIncoming().incrementAndGet(); + } + + /** + * @param outgoing true to sign an outgoing message (client signing key + client sealing stream), + * false to verify an incoming one (server signing key + server sealing stream). + */ + private static void calculateSignature( + final WinRMSession session, + final byte[] messageBody, + final long seqNum, + final ByteArrayOutputStream signature, + final boolean outgoing + ) { + try { + final byte[] signingKey = outgoing ? session.getClientSigningKey() : session.getServerSigningKey(); + if (session.hasNegotiateFlag(NTLMEngineUtils.NTLMSSP_NEGOTIATE_EXTENDED_SESSIONSECURITY)) { + byte[] checksum = EncryptionUtils.hmacMd5( + signingKey, + ByteArrayUtils.concat(ByteArrayUtils.getLittleEndianUnsignedInt(seqNum), messageBody) + ); + checksum = Arrays.copyOfRange(checksum, 0, 8); + if (session.hasNegotiateFlag(NTLMEngineUtils.NTLMSSP_NEGOTIATE_KEY_EXCH)) { + checksum = outgoing ? session.seal(checksum) : session.unseal(checksum); + } + signature.write(new byte[] { 1, 0, 0, 0 }); + signature.write(checksum); + signature.write(ByteArrayUtils.getLittleEndianUnsignedInt(seqNum)); + } else { + final CRC32 crc = new CRC32(); + crc.update(messageBody); + final long messageCrc = crc.getValue(); + signature.write(new byte[] { 1, 0, 0, 0 }); + signature.write(sealPad(session, outgoing, 0)); + signature.write(sealPad(session, outgoing, messageCrc)); + signature.write(sealPad(session, outgoing, seqNum)); + } + } catch (final Exception e) { + throw new IllegalStateException(e); + } + } + + private static byte[] sealPad(final WinRMSession session, final boolean outgoing, final long value) { + final byte[] v = ByteArrayUtils.getLittleEndianUnsignedInt(value); + return outgoing ? session.seal(v) : session.unseal(v); + } + + /** Strip the two MIME parts and return the raw {sig-len | signature | sealed-body} block. */ + private static byte[] unwrap(final byte[] rawBytes) { + final Cursor c = new Cursor(rawBytes); + c.skipOver(BOUNDARY_CR); + c.skipUntil("\n" + BOUNDARY_CR); + c.skipUntil("\r\n"); + final int start = c.index; + final int end = rawBytes.length - BOUNDARY_END.length(); + return Arrays.copyOfRange(rawBytes, start, end); + } + + /** Minimal forward scanner over the response bytes (ported from Decryptor's skip logic). */ + private static final class Cursor { + + private final byte[] bytes; + private int index; + + Cursor(final byte[] bytes) { + this.bytes = bytes; + } + + void skipOver(final String s) { + final byte[] expected = s.getBytes(StandardCharsets.US_ASCII); + for (int i = 0; i < expected.length; i++) { + if (index >= bytes.length || expected[i] != bytes[index++]) { + throw new IllegalStateException("Unexpected encrypted-response framing at byte " + index); + } + } + } + + void skipUntil(final String s) { + final byte[] expected = s.getBytes(StandardCharsets.US_ASCII); + int next = index; + outer:while (true) { + for (int i = 0; i < expected.length; i++) { + if (next + i >= bytes.length) { + throw new IllegalStateException("Encrypted-response framing terminated early looking for delimiter"); + } + if (expected[i] != bytes[next + i]) { + next++; + continue outer; + } + } + index = next + expected.length; + return; + } + } + } +} diff --git a/src/main/java/org/metricshub/winrm/exceptions/KerberosCredentialsException.java b/src/main/java/org/metricshub/winrm/light/NtlmException.java similarity index 73% rename from src/main/java/org/metricshub/winrm/exceptions/KerberosCredentialsException.java rename to src/main/java/org/metricshub/winrm/light/NtlmException.java index 9aeb31c..5b15d10 100644 --- a/src/main/java/org/metricshub/winrm/exceptions/KerberosCredentialsException.java +++ b/src/main/java/org/metricshub/winrm/light/NtlmException.java @@ -1,34 +1,35 @@ -package org.metricshub.winrm.exceptions; - -/*- - * ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲ - * WinRM Java Client - * ჻჻჻჻჻჻ - * Copyright 2023 - 2024 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. - * ╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱ - */ - -public class KerberosCredentialsException extends RuntimeException { - - private static final long serialVersionUID = 1L; - - public KerberosCredentialsException(final Throwable cause) { - super(cause); - } - - public KerberosCredentialsException(final String message, final Throwable cause) { - super(message, cause); - } -} +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. + * ╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱ + */ + +/** Replaces org.apache.http.impl.auth.NTLMEngineException in the ported NTLM code. */ +public class NtlmException extends Exception { + + private static final long serialVersionUID = 1L; + + public NtlmException(final String message) { + super(message); + } + + public NtlmException(final String message, final Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/org/metricshub/winrm/service/client/auth/ntlm/Type1Message.java b/src/main/java/org/metricshub/winrm/light/Type1Message.java similarity index 90% rename from src/main/java/org/metricshub/winrm/service/client/auth/ntlm/Type1Message.java rename to src/main/java/org/metricshub/winrm/light/Type1Message.java index 1a3412a..cfa3115 100644 --- a/src/main/java/org/metricshub/winrm/service/client/auth/ntlm/Type1Message.java +++ b/src/main/java/org/metricshub/winrm/light/Type1Message.java @@ -1,119 +1,119 @@ -package org.metricshub.winrm.service.client.auth.ntlm; - -/*- - * ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲ - * WinRM Java Client - * ჻჻჻჻჻჻ - * Copyright 2023 - 2024 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. - * ╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱ - */ - -import java.util.Locale; - -/** - * Type 1 message assembly class - * - * Code from io.cloudsoft.winrm4j.client.ntlm.forks.httpclient.NTLMEngineImpl - * release 0.12.3 @link https://github.com/cloudsoft/winrm4j - * io.cloudsoft.winrm4j.client.ntlm.forks.httpclient is a fork of apache-httpclient 4.5.13 - */ -class Type1Message extends NTLMMessage { - - private final byte[] hostBytes; - private final byte[] domainBytes; - private final int flags; - - Type1Message(final String domain, final String host, final Integer flags) { - super(); - this.flags = flags == null ? getDefaultFlags() : flags; - - // Strip off domain name from the host! - final String unqualifiedHost = NTLMEngineImpl.convertHost(host); - // Use only the base domain name! - final String unqualifiedDomain = NTLMEngineImpl.convertDomain(domain); - - hostBytes = unqualifiedHost != null ? unqualifiedHost.getBytes(NTLMEngineUtils.UNICODE_LITTLE_UNMARKED) : null; - domainBytes = - unqualifiedDomain != null - ? unqualifiedDomain.toUpperCase(Locale.ROOT).getBytes(NTLMEngineUtils.UNICODE_LITTLE_UNMARKED) - : null; - } - - static int getDefaultFlags() { - return ( // Required flags - NTLMEngineUtils.FLAG_REQUEST_NTLM_V1 | - NTLMEngineUtils.FLAG_REQUEST_NTLM2_SESSION | - NTLMEngineUtils.FLAG_REQUEST_VERSION | - NTLMEngineUtils.FLAG_REQUEST_ALWAYS_SIGN | - NTLMEngineUtils.FLAG_REQUEST_128BIT_KEY_EXCH | - NTLMEngineUtils.FLAG_REQUEST_56BIT_ENCRYPTION | - NTLMEngineUtils.FLAG_REQUEST_UNICODE_ENCODING - ); - } - - /** - * Getting the response involves building the message before returning it - */ - @Override - protected void buildMessage() { - int domainBytesLength = 0; - if (domainBytes != null) { - domainBytesLength = domainBytes.length; - } - int hostBytesLength = 0; - if (hostBytes != null) { - hostBytesLength = hostBytes.length; - } - - // Now, build the message. Calculate its length first, including signature or type. - final int finalLength = 32 + 8 + hostBytesLength + domainBytesLength; - - // Set up the response. This will initialize the signature, message, type, and flags. - prepareResponse(finalLength, 1); - - // Flags. These are the complete set of flags we support. - addULong(flags); - - // Domain length (two times). - addUShort(domainBytesLength); - addUShort(domainBytesLength); - - // Domain offset. - addULong(hostBytesLength + 32 + 8); - - // Host length (two times). - addUShort(hostBytesLength); - addUShort(hostBytesLength); - - // Host offset (always 32 + 8). - addULong(32 + 8); - - // Version - addUShort(0x0105); - // Build - addULong(2600); - // NTLM revision - addUShort(0x0f00); - - // Host (workstation) String. - if (hostBytes != null) { - addBytes(hostBytes); - } - // Domain String. - if (domainBytes != null) { - addBytes(domainBytes); - } - } -} +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. + * ╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱ + */ + +import java.util.Locale; + +/** + * Type 1 message assembly class + * + * Code from io.cloudsoft.winrm4j.client.ntlm.forks.httpclient.NTLMEngineImpl + * release 0.12.3 @link https://github.com/cloudsoft/winrm4j + * io.cloudsoft.winrm4j.client.ntlm.forks.httpclient is a fork of apache-httpclient 4.5.13 + */ +class Type1Message extends NTLMMessage { + + private final byte[] hostBytes; + private final byte[] domainBytes; + private final int flags; + + Type1Message(final String domain, final String host, final Integer flags) { + super(); + this.flags = flags == null ? getDefaultFlags() : flags; + + // Strip off domain name from the host! + final String unqualifiedHost = NTLMEngineUtils.convertHost(host); + // Use only the base domain name! + final String unqualifiedDomain = NTLMEngineUtils.convertDomain(domain); + + hostBytes = unqualifiedHost != null ? unqualifiedHost.getBytes(NTLMEngineUtils.UNICODE_LITTLE_UNMARKED) : null; + domainBytes = + unqualifiedDomain != null + ? unqualifiedDomain.toUpperCase(Locale.ROOT).getBytes(NTLMEngineUtils.UNICODE_LITTLE_UNMARKED) + : null; + } + + static int getDefaultFlags() { + return ( // Required flags + NTLMEngineUtils.FLAG_REQUEST_NTLM_V1 | + NTLMEngineUtils.FLAG_REQUEST_NTLM2_SESSION | + NTLMEngineUtils.FLAG_REQUEST_VERSION | + NTLMEngineUtils.FLAG_REQUEST_ALWAYS_SIGN | + NTLMEngineUtils.FLAG_REQUEST_128BIT_KEY_EXCH | + NTLMEngineUtils.FLAG_REQUEST_56BIT_ENCRYPTION | + NTLMEngineUtils.FLAG_REQUEST_UNICODE_ENCODING + ); + } + + /** + * Getting the response involves building the message before returning it + */ + @Override + protected void buildMessage() { + int domainBytesLength = 0; + if (domainBytes != null) { + domainBytesLength = domainBytes.length; + } + int hostBytesLength = 0; + if (hostBytes != null) { + hostBytesLength = hostBytes.length; + } + + // Now, build the message. Calculate its length first, including signature or type. + final int finalLength = 32 + 8 + hostBytesLength + domainBytesLength; + + // Set up the response. This will initialize the signature, message, type, and flags. + prepareResponse(finalLength, 1); + + // Flags. These are the complete set of flags we support. + addULong(flags); + + // Domain length (two times). + addUShort(domainBytesLength); + addUShort(domainBytesLength); + + // Domain offset. + addULong(hostBytesLength + 32 + 8); + + // Host length (two times). + addUShort(hostBytesLength); + addUShort(hostBytesLength); + + // Host offset (always 32 + 8). + addULong(32 + 8); + + // Version + addUShort(0x0105); + // Build + addULong(2600); + // NTLM revision + addUShort(0x0f00); + + // Host (workstation) String. + if (hostBytes != null) { + addBytes(hostBytes); + } + // Domain String. + if (domainBytes != null) { + addBytes(domainBytes); + } + } +} diff --git a/src/main/java/org/metricshub/winrm/service/client/auth/ntlm/Type2Message.java b/src/main/java/org/metricshub/winrm/light/Type2Message.java similarity index 86% rename from src/main/java/org/metricshub/winrm/service/client/auth/ntlm/Type2Message.java rename to src/main/java/org/metricshub/winrm/light/Type2Message.java index 8f92c3d..904f8d1 100644 --- a/src/main/java/org/metricshub/winrm/service/client/auth/ntlm/Type2Message.java +++ b/src/main/java/org/metricshub/winrm/light/Type2Message.java @@ -1,139 +1,136 @@ -package org.metricshub.winrm.service.client.auth.ntlm; - -/*- - * ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲ - * WinRM Java Client - * ჻჻჻჻჻჻ - * Copyright 2023 - 2024 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. - * ╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱ - */ - -import org.apache.commons.codec.binary.Base64; -import org.apache.http.impl.auth.NTLMEngineException; - -/** - * Type 2 message class - * - * Code from io.cloudsoft.winrm4j.client.ntlm.forks.httpclient.NTLMEngineImpl - * release 0.12.3 @link https://github.com/cloudsoft/winrm4j - * io.cloudsoft.winrm4j.client.ntlm.forks.httpclient is a fork of apache-httpclient 4.5.13 - */ -class Type2Message extends NTLMMessage { - - private final byte[] challenge; - private String target; - private byte[] targetInfo; - private final int flags; - - Type2Message(final String messageBody) throws NTLMEngineException { - this(Base64.decodeBase64(messageBody.getBytes(NTLMEngineUtils.DEFAULT_CHARSET))); - } - - private Type2Message(final byte[] message) throws NTLMEngineException { - super(message, 2); - // Type 2 message is laid out as follows: - // First 8 bytes: NTLMSSP[0] - // Next 4 bytes: Ulong, value 2 - // Next 8 bytes, starting at offset 12: target field (2 ushort lengths, 1 ulong offset) - // Next 4 bytes, starting at offset 20: Flags, e.g. 0x22890235 - // Next 8 bytes, starting at offset 24: Challenge - // Next 8 bytes, starting at offset 32: ??? (8 bytes of zeros) - // Next 8 bytes, starting at offset 40: targetinfo field (2 ushort lengths, 1 ulong offset) - // Next 2 bytes, major/minor version number (e.g. 0x05 0x02) - // Next 8 bytes, build number - // Next 2 bytes, protocol version number (e.g. 0x00 0x0f) - // Next, various text fields, and a ushort of value 0 at the end - - // Parse out the rest of the info we need from the message - // The nonce is the 8 bytes starting from the byte in position 24. - challenge = new byte[8]; - readBytes(challenge, 24); - - flags = readULong(20); - - // Do the target! - target = null; - // The TARGET_DESIRED flag is said to not have understood semantics - // in Type2 messages, so use the length of the packet to decide how to proceed instead - if (getMessageLength() >= 12 + 8) { - final byte[] bytes = readSecurityBuffer(12); - if (bytes.length != 0) { - target = new String(bytes, NTLMEngineUtils.getCharset(flags)); - } - } - - // Do the target info! - targetInfo = null; - // TARGET_DESIRED flag cannot be relied on, so use packet length - if (getMessageLength() >= 40 + 8) { - final byte[] bytes = readSecurityBuffer(40); - if (bytes.length != 0) { - targetInfo = bytes; - } - } - } - - /** Get the message length */ - private int getMessageLength() { - return currentOutputPosition; - } - - /** Read a bunch of bytes from a position in the message buffer */ - private void readBytes(final byte[] buffer, final int position) throws NTLMEngineException { - if (messageContents.length < position + buffer.length) { - throw new NTLMEngineException("NTLM: Message too short"); - } - System.arraycopy(messageContents, position, buffer, 0, buffer.length); - } - - /** Read a security buffer from a position within the message buffer */ - private byte[] readSecurityBuffer(final int position) { - final int length = readUShort(messageContents, position); - final int offset = readULong(messageContents, position + 4); - if (messageContents.length < offset + length) { - return new byte[length]; - } - final byte[] buffer = new byte[length]; - System.arraycopy(messageContents, offset, buffer, 0, length); - return buffer; - } - - private static int readUShort(final byte[] src, final int index) { - if (src.length < index + 2) { - return 0; - } - return (src[index] & 0xff) | ((src[index + 1] & 0xff) << 8); - } - - /** Retrieve the challenge */ - byte[] getChallenge() { - return challenge; - } - - /** Retrieve the target */ - String getTarget() { - return target; - } - - /** Retrieve the target info */ - byte[] getTargetInfo() { - return targetInfo; - } - - /** Retrieve the response flags */ - int getFlags() { - return flags; - } -} +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. + * ╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱ + */ + +/** + * Type 2 message class + * + * Code from io.cloudsoft.winrm4j.client.ntlm.forks.httpclient.NTLMEngineImpl + * release 0.12.3 @link https://github.com/cloudsoft/winrm4j + * io.cloudsoft.winrm4j.client.ntlm.forks.httpclient is a fork of apache-httpclient 4.5.13 + */ +class Type2Message extends NTLMMessage { + + private final byte[] challenge; + private String target; + private byte[] targetInfo; + private final int flags; + + Type2Message(final String messageBody) throws NtlmException { + this(java.util.Base64.getDecoder().decode(messageBody.getBytes(NTLMEngineUtils.DEFAULT_CHARSET))); + } + + private Type2Message(final byte[] message) throws NtlmException { + super(message, 2); + // Type 2 message is laid out as follows: + // First 8 bytes: NTLMSSP[0] + // Next 4 bytes: Ulong, value 2 + // Next 8 bytes, starting at offset 12: target field (2 ushort lengths, 1 ulong offset) + // Next 4 bytes, starting at offset 20: Flags, e.g. 0x22890235 + // Next 8 bytes, starting at offset 24: Challenge + // Next 8 bytes, starting at offset 32: ??? (8 bytes of zeros) + // Next 8 bytes, starting at offset 40: targetinfo field (2 ushort lengths, 1 ulong offset) + // Next 2 bytes, major/minor version number (e.g. 0x05 0x02) + // Next 8 bytes, build number + // Next 2 bytes, protocol version number (e.g. 0x00 0x0f) + // Next, various text fields, and a ushort of value 0 at the end + + // Parse out the rest of the info we need from the message + // The nonce is the 8 bytes starting from the byte in position 24. + challenge = new byte[8]; + readBytes(challenge, 24); + + flags = readULong(20); + + // Do the target! + target = null; + // The TARGET_DESIRED flag is said to not have understood semantics + // in Type2 messages, so use the length of the packet to decide how to proceed instead + if (getMessageLength() >= 12 + 8) { + final byte[] bytes = readSecurityBuffer(12); + if (bytes.length != 0) { + target = new String(bytes, NTLMEngineUtils.getCharset(flags)); + } + } + + // Do the target info! + targetInfo = null; + // TARGET_DESIRED flag cannot be relied on, so use packet length + if (getMessageLength() >= 40 + 8) { + final byte[] bytes = readSecurityBuffer(40); + if (bytes.length != 0) { + targetInfo = bytes; + } + } + } + + /** Get the message length */ + private int getMessageLength() { + return currentOutputPosition; + } + + /** Read a bunch of bytes from a position in the message buffer */ + private void readBytes(final byte[] buffer, final int position) throws NtlmException { + if (messageContents.length < position + buffer.length) { + throw new NtlmException("NTLM: Message too short"); + } + System.arraycopy(messageContents, position, buffer, 0, buffer.length); + } + + /** Read a security buffer from a position within the message buffer */ + private byte[] readSecurityBuffer(final int position) { + final int length = readUShort(messageContents, position); + final int offset = readULong(messageContents, position + 4); + if (messageContents.length < offset + length) { + return new byte[length]; + } + final byte[] buffer = new byte[length]; + System.arraycopy(messageContents, offset, buffer, 0, length); + return buffer; + } + + private static int readUShort(final byte[] src, final int index) { + if (src.length < index + 2) { + return 0; + } + return (src[index] & 0xff) | ((src[index + 1] & 0xff) << 8); + } + + /** Retrieve the challenge */ + byte[] getChallenge() { + return challenge; + } + + /** Retrieve the target */ + String getTarget() { + return target; + } + + /** Retrieve the target info */ + byte[] getTargetInfo() { + return targetInfo; + } + + /** Retrieve the response flags */ + int getFlags() { + return flags; + } +} diff --git a/src/main/java/org/metricshub/winrm/service/client/auth/ntlm/Type3Message.java b/src/main/java/org/metricshub/winrm/light/Type3Message.java similarity index 89% rename from src/main/java/org/metricshub/winrm/service/client/auth/ntlm/Type3Message.java rename to src/main/java/org/metricshub/winrm/light/Type3Message.java index 235302a..bcaca9c 100644 --- a/src/main/java/org/metricshub/winrm/service/client/auth/ntlm/Type3Message.java +++ b/src/main/java/org/metricshub/winrm/light/Type3Message.java @@ -1,301 +1,298 @@ -package org.metricshub.winrm.service.client.auth.ntlm; - -/*- - * ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲ - * WinRM Java Client - * ჻჻჻჻჻჻ - * Copyright 2023 - 2024 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. - * ╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱ - */ - -import java.nio.charset.Charset; -import java.util.Locale; -import java.util.Random; -import org.apache.http.impl.auth.NTLMEngineException; -import org.metricshub.winrm.service.client.encryption.CipherGen; -import org.metricshub.winrm.service.client.encryption.EncryptionUtils; - -/** - * Type 3 message assembly class - * - * Code from io.cloudsoft.winrm4j.client.ntlm.forks.httpclient.NTLMEngineImpl - * release 0.12.3 @link https://github.com/cloudsoft/winrm4j - * io.cloudsoft.winrm4j.client.ntlm.forks.httpclient is a fork of apache-httpclient 4.5.13 - */ -public class Type3Message extends NTLMMessage { - - /** Secure random generator */ - static final java.security.SecureRandom RND_GEN; - - static { - java.security.SecureRandom rnd = null; - try { - rnd = java.security.SecureRandom.getInstance("SHA1PRNG"); - } catch (final Exception ignore) {} - RND_GEN = rnd; - } - - // Response flags from the type2 message - private final int type2Flags; - - private final byte[] domainBytes; - private final byte[] hostBytes; - private final byte[] userBytes; - - private byte[] lmResp; - private byte[] ntResp; - private final byte[] sessionKey; - private final byte[] exportedSessionKey; - - /** More primitive constructor: don't include cert or previous messages. - */ - Type3Message( - final String domain, - final String host, - final String user, - final String password, - final byte[] nonce, - final int type2Flags, - final String target, - final byte[] targetInformation - ) throws NTLMEngineException { - final Random random = RND_GEN; - if (random == null) { - throw new NTLMEngineException("Random generator not available"); - } - - final long currentTime = System.currentTimeMillis(); - - // Save the flags - this.type2Flags = type2Flags; - - // Strip off domain name from the host! - final String unqualifiedHost = NTLMEngineImpl.convertHost(host); - // Use only the base domain name! - final String unqualifiedDomain = NTLMEngineImpl.convertDomain(domain); - - byte[] responseTargetInformation = targetInformation; - - // Create a cipher generator class. Use domain BEFORE it gets modified! - final CipherGen gen = new CipherGen( - random, - currentTime, - unqualifiedDomain, - user, - password, - nonce, - target, - responseTargetInformation - ); - - // Use the new code to calculate the responses, including v2 if that - // seems warranted. - byte[] userSessionKey; - try { - // This conditional may not work on Windows Server 2008 R2 and above, where it has not yet - // been tested - if ( - ((type2Flags & NTLMEngineUtils.FLAG_TARGETINFO_PRESENT) != 0) && targetInformation != null && target != null - ) { - // NTLMv2 - ntResp = gen.getNTLMv2Response(); - lmResp = gen.getLMv2Response(); - if ((type2Flags & NTLMEngineUtils.FLAG_REQUEST_LAN_MANAGER_KEY) != 0) { - userSessionKey = gen.getLanManagerSessionKey(); - } else { - userSessionKey = gen.getNTLMv2UserSessionKey(); - } - } else { - // NTLMv1 - if ((type2Flags & NTLMEngineUtils.FLAG_REQUEST_NTLM2_SESSION) != 0) { - // NTLM2 session stuff is requested - ntResp = gen.getNTLM2SessionResponse(); - lmResp = gen.getLM2SessionResponse(); - if ((type2Flags & NTLMEngineUtils.FLAG_REQUEST_LAN_MANAGER_KEY) != 0) { - userSessionKey = gen.getLanManagerSessionKey(); - } else { - userSessionKey = gen.getNTLM2SessionResponseUserSessionKey(); - } - } else { - ntResp = gen.getNTLMResponse(); - lmResp = gen.getLMResponse(); - if ((type2Flags & NTLMEngineUtils.FLAG_REQUEST_LAN_MANAGER_KEY) != 0) { - userSessionKey = gen.getLanManagerSessionKey(); - } else { - userSessionKey = gen.getNTLMUserSessionKey(); - } - } - } - } catch (final NTLMEngineException e) { - // This likely means we couldn't find the MD4 hash algorithm - - // fail back to just using LM - ntResp = new byte[0]; - lmResp = gen.getLMResponse(); - if ((type2Flags & NTLMEngineUtils.FLAG_REQUEST_LAN_MANAGER_KEY) != 0) { - userSessionKey = gen.getLanManagerSessionKey(); - } else { - userSessionKey = gen.getLMUserSessionKey(); - } - } - - if ((type2Flags & NTLMEngineUtils.FLAG_REQUEST_SIGN) != 0) { - if ((type2Flags & NTLMEngineUtils.FLAG_REQUEST_EXPLICIT_KEY_EXCH) != 0) { - exportedSessionKey = gen.getSecondaryKey(); - try { - sessionKey = EncryptionUtils.calculateRC4(exportedSessionKey, userSessionKey); - } catch (final Exception e) { - throw new NTLMEngineException(e.getMessage(), e); - } - } else { - sessionKey = userSessionKey; - exportedSessionKey = sessionKey; - } - } else { - sessionKey = null; - exportedSessionKey = null; - } - final Charset charset = NTLMEngineUtils.getCharset(type2Flags); - hostBytes = unqualifiedHost != null ? unqualifiedHost.getBytes(charset) : null; - domainBytes = unqualifiedDomain != null ? unqualifiedDomain.toUpperCase(Locale.ROOT).getBytes(charset) : null; - userBytes = user.getBytes(charset); - } - - int getType2Flags() { - return type2Flags; - } - - byte[] getExportedSessionKey() { - return exportedSessionKey; - } - - /** Assemble the response */ - @Override - protected void buildMessage() { - final int ntRespLen = ntResp.length; - final int lmRespLen = lmResp.length; - - final int domainLen = domainBytes != null ? domainBytes.length : 0; - final int hostLen = hostBytes != null ? hostBytes.length : 0; - final int userLen = userBytes.length; - final int sessionKeyLen; - if (sessionKey != null) { - sessionKeyLen = sessionKey.length; - } else { - sessionKeyLen = 0; - } - - // Calculate the layout within the packet - final int lmRespOffset = 72; // allocate space for the version - final int ntRespOffset = lmRespOffset + lmRespLen; - final int domainOffset = ntRespOffset + ntRespLen; - final int userOffset = domainOffset + domainLen; - final int hostOffset = userOffset + userLen; - final int sessionKeyOffset = hostOffset + hostLen; - final int finalLength = sessionKeyOffset + sessionKeyLen; - - // Start the response. Length includes signature and type - prepareResponse(finalLength, 3); - - // LM Resp Length (twice) - addUShort(lmRespLen); - addUShort(lmRespLen); - - // LM Resp Offset - addULong(lmRespOffset); - - // NT Resp Length (twice) - addUShort(ntRespLen); - addUShort(ntRespLen); - - // NT Resp Offset - addULong(ntRespOffset); - - // Domain length (twice) - addUShort(domainLen); - addUShort(domainLen); - - // Domain offset. - addULong(domainOffset); - - // User Length (twice) - addUShort(userLen); - addUShort(userLen); - - // User offset - addULong(userOffset); - - // Host length (twice) - addUShort(hostLen); - addUShort(hostLen); - - // Host offset - addULong(hostOffset); - - // Session key length (twice) - addUShort(sessionKeyLen); - addUShort(sessionKeyLen); - - // Session key offset - addULong(sessionKeyOffset); - - // Flags. - addULong( - /* - //FLAG_WORKSTATION_PRESENT | - //FLAG_DOMAIN_PRESENT | - - // Required flags - (type2Flags & FLAG_REQUEST_LAN_MANAGER_KEY) | - (type2Flags & FLAG_REQUEST_NTLMv1) | - (type2Flags & FLAG_REQUEST_NTLM2_SESSION) | - - // Protocol version request - FLAG_REQUEST_VERSION | - - // Recommended privacy settings - (type2Flags & FLAG_REQUEST_ALWAYS_SIGN) | - (type2Flags & FLAG_REQUEST_SEAL) | - (type2Flags & FLAG_REQUEST_SIGN) | - - // These must be set according to documentation, based on use of SEAL above - (type2Flags & FLAG_REQUEST_128BIT_KEY_EXCH) | - (type2Flags & FLAG_REQUEST_56BIT_ENCRYPTION) | - (type2Flags & FLAG_REQUEST_EXPLICIT_KEY_EXCH) | - - (type2Flags & FLAG_TARGETINFO_PRESENT) | - (type2Flags & FLAG_REQUEST_UNICODE_ENCODING) | - (type2Flags & FLAG_REQUEST_TARGET) - */ - type2Flags - ); - - // Version - addUShort(0x0105); - // Build - addULong(2600); - // NTLM revision - addUShort(0x0f00); - - // Add the actual data - addBytes(lmResp); - addBytes(ntResp); - addBytes(domainBytes); - addBytes(userBytes); - addBytes(hostBytes); - if (sessionKey != null) { - addBytes(sessionKey); - } - } -} +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. + * ╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱ + */ + +import java.nio.charset.Charset; +import java.util.Locale; +import java.util.Random; + +/** + * Type 3 message assembly class + * + * Code from io.cloudsoft.winrm4j.client.ntlm.forks.httpclient.NTLMEngineImpl + * release 0.12.3 @link https://github.com/cloudsoft/winrm4j + * io.cloudsoft.winrm4j.client.ntlm.forks.httpclient is a fork of apache-httpclient 4.5.13 + */ +public class Type3Message extends NTLMMessage { + + /** Secure random generator */ + static final java.security.SecureRandom RND_GEN; + + static { + java.security.SecureRandom rnd = null; + try { + rnd = java.security.SecureRandom.getInstance("SHA1PRNG"); + } catch (final Exception ignore) {} + RND_GEN = rnd; + } + + // Response flags from the type2 message + private final int type2Flags; + + private final byte[] domainBytes; + private final byte[] hostBytes; + private final byte[] userBytes; + + private byte[] lmResp; + private byte[] ntResp; + private final byte[] sessionKey; + private final byte[] exportedSessionKey; + + /** More primitive constructor: don't include cert or previous messages. + */ + Type3Message( + final String domain, + final String host, + final String user, + final String password, + final byte[] nonce, + final int type2Flags, + final String target, + final byte[] targetInformation + ) throws NtlmException { + final Random random = RND_GEN; + if (random == null) { + throw new NtlmException("Random generator not available"); + } + + final long currentTime = System.currentTimeMillis(); + + // Save the flags + this.type2Flags = type2Flags; + + // Strip off domain name from the host! + final String unqualifiedHost = NTLMEngineUtils.convertHost(host); + // Use only the base domain name! + final String unqualifiedDomain = NTLMEngineUtils.convertDomain(domain); + + byte[] responseTargetInformation = targetInformation; + + // Create a cipher generator class. Use domain BEFORE it gets modified! + final CipherGen gen = new CipherGen( + random, + currentTime, + unqualifiedDomain, + user, + password, + nonce, + target, + responseTargetInformation + ); + + // Use the new code to calculate the responses, including v2 if that + // seems warranted. + byte[] userSessionKey; + try { + // This conditional may not work on Windows Server 2008 R2 and above, where it has not yet + // been tested + if ( + ((type2Flags & NTLMEngineUtils.FLAG_TARGETINFO_PRESENT) != 0) && targetInformation != null && target != null + ) { + // NTLMv2 + ntResp = gen.getNTLMv2Response(); + lmResp = gen.getLMv2Response(); + if ((type2Flags & NTLMEngineUtils.FLAG_REQUEST_LAN_MANAGER_KEY) != 0) { + userSessionKey = gen.getLanManagerSessionKey(); + } else { + userSessionKey = gen.getNTLMv2UserSessionKey(); + } + } else { + // NTLMv1 + if ((type2Flags & NTLMEngineUtils.FLAG_REQUEST_NTLM2_SESSION) != 0) { + // NTLM2 session stuff is requested + ntResp = gen.getNTLM2SessionResponse(); + lmResp = gen.getLM2SessionResponse(); + if ((type2Flags & NTLMEngineUtils.FLAG_REQUEST_LAN_MANAGER_KEY) != 0) { + userSessionKey = gen.getLanManagerSessionKey(); + } else { + userSessionKey = gen.getNTLM2SessionResponseUserSessionKey(); + } + } else { + ntResp = gen.getNTLMResponse(); + lmResp = gen.getLMResponse(); + if ((type2Flags & NTLMEngineUtils.FLAG_REQUEST_LAN_MANAGER_KEY) != 0) { + userSessionKey = gen.getLanManagerSessionKey(); + } else { + userSessionKey = gen.getNTLMUserSessionKey(); + } + } + } + } catch (final NtlmException e) { + // This likely means we couldn't find the MD4 hash algorithm - + // fail back to just using LM + ntResp = new byte[0]; + lmResp = gen.getLMResponse(); + if ((type2Flags & NTLMEngineUtils.FLAG_REQUEST_LAN_MANAGER_KEY) != 0) { + userSessionKey = gen.getLanManagerSessionKey(); + } else { + userSessionKey = gen.getLMUserSessionKey(); + } + } + + if ((type2Flags & NTLMEngineUtils.FLAG_REQUEST_SIGN) != 0) { + if ((type2Flags & NTLMEngineUtils.FLAG_REQUEST_EXPLICIT_KEY_EXCH) != 0) { + exportedSessionKey = gen.getSecondaryKey(); + try { + sessionKey = EncryptionUtils.calculateRC4(exportedSessionKey, userSessionKey); + } catch (final Exception e) { + throw new NtlmException(e.getMessage(), e); + } + } else { + sessionKey = userSessionKey; + exportedSessionKey = sessionKey; + } + } else { + sessionKey = null; + exportedSessionKey = null; + } + final Charset charset = NTLMEngineUtils.getCharset(type2Flags); + hostBytes = unqualifiedHost != null ? unqualifiedHost.getBytes(charset) : null; + domainBytes = unqualifiedDomain != null ? unqualifiedDomain.toUpperCase(Locale.ROOT).getBytes(charset) : null; + userBytes = user.getBytes(charset); + } + + int getType2Flags() { + return type2Flags; + } + + byte[] getExportedSessionKey() { + return exportedSessionKey; + } + + /** Assemble the response */ + @Override + protected void buildMessage() { + final int ntRespLen = ntResp.length; + final int lmRespLen = lmResp.length; + + final int domainLen = domainBytes != null ? domainBytes.length : 0; + final int hostLen = hostBytes != null ? hostBytes.length : 0; + final int userLen = userBytes.length; + final int sessionKeyLen; + if (sessionKey != null) { + sessionKeyLen = sessionKey.length; + } else { + sessionKeyLen = 0; + } + + // Calculate the layout within the packet + final int lmRespOffset = 72; // allocate space for the version + final int ntRespOffset = lmRespOffset + lmRespLen; + final int domainOffset = ntRespOffset + ntRespLen; + final int userOffset = domainOffset + domainLen; + final int hostOffset = userOffset + userLen; + final int sessionKeyOffset = hostOffset + hostLen; + final int finalLength = sessionKeyOffset + sessionKeyLen; + + // Start the response. Length includes signature and type + prepareResponse(finalLength, 3); + + // LM Resp Length (twice) + addUShort(lmRespLen); + addUShort(lmRespLen); + + // LM Resp Offset + addULong(lmRespOffset); + + // NT Resp Length (twice) + addUShort(ntRespLen); + addUShort(ntRespLen); + + // NT Resp Offset + addULong(ntRespOffset); + + // Domain length (twice) + addUShort(domainLen); + addUShort(domainLen); + + // Domain offset. + addULong(domainOffset); + + // User Length (twice) + addUShort(userLen); + addUShort(userLen); + + // User offset + addULong(userOffset); + + // Host length (twice) + addUShort(hostLen); + addUShort(hostLen); + + // Host offset + addULong(hostOffset); + + // Session key length (twice) + addUShort(sessionKeyLen); + addUShort(sessionKeyLen); + + // Session key offset + addULong(sessionKeyOffset); + + // Flags. + addULong( + /* + //FLAG_WORKSTATION_PRESENT | + //FLAG_DOMAIN_PRESENT | + + // Required flags + (type2Flags & FLAG_REQUEST_LAN_MANAGER_KEY) | + (type2Flags & FLAG_REQUEST_NTLMv1) | + (type2Flags & FLAG_REQUEST_NTLM2_SESSION) | + + // Protocol version request + FLAG_REQUEST_VERSION | + + // Recommended privacy settings + (type2Flags & FLAG_REQUEST_ALWAYS_SIGN) | + (type2Flags & FLAG_REQUEST_SEAL) | + (type2Flags & FLAG_REQUEST_SIGN) | + + // These must be set according to documentation, based on use of SEAL above + (type2Flags & FLAG_REQUEST_128BIT_KEY_EXCH) | + (type2Flags & FLAG_REQUEST_56BIT_ENCRYPTION) | + (type2Flags & FLAG_REQUEST_EXPLICIT_KEY_EXCH) | + + (type2Flags & FLAG_TARGETINFO_PRESENT) | + (type2Flags & FLAG_REQUEST_UNICODE_ENCODING) | + (type2Flags & FLAG_REQUEST_TARGET) + */ + type2Flags + ); + + // Version + addUShort(0x0105); + // Build + addULong(2600); + // NTLM revision + addUShort(0x0f00); + + // Add the actual data + addBytes(lmResp); + addBytes(ntResp); + addBytes(domainBytes); + addBytes(userBytes); + addBytes(hostBytes); + if (sessionKey != null) { + addBytes(sessionKey); + } + } +} diff --git a/src/main/java/org/metricshub/winrm/light/WinRMSession.java b/src/main/java/org/metricshub/winrm/light/WinRMSession.java new file mode 100644 index 0000000..6f61260 --- /dev/null +++ b/src/main/java/org/metricshub/winrm/light/WinRMSession.java @@ -0,0 +1,183 @@ +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. + * ╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱ + */ + +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.concurrent.atomic.AtomicLong; +import javax.crypto.Cipher; + +/** + * Holds the negotiated NTLM session state: the exported session key, the derived + * signing/sealing keys, the two stateful RC4 stream ciphers (one per direction), and + * the message sequence counters. Replaces the CXF/apache-coupled NTCredentialsWithEncryption + * and folds in the key-derivation logic from NtlmKeys. + */ +final class WinRMSession { + + // Protocol-defined constants: they MUST encode to the same bytes on every JVM, so pin US-ASCII + // rather than relying on the platform default charset (which could differ, e.g. UTF-16). + private static final byte[] CLIENT_SIGNING = + "session key to client-to-server signing key magic constant\0".getBytes(StandardCharsets.US_ASCII); + private static final byte[] SERVER_SIGNING = + "session key to server-to-client signing key magic constant\0".getBytes(StandardCharsets.US_ASCII); + private static final byte[] CLIENT_SEALING = + "session key to client-to-server sealing key magic constant\0".getBytes(StandardCharsets.US_ASCII); + private static final byte[] SERVER_SEALING = + "session key to server-to-client sealing key magic constant\0".getBytes(StandardCharsets.US_ASCII); + + private final String domain; + private final String workstation; + private final String username; + private final String password; + + private long negotiateFlags; + private byte[] clientSigningKey; + private byte[] serverSigningKey; + private Cipher encryptor; + private Cipher decryptor; + private boolean authenticated; + + private final AtomicLong sequenceOutgoing = new AtomicLong(-1); + private final AtomicLong sequenceIncoming = new AtomicLong(-1); + + WinRMSession(final String domain, final String workstation, final String username, final String password) { + this.domain = domain; + this.workstation = workstation; + this.username = username; + this.password = password; + } + + String getDomain() { + return domain; + } + + String getWorkstation() { + return workstation; + } + + String getUsername() { + return username; + } + + String getPassword() { + return password; + } + + boolean isAuthenticated() { + return authenticated; + } + + boolean hasNegotiateFlag(final long flag) { + return (negotiateFlags & flag) == flag; + } + + byte[] getClientSigningKey() { + return clientSigningKey; + } + + byte[] getServerSigningKey() { + return serverSigningKey; + } + + AtomicLong getSequenceNumberOutgoing() { + return sequenceOutgoing; + } + + AtomicLong getSequenceNumberIncoming() { + return sequenceIncoming; + } + + /** Continue the outgoing (client) RC4 keystream. */ + byte[] seal(final byte[] in) { + return encryptor.update(in); + } + + /** Continue the incoming (server) RC4 keystream. */ + byte[] unseal(final byte[] in) { + return decryptor.update(in); + } + + /** + * Reset to the unauthenticated state so a fresh NTLM handshake runs. Required when the + * underlying TCP connection is lost, because NTLM auth and the RC4 keystreams are bound to it. + */ + void reset() { + authenticated = false; + negotiateFlags = 0; + clientSigningKey = null; + serverSigningKey = null; + encryptor = null; + decryptor = null; + sequenceOutgoing.set(-1); + sequenceIncoming.set(-1); + } + + /** + * Mark the connection authenticated without deriving sealing keys. Used for HTTPS, where TLS + * provides confidentiality and WinRM exchanges plaintext SOAP (no NTLM message sealing). + */ + void markAuthenticated() { + authenticated = true; + } + + /** Derive signing/sealing keys from the Type 3 exported session key and open both RC4 ciphers. */ + void applyKeys(final Type3Message type3) { + applyKeys(type3.getType2Flags(), type3.getExportedSessionKey(), false); + } + + /** + * Derive the directional signing/sealing keys from the exported session key and open both RC4 + * ciphers. The client passes {@code mirror=false}: it signs and seals outgoing messages with the + * client-to-server keys and verifies/unseals incoming ones with the server-to-client keys. + * {@code mirror=true} swaps the directions — that is the server side of the same handshake, used + * by the in-process protocol-test server to speak real NTLM message encryption to the client. + */ + void applyKeys(final long flags, final byte[] exportedSessionKey, final boolean mirror) { + negotiateFlags = flags; + + clientSigningKey = signKey(exportedSessionKey, mirror ? SERVER_SIGNING : CLIENT_SIGNING); + serverSigningKey = signKey(exportedSessionKey, mirror ? CLIENT_SIGNING : SERVER_SIGNING); + encryptor = EncryptionUtils.arc4(sealKey(exportedSessionKey, mirror ? SERVER_SEALING : CLIENT_SEALING)); + decryptor = EncryptionUtils.arc4(sealKey(exportedSessionKey, mirror ? CLIENT_SEALING : SERVER_SEALING)); + authenticated = true; + } + + private static byte[] signKey(final byte[] exportedSessionKey, final byte[] magic) { + return EncryptionUtils.md5digest(ByteArrayUtils.concat(exportedSessionKey, magic)); + } + + private byte[] sealKey(final byte[] exportedSessionKey, final byte[] magic) { + if (hasNegotiateFlag(NTLMEngineUtils.NTLMSSP_NEGOTIATE_EXTENDED_SESSIONSECURITY)) { + if (hasNegotiateFlag(NTLMEngineUtils.NTLMSSP_NEGOTIATE_128)) { + return EncryptionUtils.md5digest(ByteArrayUtils.concat(exportedSessionKey, magic)); + } + if (hasNegotiateFlag(NTLMEngineUtils.NTLMSSP_NEGOTIATE_56)) { + return EncryptionUtils.md5digest(ByteArrayUtils.concat(Arrays.copyOfRange(exportedSessionKey, 0, 7), magic)); + } + return EncryptionUtils.md5digest(ByteArrayUtils.concat(Arrays.copyOfRange(exportedSessionKey, 0, 5), magic)); + } + if (hasNegotiateFlag(NTLMEngineUtils.NTLMSSP_NEGOTIATE_LM_KEY)) { + throw new UnsupportedOperationException("LM KEY negotiate mode not implemented; use extended session security"); + } + return exportedSessionKey; + } +} diff --git a/src/main/java/org/metricshub/winrm/light/WsmanClient.java b/src/main/java/org/metricshub/winrm/light/WsmanClient.java new file mode 100644 index 0000000..3a8b399 --- /dev/null +++ b/src/main/java/org/metricshub/winrm/light/WsmanClient.java @@ -0,0 +1,512 @@ +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. + * ╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱ + */ + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Base64; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.locks.ReentrantLock; +import javax.net.ssl.SSLSocketFactory; +import javax.xml.XMLConstants; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + +/** + * Dependency-free WinRM/WS-Management client: NTLM (masqueraded as Negotiate) with message + * encryption over HTTP, templated SOAP envelopes, and JDK-default XML parsing (no Apache CXF, + * JAX-WS, JAXB, or Woodstox). Supports Identify, WQL queries, and command execution. + */ +final class WsmanClient implements AutoCloseable { + + // If no output is available before the OperationTimeout expires, the server returns this WSMan + // fault code and the client is expected to immediately re-issue the Receive request. + private static final String FAULT_OPERATION_TIMEOUT = "2150858793"; + private static final String FAULT_SHELL_NOT_FOUND = "2150858843"; + + // WS-Enumeration namespace: the EndOfSequence / EnumerationContext markers live here. Match them by + // namespace, never by local name alone, so a WMI property that happens to be named "EndOfSequence" + // or "EnumerationContext" inside cannot be mistaken for the enumeration control element. + private static final String WS_ENUMERATION_NS = "http://schemas.xmlsoap.org/ws/2004/09/enumeration"; + + // WinRM also emits the Items / EndOfSequence markers in its own WSMan namespace (the wsman:Items / + // wsman:EndOfSequence variants); the CXF backend accepts both, so the light backend must too. + private static final String WSMAN_NS = "http://schemas.dmtf.org/wbem/wsman/1/wsman.xsd"; + + private final long timeoutMs; + private final String url; + private final String rawUsername; + private final AuthScheme auth; + private final HttpTransport transport; + + private String pendingAuthorization; + private String shellId; + + // A single NTLM connection is a serial channel: one socket, stateful RC4 ciphers with sequence + // numbers, and a single shellId. Concurrent callers (e.g. a cached SmbTempShare shared across + // threads) MUST NOT interleave, or they read each other's responses and desync the cipher streams. + // Every high-level operation (wql/executeCommand) runs while holding this lock; close() only + // tries it, so it can still hard-close the transport to unblock an abandoned, timed-out worker. + private final ReentrantLock operationLock = new ReentrantLock(); + + WsmanClient( + final String host, + final int port, + final long timeoutMs, + final SSLSocketFactory sslSocketFactory, + final boolean verifyHostname, + final AuthScheme auth, + final String rawUsername + ) { + this.timeoutMs = timeoutMs; + // A non-null socket factory selects HTTPS: TLS wraps the transport and the SOAP travels plaintext. + this.url = (sslSocketFactory != null ? "https" : "http") + "://" + host + ":" + port + "/wsman"; + this.rawUsername = rawUsername; + this.auth = auth; + this.transport = new HttpTransport(host, port, toSocketTimeoutMillis(timeoutMs), sslSocketFactory, verifyHostname); + } + + /** + * Convert the public {@code long} timeout to the {@code int} milliseconds a {@link java.net.Socket} + * accepts. Clamp so a large but valid timeout never narrows to a negative/garbage value, leaving + * headroom for the extra read-timeout seconds {@link HttpTransport} adds. The full {@code long} + * remains authoritative for the WSMan OperationTimeout and the wall-clock deadline in the service. + */ + private static int toSocketTimeoutMillis(final long millis) { + return (int) Math.min(millis, Integer.MAX_VALUE - 10_000L); + } + + /** A decrypted WSMan response: HTTP status plus the (decrypted) SOAP body. */ + private static final class Decoded { + + final int status; + final Document document; + + Decoded(final int status, final Document document) { + this.status = status; + this.document = document; + } + } + + /** Run a WQL query and return the rows as ordered property maps. */ + List> wql(final String namespace, final String query) throws Exception { + // Serialize the whole enumeration (Enumerate + all Pulls) against any other operation sharing + // this connection; see operationLock. + operationLock.lock(); + try { + // WMI namespaces are case-insensitive, but preserve the caller's case to match the CXF backend. + final String ns = namespace.replace('\\', '/'); + final List> rows = new ArrayList<>(); + + Document doc = expectOk(Envelopes.enumerateWql(url, ns, query, timeoutMs), "Enumerate"); + collectItems(doc, rows); + + // Pull until the server signals EndOfSequence (matching the CXF backend). The aggregate + // timeout in LightWinRMService bounds a misbehaving server that never ends the sequence. + boolean endOfSequence = hasEnumerationElement(doc, "EndOfSequence"); + String context = endOfSequence ? null : textNS(doc, WS_ENUMERATION_NS, "EnumerationContext"); + while (!endOfSequence && context != null && !context.isEmpty()) { + doc = expectOk(Envelopes.pull(url, ns, context, timeoutMs), "Pull"); + collectItems(doc, rows); + endOfSequence = hasEnumerationElement(doc, "EndOfSequence"); + context = endOfSequence ? null : textNS(doc, WS_ENUMERATION_NS, "EnumerationContext"); + } + return rows; + } finally { + operationLock.unlock(); + } + } + + /** The result of running a command in the remote shell. */ + static final class CommandOutput { + + final String stdout; + final String stderr; + final int exitCode; + + CommandOutput(final String stdout, final String stderr, final int exitCode) { + this.stdout = stdout; + this.stderr = stderr; + this.exitCode = exitCode; + } + } + + /** Execute a command in the remote command shell, creating the shell on first use. */ + CommandOutput executeCommand(final String commandLine, final String workingDirectory, final Charset charset) + throws Exception { + // Serialize the whole shell lifecycle (Create + Command + Receive loop + Signal) against any + // other operation sharing this connection and the shellId field; see operationLock. + operationLock.lock(); + try { + if (shellId == null) { + createShell(workingDirectory); + } + final Charset cs = charset != null ? charset : StandardCharsets.UTF_8; + final String commandId = startCommand(commandLine); + try { + return receiveLoop(commandId, cs); + } finally { + terminate(commandId); + } + } finally { + operationLock.unlock(); + } + } + + private void createShell(final String workingDirectory) throws Exception { + final Document doc = expectOk(Envelopes.createShell(url, workingDirectory, timeoutMs), "Create shell"); + final NodeList selectors = doc.getElementsByTagNameNS("*", "Selector"); + for (int i = 0; i < selectors.getLength(); i++) { + final Element selector = (Element) selectors.item(i); + if ("ShellId".equals(selector.getAttribute("Name"))) { + shellId = selector.getTextContent(); + return; + } + } + throw new IllegalStateException("Shell ID not found in Create response"); + } + + private String startCommand(final String commandLine) throws Exception { + final Document doc = expectOk(Envelopes.command(url, shellId, commandLine, timeoutMs), "Command"); + final String commandId = text(doc, "CommandId"); + if (commandId == null) { + throw new IllegalStateException("No CommandId in Command response"); + } + return commandId; + } + + private CommandOutput receiveLoop(final String commandId, final Charset charset) throws Exception { + // Accumulate the raw stream BYTES and decode once at the end: a multibyte character (e.g. UTF-8) + // can be split across Stream elements or Receive responses, and decoding each chunk independently + // would corrupt the boundary bytes into replacement characters. + final ByteArrayOutputStream stdout = new ByteArrayOutputStream(); + final ByteArrayOutputStream stderr = new ByteArrayOutputStream(); + while (true) { + final Decoded resp = request(Envelopes.receive(url, shellId, commandId, timeoutMs)); + if (resp.status != 200) { + final String faultCode = wsmanFaultCode(resp.document); + // No output before OperationTimeout → re-issue Receive immediately. + if (FAULT_OPERATION_TIMEOUT.equals(faultCode)) { + continue; + } + throw new IllegalStateException("Receive failed: " + faultSummary(resp)); + } + collectStreams(resp.document, stdout, stderr); + final Integer exitCode = doneExitCode(resp.document); + if (exitCode != null) { + return new CommandOutput( + new String(stdout.toByteArray(), charset), + new String(stderr.toByteArray(), charset), + exitCode + ); + } + } + } + + private void terminate(final String commandId) throws Exception { + final Decoded resp = request(Envelopes.signal(url, shellId, commandId, timeoutMs)); + // A missing shell is fine here — the command already finished and the shell may be gone. + if (resp.status != 200 && !FAULT_SHELL_NOT_FOUND.equals(wsmanFaultCode(resp.document))) { + throw new IllegalStateException("Signal failed: " + faultSummary(resp)); + } + } + + // --- transport / crypto ------------------------------------------------- + + /** Send a request, expecting HTTP 200; throw with the WSMan fault detail otherwise. */ + private Document expectOk(final String soap, final String operation) throws Exception { + final Decoded resp = request(soap); + if (resp.status != 200) { + throw new IllegalStateException(operation + " failed: " + faultSummary(resp)); + } + return resp.document; + } + + /** + * Send one SOAP request (authenticating the connection on first use via the {@link AuthScheme}) + * and decode the response. The caller must hold {@link #operationLock}; every path here is reached + * from a locked wql/executeCommand/close, so requests never interleave on the stateful connection. + */ + private Decoded request(final String soap) throws Exception { + // If the connection was dropped (e.g. the server sent "Connection: close"), the session bound + // to it is dead — re-handshake on the fresh connection rather than sending unauthenticated. + if (auth.isAuthenticated() && !transport.isConnected()) { + auth.reset(); + } + final byte[] body = soap.getBytes(StandardCharsets.UTF_8); + while (true) { + if (!auth.isAuthenticated()) { + pendingAuthorization = auth.authenticate(transport); + } + // The handshake's Authorization accompanies the first real request; later requests on the + // already-authenticated connection carry no Authorization header. + final String authorization = pendingAuthorization; + pendingAuthorization = null; + + final HttpTransport.Response resp = transport.post( + "/wsman", + auth.wrap(body), + auth.wrapContentType(), + authorization + ); + + // HTTP 401 = the server rejected the credentials/token. For NTLM and Kerberos this can only + // surface here — the Type 3 / AP-REQ rides the first real request, not the handshake — so we + // must NOT keep the "authenticated" connection (it would loop resending with no Authorization + // header, wedging the executor). Drop it and, for an ordered fallback list, retry the next + // scheme once on a fresh connection. A 401'd request was rejected before processing, so + // re-sending it is safe. + if (resp.status == 401) { + transport.close(); + auth.reset(); + if (auth.advance()) { + continue; + } + // Same message format as the CXF backend's credential-rejection path — callers (and their + // operators) match on it. + throw new IllegalStateException( + String.format("Authentication error on %s with user name \"%s\"", url, rawUsername) + ); + } + + // 200 = success, 500 = SOAP fault. Anything else is a protocol failure whose body is not a + // usable WSMan response. + if (resp.status != 200 && resp.status != 500) { + throw new IllegalStateException("WSMan request failed: HTTP " + resp.status); + } + return new Decoded(resp.status, parse(auth.unwrap(resp))); + } + } + + // --- XML helpers -------------------------------------------------------- + + static Document parse(final byte[] xml) throws Exception { + final DocumentBuilderFactory factory = DocumentBuilderFactory.newDefaultInstance(); + factory.setNamespaceAware(true); + // Harden against XXE: a malicious/compromised WinRM endpoint must not be able to make us + // resolve external entities (local file read, SSRF, entity-expansion DoS). WSMan responses + // never carry a DOCTYPE, so rejecting it outright is the strongest and safest defence. + factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); + factory.setFeature("http://xml.org/sax/features/external-general-entities", false); + factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false); + factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true); + factory.setXIncludeAware(false); + factory.setExpandEntityReferences(false); + final DocumentBuilder builder = factory.newDocumentBuilder(); + // Throw parse errors instead of letting the default handler print them to stderr — a request + // abandoned by the timeout may parse a truncated response on a soon-to-die background thread. + builder.setErrorHandler( + new org.xml.sax.helpers.DefaultHandler() { + @Override + public void error(final org.xml.sax.SAXParseException e) throws org.xml.sax.SAXException { + throw e; + } + + @Override + public void fatalError(final org.xml.sax.SAXParseException e) throws org.xml.sax.SAXException { + throw e; + } + } + ); + return builder.parse(new ByteArrayInputStream(xml)); + } + + private static String text(final Document doc, final String localName) { + final NodeList nodes = doc.getElementsByTagNameNS("*", localName); + return nodes.getLength() > 0 ? nodes.item(0).getTextContent() : null; + } + + /** + * Whether the document contains the given enumeration control element (namespace-scoped). + * WinRM emits these markers either in the WS-Enumeration namespace or in its own WSMan namespace + * (e.g. {@code wsen:EndOfSequence} vs {@code wsman:EndOfSequence}); accept both, like the CXF + * backend does. + */ + static boolean hasEnumerationElement(final Document doc, final String localName) { + return ( + doc.getElementsByTagNameNS(WS_ENUMERATION_NS, localName).getLength() > 0 || + doc.getElementsByTagNameNS(WSMAN_NS, localName).getLength() > 0 + ); + } + + /** First text content of an element matched by both namespace and local name. */ + private static String textNS(final Document doc, final String namespace, final String localName) { + final NodeList nodes = doc.getElementsByTagNameNS(namespace, localName); + return nodes.getLength() > 0 ? nodes.item(0).getTextContent() : null; + } + + static void collectItems(final Document doc, final List> rows) { + // The Items wrapper comes in the WS-Enumeration namespace (EnumerateResponse) or the WSMan + // namespace (PullResponse) depending on the operation; accept both, like the CXF backend, and + // nothing else — a WMI property or class named "Items" must not be mistaken for the wrapper. + collectItems(doc.getElementsByTagNameNS(WS_ENUMERATION_NS, "Items"), rows); + collectItems(doc.getElementsByTagNameNS(WSMAN_NS, "Items"), rows); + } + + private static void collectItems(final NodeList items, final List> rows) { + for (int i = 0; i < items.getLength(); i++) { + final NodeList instances = items.item(i).getChildNodes(); + for (int j = 0; j < instances.getLength(); j++) { + final Node instance = instances.item(j); + if (instance.getNodeType() != Node.ELEMENT_NODE) { + continue; + } + final Map row = new LinkedHashMap<>(); + final NodeList props = instance.getChildNodes(); + for (int k = 0; k < props.getLength(); k++) { + final Node prop = props.item(k); + if (prop.getNodeType() == Node.ELEMENT_NODE) { + row.put(((Element) prop).getLocalName(), prop.getTextContent()); + } + } + if (!row.isEmpty()) { + rows.add(row); + } + } + } + } + + private static void collectStreams( + final Document doc, + final ByteArrayOutputStream stdout, + final ByteArrayOutputStream stderr + ) { + final NodeList streams = doc.getElementsByTagNameNS("*", "Stream"); + for (int i = 0; i < streams.getLength(); i++) { + final Element stream = (Element) streams.item(i); + final String value = stream.getTextContent(); + if (value == null || value.isEmpty()) { + continue; + } + final byte[] bytes = Base64.getDecoder().decode(value); + if ("stdout".equals(stream.getAttribute("Name"))) { + stdout.write(bytes, 0, bytes.length); + } else if ("stderr".equals(stream.getAttribute("Name"))) { + stderr.write(bytes, 0, bytes.length); + } + } + } + + /** Return the exit code if the response carries a CommandState of Done, otherwise null. */ + private static Integer doneExitCode(final Document doc) { + final NodeList states = doc.getElementsByTagNameNS("*", "CommandState"); + for (int i = 0; i < states.getLength(); i++) { + final Element state = (Element) states.item(i); + if (Envelopes.COMMAND_STATE_DONE.equals(state.getAttribute("State"))) { + final NodeList exit = state.getElementsByTagNameNS("*", "ExitCode"); + return exit.getLength() > 0 ? Integer.valueOf(exit.item(0).getTextContent().trim()) : 0; + } + } + return null; + } + + private static String wsmanFaultCode(final Document doc) { + final NodeList faults = doc.getElementsByTagNameNS("*", "WSManFault"); + return faults.getLength() > 0 ? ((Element) faults.item(0)).getAttribute("Code") : null; + } + + /** + * The detailed WSManFault Message text, or null. This is where WinRM puts the provider-level + * detail — notably the WMI error mnemonics (WBEM_E_INVALID_CLASS, WBEM_E_INVALID_NAMESPACE, + * WBEM_E_NOT_FOUND, ...) that callers match on to tell a bad query from a broken connection. + * {@code getTextContent()} also flattens any nested ProviderFault detail into the message. + */ + private static String wsmanFaultMessage(final Document doc) { + final NodeList faults = doc.getElementsByTagNameNS("*", "WSManFault"); + if (faults.getLength() == 0) { + return null; + } + final NodeList messages = ((Element) faults.item(0)).getElementsByTagNameNS("*", "Message"); + return messages.getLength() > 0 ? messages.item(0).getTextContent() : null; + } + + static String faultSummary(final int status, final Document doc) { + final String reason = trimToNull(text(doc, "Text")); + final String detail = trimToNull(wsmanFaultMessage(doc)); + final String code = wsmanFaultCode(doc); + final StringBuilder summary = new StringBuilder("HTTP ").append(status); + if (code != null && !code.isEmpty()) { + summary.append(" (WSManFault ").append(code).append(')'); + } + if (reason != null) { + summary.append(": ").append(reason); + } + // Append the detailed fault message when it adds anything beyond the Reason text: the WMI + // mnemonics it carries are part of the exception-message contract inherited from CXF. + if (detail != null && (reason == null || !reason.contains(detail))) { + summary.append(reason == null ? ": " : " - ").append(detail); + } + return summary.toString(); + } + + private static String faultSummary(final Decoded resp) { + return faultSummary(resp.status, resp.document); + } + + private static String trimToNull(final String s) { + if (s == null) { + return null; + } + final String trimmed = s.trim(); + return trimmed.isEmpty() ? null : trimmed; + } + + @Override + public void close() { + // Only attempt a graceful shell Delete if no operation is currently using the connection: a + // blocking tryLock (never a lock()) keeps close() from waiting on an abandoned, timed-out worker + // still holding operationLock while blocked on a socket read. When we cannot acquire the lock, + // or a request would otherwise race the worker, we skip the Delete and just hard-close the + // transport below — which unblocks that worker's read; the shell is reaped by the server IdleTimeout. + final boolean locked = operationLock.tryLock(); + try { + final String shell = shellId; + shellId = null; + if (locked) { + if (shell != null) { + try { + request(Envelopes.deleteShell(url, shell, timeoutMs)); + } catch (final Exception ignore) { + // best-effort shell cleanup + } + } + // Release the connection-bound auth state — notably the Kerberos GSSContext, whose only + // disposal path is reset(). Skipped when not locked: another (timed-out) worker still owns + // the auth scheme, and the transport hard-close below unblocks it. + auth.reset(); + } + } finally { + if (locked) { + operationLock.unlock(); + } + transport.close(); + } + } +} diff --git a/src/main/java/org/metricshub/winrm/service/StripShellResponseHandler.java b/src/main/java/org/metricshub/winrm/service/StripShellResponseHandler.java deleted file mode 100644 index 5ea4947..0000000 --- a/src/main/java/org/metricshub/winrm/service/StripShellResponseHandler.java +++ /dev/null @@ -1,95 +0,0 @@ -package org.metricshub.winrm.service; - -/*- - * ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲ - * WinRM Java Client - * ჻჻჻჻჻჻ - * Copyright 2023 - 2024 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. - * ╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱ - */ - -import static jakarta.xml.ws.handler.MessageContext.WSDL_OPERATION; - -import jakarta.xml.soap.SOAPBody; -import jakarta.xml.soap.SOAPElement; -import jakarta.xml.soap.SOAPEnvelope; -import jakarta.xml.soap.SOAPException; -import jakarta.xml.ws.handler.MessageContext; -import jakarta.xml.ws.handler.soap.SOAPHandler; -import jakarta.xml.ws.handler.soap.SOAPMessageContext; -import java.util.Collections; -import java.util.Iterator; -import java.util.Set; -import javax.xml.namespace.QName; - -/** - * Code from io.cloudsoft.winrm4j.client.StripShellResponseHandler - * release 0.12.3 @link https://github.com/cloudsoft/winrm4j - */ -public class StripShellResponseHandler implements SOAPHandler { - - @Override - public boolean handleMessage(final SOAPMessageContext context) { - final Boolean messageOutbound = (Boolean) context.get(MessageContext.MESSAGE_OUTBOUND_PROPERTY); - if (messageOutbound != null && messageOutbound.booleanValue()) { - return true; - } - - final QName action = (QName) context.get(WSDL_OPERATION); - if (action != null && !"Create".equals(action.getLocalPart())) { - return true; - } - - final Iterator childIterator = getBodyChildren(context); - while (childIterator.hasNext()) { - final Object node = childIterator.next(); - - if (node instanceof SOAPElement) { - final SOAPElement soapElement = (SOAPElement) node; - if ("Shell".equals(soapElement.getLocalName())) { - childIterator.remove(); - } - } - } - - return true; - } - - private Iterator getBodyChildren(final SOAPMessageContext context) { - try { - final SOAPEnvelope envelope = context.getMessage().getSOAPPart().getEnvelope(); - final SOAPBody body = envelope.getBody(); - - return body.getChildElements(); - } catch (final SOAPException e) { - throw new IllegalStateException(e); - } - } - - @Override - public boolean handleFault(final SOAPMessageContext context) { - return true; - } - - @Override - public void close(final MessageContext context) { - // Do nothing - } - - @Override - public Set getHeaders() { - return Collections.emptySet(); - } -} diff --git a/src/main/java/org/metricshub/winrm/service/WSManHeaderInterceptor.java b/src/main/java/org/metricshub/winrm/service/WSManHeaderInterceptor.java deleted file mode 100644 index d2ec8a1..0000000 --- a/src/main/java/org/metricshub/winrm/service/WSManHeaderInterceptor.java +++ /dev/null @@ -1,73 +0,0 @@ -package org.metricshub.winrm.service; - -/*- - * ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲ - * WinRM Java Client - * ჻჻჻჻჻჻ - * Copyright 2023 - 2024 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. - * ╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱ - */ - -import jakarta.xml.bind.JAXBElement; -import jakarta.xml.bind.JAXBException; -import java.util.List; -import org.apache.cxf.binding.soap.SoapMessage; -import org.apache.cxf.binding.soap.interceptor.AbstractSoapInterceptor; -import org.apache.cxf.binding.soap.interceptor.SoapPreProtocolOutInterceptor; -import org.apache.cxf.headers.Header; -import org.apache.cxf.interceptor.Fault; -import org.apache.cxf.jaxb.JAXBDataBinding; -import org.apache.cxf.phase.Phase; -import org.metricshub.winrm.Utils; -import org.metricshub.winrm.service.wsman.AttributableURI; -import org.metricshub.winrm.service.wsman.ObjectFactory; - -/** - * Code from org.opennms.core.wsman.cxf.WSManHeaderInterceptor - * release 1.2.3 @link https://github.com/OpenNMS/wsman - */ -public class WSManHeaderInterceptor extends AbstractSoapInterceptor { - - private static final JAXBDataBinding ATTRIBUTABLE_URI_JAXB_DATA_BINDING; - - static { - try { - ATTRIBUTABLE_URI_JAXB_DATA_BINDING = new JAXBDataBinding(AttributableURI.class); - } catch (final JAXBException e) { - throw new RuntimeException("Failed to create JAXBDataBinding for: AttributableURI" + AttributableURI.class, e); - } - } - - private final String resourceUri; - - public WSManHeaderInterceptor(final String resourceUri) { - super(Phase.POST_LOGICAL); - addAfter(SoapPreProtocolOutInterceptor.class.getName()); - - Utils.checkNonNull(resourceUri, "resourceUri"); - - this.resourceUri = resourceUri; - } - - @Override - public void handleMessage(final SoapMessage message) throws Fault { - final JAXBElement resourceURI = new ObjectFactory().createResourceURI(resourceUri); - - final List

headers = message.getHeaders(); - headers.add(new Header(resourceURI.getName(), resourceURI, ATTRIBUTABLE_URI_JAXB_DATA_BINDING)); - - message.put(Header.HEADER_LIST, headers); - } -} diff --git a/src/main/java/org/metricshub/winrm/service/WinRMEndpoint.java b/src/main/java/org/metricshub/winrm/service/WinRMEndpoint.java index bf2885c..81aa82f 100644 --- a/src/main/java/org/metricshub/winrm/service/WinRMEndpoint.java +++ b/src/main/java/org/metricshub/winrm/service/WinRMEndpoint.java @@ -41,6 +41,7 @@ public class WinRMEndpoint { private final String namespace; private final String rawUsername; private final WinRMHttpProtocolEnum protocol; + private final int port; /** * Constructor of the endpoint and credentials for WinRM. @@ -81,6 +82,7 @@ public WinRMEndpoint( } this.protocol = protocol != null ? protocol : WinRMHttpProtocolEnum.HTTP; + this.port = getEndpointPort(this.protocol, port); final String endpointUrl = buildEndpointUrl(this.protocol, this.hostname, port); endpoint = buildWSManEndpoint(endpointUrl); @@ -126,6 +128,11 @@ public WinRMHttpProtocolEnum getProtocol() { return protocol; } + /** Get the resolved endpoint port (the port provided to the constructor, or the protocol default). */ + public int getPort() { + return port; + } + /** * Build the endpoint URL. * diff --git a/src/main/java/org/metricshub/winrm/service/WinRMExecutorFactory.java b/src/main/java/org/metricshub/winrm/service/WinRMExecutorFactory.java new file mode 100644 index 0000000..d63f227 --- /dev/null +++ b/src/main/java/org/metricshub/winrm/service/WinRMExecutorFactory.java @@ -0,0 +1,81 @@ +package org.metricshub.winrm.service; + +/*- + * ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲ + * 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. + * ╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱ + */ + +import java.nio.file.Path; +import java.util.List; +import java.util.Locale; +import org.metricshub.winrm.WindowsRemoteExecutor; +import org.metricshub.winrm.exceptions.WinRMException; +import org.metricshub.winrm.light.LightWinRMService; +import org.metricshub.winrm.service.client.auth.AuthenticationEnum; + +/** + * Creates the {@link WindowsRemoteExecutor} that fulfils a request. Since 2.0.0 the dependency-free + * {@link LightWinRMService} is the only backend: the legacy CXF-based backend has been removed. + * The {@value #BACKEND_PROPERTY} system property is kept so operators who still set it get a clear + * error ({@code cxf}) or a no-op ({@code light}) instead of a silent behavior change. + */ +public final class WinRMExecutorFactory { + + /** System property selecting the backend; {@code light} is the only supported value. */ + public static final String BACKEND_PROPERTY = "org.metricshub.winrm.backend"; + + private static final String LIGHT = "light"; + private static final String CXF = "cxf"; + + private WinRMExecutorFactory() {} + + /** + * Create a {@link WindowsRemoteExecutor} (light backend). + * + * @param winRMEndpoint endpoint with credentials (mandatory) + * @param timeout timeout in milliseconds (must be > 0) + * @param ticketCache Kerberos ticket cache path (may be {@code null}) + * @param authentications requested authentication schemes (may be {@code null}) + * @return a light-backed executor + * @throws WinRMException for any problem creating the executor, or when {@value #BACKEND_PROPERTY} + * requests the removed CXF backend or an unknown value + */ + public static WindowsRemoteExecutor createInstance( + final WinRMEndpoint winRMEndpoint, + final long timeout, + final Path ticketCache, + final List authentications + ) throws WinRMException { + final String backend = System.getProperty(BACKEND_PROPERTY, LIGHT).trim().toLowerCase(Locale.ROOT); + if (LIGHT.equals(backend)) { + return LightWinRMService.createInstance(winRMEndpoint, timeout, ticketCache, authentications); + } + if (CXF.equals(backend)) { + // Fail loudly: an operator who explicitly pinned the legacy backend must not be silently + // switched to another implementation. + throw new WinRMException( + "The CXF WinRM backend was removed in winrm-java 2.0.0; remove the " + + BACKEND_PROPERTY + + " system property to use the light backend (or stay on winrm-java 1.x)." + ); + } + throw new WinRMException( + "Unsupported value \"" + backend + "\" for system property " + BACKEND_PROPERTY + "; expected \"" + LIGHT + "\"." + ); + } +} diff --git a/src/main/java/org/metricshub/winrm/service/WinRMInvocationHandler.java b/src/main/java/org/metricshub/winrm/service/WinRMInvocationHandler.java deleted file mode 100644 index 8e5aadd..0000000 --- a/src/main/java/org/metricshub/winrm/service/WinRMInvocationHandler.java +++ /dev/null @@ -1,549 +0,0 @@ -package org.metricshub.winrm.service; - -/*- - * ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲ - * WinRM Java Client - * ჻჻჻჻჻჻ - * Copyright 2023 - 2024 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. - * ╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱ - */ - -import jakarta.xml.ws.BindingProvider; -import jakarta.xml.ws.WebServiceException; -import jakarta.xml.ws.handler.Handler; -import jakarta.xml.ws.soap.SOAPFaultException; -import java.io.IOException; -import java.lang.reflect.InvocationHandler; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.net.URL; -import java.nio.file.Path; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Queue; -import java.util.concurrent.ConcurrentHashMap; -import java.util.stream.Collectors; -import java.util.stream.Stream; -import javax.net.ssl.TrustManager; -import javax.xml.namespace.QName; -import org.apache.cxf.Bus; -import org.apache.cxf.binding.soap.SoapBindingConstants; -import org.apache.cxf.configuration.jsse.TLSClientParameters; -import org.apache.cxf.endpoint.Client; -import org.apache.cxf.frontend.ClientProxy; -import org.apache.cxf.jaxws.JaxWsProxyFactoryBean; -import org.apache.cxf.message.Message; -import org.apache.cxf.service.model.ServiceInfo; -import org.apache.cxf.transport.http.HTTPConduitFactory; -import org.apache.cxf.transport.http.asyncclient.AsyncHTTPConduit; -import org.apache.cxf.transports.http.configuration.HTTPClientPolicy; -import org.apache.cxf.ws.addressing.WSAddressingFeature; -import org.apache.cxf.ws.addressing.WSAddressingFeature.AddressingResponses; -import org.apache.cxf.ws.addressing.policy.MetadataConstants; -import org.apache.cxf.ws.policy.PolicyConstants; -import org.apache.http.auth.AuthSchemeProvider; -import org.apache.http.auth.Credentials; -import org.apache.http.auth.NTCredentials; -import org.apache.http.client.config.AuthSchemes; -import org.apache.http.config.Registry; -import org.apache.http.config.RegistryBuilder; -import org.apache.http.impl.auth.KerberosSchemeFactory; -import org.apache.neethi.Policy; -import org.apache.neethi.builders.PrimitiveAssertion; -import org.metricshub.winrm.Utils; -import org.metricshub.winrm.WinRMHttpProtocolEnum; -import org.metricshub.winrm.service.client.auth.AuthenticationEnum; -import org.metricshub.winrm.service.client.auth.TrustAllX509Manager; -import org.metricshub.winrm.service.client.auth.kerberos.KerberosUtils; -import org.metricshub.winrm.service.client.auth.ntlm.NTCredentialsWithEncryption; -import org.metricshub.winrm.service.client.auth.ntlm.NtlmMasqAsSpnegoSchemeFactory; -import org.metricshub.winrm.service.client.encryption.AsyncHttpEncryptionAwareConduitFactory; -import org.metricshub.winrm.service.client.encryption.DecryptAndVerifyInInterceptor; -import org.metricshub.winrm.service.client.encryption.SignAndEncryptOutInterceptor; - -public class WinRMInvocationHandler implements InvocationHandler { - - public static final String WSMAN_SCHEMA_NAMESPACE = "http://schemas.dmtf.org/wbem/wsman/1/wsman.xsd"; - - private static final long PAUSE_TIME_MILLISECONDS = 500; - private static final int MAX_RETRY = 3; - - private static final URL WSDL_LOCATION_URL = - WinRMWebServiceClient.class.getClassLoader().getResource("wsdl/WinRM.wsdl"); - - private static final QName SERVICE = new QName(WSMAN_SCHEMA_NAMESPACE, "WinRMWebServiceClient"); - - private static final QName PORT = new QName(WSMAN_SCHEMA_NAMESPACE, "WinRMPort"); - - private static final List CONTENT_TYPE_LIST = Collections.singletonList("application/soap+xml;charset=UTF-8"); - - @SuppressWarnings("rawtypes") - private static final List HANDLER_CHAIN = Arrays.asList(new StripShellResponseHandler()); - - private static final Registry AUTH_SCHEME_REGISTRY = RegistryBuilder - .create() - .register(AuthSchemes.SPNEGO, new NtlmMasqAsSpnegoSchemeFactory()) - .register(AuthSchemes.KERBEROS, new KerberosSchemeFactory(true)) - .build(); - - private static final Policy POLICY; - - static { - POLICY = new Policy(); - POLICY.addAssertion(new PrimitiveAssertion(MetadataConstants.USING_ADDRESSING_2004_QNAME)); - } - - private static final WSAddressingFeature WS_ADDRESSING_FEATURE; - - static { - WS_ADDRESSING_FEATURE = new WSAddressingFeature(); - WS_ADDRESSING_FEATURE.setResponses(AddressingResponses.ANONYMOUS); - } - - private static final TLSClientParameters TLS_CLIENT_PARAMETERS; - - static { - TLS_CLIENT_PARAMETERS = new TLSClientParameters(); - TLS_CLIENT_PARAMETERS.setDisableCNCheck(true); - // Accept all certificates - TLS_CLIENT_PARAMETERS.setTrustManagers(new TrustManager[] { new TrustAllX509Manager() }); - } - - private static final Map CREDENTIALS = new ConcurrentHashMap<>(); - - private final WinRMWebService winRMWebService; - private final WinRMEndpoint winRMEndpoint; - private final long timeout; - private final String resourceUri; - private final Path ticketCache; - private final Queue authenticationsQueue; - private AuthenticationEnum authentication; - private Client wsClient; - - /** - * WinRMInvocationHandler constructor - * - * @param winRMEndpoint Endpoint with credentials (mandatory) - * @param bus Apache CXF Bus (mandatory) - * @param timeout Timeout used for Connection, Connection Request and Receive Request in milliseconds - * @param resourceUri The enumerate resource URI - * @param ticketCache The Ticket Cache path - * @param authentications List of authentications. (mandatory) - */ - public WinRMInvocationHandler( - final WinRMEndpoint winRMEndpoint, - final Bus bus, - final long timeout, - final String resourceUri, - final Path ticketCache, - final List authentications - ) { - Utils.checkNonNull(winRMEndpoint, "winRMEndpoint"); - Utils.checkNonNull(bus, "bus"); - Utils.checkNonNull(authentications, "authentications"); - - this.winRMEndpoint = winRMEndpoint; - this.timeout = timeout; - this.resourceUri = resourceUri; - this.ticketCache = ticketCache; - authenticationsQueue = authentications.stream().collect(Collectors.toCollection(LinkedList::new)); - - winRMWebService = createWinRMWebService(winRMEndpoint, bus); - - final AuthCredentials authCredentials = computeCredentials(winRMEndpoint, ticketCache, authenticationsQueue); - - authentication = authCredentials.getAuthentication(); - - wsClient = - getWebServiceClient(winRMEndpoint, timeout, resourceUri, winRMWebService, authCredentials.getCredentials()); - } - - public Client getClient() { - return wsClient; - } - - @Override - public Object invoke(final Object proxy, final Method method, final Object[] args) throws Throwable { - Utils.checkNonNull(method, "method"); - - try { - return invokeMethod(method, args); - } catch (final RetryTgtExpirationException e) { - // retry with a new TGT in case of current TGT expiration - authentication = null; - - Credentials credentials; - try { - credentials = - KerberosUtils.createCredentials(winRMEndpoint.getUsername(), winRMEndpoint.getPassword(), ticketCache); - - CREDENTIALS.put(new CredentialsMapKey(winRMEndpoint, ticketCache, AuthenticationEnum.KERBEROS), credentials); - // Normally that should not happen as any other exception on KERBEROs should had been throw - // at the first KERBEROS call - } catch (final Exception e1) { - if (continueToRetry()) { - final AuthCredentials authCredentials = computeCredentials(winRMEndpoint, ticketCache, authenticationsQueue); - - authentication = authCredentials.getAuthentication(); - credentials = authCredentials.getCredentials(); - } else { - throw e1; - } - } - - wsClient = getWebServiceClient(winRMEndpoint, timeout, resourceUri, winRMWebService, credentials); - - return invoke(proxy, method, args); - } catch (final RetryAuthenticationException e) { - if (continueToRetry()) { - final AuthCredentials authCredentials = computeCredentials(winRMEndpoint, ticketCache, authenticationsQueue); - - authentication = authCredentials.getAuthentication(); - - wsClient = - getWebServiceClient(winRMEndpoint, timeout, resourceUri, winRMWebService, authCredentials.getCredentials()); - - return invoke(proxy, method, args); - } - - // No more retries - final Throwable cause = e.getCause(); - if (cause instanceof SOAPFaultException) { - throw new RuntimeException("KERBEROS with encryption over HTTP is not implemented.", cause); - } - throw cause; - } - } - - // this function is only needed for the unit testing - boolean continueToRetry() { - return !authenticationsQueue.isEmpty(); - } - - Object invokeMethod(final Method method, final Object[] args) - throws IllegalAccessException, RetryAuthenticationException { - Throwable firstEx = null; - int retry = 0; - - while (retry < MAX_RETRY) { - retry++; - - try { - return method.invoke(winRMWebService, args); - } catch (final InvocationTargetException ite) { - final Throwable targetEx = ite.getTargetException(); - - if (targetEx instanceof SOAPFaultException) { - // Could retry with a different authentication than NTLM - // because it could be a "WstxEOFException: Unexpected EOF in prolog" - // due to a KERBEROS with HTTP and AllowUnencrypted=false - if (winRMEndpoint.getProtocol() == WinRMHttpProtocolEnum.HTTP && authentication != AuthenticationEnum.NTLM) { - throw new RetryAuthenticationException(targetEx); - } - throw (SOAPFaultException) targetEx; - } - - if (!(targetEx instanceof WebServiceException)) { - throw new IllegalStateException("Failure when calling " + createCallInfos(method, args), targetEx); - } - - final WebServiceException wsEx = (WebServiceException) targetEx; - - if (!(wsEx.getCause() instanceof IOException)) { - throw new RuntimeException( - "Exception occurred while making WinRM WebService call " + createCallInfos(method, args), - wsEx - ); - } - - if ( - wsEx.getCause().getMessage() != null && - wsEx.getCause().getMessage().startsWith("Authorization loop detected on Conduit") - ) { - final RuntimeException authEx = new RuntimeException( - String.format( - "Authentication error on %s with user name \"%s\"", - winRMEndpoint.getEndpoint(), - winRMEndpoint.getRawUsername() - ) - ); - - // Could be due to a TGT expiration - if (authentication == AuthenticationEnum.KERBEROS) { - throw new RetryTgtExpirationException(authEx); - } - // Could retry with a different authentication - throw new RetryAuthenticationException(authEx); - } - - if (firstEx == null) { - firstEx = wsEx; - } - - if (retry < MAX_RETRY) { - try { - Utils.sleep(PAUSE_TIME_MILLISECONDS); - } catch (final InterruptedException ie) { - Thread.currentThread().interrupt(); - throw new RuntimeException( - "Exception occured while making WinRM WebService call " + createCallInfos(method, args), - ie - ); - } - } - } - } - - throw new RuntimeException( - String.format("failed task \"%s\" after %d attempts", createCallInfos(method, args), MAX_RETRY), - firstEx - ); - } - - static String createCallInfos(final Method method, final Object[] args) { - final String name = method != null && method.getName() != null ? method.getName() : Utils.EMPTY; - return args == null - ? name - : Stream - .concat(Stream.of(name), Stream.of(args)) - .filter(Objects::nonNull) - .map(Object::toString) - .collect(Collectors.joining(" ")); - } - - static Credentials createCredentials( - final WinRMEndpoint winRMEndpoint, - final AuthenticationEnum authentication, - final Path ticketCache - ) { - switch (authentication) { - case KERBEROS: - return KerberosUtils.createCredentials(winRMEndpoint.getUsername(), winRMEndpoint.getPassword(), ticketCache); - case NTLM: - default: - final String password = String.valueOf(winRMEndpoint.getPassword()); - return winRMEndpoint.getProtocol() == WinRMHttpProtocolEnum.HTTP - ? new NTCredentialsWithEncryption(winRMEndpoint.getUsername(), password, null, winRMEndpoint.getDomain()) - : new NTCredentials(winRMEndpoint.getUsername(), password, null, winRMEndpoint.getDomain()); - } - } - - static AuthCredentials computeCredentials( - final WinRMEndpoint winRMEndpoint, - final Path ticketCache, - final Queue authenticationsQueue - ) { - try { - final AuthenticationEnum authenticationEnum = authenticationsQueue.remove(); - - final Credentials credentials = CREDENTIALS.compute( - new CredentialsMapKey(winRMEndpoint, ticketCache, authenticationEnum), - (user, cred) -> cred != null ? cred : createCredentials(winRMEndpoint, authenticationEnum, ticketCache) - ); - - return new AuthCredentials(authenticationEnum, credentials); - } catch (final Exception e) { - // if there's still retry - if (!authenticationsQueue.isEmpty()) { - return computeCredentials(winRMEndpoint, ticketCache, authenticationsQueue); - } - throw e; - } - } - - static WinRMWebService createWinRMWebService(final WinRMEndpoint winRMEndpoint, final Bus bus) { - final JaxWsProxyFactoryBean jaxWsProxyFactoryBean = new JaxWsProxyFactoryBean(); - jaxWsProxyFactoryBean.setServiceName(SERVICE); - jaxWsProxyFactoryBean.setEndpointName(PORT); - jaxWsProxyFactoryBean.setBus(bus); - jaxWsProxyFactoryBean.setServiceClass(WinRMWebService.class); - jaxWsProxyFactoryBean.setAddress(winRMEndpoint.getEndpoint()); - jaxWsProxyFactoryBean.getFeatures().add(WS_ADDRESSING_FEATURE); - jaxWsProxyFactoryBean.setBindingId(SoapBindingConstants.SOAP12_BINDING_ID); - jaxWsProxyFactoryBean.getClientFactoryBean().getServiceFactory().setWsdlURL(WSDL_LOCATION_URL); - - return jaxWsProxyFactoryBean.create(WinRMWebService.class); - } - - static Client getWebServiceClient( - final WinRMEndpoint winRMEndpoint, - final long timeout, - final String enumerateResourceUri, - final WinRMWebService winRMWebService, - final Credentials credentials - ) { - final Client client = ClientProxy.getClient(winRMWebService); - - if (enumerateResourceUri != null) { - final WSManHeaderInterceptor interceptor = new WSManHeaderInterceptor(enumerateResourceUri); - client.getOutInterceptors().add(interceptor); - } - - client.getInInterceptors().add(new DecryptAndVerifyInInterceptor()); - client.getOutInterceptors().add(new SignAndEncryptOutInterceptor()); - - // this is different to endpoint properties - client - .getEndpoint() - .getEndpointInfo() - .setProperty(HTTPConduitFactory.class.getName(), new AsyncHttpEncryptionAwareConduitFactory()); - - final ServiceInfo serviceInfo = client.getEndpoint().getEndpointInfo().getService(); - serviceInfo.setProperty("soap.force.doclit.bare", true); - - final BindingProvider bindingProvider = (BindingProvider) winRMWebService; - bindingProvider.getBinding().setHandlerChain(HANDLER_CHAIN); - bindingProvider.getRequestContext().put(PolicyConstants.POLICY_OVERRIDE, POLICY); - bindingProvider.getRequestContext().put("http.autoredirect", true); - - bindingProvider.getRequestContext().put(BindingProvider.ENDPOINT_ADDRESS_PROPERTY, winRMEndpoint.getEndpoint()); - - final Map> headers = new HashMap<>(); - headers.put("Content-Type", CONTENT_TYPE_LIST); - - bindingProvider.getRequestContext().put(Message.PROTOCOL_HEADERS, headers); - - // Setup timeouts - final HTTPClientPolicy httpClientPolicy = new HTTPClientPolicy(); - httpClientPolicy.setConnectionTimeout(timeout); - httpClientPolicy.setConnectionRequestTimeout(timeout); - httpClientPolicy.setReceiveTimeout(timeout); - httpClientPolicy.setAllowChunking(false); - - bindingProvider.getRequestContext().put(Credentials.class.getName(), credentials); - bindingProvider.getRequestContext().put(AuthSchemeProvider.class.getName(), AUTH_SCHEME_REGISTRY); - - final AsyncHTTPConduit asyncHTTPConduit = (AsyncHTTPConduit) client.getConduit(); - asyncHTTPConduit.setClient(httpClientPolicy); - asyncHTTPConduit.getClient().setAutoRedirect(true); - asyncHTTPConduit.setTlsClientParameters(TLS_CLIENT_PARAMETERS); - - return client; - } - - static class RetryAuthenticationException extends Exception { - - private static final long serialVersionUID = 1L; - - RetryAuthenticationException(final Throwable throwable) { - super(throwable); - } - } - - static class RetryTgtExpirationException extends RetryAuthenticationException { - - private static final long serialVersionUID = 1L; - - RetryTgtExpirationException(final Throwable throwable) { - super(throwable); - } - } - - static class AuthCredentials { - - private final AuthenticationEnum authentication; - private final Credentials credentials; - - AuthCredentials(final AuthenticationEnum authentication, final Credentials credentials) { - this.authentication = authentication; - this.credentials = credentials; - } - - public AuthenticationEnum getAuthentication() { - return authentication; - } - - public Credentials getCredentials() { - return credentials; - } - - @Override - public int hashCode() { - return Objects.hash(authentication, credentials); - } - - @Override - public boolean equals(final Object obj) { - if (this == obj) { - return true; - } - if (obj == null) { - return false; - } - if (!(obj instanceof AuthCredentials)) { - return false; - } - final AuthCredentials other = (AuthCredentials) obj; - return authentication == other.authentication && Objects.equals(credentials, other.credentials); - } - } - - static class CredentialsMapKey { - - private final String canonizedRawUsername; - private final char[] password; - private final Path ticketCache; - private final AuthenticationEnum authentication; - - CredentialsMapKey( - final WinRMEndpoint winRMEndpoint, - final Path ticketCache, - final AuthenticationEnum authentication - ) { - this.ticketCache = ticketCache; - this.authentication = authentication; - - password = winRMEndpoint.getPassword(); - canonizedRawUsername = - winRMEndpoint.getRawUsername() != null - ? winRMEndpoint.getRawUsername().replaceAll("\\s", Utils.EMPTY).toUpperCase() - : null; - } - - @Override - public int hashCode() { - final int prime = 31; - int result = 1; - result = prime * result + Arrays.hashCode(password); - result = prime * result + Objects.hash(authentication, canonizedRawUsername, ticketCache); - return result; - } - - @Override - public boolean equals(final Object obj) { - if (this == obj) { - return true; - } - if (obj == null) { - return false; - } - if (!(obj instanceof CredentialsMapKey)) { - return false; - } - final CredentialsMapKey other = (CredentialsMapKey) obj; - return ( - authentication == other.authentication && - Objects.equals(canonizedRawUsername, other.canonizedRawUsername) && - Arrays.equals(password, other.password) && - Objects.equals(ticketCache, other.ticketCache) - ); - } - } -} diff --git a/src/main/java/org/metricshub/winrm/service/WinRMService.java b/src/main/java/org/metricshub/winrm/service/WinRMService.java deleted file mode 100644 index 6819c3c..0000000 --- a/src/main/java/org/metricshub/winrm/service/WinRMService.java +++ /dev/null @@ -1,886 +0,0 @@ -package org.metricshub.winrm.service; - -/*- - * ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲ - * WinRM Java Client - * ჻჻჻჻჻჻ - * Copyright 2023 - 2024 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. - * ╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱ - */ - -import jakarta.xml.bind.JAXBElement; -import jakarta.xml.ws.BindingProvider; -import jakarta.xml.ws.soap.SOAPFaultException; -import java.io.IOException; -import java.io.StringWriter; -import java.io.Writer; -import java.lang.reflect.Proxy; -import java.math.BigDecimal; -import java.nio.charset.Charset; -import java.nio.charset.StandardCharsets; -import java.nio.file.Path; -import java.text.DecimalFormat; -import java.text.DecimalFormatSymbols; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.TimeoutException; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.stream.Collectors; -import java.util.stream.IntStream; -import javax.xml.namespace.QName; -import javax.xml.parsers.DocumentBuilderFactory; -import javax.xml.parsers.ParserConfigurationException; -import javax.xml.xpath.XPath; -import javax.xml.xpath.XPathExpressionException; -import javax.xml.xpath.XPathFactory; -import org.apache.cxf.Bus; -import org.apache.cxf.Bus.BusState; -import org.apache.cxf.BusFactory; -import org.apache.cxf.endpoint.Client; -import org.apache.cxf.transport.http.HTTPConduitFactory; -import org.apache.cxf.transport.http.asyncclient.AsyncHTTPConduit; -import org.apache.cxf.transport.http.asyncclient.AsyncHTTPConduitFactory; -import org.apache.cxf.transport.http.asyncclient.AsyncHTTPConduitFactory.UseAsyncPolicy; -import org.metricshub.winrm.Utils; -import org.metricshub.winrm.WindowsRemoteCommandResult; -import org.metricshub.winrm.WindowsRemoteExecutor; -import org.metricshub.winrm.WmiHelper; -import org.metricshub.winrm.exceptions.WinRMException; -import org.metricshub.winrm.exceptions.WqlQuerySyntaxException; -import org.metricshub.winrm.service.client.WinRMInvocationHandler; -import org.metricshub.winrm.service.client.auth.AuthenticationEnum; -import org.metricshub.winrm.service.enumeration.Enumerate; -import org.metricshub.winrm.service.enumeration.EnumerateResponse; -import org.metricshub.winrm.service.enumeration.EnumerationContextType; -import org.metricshub.winrm.service.enumeration.FilterType; -import org.metricshub.winrm.service.enumeration.Pull; -import org.metricshub.winrm.service.enumeration.PullResponse; -import org.metricshub.winrm.service.shell.CommandLine; -import org.metricshub.winrm.service.shell.CommandStateType; -import org.metricshub.winrm.service.shell.DesiredStreamType; -import org.metricshub.winrm.service.shell.Receive; -import org.metricshub.winrm.service.shell.ReceiveResponse; -import org.metricshub.winrm.service.shell.Shell; -import org.metricshub.winrm.service.shell.StreamType; -import org.metricshub.winrm.service.transfer.ResourceCreated; -import org.metricshub.winrm.service.wsman.AnyListType; -import org.metricshub.winrm.service.wsman.CommandResponse; -import org.metricshub.winrm.service.wsman.Delete; -import org.metricshub.winrm.service.wsman.Locale; -import org.metricshub.winrm.service.wsman.MixedDataType; -import org.metricshub.winrm.service.wsman.OptionSetType; -import org.metricshub.winrm.service.wsman.OptionType; -import org.metricshub.winrm.service.wsman.SelectorSetType; -import org.metricshub.winrm.service.wsman.SelectorType; -import org.metricshub.winrm.service.wsman.Signal; -import org.w3c.dom.Document; -import org.w3c.dom.Element; -import org.w3c.dom.Node; -import org.w3c.dom.NodeList; - -public class WinRMService implements WindowsRemoteExecutor { - - public static final List DEFAULT_AUTHENTICATION = Collections.singletonList( - AuthenticationEnum.NTLM - ); - - private static final String STDERR = "stderr"; - private static final String STDOUT = "stdout"; - - private static final int MAX_ENVELOPE_SIZE = 153600; - - private static final String ENUMERATION_NAMESPACE = "http://schemas.xmlsoap.org/ws/2004/09/enumeration"; - - private static final String WSMAN_URI = "http://schemas.microsoft.com/wbem/wsman/1"; - - private static final String DIALECT_WQL = WSMAN_URI + "/WQL"; - - private static final String SHELL_URI = WSMAN_URI + "/windows/shell"; - private static final String COMMAND_RESOURCE_URI = SHELL_URI + "/cmd"; - private static final String COMMAND_STATE_DONE = SHELL_URI + "/CommandState/Done"; - private static final String TERMINATE_CODE = SHELL_URI + "/signal/terminate"; - - private static final QName WSEN_ITEMS_QNAME = new QName(ENUMERATION_NAMESPACE, "Items"); - - private static final QName WSMAN_ITEMS_QNAME = new QName(WinRMInvocationHandler.WSMAN_SCHEMA_NAMESPACE, "Items"); - - private static final QName WSMAN_END_OF_SEQUENCE_QNAME = new QName( - WinRMInvocationHandler.WSMAN_SCHEMA_NAMESPACE, - "EndOfSequence" - ); - - private static final QName WSEN_END_OF_SEQUENCE_QNAME = new QName(ENUMERATION_NAMESPACE, "EndOfSequence"); - - private static final QName WSMAN_XML_FRAGMENT_QNAME = new QName( - WinRMInvocationHandler.WSMAN_SCHEMA_NAMESPACE, - "XmlFragment" - ); - - private static final DocumentBuilderFactory DOCUMENT_BUILDER_FACTORY = DocumentBuilderFactory.newInstance(); - - /** - * If no output is available before the wsman:OperationTimeout expires, the server MUST return a WSManFault with - * the Code attribute equal to "2150858793" - * https://msdn.microsoft.com/en-us/library/cc251676.aspx - */ - private static final String WSMAN_FAULT_CODE_OPERATION_TIMEOUT_EXPIRED = "2150858793"; - - /** - * Example response: - * [truncated]The request for the Windows Remote Shell with ShellId xxxx-yyyy-ccc... failed because the shell - * was not found on the server. - * Possible causes are: the specified ShellId is incorrect or the shell no longer exist - */ - private static final String WSMAN_FAULT_CODE_SHELL_WAS_NOT_FOUND = "2150858843"; - - private static final Locale LOCALE; - - static { - LOCALE = new Locale(); - LOCALE.setLang(java.util.Locale.US.toLanguageTag()); - } - - private static final OptionSetType OPTION_SET_CREATE; - - static { - final OptionType optNoProfile = new OptionType(); - optNoProfile.setName("WINRS_NOPROFILE"); - optNoProfile.setValue("true"); - - final OptionType optCodepage = new OptionType(); - optCodepage.setName("WINRS_CODEPAGE"); - optCodepage.setValue("437"); - - OPTION_SET_CREATE = new OptionSetType(); - OPTION_SET_CREATE.getOption().add(optNoProfile); - OPTION_SET_CREATE.getOption().add(optCodepage); - } - - private static final OptionSetType OPTION_SET_COMMAND; - - static { - final OptionType optConsoleModeStdin = new OptionType(); - optConsoleModeStdin.setName("WINRS_CONSOLEMODE_STDIN"); - optConsoleModeStdin.setValue("true"); - - final OptionType optSkipCmdShell = new OptionType(); - optSkipCmdShell.setName("WINRS_SKIP_CMD_SHELL"); - optSkipCmdShell.setValue("false"); - - OPTION_SET_COMMAND = new OptionSetType(); - OPTION_SET_COMMAND.getOption().add(optConsoleModeStdin); - OPTION_SET_COMMAND.getOption().add(optSkipCmdShell); - } - - private static final ConcurrentHashMap CONNECTIONS_CACHE = new ConcurrentHashMap<>(); - - private final AtomicInteger useCount = new AtomicInteger(1); - - private final WinRMEndpoint winRMEndpoint; - private final Bus bus; - private final WinRMWebService cmdWS; - private final WinRMWebService wqlWS; - private final Client cmdClient; - private final Client wqlClient; - private final String strTimeout; - - private SelectorSetType shellSelector = null; - - /** - * The WinRMService constructor. - * - * @param winRMEndpoint Endpoint with credentials - * @param bus Apache CXF Bus - * @param cmdInvocation The WinRM web service for executing commands - * @param wqlInvocation The WinRM web service for executing WQL queries - * @param timeout Timeout in milliseconds - */ - private WinRMService( - final WinRMEndpoint winRMEndpoint, - final Bus bus, - final WinRMInvocationHandler cmdInvocation, - final WinRMInvocationHandler wqlInvocation, - final long timeout - ) { - this.winRMEndpoint = winRMEndpoint; - this.bus = bus; - this.cmdWS = createProxyService(cmdInvocation); - this.wqlWS = createProxyService(wqlInvocation); - this.cmdClient = cmdInvocation.getClient(); - this.wqlClient = wqlInvocation.getClient(); - - final BigDecimal timeoutSec = BigDecimal.valueOf(timeout).divide(BigDecimal.valueOf(1000)); - final DecimalFormat decimalFormat = new DecimalFormat("PT#.###S", new DecimalFormatSymbols(java.util.Locale.ROOT)); - this.strTimeout = decimalFormat.format(timeoutSec); - } - - /** - * Create a WinRMService instance - * - * @param winRMEndpoint Endpoint with credentials (mandatory) - * @param timeout Timeout used for Connection, Connection Request and Receive Request - * in milliseconds (throws an IllegalArgumentException if negative or zero) - * @param ticketCache The Ticket Cache path - * @param authentications List of authentications. only NTLM if absent - * - * @return WinRMService instance - * - * @throws WinRMException For any problem encountered - */ - public static WinRMService createInstance( - final WinRMEndpoint winRMEndpoint, - final long timeout, - final Path ticketCache, - final List authentications - ) throws WinRMException { - Utils.checkNonNull(winRMEndpoint, "winRMEndpoint"); - Utils.checkArgumentNotZeroOrNegative(timeout, "timeout"); - - final List normalizedAuthentications = authentications == null - ? DEFAULT_AUTHENTICATION - : authentications.stream().distinct().collect(Collectors.toList()); - - try { - return CONNECTIONS_CACHE.compute( - winRMEndpoint, - (key, win) -> { - if (win == null) { - final Bus bus = BusFactory.newInstance().createBus(); - - // Needed to be async to force the use of Apache HTTP Components client. - // Details at http://cxf.apache.org/docs/asynchronous-client-http-transport.html. - // Apache HTTP Components needed to support NTLM authentication. - bus.getProperties().put(AsyncHTTPConduit.USE_ASYNC, Boolean.TRUE); - bus.getProperties().put(AsyncHTTPConduitFactory.USE_POLICY, UseAsyncPolicy.ALWAYS); - - final WinRMInvocationHandler cmdInvocation = createWinRMInvocationHandlerInstance( - winRMEndpoint, - bus, - timeout, - null, - ticketCache, - normalizedAuthentications - ); - - final WinRMInvocationHandler wqlInvocation = createWinRMInvocationHandlerInstance( - winRMEndpoint, - bus, - timeout, - String.format("%s/wmi/%s/*", WSMAN_URI, winRMEndpoint.getNamespace()), - ticketCache, - normalizedAuthentications - ); - - return new WinRMService(winRMEndpoint, bus, cmdInvocation, wqlInvocation, timeout); - } else { - synchronized (win) { - win.incrementUseCount(); - - return win; - } - } - } - ); - } catch (final RuntimeException e) { - if (e.getCause() != null) { - final String message = e.getMessage() != null - ? String.format( - "%s\n%s: %s", - e.getMessage(), - e.getCause().getClass().getSimpleName(), - e.getCause().getMessage() - ) - : String.format("%s: %s", e.getCause().getClass().getSimpleName(), e.getCause().getMessage()); - throw new WinRMException(e.getCause(), message); - } - - throw new WinRMException(e.getMessage()); - } - } - - public int getUseCount() { - return useCount.get(); - } - - /** - * @return whether this WbemServices instance is connected and usable - */ - public boolean isConnected() { - return getUseCount() > 0; - } - - void incrementUseCount() { - useCount.incrementAndGet(); - } - - /** - * Check if it's connected. If not, throw an IllegalStateException. - */ - public void checkConnectedFirst() { - if (!isConnected()) { - throw new IllegalStateException("This instance has been closed and a new one must be created."); - } - } - - @Override - public void close() { - if (useCount.decrementAndGet() == 0) { - CONNECTIONS_CACHE.remove(winRMEndpoint); - - if (shellSelector != null) { - cmdWS.delete(new Delete(), COMMAND_RESOURCE_URI, MAX_ENVELOPE_SIZE, strTimeout, LOCALE, shellSelector); - - shellSelector = null; - } - - if (cmdClient != null) { - shutdownConduitFactory(cmdClient); - cmdClient.destroy(); - } - - if (wqlClient != null) { - shutdownConduitFactory(wqlClient); - wqlClient.destroy(); - } - - if (bus != null && bus.getState() != BusState.SHUTDOWN) { - bus.shutdown(true); - } - } - } - - /** - * Retrieves the {@link AsyncHTTPConduitFactory} registered on the given client's endpoint and calls - * {@link AsyncHTTPConduitFactory#shutdown()} on it to stop any background threads (e.g. the idle-connection - * reaper thread). This must be done before destroying the client to prevent thread leaks. - * - * @param client the CXF {@link Client} whose conduit factory should be shut down - */ - private void shutdownConduitFactory(final Client client) { - final Object factory = client.getEndpoint().getEndpointInfo().getProperty(HTTPConduitFactory.class.getName()); - if (factory instanceof AsyncHTTPConduitFactory) { - ((AsyncHTTPConduitFactory) factory).shutdown(); - } - } - - @Override - public WindowsRemoteCommandResult executeCommand( - final String command, - final String workingDirectory, - final Charset charset, - final long timeout - ) throws WinRMException, TimeoutException { - Utils.checkNonNull(command, "command"); - Utils.checkArgumentNotZeroOrNegative(timeout, "timeout"); - - checkConnectedFirst(); - - try { - return Utils.execute( - () -> { - if (getShellSelector() == null) { - create(workingDirectory); - } - - try { - final StringWriter stdout = new StringWriter(); - final StringWriter stderr = new StringWriter(); - final Charset cs = charset != null ? charset : StandardCharsets.UTF_8; - - final long start = Utils.getCurrentTimeMillis(); - final int statusCode = execute(command, stdout, stderr, cs); - final float executionTime = (Utils.getCurrentTimeMillis() - start) / 1000.0f; - - return new WindowsRemoteCommandResult(stdout.toString(), stderr.toString(), executionTime, statusCode); - } catch (final WinRMException e) { - throw new RuntimeException(e); - } - }, - timeout - ); - } catch (final InterruptedException | ExecutionException e) { - if (e.getCause() != null) { - throw new WinRMException(e.getCause(), e.getCause().getMessage()); - } - throw new WinRMException(e); - } - } - - @Override - public List> executeWql(final String wqlQuery, final long timeout) - throws WinRMException, WqlQuerySyntaxException, TimeoutException { - Utils.checkNonNull(wqlQuery, "wqlQuery"); - if (!WmiHelper.isValidWql(wqlQuery)) { - throw new WqlQuerySyntaxException(wqlQuery); - } - Utils.checkArgumentNotZeroOrNegative(timeout, "timeout"); - - checkConnectedFirst(); - - try { - return Utils.execute( - () -> { - final List nodes = new ArrayList<>(); - - final EnumerateResponse enumerateResponse = enumerate(wqlQuery); - - final boolean endOfSequence = getItemsFrom(enumerateResponse, nodes); - if (!endOfSequence) { - final String nextContextId = getContextIdFrom(enumerateResponse.getEnumerationContext()); - pull(nextContextId, nodes); - } - - return nodes.stream().map(WinRMService::convertRow).collect(Collectors.toList()); - }, - timeout - ); - } catch (final InterruptedException | ExecutionException e) { - if (e.getCause() != null) { - throw new WinRMException(e.getCause(), e.getCause().getMessage()); - } - throw new WinRMException(e); - } - } - - public static WinRMInvocationHandler createWinRMInvocationHandlerInstance( - final WinRMEndpoint winRMEndpoint, - final Bus bus, - final long timeout, - final String resourceUri, - final Path ticketCache, - final List authentications - ) { - return new WinRMInvocationHandler(winRMEndpoint, bus, timeout, resourceUri, ticketCache, authentications); - } - - private static WinRMWebService createProxyService(final WinRMInvocationHandler winRMInvocationHandler) { - return (WinRMWebService) Proxy.newProxyInstance( - WinRMWebService.class.getClassLoader(), - new Class[] { WinRMWebService.class, BindingProvider.class }, - winRMInvocationHandler - ); - } - - public EnumerateResponse enumerate(final String wqlQuery) { - final FilterType filterType = new FilterType(); - filterType.setDialect(DIALECT_WQL); - filterType.getContent().add(wqlQuery); - - final Enumerate body = new Enumerate(); - body.setFilter(filterType); - - return wqlWS.enumerate(body); - } - - public String pull(final String contextId, final List nodes) throws WinRMException { - final EnumerationContextType enumContext = new EnumerationContextType(); - enumContext.getContent().add(contextId); - - final Pull body = new Pull(); - body.setEnumerationContext(enumContext); - - final PullResponse response = wqlWS.pull(body); - - if (response == null) { - throw new WinRMException(String.format("Pull failed for context id: %s", contextId)); - } - - final boolean endOfSequence = getItemsFrom(response, nodes); - final String nextContextId = response.getEnumerationContext() == null - ? null // The PullResponse will not contain an EnumerationContext if EndOfSequence is set - : getContextIdFrom(response.getEnumerationContext()); - - return endOfSequence ? nextContextId : pull(nextContextId, nodes); // If we're pulling recursively, and we haven't hit the last element, continue pulling - } - - public ResourceCreated create(final String workingDirectory) { - final Shell shell = new Shell(); - shell.getInputStreams().add("stdin"); - shell.getOutputStreams().add(STDOUT); - shell.getOutputStreams().add(STDERR); - - if (Utils.isNotBlank(workingDirectory)) { - shell.setWorkingDirectory(workingDirectory); - } - - final ResourceCreated resourceCreated = cmdWS.create( - shell, - COMMAND_RESOURCE_URI, - MAX_ENVELOPE_SIZE, - strTimeout, - LOCALE, - OPTION_SET_CREATE - ); - - final String shellId = getShellId(resourceCreated); - - shellSelector = new SelectorSetType(); - final SelectorType selectorType = new SelectorType(); - selectorType.setName("ShellId"); - selectorType.getContent().add(shellId); - shellSelector.getSelector().add(selectorType); - - return resourceCreated; - } - - public int execute(final String command, final Writer out, final Writer err, final Charset charset) - throws WinRMException { - final CommandLine body = new CommandLine(); - body.setCommand(command); - - final CommandResponse commandResponse = cmdWS.command( - body, - COMMAND_RESOURCE_URI, - MAX_ENVELOPE_SIZE, - strTimeout, - LOCALE, - shellSelector, - OPTION_SET_COMMAND - ); - - final String commandId = commandResponse.getCommandId(); - - try { - return receiveCommand(commandId, out, err, charset); - } finally { - try { - final Signal signal = new Signal(); - signal.setCommandId(commandId); - signal.setCode(TERMINATE_CODE); - - cmdWS.signal(signal, COMMAND_RESOURCE_URI, MAX_ENVELOPE_SIZE, strTimeout, LOCALE, shellSelector); - } catch (final SOAPFaultException soapFault) { - assertFaultCode(soapFault, WSMAN_FAULT_CODE_SHELL_WAS_NOT_FOUND, true); - } - } - } - - private int receiveCommand(final String commandId, final Writer out, final Writer err, final Charset charset) - throws WinRMException { - while (true) { - final DesiredStreamType stream = new DesiredStreamType(); - stream.setCommandId(commandId); - stream.setValue("stdout stderr"); - - final Receive receive = new Receive(); - receive.setDesiredStream(stream); - - try { - final ReceiveResponse receiveResponse = cmdWS.receive( - receive, - COMMAND_RESOURCE_URI, - MAX_ENVELOPE_SIZE, - strTimeout, - LOCALE, - shellSelector - ); - getStreams(receiveResponse, out, err, charset); - - final CommandStateType state = receiveResponse.getCommandState(); - if (COMMAND_STATE_DONE.equals(state.getState())) { - return state.getExitCode().intValue(); - } - } catch (final SOAPFaultException soapFault) { - // If such Exception which has a code 2150858793 the client is expected to again trigger immediately - // a receive request. https://msdn.microsoft.com/en-us/library/cc251676.aspx - assertFaultCode(soapFault, WSMAN_FAULT_CODE_OPERATION_TIMEOUT_EXPIRED, false); - } - } - } - - private static Map convertRow(final Node node) { - return IntStream - .range(0, node.getChildNodes().getLength()) - .mapToObj(node.getChildNodes()::item) - .filter(Objects::nonNull) - .collect(HashMap::new, (map, child) -> map.put(child.getLocalName(), child.getTextContent()), HashMap::putAll); - } - - private static String getShellId(final ResourceCreated resourceCreated) { - final XPath xpath = XPathFactory.newInstance().newXPath(); - - for (final Element element : resourceCreated.getAny()) { - try { - final String shellId = xpath.evaluate("//*[local-name()='Selector' and @Name='ShellId']", element); - if (shellId != null && !shellId.isEmpty()) { - return shellId; - } - } catch (final XPathExpressionException e) { - throw new IllegalStateException(e); - } - } - throw new IllegalStateException("Shell ID not fount in " + resourceCreated); - } - - private static void assertFaultCode(final SOAPFaultException soapFault, final String code, final boolean retry) { - try { - final NodeList faultDetails = soapFault.getFault().getDetail().getChildNodes(); - - for (int i = 0; i < faultDetails.getLength(); i++) { - final Node item = faultDetails.item(i); - - if ("WSManFault".equals(item.getLocalName())) { - if (retry && code.equals(item.getAttributes().getNamedItem("Code").getNodeValue())) { - return; - } - throw soapFault; - } - } - throw soapFault; - } catch (final NullPointerException e) { - throw soapFault; - } - } - - private void getStreams( - final ReceiveResponse receiveResponse, - final Writer out, - final Writer err, - final Charset charset - ) throws WinRMException { - final List streams = receiveResponse.getStream(); - for (final StreamType streamType : streams) { - final byte[] value = streamType.getValue(); - if (value == null) { - continue; - } - - writeStd(out, STDOUT, streamType, value, charset); - writeStd(err, STDERR, streamType, value, charset); - } - } - - private void writeStd( - final Writer std, - final String name, - final StreamType streamType, - final byte[] value, - final Charset charset - ) throws WinRMException { - if (std == null || !name.equals(streamType.getName())) { - return; - } - - try { - if (value.length > 0) { - std.write(new String(value, charset)); - std.flush(); - } - - if (streamType.isEnd() != null && streamType.isEnd().booleanValue()) { - std.close(); - } - } catch (final IOException e) { - throw new WinRMException(e); - } - } - - /** - * Retrieves the list of items from the given response, adding them to the given - * list and returns true if the response contains an 'end-of-sequence' marker. - * @throws WinRMException - */ - public boolean getItemsFrom(final EnumerateResponse response, final List items) throws WinRMException { - for (final Object object : response.getAny()) { - if (object instanceof JAXBElement) { - final JAXBElement jaxbElement = (JAXBElement) object; - - if (WSEN_ITEMS_QNAME.equals(jaxbElement.getName()) || WSMAN_ITEMS_QNAME.equals(jaxbElement.getName())) { - if (jaxbElement.isNil()) { - // No items - } else if (jaxbElement.getValue() instanceof AnyListType) { - // some items - final AnyListType itemList = (AnyListType) jaxbElement.getValue(); - for (final Object item : itemList.getAny()) { - final Node node = toNode(item) - .orElseThrow(() -> - new WinRMException( - "Unsupported element of type %s in EnumerateResponse: %s", - object.getClass(), - object - ) - ); - - items.add(node); - } - } else { - throw new WinRMException( - "Unsupported value in EnumerateResponse Items: %s of type: %s", - jaxbElement.getValue(), - jaxbElement.getValue().getClass() - ); - } - } else if ( - WSEN_END_OF_SEQUENCE_QNAME.equals(jaxbElement.getName()) || - WSMAN_END_OF_SEQUENCE_QNAME.equals(jaxbElement.getName()) - ) { - return true; - } else { - throw new WinRMException( - "Unsupported element in EnumerateResponse: %s with name: %s", - jaxbElement, - jaxbElement.getName() - ); - } - } else if (object instanceof Node) { - final Node node = (Node) object; - - if ( - (WSEN_END_OF_SEQUENCE_QNAME.getNamespaceURI().equals(node.getNamespaceURI()) && - WSEN_END_OF_SEQUENCE_QNAME.getLocalPart().equals(node.getLocalName())) || - (WSMAN_END_OF_SEQUENCE_QNAME.getNamespaceURI().equals(node.getNamespaceURI()) && - WSMAN_END_OF_SEQUENCE_QNAME.getLocalPart().equals(node.getLocalName())) - ) { - return true; - } - throw new WinRMException( - "Unsupported node in EnumerateResponse: %s with namespace: %s", - node.toString(), - node.getNamespaceURI() - ); - } else { - throw new WinRMException( - "Unsupported element in EnumerateResponse: %s, with type: %s", - object, - object != null ? object.getClass() : null - ); - } - } - - return false; - } - - private static boolean getItemsFrom(final PullResponse response, final List items) throws WinRMException { - for (final Object item : response.getItems().getAny()) { - final Node node = toNode(item) - .orElseThrow(() -> - new WinRMException( - "The pull response contains an unsupported item %s of type %s", - item, - item != null ? item.getClass() : null - ) - ); - - items.add(node); - } - return response.getEndOfSequence() != null; - } - - private static Optional toNode(final Object item) throws WinRMException { - if (item instanceof Node) { - return Optional.of((Node) item); - } - - if (item instanceof JAXBElement) { - final JAXBElement nestedElement = (JAXBElement) item; - if ( - WSMAN_XML_FRAGMENT_QNAME.equals(nestedElement.getName()) && - !nestedElement.isNil() && - nestedElement.getValue() instanceof MixedDataType - ) { - // Create a new document/node that contains the elements within the fragment - final Document document = createNewDocument(); - final Element rootElement = document.createElementNS( - WSMAN_XML_FRAGMENT_QNAME.getNamespaceURI(), - WSMAN_XML_FRAGMENT_QNAME.getLocalPart() - ); - document.appendChild(rootElement); - - final MixedDataType mixed = (MixedDataType) nestedElement.getValue(); - for (final Object nestedItem : mixed.getContent()) { - if (nestedItem instanceof String) { - // Skip over whitespace - } else if (nestedItem instanceof Node) { - // Node's can't belong to two different documents, so we need to import it first - final Node nestedNode = document.importNode((Node) nestedItem, true); - rootElement.appendChild(nestedNode); - } else { - throw new WinRMException( - "Unsupported element of type %s in XmlFragment: %s", - nestedItem.getClass(), - nestedItem - ); - } - } - return Optional.of(rootElement); - } - } - return Optional.empty(); - } - - private static Document createNewDocument() throws WinRMException { - // The DocumentBuilderFactory provides no guarantees on thread safety - // so we lock it in order to avoid creating new or separate instances per thread - synchronized (DOCUMENT_BUILDER_FACTORY) { - try { - return DOCUMENT_BUILDER_FACTORY.newDocumentBuilder().newDocument(); - } catch (final ParserConfigurationException e) { - throw new WinRMException(e); - } - } - } - - public String getContextIdFrom(final EnumerationContextType context) throws WinRMException { - // The content of the EnumerationContext should contain a single string, the context id - if (context == null || context.getContent() == null) { - throw new WinRMException("EnumerationContext %s has no content.", context); - } - - if (context.getContent().isEmpty()) { - // The EnumerationContext can be empty if we issue an optimized enumeration - // and all of the records are immediately returned - return null; - } - - if (context.getContent().size() == 1) { - final Object content = context.getContent().get(0); - if (content instanceof String) { - return (String) content; - } - throw new WinRMException("Unsupported EnumerationContext content: %s", content); - } - - throw new WinRMException( - "EnumerationContext contains too many elements, expected: 1 actual: %d", - context.getContent().size() - ); - } - - public SelectorSetType getShellSelector() { - return shellSelector; - } - - @Override - public String getHostname() { - return winRMEndpoint.getHostname(); - } - - @Override - public String getUsername() { - return winRMEndpoint.getRawUsername(); - } - - @Override - public char[] getPassword() { - return winRMEndpoint.getPassword(); - } -} diff --git a/src/main/java/org/metricshub/winrm/service/client/StripShellResponseHandler.java b/src/main/java/org/metricshub/winrm/service/client/StripShellResponseHandler.java deleted file mode 100644 index cd21e26..0000000 --- a/src/main/java/org/metricshub/winrm/service/client/StripShellResponseHandler.java +++ /dev/null @@ -1,95 +0,0 @@ -package org.metricshub.winrm.service.client; - -/*- - * ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲ - * WinRM Java Client - * ჻჻჻჻჻჻ - * Copyright 2023 - 2024 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. - * ╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱ - */ - -import static jakarta.xml.ws.handler.MessageContext.WSDL_OPERATION; - -import jakarta.xml.soap.SOAPBody; -import jakarta.xml.soap.SOAPElement; -import jakarta.xml.soap.SOAPEnvelope; -import jakarta.xml.soap.SOAPException; -import jakarta.xml.ws.handler.MessageContext; -import jakarta.xml.ws.handler.soap.SOAPHandler; -import jakarta.xml.ws.handler.soap.SOAPMessageContext; -import java.util.Collections; -import java.util.Iterator; -import java.util.Set; -import javax.xml.namespace.QName; - -/** - * Code from io.cloudsoft.winrm4j.client.StripShellResponseHandler - * release 0.12.3 @link https://github.com/cloudsoft/winrm4j - */ -public class StripShellResponseHandler implements SOAPHandler { - - @Override - public boolean handleMessage(final SOAPMessageContext context) { - final Boolean messageOutbound = (Boolean) context.get(MessageContext.MESSAGE_OUTBOUND_PROPERTY); - if (messageOutbound != null && messageOutbound.booleanValue()) { - return true; - } - - final QName action = (QName) context.get(WSDL_OPERATION); - if (action != null && !"Create".equals(action.getLocalPart())) { - return true; - } - - final Iterator childIterator = getBodyChildren(context); - while (childIterator.hasNext()) { - final Object node = childIterator.next(); - - if (node instanceof SOAPElement) { - final SOAPElement soapElement = (SOAPElement) node; - if ("Shell".equals(soapElement.getLocalName())) { - childIterator.remove(); - } - } - } - - return true; - } - - private Iterator getBodyChildren(final SOAPMessageContext context) { - try { - final SOAPEnvelope envelope = context.getMessage().getSOAPPart().getEnvelope(); - final SOAPBody body = envelope.getBody(); - - return body.getChildElements(); - } catch (final SOAPException e) { - throw new IllegalStateException(e); - } - } - - @Override - public boolean handleFault(final SOAPMessageContext context) { - return true; - } - - @Override - public void close(final MessageContext context) { - // Do nothing - } - - @Override - public Set getHeaders() { - return Collections.emptySet(); - } -} diff --git a/src/main/java/org/metricshub/winrm/service/client/WSManHeaderInterceptor.java b/src/main/java/org/metricshub/winrm/service/client/WSManHeaderInterceptor.java deleted file mode 100644 index 4753803..0000000 --- a/src/main/java/org/metricshub/winrm/service/client/WSManHeaderInterceptor.java +++ /dev/null @@ -1,73 +0,0 @@ -package org.metricshub.winrm.service.client; - -import jakarta.xml.bind.JAXBElement; -import jakarta.xml.bind.JAXBException; -/*- - * ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲ - * WinRM Java Client - * ჻჻჻჻჻჻ - * Copyright 2023 - 2024 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. - * ╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱ - */ - -import java.util.List; -import org.apache.cxf.binding.soap.SoapMessage; -import org.apache.cxf.binding.soap.interceptor.AbstractSoapInterceptor; -import org.apache.cxf.binding.soap.interceptor.SoapPreProtocolOutInterceptor; -import org.apache.cxf.headers.Header; -import org.apache.cxf.interceptor.Fault; -import org.apache.cxf.jaxb.JAXBDataBinding; -import org.apache.cxf.phase.Phase; -import org.metricshub.winrm.Utils; -import org.metricshub.winrm.service.wsman.AttributableURI; -import org.metricshub.winrm.service.wsman.ObjectFactory; - -/** - * Code from org.opennms.core.wsman.cxf.WSManHeaderInterceptor - * release 1.2.3 @link https://github.com/OpenNMS/wsman - */ -public class WSManHeaderInterceptor extends AbstractSoapInterceptor { - - private static final JAXBDataBinding ATTRIBUTABLE_URI_JAXB_DATA_BINDING; - - static { - try { - ATTRIBUTABLE_URI_JAXB_DATA_BINDING = new JAXBDataBinding(AttributableURI.class); - } catch (final JAXBException e) { - throw new RuntimeException("Failed to create JAXBDataBinding for: AttributableURI" + AttributableURI.class, e); - } - } - - private final String resourceUri; - - public WSManHeaderInterceptor(final String resourceUri) { - super(Phase.POST_LOGICAL); - addAfter(SoapPreProtocolOutInterceptor.class.getName()); - - Utils.checkNonNull(resourceUri, "resourceUri"); - - this.resourceUri = resourceUri; - } - - @Override - public void handleMessage(final SoapMessage message) throws Fault { - final JAXBElement resourceURI = new ObjectFactory().createResourceURI(resourceUri); - - final List
headers = message.getHeaders(); - headers.add(new Header(resourceURI.getName(), resourceURI, ATTRIBUTABLE_URI_JAXB_DATA_BINDING)); - - message.put(Header.HEADER_LIST, headers); - } -} diff --git a/src/main/java/org/metricshub/winrm/service/client/WinRMInvocationHandler.java b/src/main/java/org/metricshub/winrm/service/client/WinRMInvocationHandler.java deleted file mode 100644 index f90de91..0000000 --- a/src/main/java/org/metricshub/winrm/service/client/WinRMInvocationHandler.java +++ /dev/null @@ -1,561 +0,0 @@ -package org.metricshub.winrm.service.client; - -import jakarta.xml.ws.BindingProvider; -import jakarta.xml.ws.WebServiceException; -import jakarta.xml.ws.handler.Handler; -import jakarta.xml.ws.soap.SOAPFaultException; -/*- - * ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲ - * WinRM Java Client - * ჻჻჻჻჻჻ - * Copyright 2023 - 2024 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. - * ╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱ - */ - -import java.io.IOException; -import java.lang.reflect.InvocationHandler; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.net.URL; -import java.nio.file.Path; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Queue; -import java.util.concurrent.ConcurrentHashMap; -import java.util.stream.Collectors; -import java.util.stream.Stream; -import javax.net.ssl.TrustManager; -import javax.xml.namespace.QName; -import org.apache.cxf.Bus; -import org.apache.cxf.binding.soap.SoapBindingConstants; -import org.apache.cxf.configuration.jsse.TLSClientParameters; -import org.apache.cxf.endpoint.Client; -import org.apache.cxf.frontend.ClientProxy; -import org.apache.cxf.jaxws.JaxWsProxyFactoryBean; -import org.apache.cxf.message.Message; -import org.apache.cxf.service.model.EndpointInfo; -import org.apache.cxf.service.model.ServiceInfo; -import org.apache.cxf.transport.http.HTTPConduitFactory; -import org.apache.cxf.transport.http.asyncclient.AsyncHTTPConduit; -import org.apache.cxf.transports.http.configuration.HTTPClientPolicy; -import org.apache.cxf.ws.addressing.WSAddressingFeature; -import org.apache.cxf.ws.addressing.WSAddressingFeature.AddressingResponses; -import org.apache.cxf.ws.addressing.policy.MetadataConstants; -import org.apache.cxf.ws.policy.PolicyConstants; -import org.apache.http.auth.AuthSchemeProvider; -import org.apache.http.auth.Credentials; -import org.apache.http.auth.NTCredentials; -import org.apache.http.client.config.AuthSchemes; -import org.apache.http.config.Registry; -import org.apache.http.config.RegistryBuilder; -import org.apache.http.impl.auth.KerberosSchemeFactory; -import org.apache.neethi.Policy; -import org.apache.neethi.builders.PrimitiveAssertion; -import org.metricshub.winrm.Utils; -import org.metricshub.winrm.WinRMHttpProtocolEnum; -import org.metricshub.winrm.service.WinRMEndpoint; -import org.metricshub.winrm.service.WinRMWebService; -import org.metricshub.winrm.service.WinRMWebServiceClient; -import org.metricshub.winrm.service.client.auth.AuthenticationEnum; -import org.metricshub.winrm.service.client.auth.TrustAllX509Manager; -import org.metricshub.winrm.service.client.auth.kerberos.KerberosUtils; -import org.metricshub.winrm.service.client.auth.ntlm.NTCredentialsWithEncryption; -import org.metricshub.winrm.service.client.auth.ntlm.NtlmMasqAsSpnegoSchemeFactory; -import org.metricshub.winrm.service.client.encryption.AsyncHttpEncryptionAwareConduitFactory; -import org.metricshub.winrm.service.client.encryption.DecryptAndVerifyInInterceptor; -import org.metricshub.winrm.service.client.encryption.SignAndEncryptOutInterceptor; - -public class WinRMInvocationHandler implements InvocationHandler { - - public static final String WSMAN_SCHEMA_NAMESPACE = "http://schemas.dmtf.org/wbem/wsman/1/wsman.xsd"; - - private static final long PAUSE_TIME_MILLISECONDS = 500; - private static final int MAX_RETRY = 3; - - private static final URL WSDL_LOCATION_URL = - WinRMWebServiceClient.class.getClassLoader().getResource("wsdl/WinRM.wsdl"); - - private static final QName SERVICE = new QName(WSMAN_SCHEMA_NAMESPACE, "WinRMWebServiceClient"); - - private static final QName PORT = new QName(WSMAN_SCHEMA_NAMESPACE, "WinRMPort"); - - private static final List CONTENT_TYPE_LIST = Collections.singletonList("application/soap+xml;charset=UTF-8"); - - @SuppressWarnings("rawtypes") - private static final List HANDLER_CHAIN = Arrays.asList(new StripShellResponseHandler()); - - private static final Registry AUTH_SCHEME_REGISTRY = RegistryBuilder - .create() - .register(AuthSchemes.SPNEGO, new NtlmMasqAsSpnegoSchemeFactory()) - .register(AuthSchemes.KERBEROS, new KerberosSchemeFactory(true)) - .build(); - - private static final Policy POLICY; - - static { - POLICY = new Policy(); - POLICY.addAssertion(new PrimitiveAssertion(MetadataConstants.USING_ADDRESSING_2004_QNAME)); - } - - private static final WSAddressingFeature WS_ADDRESSING_FEATURE; - - static { - WS_ADDRESSING_FEATURE = new WSAddressingFeature(); - WS_ADDRESSING_FEATURE.setResponses(AddressingResponses.ANONYMOUS); - } - - private static final TLSClientParameters TLS_CLIENT_PARAMETERS; - - static { - TLS_CLIENT_PARAMETERS = new TLSClientParameters(); - TLS_CLIENT_PARAMETERS.setDisableCNCheck(true); - // Accept all certificates - TLS_CLIENT_PARAMETERS.setTrustManagers(new TrustManager[] { new TrustAllX509Manager() }); - } - - private static final Map CREDENTIALS = new ConcurrentHashMap<>(); - - private final WinRMWebService winRMWebService; - private final WinRMEndpoint winRMEndpoint; - private final long timeout; - private final String resourceUri; - private final Path ticketCache; - private final Queue authenticationsQueue; - private AuthenticationEnum authentication; - private Client wsClient; - - /** - * WinRMInvocationHandler constructor - * - * @param winRMEndpoint Endpoint with credentials (mandatory) - * @param bus Apache CXF Bus (mandatory) - * @param timeout Timeout used for Connection, Connection Request and Receive Request in milliseconds - * @param resourceUri The enumerate resource URI - * @param ticketCache The Ticket Cache path - * @param authentications List of authentications. (mandatory) - */ - public WinRMInvocationHandler( - final WinRMEndpoint winRMEndpoint, - final Bus bus, - final long timeout, - final String resourceUri, - final Path ticketCache, - final List authentications - ) { - Utils.checkNonNull(winRMEndpoint, "winRMEndpoint"); - Utils.checkNonNull(bus, "bus"); - Utils.checkNonNull(authentications, "authentications"); - - this.winRMEndpoint = winRMEndpoint; - this.timeout = timeout; - this.resourceUri = resourceUri; - this.ticketCache = ticketCache; - authenticationsQueue = authentications.stream().collect(Collectors.toCollection(LinkedList::new)); - - winRMWebService = createWinRMWebService(winRMEndpoint, bus); - - final AuthCredentials authCredentials = computeCredentials(winRMEndpoint, ticketCache, authenticationsQueue); - - authentication = authCredentials.getAuthentication(); - - wsClient = - getWebServiceClient(winRMEndpoint, timeout, resourceUri, winRMWebService, authCredentials.getCredentials()); - } - - public Client getClient() { - return wsClient; - } - - @Override - public Object invoke(final Object proxy, final Method method, final Object[] args) throws Throwable { - Utils.checkNonNull(method, "method"); - - try { - return invokeMethod(method, args); - } catch (final RetryTgtExpirationException e) { - // retry with a new TGT in case of current TGT expiration - authentication = null; - - Credentials credentials; - try { - credentials = - KerberosUtils.createCredentials(winRMEndpoint.getUsername(), winRMEndpoint.getPassword(), ticketCache); - - CREDENTIALS.put(new CredentialsMapKey(winRMEndpoint, ticketCache, AuthenticationEnum.KERBEROS), credentials); - // Normally that should not happen as any other exception on KERBEROs should had been throw - // at the first KERBEROS call - } catch (final Exception e1) { - if (continueToRetry()) { - final AuthCredentials authCredentials = computeCredentials(winRMEndpoint, ticketCache, authenticationsQueue); - - authentication = authCredentials.getAuthentication(); - credentials = authCredentials.getCredentials(); - } else { - throw e1; - } - } - - wsClient = getWebServiceClient(winRMEndpoint, timeout, resourceUri, winRMWebService, credentials); - - return invoke(proxy, method, args); - } catch (final RetryAuthenticationException e) { - if (continueToRetry()) { - final AuthCredentials authCredentials = computeCredentials(winRMEndpoint, ticketCache, authenticationsQueue); - - authentication = authCredentials.getAuthentication(); - - wsClient = - getWebServiceClient(winRMEndpoint, timeout, resourceUri, winRMWebService, authCredentials.getCredentials()); - - return invoke(proxy, method, args); - } - - // No more retries - final Throwable cause = e.getCause(); - if (cause instanceof SOAPFaultException) { - throw new RuntimeException("KERBEROS with encryption over HTTP is not implemented.", cause); - } - throw cause; - } - } - - // this function is only needed for the unit testing - boolean continueToRetry() { - return !authenticationsQueue.isEmpty(); - } - - Object invokeMethod(final Method method, final Object[] args) - throws IllegalAccessException, RetryAuthenticationException { - Throwable firstEx = null; - int retry = 0; - - while (retry < MAX_RETRY) { - retry++; - - try { - return method.invoke(winRMWebService, args); - } catch (final InvocationTargetException ite) { - final Throwable targetEx = ite.getTargetException(); - - if (targetEx instanceof SOAPFaultException) { - // Could retry with a different authentication than NTLM - // because it could be a "WstxEOFException: Unexpected EOF in prolog" - // due to a KERBEROS with HTTP and AllowUnencrypted=false - if (winRMEndpoint.getProtocol() == WinRMHttpProtocolEnum.HTTP && authentication != AuthenticationEnum.NTLM) { - throw new RetryAuthenticationException(targetEx); - } - throw (SOAPFaultException) targetEx; - } - - if (!(targetEx instanceof WebServiceException)) { - throw new IllegalStateException("Failure when calling " + createCallInfos(method, args), targetEx); - } - - final WebServiceException wsEx = (WebServiceException) targetEx; - - if (!(wsEx.getCause() instanceof IOException)) { - throw new RuntimeException( - "Exception occurred while making WinRM WebService call " + createCallInfos(method, args), - wsEx - ); - } - - if ( - wsEx.getCause().getMessage() != null && - wsEx.getCause().getMessage().startsWith("Authorization loop detected on Conduit") - ) { - final RuntimeException authEx = new RuntimeException( - String.format( - "Authentication error on %s with user name \"%s\"", - winRMEndpoint.getEndpoint(), - winRMEndpoint.getRawUsername() - ) - ); - - // Could be due to a TGT expiration - if (authentication == AuthenticationEnum.KERBEROS) { - throw new RetryTgtExpirationException(authEx); - } - // Could retry with a different authentication - throw new RetryAuthenticationException(authEx); - } - - if (firstEx == null) { - firstEx = wsEx; - } - - if (retry < MAX_RETRY) { - try { - Utils.sleep(PAUSE_TIME_MILLISECONDS); - } catch (final InterruptedException ie) { - Thread.currentThread().interrupt(); - throw new RuntimeException( - "Exception occured while making WinRM WebService call " + createCallInfos(method, args), - ie - ); - } - } - } - } - - throw new RuntimeException( - String.format("failed task \"%s\" after %d attempts", createCallInfos(method, args), MAX_RETRY), - firstEx - ); - } - - static String createCallInfos(final Method method, final Object[] args) { - final String name = method != null && method.getName() != null ? method.getName() : Utils.EMPTY; - return args == null - ? name - : Stream - .concat(Stream.of(name), Stream.of(args)) - .filter(Objects::nonNull) - .map(Object::toString) - .collect(Collectors.joining(" ")); - } - - static Credentials createCredentials( - final WinRMEndpoint winRMEndpoint, - final AuthenticationEnum authentication, - final Path ticketCache - ) { - switch (authentication) { - case KERBEROS: - return KerberosUtils.createCredentials(winRMEndpoint.getUsername(), winRMEndpoint.getPassword(), ticketCache); - case NTLM: - default: - final String password = String.valueOf(winRMEndpoint.getPassword()); - return winRMEndpoint.getProtocol() == WinRMHttpProtocolEnum.HTTP - ? new NTCredentialsWithEncryption(winRMEndpoint.getUsername(), password, null, winRMEndpoint.getDomain()) - : new NTCredentials(winRMEndpoint.getUsername(), password, null, winRMEndpoint.getDomain()); - } - } - - static AuthCredentials computeCredentials( - final WinRMEndpoint winRMEndpoint, - final Path ticketCache, - final Queue authenticationsQueue - ) { - try { - final AuthenticationEnum authenticationEnum = authenticationsQueue.remove(); - - final Credentials credentials = CREDENTIALS.compute( - new CredentialsMapKey(winRMEndpoint, ticketCache, authenticationEnum), - (user, cred) -> cred != null ? cred : createCredentials(winRMEndpoint, authenticationEnum, ticketCache) - ); - - return new AuthCredentials(authenticationEnum, credentials); - } catch (final Exception e) { - // if there's still retry - if (!authenticationsQueue.isEmpty()) { - return computeCredentials(winRMEndpoint, ticketCache, authenticationsQueue); - } - throw e; - } - } - - static WinRMWebService createWinRMWebService(final WinRMEndpoint winRMEndpoint, final Bus bus) { - final JaxWsProxyFactoryBean jaxWsProxyFactoryBean = new JaxWsProxyFactoryBean(); - jaxWsProxyFactoryBean.setServiceName(SERVICE); - jaxWsProxyFactoryBean.setEndpointName(PORT); - jaxWsProxyFactoryBean.setBus(bus); - jaxWsProxyFactoryBean.setServiceClass(WinRMWebService.class); - jaxWsProxyFactoryBean.setAddress(winRMEndpoint.getEndpoint()); - jaxWsProxyFactoryBean.getFeatures().add(WS_ADDRESSING_FEATURE); - jaxWsProxyFactoryBean.setBindingId(SoapBindingConstants.SOAP12_BINDING_ID); - jaxWsProxyFactoryBean.getClientFactoryBean().getServiceFactory().setWsdlURL(WSDL_LOCATION_URL); - - return jaxWsProxyFactoryBean.create(WinRMWebService.class); - } - - static Client getWebServiceClient( - final WinRMEndpoint winRMEndpoint, - final long timeout, - final String enumerateResourceUri, - final WinRMWebService winRMWebService, - final Credentials credentials - ) { - final Client client = ClientProxy.getClient(winRMWebService); - - if (enumerateResourceUri != null) { - final WSManHeaderInterceptor interceptor = new WSManHeaderInterceptor(enumerateResourceUri); - client.getOutInterceptors().add(interceptor); - } - - client.getInInterceptors().add(new DecryptAndVerifyInInterceptor()); - client.getOutInterceptors().add(new SignAndEncryptOutInterceptor()); - - // this is different to endpoint properties - // Register the conduit factory only once: on authentication retries this method is re-invoked on the - // same client, whose cached conduit keeps using the factory it was created with. Replacing the property - // would orphan factory instances, and shutting down the in-use factory would silently downgrade the - // conduit to the synchronous transport (AsyncHTTPConduit.setupConnection checks factory.isShutdown()). - // Reusing the factory also guarantees WinRMService.close() shuts down the instance that owns the - // background threads. - final EndpointInfo endpointInfo = client.getEndpoint().getEndpointInfo(); - if ( - !(endpointInfo.getProperty(HTTPConduitFactory.class.getName()) instanceof AsyncHttpEncryptionAwareConduitFactory) - ) { - endpointInfo.setProperty(HTTPConduitFactory.class.getName(), new AsyncHttpEncryptionAwareConduitFactory()); - } - - final ServiceInfo serviceInfo = client.getEndpoint().getEndpointInfo().getService(); - serviceInfo.setProperty("soap.force.doclit.bare", true); - - final BindingProvider bindingProvider = (BindingProvider) winRMWebService; - bindingProvider.getBinding().setHandlerChain(HANDLER_CHAIN); - bindingProvider.getRequestContext().put(PolicyConstants.POLICY_OVERRIDE, POLICY); - bindingProvider.getRequestContext().put("http.autoredirect", true); - - bindingProvider.getRequestContext().put(BindingProvider.ENDPOINT_ADDRESS_PROPERTY, winRMEndpoint.getEndpoint()); - - final Map> headers = new HashMap<>(); - headers.put("Content-Type", CONTENT_TYPE_LIST); - - bindingProvider.getRequestContext().put(Message.PROTOCOL_HEADERS, headers); - - // Setup timeouts - final HTTPClientPolicy httpClientPolicy = new HTTPClientPolicy(); - httpClientPolicy.setConnectionTimeout(timeout); - httpClientPolicy.setConnectionRequestTimeout(timeout); - httpClientPolicy.setReceiveTimeout(timeout); - httpClientPolicy.setAllowChunking(false); - - bindingProvider.getRequestContext().put(Credentials.class.getName(), credentials); - bindingProvider.getRequestContext().put(AuthSchemeProvider.class.getName(), AUTH_SCHEME_REGISTRY); - - final AsyncHTTPConduit asyncHTTPConduit = (AsyncHTTPConduit) client.getConduit(); - asyncHTTPConduit.setClient(httpClientPolicy); - asyncHTTPConduit.getClient().setAutoRedirect(true); - asyncHTTPConduit.setTlsClientParameters(TLS_CLIENT_PARAMETERS); - - return client; - } - - static class RetryAuthenticationException extends Exception { - - private static final long serialVersionUID = 1L; - - RetryAuthenticationException(final Throwable throwable) { - super(throwable); - } - } - - static class RetryTgtExpirationException extends RetryAuthenticationException { - - private static final long serialVersionUID = 1L; - - RetryTgtExpirationException(final Throwable throwable) { - super(throwable); - } - } - - static class AuthCredentials { - - private final AuthenticationEnum authentication; - private final Credentials credentials; - - AuthCredentials(final AuthenticationEnum authentication, final Credentials credentials) { - this.authentication = authentication; - this.credentials = credentials; - } - - public AuthenticationEnum getAuthentication() { - return authentication; - } - - public Credentials getCredentials() { - return credentials; - } - - @Override - public int hashCode() { - return Objects.hash(authentication, credentials); - } - - @Override - public boolean equals(final Object obj) { - if (this == obj) { - return true; - } - if (obj == null) { - return false; - } - if (!(obj instanceof AuthCredentials)) { - return false; - } - final AuthCredentials other = (AuthCredentials) obj; - return authentication == other.authentication && Objects.equals(credentials, other.credentials); - } - } - - static class CredentialsMapKey { - - private final String canonizedRawUsername; - private final char[] password; - private final Path ticketCache; - private final AuthenticationEnum authentication; - - CredentialsMapKey( - final WinRMEndpoint winRMEndpoint, - final Path ticketCache, - final AuthenticationEnum authentication - ) { - this.ticketCache = ticketCache; - this.authentication = authentication; - - password = winRMEndpoint.getPassword(); - canonizedRawUsername = - winRMEndpoint.getRawUsername() != null - ? winRMEndpoint.getRawUsername().replaceAll("\\s", Utils.EMPTY).toUpperCase() - : null; - } - - @Override - public int hashCode() { - final int prime = 31; - int result = 1; - result = prime * result + Arrays.hashCode(password); - result = prime * result + Objects.hash(authentication, canonizedRawUsername, ticketCache); - return result; - } - - @Override - public boolean equals(final Object obj) { - if (this == obj) { - return true; - } - if (obj == null) { - return false; - } - if (!(obj instanceof CredentialsMapKey)) { - return false; - } - final CredentialsMapKey other = (CredentialsMapKey) obj; - return ( - authentication == other.authentication && - Objects.equals(canonizedRawUsername, other.canonizedRawUsername) && - Arrays.equals(password, other.password) && - Objects.equals(ticketCache, other.ticketCache) - ); - } - } -} diff --git a/src/main/java/org/metricshub/winrm/service/client/auth/TrustAllX509Manager.java b/src/main/java/org/metricshub/winrm/service/client/auth/TrustAllX509Manager.java deleted file mode 100644 index 30ea9be..0000000 --- a/src/main/java/org/metricshub/winrm/service/client/auth/TrustAllX509Manager.java +++ /dev/null @@ -1,43 +0,0 @@ -package org.metricshub.winrm.service.client.auth; - -/*- - * ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲ - * WinRM Java Client - * ჻჻჻჻჻჻ - * Copyright 2023 - 2024 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. - * ╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱ - */ - -import java.security.cert.CertificateException; -import java.security.cert.X509Certificate; -import javax.net.ssl.X509TrustManager; - -public class TrustAllX509Manager implements X509TrustManager { - - @Override - public void checkClientTrusted(final X509Certificate[] chain, final String authType) throws CertificateException { - // Do nothing - } - - @Override - public void checkServerTrusted(final X509Certificate[] chain, final String authType) throws CertificateException { - // Do nothing - } - - @Override - public X509Certificate[] getAcceptedIssuers() { - return new X509Certificate[0]; - } -} diff --git a/src/main/java/org/metricshub/winrm/service/client/auth/UsernamePasswordCallbackHandler.java b/src/main/java/org/metricshub/winrm/service/client/auth/UsernamePasswordCallbackHandler.java deleted file mode 100644 index c2210b7..0000000 --- a/src/main/java/org/metricshub/winrm/service/client/auth/UsernamePasswordCallbackHandler.java +++ /dev/null @@ -1,64 +0,0 @@ -package org.metricshub.winrm.service.client.auth; - -/*- - * ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲ - * WinRM Java Client - * ჻჻჻჻჻჻ - * Copyright 2023 - 2024 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. - * ╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱ - */ - -import java.io.IOException; -import javax.security.auth.callback.Callback; -import javax.security.auth.callback.CallbackHandler; -import javax.security.auth.callback.NameCallback; -import javax.security.auth.callback.PasswordCallback; -import javax.security.auth.callback.UnsupportedCallbackException; - -public class UsernamePasswordCallbackHandler implements CallbackHandler { - - private final String username; - private final char[] password; - - /** - * UsernamePasswordCallbackHandler constructor - * - * @param username name of the user to authenticate - * @param password The password - */ - public UsernamePasswordCallbackHandler(final String username, final char[] password) { - this.username = username; - this.password = password; - } - - @Override - public void handle(final Callback[] callbacks) throws IOException, UnsupportedCallbackException { - if (callbacks == null) { - return; - } - - for (final Callback callback : callbacks) { - if (callback instanceof NameCallback) { - final NameCallback nameCallback = (NameCallback) callback; - nameCallback.setName(username); - } else if (callback instanceof PasswordCallback) { - final PasswordCallback passwordCallback = (PasswordCallback) callback; - passwordCallback.setPassword(password); - } else { - throw new UnsupportedCallbackException(callback, "Unknown Callback"); - } - } - } -} diff --git a/src/main/java/org/metricshub/winrm/service/client/auth/kerberos/KerberosUtils.java b/src/main/java/org/metricshub/winrm/service/client/auth/kerberos/KerberosUtils.java deleted file mode 100644 index 83547c7..0000000 --- a/src/main/java/org/metricshub/winrm/service/client/auth/kerberos/KerberosUtils.java +++ /dev/null @@ -1,209 +0,0 @@ -package org.metricshub.winrm.service.client.auth.kerberos; - -/*- - * ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲ - * WinRM Java Client - * ჻჻჻჻჻჻ - * Copyright 2023 - 2024 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. - * ╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱ - */ - -import java.nio.file.Path; -import java.security.PrivilegedAction; -import java.util.HashMap; -import java.util.Map; -import javax.security.auth.Subject; -import javax.security.auth.callback.CallbackHandler; -import javax.security.auth.login.AppConfigurationEntry; -import javax.security.auth.login.AppConfigurationEntry.LoginModuleControlFlag; -import javax.security.auth.login.Configuration; -import javax.security.auth.login.LoginContext; -import javax.security.auth.login.LoginException; -import org.apache.http.auth.KerberosCredentials; -import org.ietf.jgss.GSSContext; -import org.ietf.jgss.GSSCredential; -import org.ietf.jgss.GSSException; -import org.ietf.jgss.GSSManager; -import org.ietf.jgss.GSSName; -import org.ietf.jgss.Oid; -import org.metricshub.winrm.Utils; -import org.metricshub.winrm.exceptions.KerberosCredentialsException; -import org.metricshub.winrm.service.client.auth.UsernamePasswordCallbackHandler; - -public class KerberosUtils { - - private KerberosUtils() {} - - private static final String TRUE = Boolean.TRUE.toString(); - private static final String FALSE = Boolean.FALSE.toString(); - - private static final String DO_NOT_PROMPT = "doNotPrompt"; - private static final String USE_TICKET_CACHE = "useTicketCache"; - private static final String IS_INITIATOR = "isInitiator"; - private static final String CLIENT = "client"; - private static final String REFRESH_KRB5_CONFIG = "refreshKrb5Config"; - - /** - * Object identifier of Kerberos as mechanism used by GSS to obtain the Ticket Granting Ticket (TGT). - * @see http://oid-info.com/get/1.2.840.113554.1.2.2 - */ - private static final String KERBEROS_V5_OID = "1.2.840.113554.1.2.2"; - - private static final Configuration JAAS_CONFIG; - - static { - final Map moduleOptions = new HashMap<>(); - moduleOptions.put(REFRESH_KRB5_CONFIG, TRUE); - moduleOptions.put(CLIENT, TRUE); - moduleOptions.put(IS_INITIATOR, TRUE); - - // useTicketCache = false, The TGT cache is not used, and the user is prompted for credentials login - moduleOptions.put(USE_TICKET_CACHE, FALSE); - moduleOptions.put(DO_NOT_PROMPT, FALSE); - - JAAS_CONFIG = - new Configuration() { - @Override - public AppConfigurationEntry[] getAppConfigurationEntry(String name) { - return createAppConfigurationEntries(moduleOptions); - } - }; - } - - private static AppConfigurationEntry[] createAppConfigurationEntries(final Map moduleOptions) { - return new AppConfigurationEntry[] { - new AppConfigurationEntry( - "com.sun.security.auth.module.Krb5LoginModule", - LoginModuleControlFlag.REQUIRED, - moduleOptions - ) - }; - } - - /** - * Get Kerberos credentials (i.e a TGT) with the username and password provided. - * - * @param username The user name (mandatory) - * @param password The password (mandatory) - * @param ticketCache The Ticket Cache path - * - * @return credentials wrapping the TGT which will be used for obtaining the SPNego token - * @throws KerberosCredentialsException when an error occurred on Kerberos authentication - */ - public static KerberosCredentials createCredentials( - final String username, - final char[] password, - final Path ticketCache - ) { - Utils.checkNonNull(username, "username"); - Utils.checkNonNull(password, "password"); - - try { - // If the Kerberos Realm is in uppercases (which is the norm) and the domain in the user principal (UPN) is in - // lowercases, a KrbException: "Message stream modified" is thrown. - // To avoid this exception we force the UPN in uppercases - final String canonizedUsername = username.trim().toUpperCase(); - - final Configuration configuration = ticketCache != null - ? createConfigurationWithTicketCache(ticketCache) - : JAAS_CONFIG; - - final Subject subject = authenticate(canonizedUsername, password, configuration); - - final PrivilegedAction privilegedAction = createPrivilegedAction(canonizedUsername); - - final GSSCredential gssUserCredential = Subject.doAs(subject, privilegedAction); - - return new KerberosCredentials(gssUserCredential); - } catch (final KerberosCredentialsException e) { - throw e; - } catch (final Exception e) { - throw new KerberosCredentialsException(e); - } - } - - public static Configuration createConfigurationWithTicketCache(final Path ticketCache) { - final Map moduleOptions = new HashMap<>(); - moduleOptions.put(REFRESH_KRB5_CONFIG, TRUE); - moduleOptions.put(CLIENT, TRUE); - moduleOptions.put(IS_INITIATOR, TRUE); - - // useTicketCache = true, The default TGT cache is used, the user is not prompt for authentication - // and then failed if the user TGT is not in the cache. - moduleOptions.put(USE_TICKET_CACHE, TRUE); - moduleOptions.put(DO_NOT_PROMPT, TRUE); - moduleOptions.put("ticketCache", ticketCache.toString()); - - return new Configuration() { - @Override - public AppConfigurationEntry[] getAppConfigurationEntry(String name) { - return createAppConfigurationEntries(moduleOptions); - } - }; - } - - /** - * Authenticate the user with the provided password. The login send a request AS-REQ to the Authentication Server. - * The response will contain the TGT which will be store in the Subject. - * - * @param username name of the user to authenticate - * @param password The password - * @param configuration the {@code Configuration} lists the login modules to be called to perform the authentication - * - * @return subject of the authenticated user - */ - public static Subject authenticate(final String username, final char[] password, final Configuration configuration) { - try { - final CallbackHandler callbackHandler = new UsernamePasswordCallbackHandler(username, password); - - final LoginContext loginContext = createLoginContext(callbackHandler, configuration); - - loginContext.login(); - - return loginContext.getSubject(); - } catch (final LoginException e) { - throw new KerberosCredentialsException("Kerberos Login failure. Make sure Kerberos is properly configured.", e); - } - } - - private static PrivilegedAction createPrivilegedAction(final String username) { - return () -> { - try { - final GSSManager gssManager = GSSManager.getInstance(); - - final GSSName gssUserName = gssManager.createName(username, null); - - return gssManager.createCredential( - gssUserName, - GSSContext.DEFAULT_LIFETIME, - new Oid(KERBEROS_V5_OID), - GSSCredential.INITIATE_ONLY - ); - } catch (final GSSException e) { - throw new KerberosCredentialsException( - String.format("Unable to create credential for user \"%s\" after login", username), - e - ); - } - }; - } - - public static LoginContext createLoginContext( - final CallbackHandler callbackHandler, - final Configuration configuration - ) throws LoginException { - return new LoginContext(Utils.EMPTY, null, callbackHandler, configuration); - } -} diff --git a/src/main/java/org/metricshub/winrm/service/client/auth/ntlm/ModeEnum.java b/src/main/java/org/metricshub/winrm/service/client/auth/ntlm/ModeEnum.java deleted file mode 100644 index 070ef1a..0000000 --- a/src/main/java/org/metricshub/winrm/service/client/auth/ntlm/ModeEnum.java +++ /dev/null @@ -1,31 +0,0 @@ -package org.metricshub.winrm.service.client.auth.ntlm; - -/*- - * ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲ - * WinRM Java Client - * ჻჻჻჻჻჻ - * Copyright 2023 - 2024 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. - * ╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱ - */ - -/** - * Code from io.cloudsoft.winrm4j.client.ntlm.forks.httpclient.NTLMEngineImpl - * release 0.12.3 @link https://github.com/cloudsoft/winrm4j - * io.cloudsoft.winrm4j.client.ntlm.forks.httpclient is a fork of apache-httpclient 4.5.13 - */ -enum ModeEnum { - CLIENT, - SERVER -} diff --git a/src/main/java/org/metricshub/winrm/service/client/auth/ntlm/NTCredentialsWithEncryption.java b/src/main/java/org/metricshub/winrm/service/client/auth/ntlm/NTCredentialsWithEncryption.java deleted file mode 100644 index 9193cf3..0000000 --- a/src/main/java/org/metricshub/winrm/service/client/auth/ntlm/NTCredentialsWithEncryption.java +++ /dev/null @@ -1,209 +0,0 @@ -package org.metricshub.winrm.service.client.auth.ntlm; - -/*- - * ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲ - * WinRM Java Client - * ჻჻჻჻჻჻ - * Copyright 2023 - 2024 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. - * ╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱ - */ - -import java.util.Arrays; -import java.util.Objects; -import java.util.concurrent.atomic.AtomicLong; -import javax.crypto.Cipher; -import org.apache.http.HttpEntityEnclosingRequest; -import org.apache.http.HttpRequest; -import org.apache.http.auth.NTCredentials; -import org.metricshub.winrm.service.client.encryption.EncryptionAwareHttpEntity; -import org.metricshub.winrm.service.client.encryption.EncryptionUtils; - -/** - * NTCredentials with encryption. - * Code from io.cloudsoft.winrm4j.client.ntlm.NTCredentialsWithEncryption - * release 0.12.3 @link https://github.com/cloudsoft/winrm4j - */ -public class NTCredentialsWithEncryption extends NTCredentials { - - private static final long serialVersionUID = 1L; - - private boolean isAuthenticated = false; - private long negotiateFlags; - private byte[] clientSigningKey; - private byte[] serverSigningKey; - private byte[] clientSealingKey; - private byte[] serverSealingKey; - private AtomicLong sequenceNumberIncoming = new AtomicLong(-1); - private AtomicLong sequenceNumberOutgoing = new AtomicLong(-1); - - public NTCredentialsWithEncryption( - final String userName, - final String password, - final String workstation, - final String domain - ) { - super(userName, password, workstation, domain); - } - - public boolean isAuthenticated() { - return isAuthenticated; - } - - public void setIsAuthenticated(boolean isAuthenticated) { - this.isAuthenticated = isAuthenticated; - } - - public void setClientSigningKey(byte[] clientSigningKey) { - this.clientSigningKey = clientSigningKey; - } - - public void setServerSigningKey(byte[] serverSigningKey) { - this.serverSigningKey = serverSigningKey; - } - - public byte[] getClientSigningKey() { - return clientSigningKey; - } - - public byte[] getServerSigningKey() { - return serverSigningKey; - } - - public void setClientSealingKey(byte[] clientSealingKey) { - this.clientSealingKey = clientSealingKey; - } - - public void setServerSealingKey(byte[] serverSealingKey) { - this.serverSealingKey = serverSealingKey; - } - - public byte[] getClientSealingKey() { - return clientSealingKey; - } - - public byte[] getServerSealingKey() { - return serverSealingKey; - } - - public long getNegotiateFlags() { - return negotiateFlags; - } - - public boolean hasNegotiateFlag(long flag) { - return (getNegotiateFlags() & flag) == flag; - } - - public void setNegotiateFlags(long negotiateFlags) { - this.negotiateFlags = negotiateFlags; - } - - public AtomicLong getSequenceNumberIncoming() { - return sequenceNumberIncoming; - } - - public AtomicLong getSequenceNumberOutgoing() { - return sequenceNumberOutgoing; - } - - private transient Cipher encryptor; - - public Cipher getStatefulEncryptor() { - if (encryptor == null) { - encryptor = EncryptionUtils.arc4(getClientSealingKey()); - } - return encryptor; - } - - private transient Cipher decryptor; - - public Cipher getStatefulDecryptor() { - if (decryptor == null) { - decryptor = EncryptionUtils.arc4(getServerSealingKey()); - } - return decryptor; - } - - void resetEncryption(final HttpRequest request) { - setIsAuthenticated(false); - clientSealingKey = null; - clientSigningKey = null; - serverSealingKey = null; - serverSigningKey = null; - encryptor = null; - decryptor = null; - sequenceNumberIncoming.set(-1); - sequenceNumberOutgoing.set(-1); - - if ( - request instanceof HttpEntityEnclosingRequest && - ((HttpEntityEnclosingRequest) request).getEntity() instanceof EncryptionAwareHttpEntity - ) { - ((EncryptionAwareHttpEntity) ((HttpEntityEnclosingRequest) request).getEntity()).refreshHeaders( - (HttpEntityEnclosingRequest) request - ); - } - } - - void initEncryption(final Type3Message signAndSealData, final HttpRequest request) { - setIsAuthenticated(true); - if (signAndSealData != null && signAndSealData.getExportedSessionKey() != null) { - new NtlmKeys(signAndSealData).apply(this); - } - if ( - request instanceof HttpEntityEnclosingRequest && - ((HttpEntityEnclosingRequest) request).getEntity() instanceof EncryptionAwareHttpEntity - ) { - ((EncryptionAwareHttpEntity) ((HttpEntityEnclosingRequest) request).getEntity()).refreshHeaders( - (HttpEntityEnclosingRequest) request - ); - } - } - - @Override - public String toString() { - return getClass().getSimpleName() + super.toString() + "{auth=" + isAuthenticated() + "}"; - } - - @Override - public int hashCode() { - final int prime = 31; - int result = super.hashCode(); - result = prime * result + Arrays.hashCode(clientSealingKey); - result = prime * result + Arrays.hashCode(clientSigningKey); - result = prime * result + Arrays.hashCode(serverSealingKey); - result = prime * result + Arrays.hashCode(serverSigningKey); - result = - prime * result + Objects.hash(isAuthenticated, negotiateFlags, sequenceNumberIncoming, sequenceNumberOutgoing); - return result; - } - - @Override - public boolean equals(Object obj) { - if (this == obj) return true; - if (!super.equals(obj)) return false; - if (!(obj instanceof NTCredentialsWithEncryption)) return false; - NTCredentialsWithEncryption other = (NTCredentialsWithEncryption) obj; - return ( - Arrays.equals(clientSealingKey, other.clientSealingKey) && - Arrays.equals(clientSigningKey, other.clientSigningKey) && - isAuthenticated == other.isAuthenticated && - negotiateFlags == other.negotiateFlags && - Objects.equals(sequenceNumberIncoming, other.sequenceNumberIncoming) && - Objects.equals(sequenceNumberOutgoing, other.sequenceNumberOutgoing) && - Arrays.equals(serverSealingKey, other.serverSealingKey) && - Arrays.equals(serverSigningKey, other.serverSigningKey) - ); - } -} diff --git a/src/main/java/org/metricshub/winrm/service/client/auth/ntlm/NTLMEngine.java b/src/main/java/org/metricshub/winrm/service/client/auth/ntlm/NTLMEngine.java deleted file mode 100644 index 8cdce9c..0000000 --- a/src/main/java/org/metricshub/winrm/service/client/auth/ntlm/NTLMEngine.java +++ /dev/null @@ -1,84 +0,0 @@ -// copy of code from apache-httpclient 4.5.13 package org.apache.http.impl.auth -// changes: -// - package name, this header, imports -// - expose Type3 message (package-private) so keys can be gathered - -/* - * ==================================================================== - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); -/*- - * ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲ - * WinRM Java Client - * ჻჻჻჻჻჻ - * Copyright 2023 - 2024 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. - * ╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱ - */ -package org.metricshub.winrm.service.client.auth.ntlm; - -import org.apache.http.impl.auth.NTLMEngineException; - -/** - * Abstract NTLM authentication engine. The engine can be used to - * generate Type1 messages and Type3 messages in response to a Type2 challenge. - * - * Code from io.cloudsoft.winrm4j.client.ntlm.forks.httpclient.NTLMEngine - * release 0.12.3 @link https://github.com/cloudsoft/winrm4j - * io.cloudsoft.winrm4j.client.ntlm.forks.httpclient is a fork of apache-httpclient 4.5.13 - */ -public interface NTLMEngine { - /** - * Generates a Type1 message given the domain and workstation. - * - * @param domain Optional Windows domain name. Can be {@code null}. - * @param workstation Optional Windows workstation name. Can be - * {@code null}. - * @return Type1 message - * @throws NTLMEngineException - */ - String generateType1Msg(final String domain, final String workstation) throws NTLMEngineException; - - /** - * Generates a Type3 message given the user credentials and the - * authentication challenge. - * - * @param username Windows user name - * @param password Password - * @param domain Windows domain name - * @param workstation Windows workstation name - * @param challenge Type2 challenge. - * @return Type3 response. - * @throws NTLMEngineException - */ - String generateType3Msg( - final String username, - final String password, - final String domain, - final String workstation, - final String challenge - ) throws NTLMEngineException; - - Type3Message generateType3MsgObject( - final String username, - final String password, - final String domain, - final String workstation, - final String challenge - ) throws NTLMEngineException; -} diff --git a/src/main/java/org/metricshub/winrm/service/client/auth/ntlm/NTLMEngineImpl.java b/src/main/java/org/metricshub/winrm/service/client/auth/ntlm/NTLMEngineImpl.java deleted file mode 100644 index 659cc0a..0000000 --- a/src/main/java/org/metricshub/winrm/service/client/auth/ntlm/NTLMEngineImpl.java +++ /dev/null @@ -1,114 +0,0 @@ -// copy of code from apache-httpclient 4.5.13 package org.apache.http.impl.auth -// changes: -// - package name, this header, imports -// - fix minor errors/typos -// - allow class to be extended and flags to be customized (increase many things' visibility to protected and make class non-final) -// - expose Type3 message (public) so keys can be gathered -// - expose encryption methods -// - make flags injectable to Type1 message - -/* - * ==================================================================== - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); -/*- - * ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲ - * WinRM Java Client - * ჻჻჻჻჻჻ - * Copyright 2023 - 2024 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. - * ╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱ - */ -package org.metricshub.winrm.service.client.auth.ntlm; - -import org.apache.http.impl.auth.NTLMEngineException; - -/** - * Provides an implementation for NTLMv1, NTLMv2, and NTLM2 Session forms of the NTLM - * authentication protocol. - * - * Code from io.cloudsoft.winrm4j.client.ntlm.forks.httpclient.NTLMEngineImpl - * release 0.12.3 @link https://github.com/cloudsoft/winrm4j - * io.cloudsoft.winrm4j.client.ntlm.forks.httpclient is a fork of apache-httpclient 4.5.13 - */ -class NTLMEngineImpl implements NTLMEngine { - - /** Strip dot suffix from a name */ - private static String stripDotSuffix(final String value) { - if (value == null) { - return null; - } - final int index = value.indexOf('.'); - if (index != -1) { - return value.substring(0, index); - } - return value; - } - - /** Convert host to standard form */ - static String convertHost(final String host) { - return stripDotSuffix(host); - } - - /** Convert domain to standard form */ - static String convertDomain(final String domain) { - return stripDotSuffix(domain); - } - - @Override - public String generateType1Msg(final String domain, final String workstation) throws NTLMEngineException { - return new Type1Message(null, null, getDefaultFlags()).getResponse(); - } - - // function overriden in NtlmMasqAsSpnegoScheme - Integer getDefaultFlags() { - return Type1Message.getDefaultFlags(); - } - - @Override - public String generateType3Msg( - final String username, - final String password, - final String domain, - final String workstation, - final String challenge - ) throws NTLMEngineException { - return generateType3MsgObject(username, password, domain, workstation, challenge).getResponse(); - } - - @Override - public Type3Message generateType3MsgObject( - final String username, - final String password, - final String domain, - final String workstation, - final String challenge - ) throws NTLMEngineException { - final Type2Message t2m = new Type2Message(challenge); - return new Type3Message( - domain, - workstation, - username, - password, - t2m.getChallenge(), - t2m.getFlags(), - t2m.getTarget(), - t2m.getTargetInfo() - ); - } -} diff --git a/src/main/java/org/metricshub/winrm/service/client/auth/ntlm/NTLMScheme.java b/src/main/java/org/metricshub/winrm/service/client/auth/ntlm/NTLMScheme.java deleted file mode 100644 index 5f1b0c2..0000000 --- a/src/main/java/org/metricshub/winrm/service/client/auth/ntlm/NTLMScheme.java +++ /dev/null @@ -1,181 +0,0 @@ -// copy of code from apache-httpclient 4.5.13 package org.apache.http.impl.auth -// changes: -// - package name, this header, imports -// - gather NTLM signing key and attach to context - -/* - * ==================================================================== - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); -/*- - * ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲ - * WinRM Java Client - * ჻჻჻჻჻჻ - * Copyright 2023 - 2024 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. - * ╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱ - */ -package org.metricshub.winrm.service.client.auth.ntlm; - -import org.apache.http.Header; -import org.apache.http.HttpRequest; -import org.apache.http.auth.AUTH; -import org.apache.http.auth.AuthenticationException; -import org.apache.http.auth.Credentials; -import org.apache.http.auth.InvalidCredentialsException; -import org.apache.http.auth.MalformedChallengeException; -import org.apache.http.auth.NTCredentials; -import org.apache.http.impl.auth.AuthSchemeBase; -import org.apache.http.message.BufferedHeader; -import org.apache.http.protocol.HttpContext; -import org.apache.http.util.CharArrayBuffer; -import org.metricshub.winrm.Utils; - -/** - * NTLM is a proprietary authentication scheme developed by Microsoft - * and optimized for Windows platforms. - * - * Code from io.cloudsoft.winrm4j.client.ntlm.forks.httpclient.NTLMScheme - * release 0.12.3 @link https://github.com/cloudsoft/winrm4j - * io.cloudsoft.winrm4j.client.ntlm.forks.httpclient is a fork of apache-httpclient 4.5.13 - */ -public class NTLMScheme extends AuthSchemeBase { - - private enum State { - UNINITIATED, - CHALLENGE_RECEIVED, - MSG_TYPE1_GENERATED, - MSG_TYPE2_RECEVIED, - MSG_TYPE3_GENERATED, - FAILED - } - - private final NTLMEngine engine; - - private State state; - private String challenge; - - public NTLMScheme(final NTLMEngine engine) { - super(); - Utils.checkNonNull(engine, "engine"); - this.engine = engine; - state = State.UNINITIATED; - challenge = null; - } - - @Override - public String getSchemeName() { - return "ntlm"; - } - - @Override - public String getParameter(final String name) { - // String parameters not supported - return null; - } - - @Override - public String getRealm() { - // NTLM does not support the concept of an authentication realm - return null; - } - - @Override - public boolean isConnectionBased() { - return true; - } - - @Override - protected void parseChallenge(final CharArrayBuffer buffer, final int beginIndex, final int endIndex) - throws MalformedChallengeException { - challenge = buffer.substringTrimmed(beginIndex, endIndex); - if (challenge.isEmpty()) { - if (state == State.UNINITIATED) { - state = State.CHALLENGE_RECEIVED; - } else { - state = State.FAILED; - } - } else { - if (state.compareTo(State.MSG_TYPE1_GENERATED) < 0) { - state = State.FAILED; - throw new MalformedChallengeException("Out of sequence NTLM response message"); - } else if (state == State.MSG_TYPE1_GENERATED) { - state = State.MSG_TYPE2_RECEVIED; - } - } - } - - @Override - public Header authenticate(final Credentials credentials, final HttpRequest request) throws AuthenticationException { - NTCredentials ntcredentials = null; - try { - ntcredentials = (NTCredentials) credentials; - } catch (final ClassCastException e) { - throw new InvalidCredentialsException( - "Credentials cannot be used for NTLM authentication: " + credentials.getClass().getName() - ); - } - String response = null; - if (state == State.FAILED) { - throw new AuthenticationException("NTLM authentication failed"); - } else if (state == State.CHALLENGE_RECEIVED) { - response = this.engine.generateType1Msg(ntcredentials.getDomain(), ntcredentials.getWorkstation()); - state = State.MSG_TYPE1_GENERATED; - - if (credentials instanceof NTCredentialsWithEncryption) { - ((NTCredentialsWithEncryption) credentials).resetEncryption(request); - } - } else if (state == State.MSG_TYPE2_RECEVIED) { - final Type3Message responseO = engine.generateType3MsgObject( - ntcredentials.getUserName(), - ntcredentials.getPassword(), - ntcredentials.getDomain(), - ntcredentials.getWorkstation(), - challenge - ); - - response = responseO.getResponse(); - state = State.MSG_TYPE3_GENERATED; - if (credentials instanceof NTCredentialsWithEncryption) { - ((NTCredentialsWithEncryption) credentials).initEncryption(responseO, request); - } - } else { - throw new AuthenticationException("Unexpected state: " + state); - } - final CharArrayBuffer buffer = new CharArrayBuffer(32); - if (isProxy()) { - buffer.append(AUTH.PROXY_AUTH_RESP); - } else { - buffer.append(AUTH.WWW_AUTH_RESP); - } - buffer.append(": NTLM "); - buffer.append(response); - return new BufferedHeader(buffer); - } - - @Override - public boolean isComplete() { - return state == State.MSG_TYPE3_GENERATED || state == State.FAILED; - } - - @Override - public Header authenticate(final Credentials credentials, final HttpRequest request, final HttpContext context) - throws AuthenticationException { - return authenticate(credentials, request); - } -} diff --git a/src/main/java/org/metricshub/winrm/service/client/auth/ntlm/NtlmKeys.java b/src/main/java/org/metricshub/winrm/service/client/auth/ntlm/NtlmKeys.java deleted file mode 100644 index 0ee7d1f..0000000 --- a/src/main/java/org/metricshub/winrm/service/client/auth/ntlm/NtlmKeys.java +++ /dev/null @@ -1,117 +0,0 @@ -package org.metricshub.winrm.service.client.auth.ntlm; - -/*- - * ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲ - * WinRM Java Client - * ჻჻჻჻჻჻ - * Copyright 2023 - 2024 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. - * ╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱ - */ - -import java.util.Arrays; -import org.metricshub.winrm.service.client.encryption.ByteArrayUtils; -import org.metricshub.winrm.service.client.encryption.EncryptionUtils; - -/** - * Code from io.cloudsoft.winrm4j.client.ntlm.NtlmKeys - * release 0.12.3 @link https://github.com/cloudsoft/winrm4j - */ -public class NtlmKeys { - - // adapted from python ntlm-auth - // also see NTLMEngineImpl.Handle - - // # Copyright: (c) 2018, Jordan Borean (@jborean93) - // # MIT License (see LICENSE or https://opensource.org/licenses/MIT) - - private static final byte[] CLIENT_SIGNING = - "session key to client-to-server signing key magic constant\0".getBytes(); - private static final byte[] SERVER_SIGNING = - "session key to server-to-client signing key magic constant\0".getBytes(); - private static final byte[] CLIENT_SEALING = - "session key to client-to-server sealing key magic constant\0".getBytes(); - private static final byte[] SERVER_SEALING = - "session key to server-to-client sealing key magic constant\0".getBytes(); - - private final byte[] exportedSessionKey; - private final long negotiateFlags; - - public NtlmKeys(final Type3Message signAndSealData) { - exportedSessionKey = signAndSealData.getExportedSessionKey(); - negotiateFlags = signAndSealData.getType2Flags(); - } - - public void apply(final NTCredentialsWithEncryption credentials) { - credentials.setNegotiateFlags(negotiateFlags); - - credentials.setClientSigningKey(getSignKey(CLIENT_SIGNING)); - credentials.setServerSigningKey(getSignKey(SERVER_SIGNING)); - credentials.setClientSealingKey(getSealKey(CLIENT_SEALING)); - credentials.setServerSealingKey(getSealKey(SERVER_SEALING)); - } - - /** - * - * @param magicConstant a constant value set in the MS-NLMP documentation (constants.SignSealConstants) - * - * @return Key used to sign messages - */ - private byte[] getSignKey(final byte[] magicConstant) { - return EncryptionUtils.md5digest(ByteArrayUtils.concat(exportedSessionKey, magicConstant)); - } - - /** - * Main method to use to calculate the seal_key used to seal (encrypt) messages. - * This will determine the correct method below to use based on the compatibility flags set - * and should be called instead of the others - * - * @param magicConstant a constant value set in the MS-NLMP documentation (constants.SignSealConstants) - * - * @return Key used to seal messages - */ - private byte[] getSealKey(final byte[] magicConstant) { - // This for authentication where NTLMSSP_NEGOTIATE_EXTENDED_SESSIONSECURITY has been - // negotiated. Will weaken the keys if NTLMSSP_NEGOTIATE_128 is not negotiated, - // will try NEGOTIATE_56 and then will default to the 40-bit key - if (hasNegotiateFlag(NTLMEngineUtils.NTLMSSP_NEGOTIATE_EXTENDED_SESSIONSECURITY)) { - if (hasNegotiateFlag(NTLMEngineUtils.NTLMSSP_NEGOTIATE_128)) { - return EncryptionUtils.md5digest(ByteArrayUtils.concat(exportedSessionKey, magicConstant)); - } - if (hasNegotiateFlag(NTLMEngineUtils.NTLMSSP_NEGOTIATE_56)) { - return EncryptionUtils.md5digest( - ByteArrayUtils.concat(Arrays.copyOfRange(exportedSessionKey, 0, 7), magicConstant) - ); - } - return EncryptionUtils.md5digest( - ByteArrayUtils.concat(Arrays.copyOfRange(exportedSessionKey, 0, 5), magicConstant) - ); - } - - // This for authentication where NTLMSSP_NEGOTIATE_EXTENDED_SESSIONSECURITY - // has not been negotiated. Will weaken the keys if NTLMSSP_NEGOTIATE_56 is not negotiated it will default - // to the 40-bit key. - if (hasNegotiateFlag(NTLMEngineUtils.NTLMSSP_NEGOTIATE_LM_KEY)) { - throw new UnsupportedOperationException( - "LM KEY negotiate mode not implemented; use extended session security instead" - ); - } - - return exportedSessionKey; - } - - private boolean hasNegotiateFlag(long flag) { - return (negotiateFlags & flag) == flag; - } -} diff --git a/src/main/java/org/metricshub/winrm/service/client/auth/ntlm/NtlmMasqAsSpnegoScheme.java b/src/main/java/org/metricshub/winrm/service/client/auth/ntlm/NtlmMasqAsSpnegoScheme.java deleted file mode 100644 index 6dec13d..0000000 --- a/src/main/java/org/metricshub/winrm/service/client/auth/ntlm/NtlmMasqAsSpnegoScheme.java +++ /dev/null @@ -1,70 +0,0 @@ -package org.metricshub.winrm.service.client.auth.ntlm; - -/*- - * ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲ - * WinRM Java Client - * ჻჻჻჻჻჻ - * Copyright 2023 - 2024 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. - * ╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱ - */ - -import java.util.function.LongUnaryOperator; -import org.apache.http.Header; -import org.apache.http.HttpRequest; -import org.apache.http.auth.AuthenticationException; -import org.apache.http.auth.Credentials; -import org.apache.http.client.config.AuthSchemes; -import org.apache.http.message.BasicHeader; - -/** - * Code from io.cloudsoft.winrm4j.client.ntlm.NtlmMasqAsSpnegoScheme - * release 0.12.3 @link https://github.com/cloudsoft/winrm4j - */ -public class NtlmMasqAsSpnegoScheme extends NTLMScheme { - - private static final LongUnaryOperator FLAG_MODIFIER = flags -> - flags | - NTLMEngineUtils.NTLMSSP_NEGOTIATE_SIGN | - NTLMEngineUtils.NTLMSSP_NEGOTIATE_SEAL | - NTLMEngineUtils.NTLMSSP_NEGOTIATE_KEY_EXCH; - - public NtlmMasqAsSpnegoScheme() { - super(newDefaultNtlmEngine()); - } - - private static NTLMEngine newDefaultNtlmEngine() { - return new NTLMEngineImpl() { - @Override - public Integer getDefaultFlags() { - final Long flags = (long) Type1Message.getDefaultFlags(); - return (int) FLAG_MODIFIER.applyAsLong(flags); - } - }; - } - - @Override - public String getSchemeName() { - return AuthSchemes.SPNEGO; - } - - @Override - public Header authenticate(final Credentials credentials, final HttpRequest httpRequest) - throws AuthenticationException { - final Header header = super.authenticate(credentials, httpRequest); - - // code from winrm4j implementation: https://github.com/cloudsoft/winrm4j - return new BasicHeader(header.getName(), header.getValue().replace("NTLM", getSchemeName())); - } -} diff --git a/src/main/java/org/metricshub/winrm/service/client/auth/ntlm/NtlmMasqAsSpnegoSchemeFactory.java b/src/main/java/org/metricshub/winrm/service/client/auth/ntlm/NtlmMasqAsSpnegoSchemeFactory.java deleted file mode 100644 index 9404efe..0000000 --- a/src/main/java/org/metricshub/winrm/service/client/auth/ntlm/NtlmMasqAsSpnegoSchemeFactory.java +++ /dev/null @@ -1,37 +0,0 @@ -package org.metricshub.winrm.service.client.auth.ntlm; - -/*- - * ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲ - * WinRM Java Client - * ჻჻჻჻჻჻ - * Copyright 2023 - 2024 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. - * ╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱ - */ - -import org.apache.http.auth.AuthScheme; -import org.apache.http.impl.auth.NTLMSchemeFactory; -import org.apache.http.protocol.HttpContext; - -/** - * Code from io.cloudsoft.winrm4j.client.ntlm.NtlmMasqAsSpnegoSchemeFactory - * release 0.12.3 @link https://github.com/cloudsoft/winrm4j - */ -public class NtlmMasqAsSpnegoSchemeFactory extends NTLMSchemeFactory { - - @Override - public AuthScheme create(HttpContext context) { - return new NtlmMasqAsSpnegoScheme(); - } -} diff --git a/src/main/java/org/metricshub/winrm/service/client/encryption/AsyncHttpEncryptionAwareConduit.java b/src/main/java/org/metricshub/winrm/service/client/encryption/AsyncHttpEncryptionAwareConduit.java deleted file mode 100644 index 0f83e7d..0000000 --- a/src/main/java/org/metricshub/winrm/service/client/encryption/AsyncHttpEncryptionAwareConduit.java +++ /dev/null @@ -1,180 +0,0 @@ -package org.metricshub.winrm.service.client.encryption; - -/*- - * ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲ - * WinRM Java Client - * ჻჻჻჻჻჻ - * Copyright 2023 - 2024 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. - * ╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱ - */ - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.net.URI; -import java.util.Arrays; -import java.util.List; -import org.apache.cxf.Bus; -import org.apache.cxf.io.CacheAndWriteOutputStream; -import org.apache.cxf.message.Message; -import org.apache.cxf.service.model.EndpointInfo; -import org.apache.cxf.transport.http.Address; -import org.apache.cxf.transport.http.asyncclient.AsyncHTTPConduit; -import org.apache.cxf.transport.http.asyncclient.CXFHttpRequest; -import org.apache.cxf.transports.http.configuration.HTTPClientPolicy; -import org.apache.cxf.ws.addressing.EndpointReferenceType; -import org.apache.http.auth.Credentials; -import org.apache.http.client.config.AuthSchemes; -import org.apache.http.client.config.RequestConfig; -import org.apache.http.entity.BasicHttpEntity; - -/** - * Creates an output stream which sends back the appropriate encrypted or unencrypted stream, - * based on the {SignAndEncryptOutInterceptor} -- which normally does the right thing, - * but during auth events it will "guess" wrongly, and we have to change the payload and - * the headers. {io.cloudsoft.winrm4j.client.ntlm.NTCredentialsWithEncryption} will do - * that by finding the {@link EncryptionAwareHttpEntity}. - * - * Code from io.cloudsoft.winrm4j.client.encryption.AsyncHttpEncryptionAwareConduit - * release 0.12.3 @link https://github.com/cloudsoft/winrm4j - */ -public class AsyncHttpEncryptionAwareConduit extends AsyncHTTPConduit { - - static final byte[] PRE_AUTH_BOGUS_PAYLOAD = "AWAITING_ENCRYPTION_KEYS".getBytes(); - - private static final List TARGET_AUTH_SCHEMES = Arrays.asList(AuthSchemes.SPNEGO, AuthSchemes.KERBEROS); - - private static ContentWithType getAppropriate(final Message msg) { - final EncryptAndSignOutputStream encryptingStream = msg.getContent(EncryptAndSignOutputStream.class); - if (encryptingStream == null) { - throw new IllegalStateException("No SignAndEncryptOutInterceptor applied to message"); - } - return encryptingStream.getAppropriate(); - } - - public AsyncHttpEncryptionAwareConduit( - final Bus bus, - final EndpointInfo endpointInfo, - final EndpointReferenceType endpointReferenceType, - final AsyncHttpEncryptionAwareConduitFactory factory - ) throws IOException { - super(bus, endpointInfo, endpointReferenceType, factory); - } - - @Override - protected OutputStream createOutputStream( - final Message message, - final boolean needToCacheRequest, - final boolean isChunking, - final int chunkThreshold - ) throws IOException { - final NtlmEncryptionUtils encryptor = NtlmEncryptionUtils.of(message.get(Credentials.class)); - if (encryptor == null) { - return super.createOutputStream(message, needToCacheRequest, isChunking, chunkThreshold); - } - - if (Boolean.TRUE.equals(message.get(USE_ASYNC))) { - // copied from super, but for our class - final CXFHttpRequest requestEntity = message.get(CXFHttpRequest.class); - final AsyncWrappedEncryptionAwareOutputStream out = new AsyncWrappedEncryptionAwareOutputStream( - message, - true, - false, - chunkThreshold, - getConduitName(), - requestEntity.getURI() - ); - - requestEntity.setOutputStream(out); - return out; - } - - throw new IllegalStateException("Encryption only available with ASYNC at present"); - // if needed could also subclass the URL stream used by super.super.createOutput - } - - @Override - protected void setupConnection(final Message message, final Address address, final HTTPClientPolicy csPolicy) - throws IOException { - super.setupConnection(message, address, csPolicy); - - // replace similar logic in super method, but with a refreshHeaders method available - - final CXFHttpRequest requestEntity = message.get(CXFHttpRequest.class); - - final BasicHttpEntity entity = new EncryptionAwareHttpEntity() { - @Override - public boolean isRepeatable() { - return requestEntity.getEntity().isRepeatable(); - } - - @Override - protected ContentWithType getAppropriate() { - return AsyncHttpEncryptionAwareConduit.getAppropriate(message); - } - }; - entity.setChunked(true); - entity.setContentType((String) message.get(Message.CONTENT_TYPE)); - - requestEntity.setEntity(entity); - - requestEntity.setConfig( - RequestConfig.copy(requestEntity.getConfig()).setTargetPreferredAuthSchemes(TARGET_AUTH_SCHEMES).build() - ); - } - - private class AsyncWrappedEncryptionAwareOutputStream extends AsyncWrappedOutputStream { - - public AsyncWrappedEncryptionAwareOutputStream( - final Message message, - final boolean needToCacheRequest, - final boolean isChunking, - final int chunkThreshold, - final String conduitName, - final URI uri - ) { - super(message, needToCacheRequest, isChunking, chunkThreshold, conduitName, uri); - } - - @Override - protected void setupWrappedStream() throws IOException { - super.setupWrappedStream(); - - if (!(cachedStream.getFlowThroughStream() instanceof EncryptionAwareCacheAndWriteOutputStream)) { - cachedStream = new EncryptionAwareCacheAndWriteOutputStream(cachedStream.getFlowThroughStream()); - wrappedStream = cachedStream; - } - } - - private class EncryptionAwareCacheAndWriteOutputStream extends CacheAndWriteOutputStream { - - public EncryptionAwareCacheAndWriteOutputStream(OutputStream outbufFlowThroughStream) { - super(outbufFlowThroughStream); - } - - @Override - public byte[] getBytes() throws IOException { - final ContentWithType appropriate = AsyncHttpEncryptionAwareConduit.getAppropriate(outMessage); - return appropriate.getPayload(); - } - - @Override - public InputStream getInputStream() throws IOException { - return new ByteArrayInputStream(getBytes()); - } - } - } -} diff --git a/src/main/java/org/metricshub/winrm/service/client/encryption/AsyncHttpEncryptionAwareConduitFactory.java b/src/main/java/org/metricshub/winrm/service/client/encryption/AsyncHttpEncryptionAwareConduitFactory.java deleted file mode 100644 index 0d7ec69..0000000 --- a/src/main/java/org/metricshub/winrm/service/client/encryption/AsyncHttpEncryptionAwareConduitFactory.java +++ /dev/null @@ -1,46 +0,0 @@ -package org.metricshub.winrm.service.client.encryption; - -/*- - * ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲ - * WinRM Java Client - * ჻჻჻჻჻჻ - * Copyright 2023 - 2024 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. - * ╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱ - */ - -import java.io.IOException; -import java.util.Map; -import org.apache.cxf.Bus; -import org.apache.cxf.service.model.EndpointInfo; -import org.apache.cxf.transport.http.HTTPConduit; -import org.apache.cxf.transport.http.asyncclient.AsyncHTTPConduitFactory; -import org.apache.cxf.ws.addressing.EndpointReferenceType; - -/** - * Code from io.cloudsoft.winrm4j.client.encryption.AsyncHttpEncryptionAwareConduitFactory - * release 0.12.3 @link https://github.com/cloudsoft/winrm4j - */ -public class AsyncHttpEncryptionAwareConduitFactory extends AsyncHTTPConduitFactory { - - public AsyncHttpEncryptionAwareConduitFactory() { - super((Map) null); - } - - @Override - public HTTPConduit createConduit(final Bus bus, final EndpointInfo localInfo, final EndpointReferenceType target) - throws IOException { - return isShutdown() ? null : new AsyncHttpEncryptionAwareConduit(bus, localInfo, target, this); - } -} diff --git a/src/main/java/org/metricshub/winrm/service/client/encryption/ContentWithType.java b/src/main/java/org/metricshub/winrm/service/client/encryption/ContentWithType.java deleted file mode 100644 index 5603a21..0000000 --- a/src/main/java/org/metricshub/winrm/service/client/encryption/ContentWithType.java +++ /dev/null @@ -1,54 +0,0 @@ -package org.metricshub.winrm.service.client.encryption; - -/*- - * ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲ - * WinRM Java Client - * ჻჻჻჻჻჻ - * Copyright 2023 - 2024 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. - * ╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱ - */ - -import org.apache.cxf.message.Message; - -/** - * Code from io.cloudsoft.winrm4j.client.encryption.SignAndEncryptOutInterceptor.ContentWithType - * release 0.12.3 @link https://github.com/cloudsoft/winrm4j - */ -class ContentWithType { - - private final String contentType; - private final byte[] payload; - - private ContentWithType(final String contentType, final byte[] payload) { - this.contentType = contentType; - this.payload = payload; - } - - static ContentWithType of(final Message message, final byte[] payload) { - return new ContentWithType((String) message.get(Message.CONTENT_TYPE), payload); - } - - ContentWithType with(final byte[] payload) { - return new ContentWithType(contentType, payload); - } - - String getContentType() { - return contentType; - } - - byte[] getPayload() { - return payload; - } -} diff --git a/src/main/java/org/metricshub/winrm/service/client/encryption/DecryptAndVerifyInInterceptor.java b/src/main/java/org/metricshub/winrm/service/client/encryption/DecryptAndVerifyInInterceptor.java deleted file mode 100644 index 08ec55e..0000000 --- a/src/main/java/org/metricshub/winrm/service/client/encryption/DecryptAndVerifyInInterceptor.java +++ /dev/null @@ -1,45 +0,0 @@ -package org.metricshub.winrm.service.client.encryption; - -/*- - * ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲ - * WinRM Java Client - * ჻჻჻჻჻჻ - * Copyright 2023 - 2024 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. - * ╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱ - */ - -import org.apache.cxf.interceptor.StaxInInterceptor; -import org.apache.cxf.message.Message; -import org.apache.cxf.phase.AbstractPhaseInterceptor; -import org.apache.cxf.phase.Phase; - -/** - * Code from io.cloudsoft.winrm4j.client.encryption.DecryptAndVerifyInInterceptor - * release 0.12.3 @link https://github.com/cloudsoft/winrm4j - */ -public class DecryptAndVerifyInInterceptor extends AbstractPhaseInterceptor { - - public DecryptAndVerifyInInterceptor() { - super(Phase.POST_STREAM); - addBefore(StaxInInterceptor.class.getName()); - } - - public void handleMessage(final Message message) { - final NtlmEncryptionUtils ntlmEncryptionUtils = NtlmEncryptionUtils.of(message); - if (ntlmEncryptionUtils != null) { - ntlmEncryptionUtils.decrypt(message); - } - } -} diff --git a/src/main/java/org/metricshub/winrm/service/client/encryption/Decryptor.java b/src/main/java/org/metricshub/winrm/service/client/encryption/Decryptor.java deleted file mode 100644 index 18eda04..0000000 --- a/src/main/java/org/metricshub/winrm/service/client/encryption/Decryptor.java +++ /dev/null @@ -1,217 +0,0 @@ -package org.metricshub.winrm.service.client.encryption; - -/*- - * ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲ - * WinRM Java Client - * ჻჻჻჻჻჻ - * Copyright 2023 - 2024 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. - * ╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱ - */ - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.util.Arrays; -import org.apache.cxf.helpers.IOUtils; -import org.apache.cxf.message.Message; -import org.metricshub.winrm.service.client.auth.ntlm.NTCredentialsWithEncryption; -import org.metricshub.winrm.service.client.auth.ntlm.NTLMEngineUtils; - -/** - * Code from io.cloudsoft.winrm4j.client.encryption.NtlmEncryptionUtils.Decryptor - * release 0.12.3 @link https://github.com/cloudsoft/winrm4j - */ -public class Decryptor { - - private final NTCredentialsWithEncryption credentials; - private byte[] rawBytes; - private byte[] encryptedPayloadBytes; - private int index; - private int lastBlockStart; - private int lastBlockEnd; - private byte[] signatureBytes; - private byte[] sealedBytes; - private byte[] unsealedBytes; - - public Decryptor(final NTCredentialsWithEncryption credentials) { - this.credentials = credentials; - } - - public void handle(final Message message) { - final Object contentType = message.get(Message.CONTENT_TYPE); - - final boolean isEncrypted = contentType != null && contentType.toString().startsWith("multipart/encrypted"); - - if (isEncrypted) { - if (credentials == null) { - throw new IllegalStateException("Encrypted payload from server when no credentials with encryption known"); - } - if (!credentials.isAuthenticated()) { - throw new IllegalStateException("Encrypted payload from server when not authenticated"); - } - - try { - decrypt(message); - } catch (final Exception e) { - throw new IllegalStateException(e); - } - } else { - if (credentials != null && credentials.isAuthenticated()) { - throw new IllegalStateException( - "Unencrypted payload from server when authenticated and encryption is required" - ); - } - } - } - - void decrypt(final Message message) throws IOException { - try (final InputStream in = message.getContent(InputStream.class)) { - rawBytes = IOUtils.readBytesFromStream(in); - } - - unwrap(); - - final int signatureLength = (int) ByteArrayUtils.readLittleEndianUnsignedInt(encryptedPayloadBytes, 0); - signatureBytes = Arrays.copyOfRange(encryptedPayloadBytes, 4, 4 + signatureLength); - sealedBytes = Arrays.copyOfRange(encryptedPayloadBytes, 4 + signatureLength, encryptedPayloadBytes.length); - - unseal(); - - // should set length and type headers - but they don't seem to be needed! - - verify(); - - message.setContent(InputStream.class, new ByteArrayInputStream(unsealedBytes)); - } - - private void verify() throws IOException { - final long seqNum = ByteArrayUtils.readLittleEndianUnsignedInt(signatureBytes, 12); - final int checkSumOffset = credentials.hasNegotiateFlag(NTLMEngineUtils.NTLMSSP_NEGOTIATE_EXTENDED_SESSIONSECURITY) - ? 4 - : 8; - - final byte[] checksum = Arrays.copyOfRange(signatureBytes, checkSumOffset, 12); - - try (final ByteArrayOutputStream signature = new ByteArrayOutputStream()) { - NtlmEncryptionUtils.calculateSignature( - unsealedBytes, - seqNum, - signature, - credentials, - NTCredentialsWithEncryption::getServerSigningKey, - credentials.getStatefulDecryptor()::update - ); - - final byte[] expectedChecksum = Arrays.copyOfRange(signature.toByteArray(), checkSumOffset, 12); - final long expectedSeqNum = ByteArrayUtils.readLittleEndianUnsignedInt(signature.toByteArray(), 12); - - if (!Arrays.equals(checksum, expectedChecksum)) { - throw new IllegalStateException( - String.format( - "Checksum mismatch\n%s--\n%s", - ByteArrayUtils.formatHexDump(checksum), - ByteArrayUtils.formatHexDump(expectedChecksum) - ) - ); - } - - if (expectedSeqNum != seqNum) { - throw new IllegalStateException(String.format("Sequence number mismatch: %d != %d", seqNum, expectedSeqNum)); - } - } - - credentials.getSequenceNumberIncoming().incrementAndGet(); - } - - void unwrap() { - index = 0; - skipOver(NtlmEncryptionUtils.ENCRYPTED_BOUNDARY_CR); - skipUntil("\n" + NtlmEncryptionUtils.ENCRYPTED_BOUNDARY_CR); - skipUntil("\r\n"); - - // for credssh de-chunking might be needed, but not for ntlm - - lastBlockStart = index; - lastBlockEnd = rawBytes.length - NtlmEncryptionUtils.ENCRYPTED_BOUNDARY_END.length(); - index = lastBlockEnd; - skipOver(NtlmEncryptionUtils.ENCRYPTED_BOUNDARY_END); - - encryptedPayloadBytes = Arrays.copyOfRange(rawBytes, lastBlockStart, lastBlockEnd); - } - - void skipOver(final String s) { - skipOver(s.getBytes()); - } - - void skipOver(final byte[] expected) { - int i = 0; - while (i < expected.length) { - if (index >= rawBytes.length) { - throw new IllegalStateException( - String.format( - "Invalid format for response from server; terminated early (%d) when expecting '%s'\n%s", - i, - new String(expected), - ByteArrayUtils.formatHexDump(rawBytes) - ) - ); - } - - if (expected[i++] != rawBytes[index++]) { - throw new IllegalStateException( - String.format( - "Invalid format for response from server; mismatch at position %d (%d) when expecting '%s'\n%s", - index, - i, - new String(expected), - ByteArrayUtils.formatHexDump(rawBytes) - ) - ); - } - } - } - - void skipUntil(final String str) { - final byte[] expected = str.getBytes(); - int nextBlock = index; - outer:while (true) { - for (int i = 0; i < expected.length && nextBlock + i < rawBytes.length; i++) { - if (nextBlock + i >= rawBytes.length) { - throw new IllegalStateException( - String.format( - "Invalid format for response from server; terminated early (%d) when looking for '%s'\n%s", - i, - new String(expected), - ByteArrayUtils.formatHexDump(rawBytes) - ) - ); - } - if (expected[i] != rawBytes[nextBlock + i]) { - nextBlock++; - continue outer; - } - } - lastBlockStart = index; - lastBlockEnd = nextBlock; - index = nextBlock + expected.length; - return; - } - } - - private void unseal() { - unsealedBytes = credentials.getStatefulDecryptor().update(sealedBytes); - } -} diff --git a/src/main/java/org/metricshub/winrm/service/client/encryption/EncryptAndSignOutputStream.java b/src/main/java/org/metricshub/winrm/service/client/encryption/EncryptAndSignOutputStream.java deleted file mode 100644 index bca4c97..0000000 --- a/src/main/java/org/metricshub/winrm/service/client/encryption/EncryptAndSignOutputStream.java +++ /dev/null @@ -1,145 +0,0 @@ -package org.metricshub.winrm.service.client.encryption; - -/*- - * ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲ - * WinRM Java Client - * ჻჻჻჻჻჻ - * Copyright 2023 - 2024 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. - * ╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱ - */ - -import java.io.IOException; -import java.io.OutputStream; -import java.util.Objects; -import org.apache.cxf.io.CachedOutputStream; -import org.apache.cxf.message.Message; -import org.apache.http.auth.Credentials; -import org.metricshub.winrm.service.client.auth.ntlm.NTCredentialsWithEncryption; - -/** - * Code from io.cloudsoft.winrm4j.client.encryption.SignAndEncryptOutInterceptor.EncryptAndSignOutputStream - * release 0.12.3 @link https://github.com/cloudsoft/winrm4j - */ -class EncryptAndSignOutputStream extends CachedOutputStream { - - private final CachedOutputStream unencrypted; - private ContentWithType unencryptedResult = null; - private ContentWithType encrypted = null; - private final Message message; - - private OutputStream wrapped; - - private NTCredentialsWithEncryption credentials; - - public EncryptAndSignOutputStream(final Message message, final OutputStream outputStream) { - super(); - this.message = message; - wrapped = outputStream; - unencrypted = new CachedOutputStream(); - - final Object creds = message.get(Credentials.class.getName()); - if (creds instanceof NTCredentialsWithEncryption) { - credentials = (NTCredentialsWithEncryption) creds; - } - } - - @Override - public void resetOut(final OutputStream outputStream, final boolean copyOldContent) throws IOException { - super.resetOut(outputStream, copyOldContent); - } - - @Override - public void close() throws IOException { - super.close(); - unencrypted.write(getBytes()); - currentStream = NullOutputStream.NULL_OUTPUT_STREAM; - - if (wrapped != null) { - processAndShip(wrapped); - wrapped.close(); - } - } - - private synchronized ContentWithType getEncrypted() { - try { - if (encrypted == null) { - final byte[] bytesEncryptedAndSigned = NtlmEncryptionUtils - .of(credentials) - .encryptAndSign(message, unencrypted.getBytes()); - - encrypted = ContentWithType.of(message, bytesEncryptedAndSigned); - } - return encrypted; - } catch (final IOException e) { - throw new IllegalStateException(e); - } - } - - private byte[] getUnencrypted() { - try { - return unencrypted.getBytes(); - } catch (final IOException e) { - throw new IllegalStateException(e); - } - } - - synchronized ContentWithType getAppropriate() { - if (unencryptedResult == null) { - unencryptedResult = ContentWithType.of(message, null); - } - - if (credentials == null || !credentials.isAuthenticated()) { - if (encrypted != null) { - // clear any previous encryption if no longer valid - encrypted = null; - } - - return credentials != null && !credentials.isAuthenticated() - ? unencryptedResult.with(AsyncHttpEncryptionAwareConduit.PRE_AUTH_BOGUS_PAYLOAD) - : unencryptedResult.with(getUnencrypted()); - } - - return getEncrypted(); - } - - private void processAndShip(final OutputStream output) throws IOException { - output.write(getAppropriate().getPayload()); - output.close(); - } - - @Override - public int hashCode() { - final int prime = 31; - int result = super.hashCode(); - result = prime * result + Objects.hash(credentials, encrypted, message, unencrypted, unencryptedResult, wrapped); - return result; - } - - @Override - public boolean equals(Object obj) { - if (this == obj) return true; - if (!super.equals(obj)) return false; - if (!(obj instanceof EncryptAndSignOutputStream)) return false; - EncryptAndSignOutputStream other = (EncryptAndSignOutputStream) obj; - return ( - Objects.equals(credentials, other.credentials) && - Objects.equals(encrypted, other.encrypted) && - Objects.equals(message, other.message) && - Objects.equals(unencrypted, other.unencrypted) && - Objects.equals(unencryptedResult, other.unencryptedResult) && - Objects.equals(wrapped, other.wrapped) - ); - } -} diff --git a/src/main/java/org/metricshub/winrm/service/client/encryption/EncryptionAwareHttpEntity.java b/src/main/java/org/metricshub/winrm/service/client/encryption/EncryptionAwareHttpEntity.java deleted file mode 100644 index 5226a90..0000000 --- a/src/main/java/org/metricshub/winrm/service/client/encryption/EncryptionAwareHttpEntity.java +++ /dev/null @@ -1,42 +0,0 @@ -package org.metricshub.winrm.service.client.encryption; - -/*- - * ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲ - * WinRM Java Client - * ჻჻჻჻჻჻ - * Copyright 2023 - 2024 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. - * ╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱ - */ - -import org.apache.http.HttpEntityEnclosingRequest; -import org.apache.http.entity.BasicHttpEntity; -import org.apache.http.protocol.HTTP; - -/** - * Code from io.cloudsoft.winrm4j.client.encryption.AsyncHttpEncryptionAwareConduit.EncryptionAwareHttpEntity - * release 0.12.3 @link https://github.com/cloudsoft/winrm4j - */ -public abstract class EncryptionAwareHttpEntity extends BasicHttpEntity { - - public void refreshHeaders(final HttpEntityEnclosingRequest request) { - final ContentWithType appropriate = getAppropriate(); - setContentLength(appropriate.getPayload().length); - - request.setHeader(HTTP.CONTENT_LEN, String.valueOf(appropriate.getPayload().length)); - request.setHeader(HTTP.CONTENT_TYPE, appropriate.getContentType()); - } - - protected abstract ContentWithType getAppropriate(); -} diff --git a/src/main/java/org/metricshub/winrm/service/client/encryption/NtlmEncryptionUtils.java b/src/main/java/org/metricshub/winrm/service/client/encryption/NtlmEncryptionUtils.java deleted file mode 100644 index 86490cc..0000000 --- a/src/main/java/org/metricshub/winrm/service/client/encryption/NtlmEncryptionUtils.java +++ /dev/null @@ -1,171 +0,0 @@ -package org.metricshub.winrm.service.client.encryption; - -/*- - * ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲ - * WinRM Java Client - * ჻჻჻჻჻჻ - * Copyright 2023 - 2024 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. - * ╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱ - */ - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.util.Arrays; -import java.util.function.Function; -import java.util.function.UnaryOperator; -import java.util.zip.CRC32; -import org.apache.cxf.message.Message; -import org.apache.http.auth.Credentials; -import org.metricshub.winrm.service.client.auth.ntlm.NTCredentialsWithEncryption; -import org.metricshub.winrm.service.client.auth.ntlm.NTLMEngineUtils; - -/** - * Code from io.cloudsoft.winrm4j.client.encryption.NtlmEncryptionUtils release - * 0.12.3 @link https://github.com/cloudsoft/winrm4j - */ -public class NtlmEncryptionUtils { - - public static final String ENCRYPTED_BOUNDARY_PREFIX = "--Encrypted Boundary"; - public static final String ENCRYPTED_BOUNDARY_CR = ENCRYPTED_BOUNDARY_PREFIX + "\r\n"; - public static final String ENCRYPTED_BOUNDARY_END = ENCRYPTED_BOUNDARY_PREFIX + "--\r\n"; - - protected final NTCredentialsWithEncryption credentials; - - private NtlmEncryptionUtils(final NTCredentialsWithEncryption credentials) { - this.credentials = credentials; - } - - static NtlmEncryptionUtils of(final Credentials credentials) { - return credentials instanceof NTCredentialsWithEncryption - ? new NtlmEncryptionUtils((NTCredentialsWithEncryption) credentials) - : null; - } - - static NtlmEncryptionUtils of(final Message message) { - final Credentials credentials = (Credentials) message.getExchange().get(Credentials.class.getName()); - return of(credentials); - } - - public byte[] encryptAndSign(final Message message, final byte[] messageBody) { - try (final ByteArrayOutputStream out = new ByteArrayOutputStream()) { - out.write(ENCRYPTED_BOUNDARY_CR.getBytes()); - out.write(("\tContent-Type: application/HTTP-SPNEGO-session-encrypted\r\n").getBytes()); - - // message.get(Message.CONTENT_TYPE); - if we need the action - // Content-Type -> application/soap+xml; - // action="http://schemas.xmlsoap.org/ws/2004/09/transfer/Create" - out.write( - String - .format("\tOriginalContent: type=application/soap+xml;charset=UTF-8;Length=%d\r\n", messageBody.length) - .getBytes() - ); - - out.write(ENCRYPTED_BOUNDARY_CR.getBytes()); - out.write("\tContent-Type: application/octet-stream\r\n".getBytes()); - - // for credssh chunking might be needed, but not for ntlm - - writeNtlmEncrypted(messageBody, out); - - out.write(ENCRYPTED_BOUNDARY_END.getBytes()); - - message.put( - Message.CONTENT_TYPE, - "multipart/encrypted;protocol=\"application/HTTP-SPNEGO-session-encrypted\";" + - "boundary=\"Encrypted Boundary\"" - ); - message.put(Message.ENCODING, null); - - return out.toByteArray(); - } catch (final Exception e) { - throw new IllegalStateException("Cannot encrypt WinRM message", e); - } - } - - private byte[] seal(final byte[] in) { - return credentials.getStatefulEncryptor().update(in); - } - - private void writeNtlmEncrypted(final byte[] messageBody, final ByteArrayOutputStream encrypted) throws IOException { - long seqNum = credentials.getSequenceNumberOutgoing().incrementAndGet(); - - try ( - final ByteArrayOutputStream signatureOs = new ByteArrayOutputStream(); - final ByteArrayOutputStream sealedOs = new ByteArrayOutputStream() - ) { - // seal first, even though appended afterwards, because encryptor is stateful - sealedOs.write(seal(messageBody)); - - calculateSignature( - messageBody, - seqNum, - signatureOs, - credentials, - NTCredentialsWithEncryption::getClientSigningKey, - this::seal - ); - - encrypted.write(ByteArrayUtils.getLittleEndianUnsignedInt(signatureOs.size())); - encrypted.write(signatureOs.toByteArray()); - encrypted.write(sealedOs.toByteArray()); - } - } - - public void decrypt(final Message message) { - new Decryptor(credentials).handle(message); - } - - static void calculateSignature( - final byte[] messageBody, - final long seqNum, - final ByteArrayOutputStream signature, - final NTCredentialsWithEncryption credentials, - final Function signingKeyFunction, - final UnaryOperator sealer - ) throws IOException { - if (credentials.hasNegotiateFlag(NTLMEngineUtils.NTLMSSP_NEGOTIATE_EXTENDED_SESSIONSECURITY)) { - // also see HMACMD5 in NTLMEngineIpml - byte[] checksum = EncryptionUtils.hmacMd5( - signingKeyFunction.apply(credentials), - ByteArrayUtils.concat(ByteArrayUtils.getLittleEndianUnsignedInt(seqNum), messageBody) - ); - - checksum = Arrays.copyOfRange(checksum, 0, 8); - - if (credentials.hasNegotiateFlag(NTLMEngineUtils.NTLMSSP_NEGOTIATE_KEY_EXCH)) { - checksum = sealer.apply(checksum); - } - // version - signature.write(new byte[] { 1, 0, 0, 0 }); - // checksum - signature.write(checksum); - // seq num - signature.write(ByteArrayUtils.getLittleEndianUnsignedInt(seqNum)); - } else { - final CRC32 crc = new CRC32(); - crc.update(messageBody); - final long messageCrc = crc.getValue(); - - // version - signature.write(new byte[] { 1, 0, 0, 0 }); - // random pad - signature.write(sealer.apply(ByteArrayUtils.getLittleEndianUnsignedInt(0))); - // checksum - signature.write(sealer.apply(ByteArrayUtils.getLittleEndianUnsignedInt(messageCrc))); - // seq num - signature.write(sealer.apply(ByteArrayUtils.getLittleEndianUnsignedInt(seqNum))); - } - } -} diff --git a/src/main/java/org/metricshub/winrm/service/client/encryption/NullOutputStream.java b/src/main/java/org/metricshub/winrm/service/client/encryption/NullOutputStream.java deleted file mode 100644 index 785dfc7..0000000 --- a/src/main/java/org/metricshub/winrm/service/client/encryption/NullOutputStream.java +++ /dev/null @@ -1,73 +0,0 @@ -package org.metricshub.winrm.service.client.encryption; - -/*- - * ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲ - * WinRM Java Client - * ჻჻჻჻჻჻ - * Copyright 2023 - 2024 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. - * ╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱ - */ - -import java.io.IOException; -import java.io.OutputStream; - -/** - * This OutputStream writes all data to the famous /dev/null. - * - * This output stream has no destination (file/socket etc.) and all - * bytes written to it are ignored and lost. - * - * - * PS: from common-io/NullOutputStream. Embedded to avoid to include common-io. 2.11.0 - */ -class NullOutputStream extends OutputStream { - - private NullOutputStream() {} - - /** - * A singleton. - */ - static final NullOutputStream NULL_OUTPUT_STREAM = new NullOutputStream(); - - /** - * Does nothing - output to /dev/null. - * @param b The bytes to write - * @param off The start offset - * @param len The number of bytes to write - */ - @Override - public void write(final byte[] b, final int off, final int len) { - //to /dev/null - } - - /** - * Does nothing - output to /dev/null. - * @param b The byte to write - */ - @Override - public void write(final int b) { - //to /dev/null - } - - /** - * Does nothing - output to /dev/null. - * @param b The bytes to write - * @throws IOException never - */ - @Override - public void write(final byte[] b) throws IOException { - //to /dev/null - } -} diff --git a/src/main/java/org/metricshub/winrm/service/client/encryption/SignAndEncryptOutInterceptor.java b/src/main/java/org/metricshub/winrm/service/client/encryption/SignAndEncryptOutInterceptor.java deleted file mode 100644 index badb5b4..0000000 --- a/src/main/java/org/metricshub/winrm/service/client/encryption/SignAndEncryptOutInterceptor.java +++ /dev/null @@ -1,57 +0,0 @@ -package org.metricshub.winrm.service.client.encryption; - -/*- - * ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲ - * WinRM Java Client - * ჻჻჻჻჻჻ - * Copyright 2023 - 2024 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. - * ╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱ - */ - -import java.io.OutputStream; -import org.apache.cxf.interceptor.StaxOutInterceptor; -import org.apache.cxf.message.Message; -import org.apache.cxf.phase.AbstractPhaseInterceptor; -import org.apache.cxf.phase.Phase; - -/** - * Not only encrypts if necessary, but must track the payload and make it available to - * {@link AsyncHttpEncryptionAwareConduit} in case we need to subsequently encrypt. - * - * Code from io.cloudsoft.winrm4j.client.encryption.SignAndEncryptOutInterceptor.SignAndEncryptOutInterceptor - * release 0.12.3 @link https://github.com/cloudsoft/winrm4j - */ -public class SignAndEncryptOutInterceptor extends AbstractPhaseInterceptor { - - private static final String APPLIED = SignAndEncryptOutInterceptor.class.getSimpleName() + ".APPLIED"; - - public SignAndEncryptOutInterceptor() { - super(Phase.PRE_STREAM); - // we need to be set before various other output devices, so they write to us - addBefore(StaxOutInterceptor.class.getName()); - } - - @Override - public void handleMessage(final Message message) { - boolean hasApplied = message.containsKey(APPLIED); - if (!hasApplied) { - message.put(APPLIED, Boolean.TRUE); - final OutputStream outputStream = message.getContent(OutputStream.class); - final EncryptAndSignOutputStream newOut = new EncryptAndSignOutputStream(message, outputStream); - message.setContent(OutputStream.class, newOut); - message.setContent(EncryptAndSignOutputStream.class, newOut); - } - } -} diff --git a/src/main/java/org/metricshub/winrm/shares/SmbTempShare.java b/src/main/java/org/metricshub/winrm/shares/SmbTempShare.java index f6539f7..ee153ef 100644 --- a/src/main/java/org/metricshub/winrm/shares/SmbTempShare.java +++ b/src/main/java/org/metricshub/winrm/shares/SmbTempShare.java @@ -40,7 +40,7 @@ import org.metricshub.winrm.exceptions.WinRMException; import org.metricshub.winrm.exceptions.WindowsRemoteException; 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; public class SmbTempShare extends WindowsTempShare implements AutoCloseable { @@ -54,7 +54,7 @@ public class SmbTempShare extends WindowsTempShare implements AutoCloseable { /** * The SmbTempShare constructor. * - * @param winRMService WinRMService instance + * @param windowsRemoteExecutor WinRM executor (CXF or light backend) * @param winRMEndpoint Endpoint with credentials * @param smbClient The SMB client * @param connection The SMB connection @@ -64,7 +64,7 @@ public class SmbTempShare extends WindowsTempShare implements AutoCloseable { * @param remotePath The path on the remote system of the directory being shared */ private SmbTempShare( - final WinRMService winRMService, + final WindowsRemoteExecutor windowsRemoteExecutor, final WinRMEndpoint winRMEndpoint, final SMBClient smbClient, final Connection connection, @@ -73,7 +73,7 @@ private SmbTempShare( final String shareNameOrUnc, final String remotePath ) { - super(winRMService, shareNameOrUnc, remotePath); + super(windowsRemoteExecutor, shareNameOrUnc, remotePath); this.winRMEndpoint = winRMEndpoint; this.smbClient = smbClient; this.connection = connection; @@ -115,17 +115,20 @@ public static SmbTempShare createInstance( winRMEndpoint, (key, smb) -> { if (smb == null) { - WinRMService winRMService = null; + WindowsRemoteExecutor windowsRemoteExecutor = null; SMBClient smbClient = null; Connection connection = null; Session session = null; DiskShare diskShare = null; try { - winRMService = WinRMService.createInstance(winRMEndpoint, timeout, ticketCache, authentications); + // Honour the backend toggle: SMB file transfer is always smbj, but the WinRM command + // orchestration follows the selected backend (so "light" does not fall back to CXF). + windowsRemoteExecutor = + WinRMExecutorFactory.createInstance(winRMEndpoint, timeout, ticketCache, authentications); final WindowsTempShare windowsTempShare = getOrCreateShare( - winRMService, + windowsRemoteExecutor, timeout, (w, r, s, t) -> { try { @@ -154,7 +157,7 @@ public static SmbTempShare createInstance( diskShare = (DiskShare) session.connectShare(windowsTempShare.getShareName()); return new SmbTempShare( - winRMService, + windowsRemoteExecutor, winRMEndpoint, smbClient, connection, @@ -164,11 +167,11 @@ public static SmbTempShare createInstance( windowsTempShare.getRemotePath() ); } catch (final RuntimeException e) { - closeResources(winRMService, smbClient, connection, session, diskShare); + closeResources(windowsRemoteExecutor, smbClient, connection, session, diskShare); throw e; } catch (final Exception e) { - closeResources(winRMService, smbClient, connection, session, diskShare); + closeResources(windowsRemoteExecutor, smbClient, connection, session, diskShare); throw new RuntimeException(e); } @@ -201,7 +204,7 @@ public static SmbTempShare createInstance( } private static void closeResources( - final WinRMService winRMService, + final WindowsRemoteExecutor windowsRemoteExecutor, final SMBClient smbClient, final Connection connection, final Session session, @@ -227,8 +230,8 @@ private static void closeResources( smbClient.close(); } - if (winRMService != null) { - winRMService.close(); + if (windowsRemoteExecutor != null) { + windowsRemoteExecutor.close(); } } @@ -277,14 +280,14 @@ public synchronized void close() throws IOException { smbClient.close(); } - ((WinRMService) getWindowsRemoteExecutor()).close(); + getWindowsRemoteExecutor().close(); } } /** * Share the remote directory on the host. * - * @param windowsRemoteExecutor WinRMService instance. + * @param windowsRemoteExecutor WinRM executor (CXF or light backend). * @param remotePath The remote path. * @param shareName The Share Name. * @param timeout Timeout in milliseconds. diff --git a/src/main/java/org/metricshub/winrm/wql/WinRMWqlExecutor.java b/src/main/java/org/metricshub/winrm/wql/WinRMWqlExecutor.java index 95a0795..e16e8e3 100644 --- a/src/main/java/org/metricshub/winrm/wql/WinRMWqlExecutor.java +++ b/src/main/java/org/metricshub/winrm/wql/WinRMWqlExecutor.java @@ -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. @@ -27,11 +27,13 @@ import java.util.stream.Collectors; import org.metricshub.winrm.Utils; import org.metricshub.winrm.WinRMHttpProtocolEnum; +import org.metricshub.winrm.WindowsRemoteExecutor; import org.metricshub.winrm.WmiHelper; import org.metricshub.winrm.exceptions.WinRMException; +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; public class WinRMWqlExecutor { @@ -117,7 +119,7 @@ public static WinRMWqlExecutor executeWql( final WinRMEndpoint winRMEndpoint = new WinRMEndpoint(protocol, hostname, port, username, password, namespace); try ( - final WinRMService winRMService = WinRMService.createInstance( + final WindowsRemoteExecutor winRMService = WinRMExecutorFactory.createInstance( winRMEndpoint, timeout, ticketCache, @@ -135,6 +137,12 @@ public static WinRMWqlExecutor executeWql( .collect(Collectors.toList()); return new WinRMWqlExecutor(Utils.getCurrentTimeMillis() - start, headers, rows); + } catch (final WinRMException | WqlQuerySyntaxException | TimeoutException e) { + throw e; + } catch (final WindowsRemoteException e) { + // The WindowsRemoteExecutor interface declares the broader WindowsRemoteException; both + // backends actually throw WinRMException. Preserve the historical checked-exception surface. + throw new WinRMException(e, e.getMessage()); } } } diff --git a/src/main/resources/META-INF/jax-ws-catalog.xml b/src/main/resources/META-INF/jax-ws-catalog.xml deleted file mode 100644 index 9a15209..0000000 --- a/src/main/resources/META-INF/jax-ws-catalog.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - diff --git a/src/main/resources/jaxws/bindings.xml b/src/main/resources/jaxws/bindings.xml deleted file mode 100644 index 029b072..0000000 --- a/src/main/resources/jaxws/bindings.xml +++ /dev/null @@ -1,48 +0,0 @@ - - - - - false - false - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/main/resources/wsdl/WinRM.wsdl b/src/main/resources/wsdl/WinRM.wsdl deleted file mode 100644 index dbfaa07..0000000 --- a/src/main/resources/wsdl/WinRM.wsdl +++ /dev/null @@ -1,475 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/main/resources/xsd/dsp8033_1.0.xsd b/src/main/resources/xsd/dsp8033_1.0.xsd deleted file mode 100644 index 6c9bd00..0000000 --- a/src/main/resources/xsd/dsp8033_1.0.xsd +++ /dev/null @@ -1,307 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/main/resources/xsd/dsp8034_1.0.xsd b/src/main/resources/xsd/dsp8034_1.0.xsd deleted file mode 100644 index 5b4dca1..0000000 --- a/src/main/resources/xsd/dsp8034_1.0.xsd +++ /dev/null @@ -1,165 +0,0 @@ - - - - - - - - - - - - - - - - - - If "Policy" elements from namespace - "http://schemas.xmlsoap.org/ws/2002/12/policy#policy" are used, - they must appear first (before any extensibility elements). - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/main/resources/xsd/transfer.xsd b/src/main/resources/xsd/transfer.xsd deleted file mode 100644 index 998fdfd..0000000 --- a/src/main/resources/xsd/transfer.xsd +++ /dev/null @@ -1,73 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/main/resources/xsd/ws-addr.xsd b/src/main/resources/xsd/ws-addr.xsd deleted file mode 100644 index 47362ed..0000000 --- a/src/main/resources/xsd/ws-addr.xsd +++ /dev/null @@ -1,137 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/main/resources/xsd/wsman.xsd b/src/main/resources/xsd/wsman.xsd deleted file mode 100644 index ff97bbf..0000000 --- a/src/main/resources/xsd/wsman.xsd +++ /dev/null @@ -1,418 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - Instances of this type can be only simple types or EPRs, not arbitrary mixed data. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/main/resources/xsd/xml.xsd b/src/main/resources/xsd/xml.xsd deleted file mode 100644 index aea7d0d..0000000 --- a/src/main/resources/xsd/xml.xsd +++ /dev/null @@ -1,287 +0,0 @@ - - - - - - -
-

About the XML namespace

- -
-

- This schema document describes the XML namespace, in a form - suitable for import by other schema documents. -

-

- See - http://www.w3.org/XML/1998/namespace.html and - - http://www.w3.org/TR/REC-xml for information - about this namespace. -

-

- Note that local names in this namespace are intended to be - defined only by the World Wide Web Consortium or its subgroups. - The names currently defined in this namespace are listed below. - They should not be used with conflicting semantics by any Working - Group, specification, or document instance. -

-

- See further below in this document for more information about how to refer to this schema document from your own - XSD schema documents and about the - namespace-versioning policy governing this schema document. -

-
-
-
-
- - - - -
- -

lang (as an attribute name)

-

- denotes an attribute whose value - is a language code for the natural language of the content of - any element; its value is inherited. This name is reserved - by virtue of its definition in the XML specification.

- -
-
-

Notes

-

- Attempting to install the relevant ISO 2- and 3-letter - codes as the enumerated possible values is probably never - going to be a realistic possibility. -

-

- See BCP 47 at - http://www.rfc-editor.org/rfc/bcp/bcp47.txt - and the IANA language subtag registry at - - http://www.iana.org/assignments/language-subtag-registry - for further information. -

-

- The union allows for the 'un-declaration' of xml:lang with - the empty string. -

-
-
-
- - - - - - - - - -
- - - - -
- -

space (as an attribute name)

-

- denotes an attribute whose - value is a keyword indicating what whitespace processing - discipline is intended for the content of the element; its - value is inherited. This name is reserved by virtue of its - definition in the XML specification.

- -
-
-
- - - - - - -
- - - -
- -

base (as an attribute name)

-

- denotes an attribute whose value - provides a URI to be used as the base for interpreting any - relative URIs in the scope of the element on which it - appears; its value is inherited. This name is reserved - by virtue of its definition in the XML Base specification.

- -

- See http://www.w3.org/TR/xmlbase/ - for information about this attribute. -

-
-
-
-
- - - - -
- -

id (as an attribute name)

-

- denotes an attribute whose value - should be interpreted as if declared to be of type ID. - This name is reserved by virtue of its definition in the - xml:id specification.

- -

- See http://www.w3.org/TR/xml-id/ - for information about this attribute. -

-
-
-
-
- - - - - - - - - - -
- -

Father (in any context at all)

- -
-

- denotes Jon Bosak, the chair of - the original XML Working Group. This name is reserved by - the following decision of the W3C XML Plenary and - XML Coordination groups: -

-
-

- In appreciation for his vision, leadership and - dedication the W3C XML Plenary on this 10th day of - February, 2000, reserves for Jon Bosak in perpetuity - the XML name "xml:Father". -

-
-
-
-
-
- - - -
-

About this schema document

- -
-

- This schema defines attributes and an attribute group suitable - for use by schemas wishing to allow xml:base, - xml:lang, xml:space or - xml:id attributes on elements they define. -

-

- To enable this, such a schema must import this schema for - the XML namespace, e.g. as follows: -

-
-          <schema . . .>
-           . . .
-           <import namespace="http://www.w3.org/XML/1998/namespace"
-                      schemaLocation="http://www.w3.org/2001/xml.xsd"/>
-     
-

- or -

-
-           <import namespace="http://www.w3.org/XML/1998/namespace"
-                      schemaLocation="http://www.w3.org/2009/01/xml.xsd"/>
-     
-

- Subsequently, qualified reference to any of the attributes or the - group defined below will have the desired effect, e.g. -

-
-          <type . . .>
-           . . .
-           <attributeGroup ref="xml:specialAttrs"/>
-     
-

- will define a type which will schema-validate an instance element - with any of those attributes. -

-
-
-
-
- - - -
-

Versioning policy for this schema document

-
-

- In keeping with the XML Schema WG's standard versioning - policy, this schema document will persist at - - http://www.w3.org/2009/01/xml.xsd. -

-

- At the date of issue it can also be found at - - http://www.w3.org/2001/xml.xsd. -

-

- The schema document at that URI may however change in the future, - in order to remain compatible with the latest version of XML - Schema itself, or with the XML namespace itself. In other words, - if the XML Schema or XML namespaces change, the version of this - document at - http://www.w3.org/2001/xml.xsd - - will change accordingly; the version at - - http://www.w3.org/2009/01/xml.xsd - - will not change. -

-

- Previous dated (and unchanging) versions of this schema - document are at: -

- -
-
-
-
- -
- diff --git a/src/site/markdown/index.md b/src/site/markdown/index.md index 095b574..ad29726 100644 --- a/src/site/markdown/index.md +++ b/src/site/markdown/index.md @@ -4,6 +4,18 @@ 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 client is the only +> implementation (same public API). Unlike the CXF-based client, which silently trusted every TLS +> certificate, it **validates the server certificate and verifies the hostname by default**, so +> **WinRM-over-HTTPS connections to hosts with self-signed or untrusted certificates will fail** +> during the TLS handshake. To restore connectivity, either install the certificate into a Java +> trust store or set `-Dorg.metricshub.winrm.tls.insecure=true` (insecure — for testing only). +> The client supports NTLM over HTTP/HTTPS and Kerberos (SPNEGO) over HTTPS. Setting +> `-Dorg.metricshub.winrm.backend=cxf` now fails with a clear error; remove the property (or stay +> on winrm-java 1.x). + # How to run the WinRM Client inside Java Add WinRM in the list of dependencies in your [Maven **pom.xml**](https://maven.apache.org/pom.html): diff --git a/src/test/java/org/metricshub/winrm/CatalogResolutionTest.java b/src/test/java/org/metricshub/winrm/CatalogResolutionTest.java deleted file mode 100644 index e57f7d4..0000000 --- a/src/test/java/org/metricshub/winrm/CatalogResolutionTest.java +++ /dev/null @@ -1,64 +0,0 @@ -package org.metricshub.winrm; - -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import java.io.InputStream; -import java.net.URI; -import org.apache.cxf.Bus; -import org.apache.cxf.BusFactory; -import org.apache.cxf.catalog.OASISCatalogManager; -import org.junit.jupiter.api.Test; - -/** - * Verifies that the JAX-WS catalog shipped at META-INF/jax-ws-catalog.xml is - * auto-discovered by Apache CXF and remaps the absolute schema URLs referenced - * by wsdl/WinRM.wsdl to local classpath resources. - * - *

Without this mapping the WSDL loader fetches dsp8033 / dsp8034 / xml.xsd / - * ws-addr.xsd from schemas.dmtf.org / www.w3.org over the network. On offline - * or restricted-egress hosts that fetch blocks for ~75 s (OS TCP timeout) and - * the connection attempt fails with - * {@code WSDLException(PARSER_ERROR) ... Caused by: ConnectException}. - */ -class CatalogResolutionTest { - - @Test - void catalogResolvesWsdlImportUrlsToClasspathResources() throws Exception { - // Dedicated Bus so the test neither leaks CXF resources nor mutates the - // JVM-wide default Bus shared with other tests. - final Bus bus = BusFactory.newInstance().createBus(); - try { - final OASISCatalogManager catalog = OASISCatalogManager.getCatalogManager(bus); - assertNotNull(catalog, "CXF OASISCatalogManager must be available"); - - assertResolvesToClasspath(catalog, "http://schemas.dmtf.org/wbem/wsman/1/dsp8033_1.0.xsd", "dsp8033_1.0.xsd"); - assertResolvesToClasspath(catalog, "http://schemas.dmtf.org/wbem/wsman/1/dsp8034_1.0.xsd", "dsp8034_1.0.xsd"); - assertResolvesToClasspath(catalog, "http://www.w3.org/2001/xml.xsd", "xml.xsd"); - assertResolvesToClasspath(catalog, "http://www.w3.org/2006/03/addressing/ws-addr.xsd", "ws-addr.xsd"); - } finally { - bus.shutdown(true); - } - } - - private static void assertResolvesToClasspath( - final OASISCatalogManager catalog, - final String systemId, - final String expectedSuffix - ) throws Exception { - final String resolved = catalog.resolveSystem(systemId); - assertNotNull(resolved, "catalog did not resolve " + systemId); - // Must NOT be a network URL — otherwise CXF will still hit the network at runtime. - assertFalse( - resolved.startsWith("http://") || resolved.startsWith("https://"), - "catalog returned a network URL for " + systemId + " -> " + resolved - ); - assertTrue(resolved.endsWith(expectedSuffix), "resolved URI does not end with " + expectedSuffix + ": " + resolved); - // The mapping must point at a resource that actually exists and is readable, - // not just a well-formed URI. - try (InputStream stream = new URI(resolved).toURL().openStream()) { - assertTrue(stream.read() != -1, "resolved URI is empty: " + resolved); - } - } -} diff --git a/src/test/java/org/metricshub/winrm/WinRMLiveTest.java b/src/test/java/org/metricshub/winrm/WinRMLiveTest.java new file mode 100644 index 0000000..b2c7984 --- /dev/null +++ b/src/test/java/org/metricshub/winrm/WinRMLiveTest.java @@ -0,0 +1,116 @@ +package org.metricshub.winrm; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.List; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledIfSystemProperty; +import org.metricshub.winrm.service.client.auth.AuthenticationEnum; +import org.metricshub.winrm.wql.WinRMWqlExecutor; + +/** + * Live smoke test against a REAL WinRM host — the successor of the pre-2.0.0 CXF-vs-light + * differential harness (the CXF baseline was removed with the backend; result parity was + * proven and gated before removal). Disabled unless {@code winrm.live.host} is set, so it + * never runs in CI. + * + *

One-command run against a lab host: + * + *

+ * 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: {@code winrm.live.port} (defaults to 5985/5986 by protocol), + * {@code winrm.live.password} (inline, instead of the file), {@code winrm.live.namespace}, + * {@code winrm.live.wql}, {@code winrm.live.command}, and {@code winrm.live.tls.insecure=true} + * to skip TLS validation for hosts with self-signed certificates. + */ +@EnabledIfSystemProperty(named = "winrm.live.host", matches = ".+") +class WinRMLiveTest { + + private static String host; + private static WinRMHttpProtocolEnum protocol; + private static Integer port; + private static String username; + private static char[] password; + private static String namespace; + private static String wql; + private static String command; + + @BeforeAll + static void readConfiguration() throws Exception { + host = System.getProperty("winrm.live.host"); + protocol = + "https".equalsIgnoreCase(System.getProperty("winrm.live.protocol", "http")) + ? WinRMHttpProtocolEnum.HTTPS + : WinRMHttpProtocolEnum.HTTP; + final String portProperty = System.getProperty("winrm.live.port"); + port = portProperty == null ? null : Integer.valueOf(portProperty); + username = System.getProperty("winrm.live.username"); + namespace = System.getProperty("winrm.live.namespace"); + wql = System.getProperty("winrm.live.wql", "SELECT Caption FROM Win32_OperatingSystem"); + command = System.getProperty("winrm.live.command", "echo winrm-live"); + + final String inline = System.getProperty("winrm.live.password"); + if (inline != null) { + password = inline.toCharArray(); + } else { + final String file = System.getProperty("winrm.live.password-file"); + if (file == null) { + throw new IllegalArgumentException("Set winrm.live.password or winrm.live.password-file"); + } + password = new String(Files.readAllBytes(Paths.get(file)), StandardCharsets.UTF_8).trim().toCharArray(); + } + + if ("true".equalsIgnoreCase(System.getProperty("winrm.live.tls.insecure"))) { + System.setProperty("org.metricshub.winrm.tls.insecure", "true"); + } + } + + @Test + void wqlReturnsRows() throws Exception { + final WinRMWqlExecutor result = WinRMWqlExecutor.executeWql( + protocol, + host, + port, + username, + password, + namespace, + wql, + 30_000L, + null, + List.of(AuthenticationEnum.NTLM) + ); + assertFalse(result.getHeaders().isEmpty(), "WQL result must have headers"); + assertFalse(result.getRows().isEmpty(), "WQL result must have rows"); + } + + @Test + void commandSucceeds() throws Exception { + final WindowsRemoteCommandResult result = org.metricshub.winrm.command.WinRMCommandExecutor.execute( + command, + protocol, + host, + port, + username, + password, + null, + 30_000L, + null, + null, + List.of(AuthenticationEnum.NTLM) + ); + assertEquals(0, result.getStatusCode(), () -> "stderr: " + result.getStderr()); + assertTrue(result.getStdout().length() > 0, "command must produce stdout"); + } +} diff --git a/src/test/java/org/metricshub/winrm/command/WinRMCommandExecutorTest.java b/src/test/java/org/metricshub/winrm/command/WinRMCommandExecutorTest.java index df46bf3..ef9f08e 100644 --- a/src/test/java/org/metricshub/winrm/command/WinRMCommandExecutorTest.java +++ b/src/test/java/org/metricshub/winrm/command/WinRMCommandExecutorTest.java @@ -26,9 +26,10 @@ import java.util.List; import org.junit.jupiter.api.Test; import org.metricshub.winrm.WindowsRemoteCommandResult; +import org.metricshub.winrm.WindowsRemoteExecutor; import org.metricshub.winrm.WindowsRemoteProcessUtils; 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; import org.mockito.MockedStatic; @@ -162,17 +163,17 @@ void testExecute() throws Exception { final MockedStatic mockedWindowsRemoteProcessUtils = mockStatic( WindowsRemoteProcessUtils.class ); - final MockedStatic mockedWinRMService = mockStatic(WinRMService.class) + final MockedStatic mockedFactory = mockStatic(WinRMExecutorFactory.class) ) { mockedWindowsRemoteProcessUtils.when(() -> getWindowsEncodingCharset(any(), anyLong())).thenReturn(UTF_8); - final WinRMService winRMService = mock(WinRMService.class); + final WindowsRemoteExecutor executor = mock(WindowsRemoteExecutor.class); - mockedWinRMService - .when(() -> WinRMService.createInstance(any(WinRMEndpoint.class), anyLong(), isNull(), isNull())) - .thenReturn(winRMService); + mockedFactory + .when(() -> WinRMExecutorFactory.createInstance(any(WinRMEndpoint.class), anyLong(), isNull(), isNull())) + .thenReturn(executor); - doReturn(expected).when(winRMService).executeCommand(eq(command), isNull(), eq(UTF_8), anyLong()); + doReturn(expected).when(executor).executeCommand(eq(command), isNull(), eq(UTF_8), anyLong()); assertEquals( expected, @@ -199,7 +200,7 @@ void testExecute() throws Exception { .thenReturn("launch remote/localFile"); final SmbTempShare smbTempShare = mock(SmbTempShare.class); - final WinRMService winRMService = mock(WinRMService.class); + final WindowsRemoteExecutor winRMService = mock(WindowsRemoteExecutor.class); mockedSmbTempShare .when(() -> SmbTempShare.createInstance(any(WinRMEndpoint.class), anyLong(), isNull(), isNull())) diff --git a/src/test/java/org/metricshub/winrm/light/FakeWsmanServer.java b/src/test/java/org/metricshub/winrm/light/FakeWsmanServer.java new file mode 100644 index 0000000..bc4d62d --- /dev/null +++ b/src/test/java/org/metricshub/winrm/light/FakeWsmanServer.java @@ -0,0 +1,414 @@ +package org.metricshub.winrm.light; + +import java.io.BufferedInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.ServerSocket; +import java.net.Socket; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Base64; +import java.util.Deque; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.TreeMap; +import java.util.concurrent.CopyOnWriteArrayList; + +/** + * In-process WSMan server for protocol tests (issue #107): speaks the real NTLM handshake + * (fixed server challenge, NTLMv2 verification against a configured password, session-key + * recovery from the wire Type 3) and real NTLM message encryption — by reusing the light + * client's own crypto primitives with a mirrored {@link WinRMSession} — then serves scripted + * SOAP response bodies. This exercises the client's full protocol path (transport, handshake + * orchestration, sealing, multipart framing, decryption, XML handling) without a Windows host. + * + *

The NTLMv2 verification is real: a client that derives a wrong hash (e.g. a domain-case + * regression) fails authentication here just like against a real host. + */ +final class FakeWsmanServer implements AutoCloseable { + + /** One scripted HTTP response: status code and the plaintext SOAP body to encrypt and serve. */ + static final class Scripted { + + final int status; + final String soapBody; + + Scripted(final int status, final String soapBody) { + this.status = status; + this.soapBody = soapBody; + } + } + + private static final Charset UTF16LE = StandardCharsets.UTF_16LE; + + // Fixed 8-byte server challenge — "recorded exchange" determinism. + private static final byte[] SERVER_CHALLENGE = { + 0x01, + 0x23, + 0x45, + 0x67, + (byte) 0x89, + (byte) 0xab, + (byte) 0xcd, + (byte) 0xef + }; + + // Type 2 flags: UNICODE | SIGN | SEAL | EXTENDED_SESSIONSECURITY | TARGETINFO | 128 | KEY_EXCH — + // what a real WinRM host negotiates for encrypted HTTP, and what drives the client down the + // NTLMv2 + explicit-key-exchange + extended-session-security path. + private static final int TYPE2_FLAGS = + 0x00000001 | 0x00000010 | 0x00000020 | 0x00080000 | 0x00800000 | 0x20000000 | 0x40000000; + + private final String expectedDomain; + private final String expectedUser; + private final String expectedPassword; + + private final ServerSocket serverSocket; + private final Thread acceptThread; + private final List connectionThreads = new CopyOnWriteArrayList<>(); + + private final Deque script = new ArrayDeque<>(); + private final List decryptedRequests = new CopyOnWriteArrayList<>(); + private volatile boolean closed; + + FakeWsmanServer(final String domain, final String user, final String password) throws IOException { + this.expectedDomain = domain.toUpperCase(Locale.ROOT); + this.expectedUser = user; + this.expectedPassword = password; + this.serverSocket = new ServerSocket(0); + this.acceptThread = new Thread(this::acceptLoop, "fake-wsman-accept"); + this.acceptThread.setDaemon(true); + this.acceptThread.start(); + } + + int port() { + return serverSocket.getLocalPort(); + } + + /** Queue the next scripted response (served in order, one per decrypted request). */ + FakeWsmanServer enqueue(final int status, final String soapBody) { + synchronized (script) { + script.addLast(new Scripted(status, soapBody)); + } + return this; + } + + /** The plaintext SOAP request bodies received so far, in order (after decryption). */ + List decryptedRequests() { + return new ArrayList<>(decryptedRequests); + } + + @Override + public void close() { + closed = true; + try { + serverSocket.close(); + } catch (final IOException ignore) { + // shutting down + } + for (final Thread t : connectionThreads) { + t.interrupt(); + } + } + + // --- connection handling -------------------------------------------------- + + private void acceptLoop() { + while (!closed) { + try { + final Socket socket = serverSocket.accept(); + final Thread t = new Thread(() -> handleConnection(socket), "fake-wsman-conn"); + t.setDaemon(true); + connectionThreads.add(t); + t.start(); + } catch (final IOException e) { + return; // server socket closed + } + } + } + + private void handleConnection(final Socket socket) { + // NTLM state is bound to the TCP connection, exactly like a real WinRM host. + WinRMSession serverSession = null; + try (socket) { + socket.setTcpNoDelay(true); + final BufferedInputStream in = new BufferedInputStream(socket.getInputStream()); + final OutputStream out = socket.getOutputStream(); + while (!closed) { + final HttpRequest request = HttpRequest.read(in); + if (request == null) { + return; // client closed the connection + } + final String authorization = request.header("authorization"); + if (serverSession == null || authorization != null) { + final byte[] token = negotiateToken(authorization); + if (token == null) { + respond(out, 401, "WWW-Authenticate: Negotiate", null, null); + continue; + } + final int messageType = NTLMMessage.readULong(token, 8); + if (messageType == 1) { + respond( + out, + 401, + "WWW-Authenticate: Negotiate " + Base64.getEncoder().encodeToString(buildType2()), + null, + null + ); + continue; + } + if (messageType != 3) { + respond(out, 401, "WWW-Authenticate: Negotiate", null, null); + continue; + } + serverSession = authenticate(token); + if (serverSession == null) { + // Bad credentials: reject like a real host — 401 on the request carrying the Type 3. + respond(out, 401, "WWW-Authenticate: Negotiate", null, null); + continue; + } + // fall through: the request that carried the Type 3 also carries the first sealed body + } + serveScripted(out, serverSession, request.body); + } + } catch (final IOException | RuntimeException e) { + // connection torn down (client close, test shutdown) — nothing to do + } + } + + private void serveScripted(final OutputStream out, final WinRMSession session, final byte[] sealedBody) + throws IOException { + final byte[] plaintext = NtlmCrypto.decrypt(session, sealedBody); + decryptedRequests.add(new String(plaintext, StandardCharsets.UTF_8)); + + Scripted next; + synchronized (script) { + next = script.pollFirst(); + } + if (next == null) { + // Loud, decryptable failure so an over-consuming test fails on an assertion, not a hang. + next = + new Scripted( + 500, + "" + + "FakeWsmanServer: no scripted response left" + + "" + ); + } + final byte[] sealed = NtlmCrypto.encryptAndSign(session, next.soapBody.getBytes(StandardCharsets.UTF_8)); + respond(out, next.status, null, NtlmCrypto.ENCRYPTED_CONTENT_TYPE, sealed); + } + + // --- NTLM server side ------------------------------------------------------- + + private static byte[] negotiateToken(final String authorization) { + if (authorization == null || !authorization.regionMatches(true, 0, "Negotiate ", 0, 10)) { + return null; + } + return Base64.getDecoder().decode(authorization.substring(10).trim()); + } + + /** Fixed Type 2 challenge message: target "FAKE", the fixed server challenge, and target info. */ + private static byte[] buildType2() { + final byte[] targetName = "FAKE".getBytes(UTF16LE); + final byte[] targetInfo = concat( + avPair(2, "FAKE"), // NetBIOS domain + avPair(1, "FAKESRV"), // NetBIOS computer + new byte[] { 0, 0, 0, 0 } // terminator + ); + final ByteArrayOutputStream msg = new ByteArrayOutputStream(); + writeBytes(msg, "NTLMSSP\0".getBytes(StandardCharsets.US_ASCII)); + writeULong(msg, 2); + final int targetNameOffset = 56; + writeSecurityBuffer(msg, targetName.length, targetNameOffset); + writeULong(msg, TYPE2_FLAGS); + writeBytes(msg, SERVER_CHALLENGE); + writeBytes(msg, new byte[8]); // context + writeSecurityBuffer(msg, targetInfo.length, targetNameOffset + targetName.length); + writeBytes(msg, new byte[8]); // version (unparsed by the client) + writeBytes(msg, targetName); + writeBytes(msg, targetInfo); + return msg.toByteArray(); + } + + /** + * Verify the Type 3 NTLMv2 response against the configured credentials and, on success, recover + * the exported session key from the wire and install the mirrored (server-side) session keys. + * Returns null when the proof does not match — i.e. the client used a wrong password/hash. + */ + private WinRMSession authenticate(final byte[] type3) { + final byte[] ntResponse = readSecurityBuffer(type3, 20); + final byte[] encryptedSessionKey = readSecurityBuffer(type3, 52); + final int flags = NTLMMessage.readULong(type3, 60); + final String domain = new String(readSecurityBuffer(type3, 28), UTF16LE); + final String user = new String(readSecurityBuffer(type3, 36), UTF16LE); + + if (!expectedDomain.equals(domain) || !expectedUser.equals(user) || ntResponse.length < 16) { + return null; + } + + // NTOWFv2 = HMAC-MD5(MD4(UTF16LE(password)), UTF16LE(UPPER(user) + domain)) — same derivation + // as the client's CipherGen, computed from the SERVER's copy of the password. + final MD4 md4 = new MD4(); + md4.update(expectedPassword.getBytes(UTF16LE)); + final byte[] ntlmHash = md4.getOutput(); + final byte[] ntowfV2 = EncryptionUtils.hmacMd5( + ntlmHash, + concat(expectedUser.toUpperCase(Locale.ROOT).getBytes(UTF16LE), expectedDomain.getBytes(UTF16LE)) + ); + + // NTProofStr (first 16 bytes) must equal HMAC-MD5(NTOWFv2, serverChallenge || blob). + final byte[] ntProofStr = Arrays.copyOfRange(ntResponse, 0, 16); + final byte[] blob = Arrays.copyOfRange(ntResponse, 16, ntResponse.length); + final byte[] expectedProof = EncryptionUtils.hmacMd5(ntowfV2, concat(SERVER_CHALLENGE, blob)); + if (!Arrays.equals(ntProofStr, expectedProof)) { + return null; + } + + // Session key: userSessionKey = HMAC-MD5(NTOWFv2, NTProofStr); with KEY_EXCH the wire field is + // RC4(exportedSessionKey, userSessionKey) — RC4 is symmetric, so decrypt with the same call. + final byte[] userSessionKey = EncryptionUtils.hmacMd5(ntowfV2, ntProofStr); + final byte[] exportedSessionKey = EncryptionUtils.calculateRC4(encryptedSessionKey, userSessionKey); + + final WinRMSession session = new WinRMSession(expectedDomain, null, expectedUser, expectedPassword); + session.applyKeys(flags, exportedSessionKey, true); + return session; + } + + // --- byte-level helpers ----------------------------------------------------- + + private static byte[] readSecurityBuffer(final byte[] src, final int position) { + final int length = (src[position] & 0xff) | ((src[position + 1] & 0xff) << 8); + final int offset = NTLMMessage.readULong(src, position + 4); + return Arrays.copyOfRange(src, offset, offset + length); + } + + private static byte[] avPair(final int id, final String value) { + final byte[] bytes = value.getBytes(UTF16LE); + final ByteArrayOutputStream out = new ByteArrayOutputStream(); + out.write(id & 0xff); + out.write((id >> 8) & 0xff); + out.write(bytes.length & 0xff); + out.write((bytes.length >> 8) & 0xff); + writeBytes(out, bytes); + return out.toByteArray(); + } + + private static byte[] concat(final byte[]... arrays) { + final ByteArrayOutputStream out = new ByteArrayOutputStream(); + for (final byte[] a : arrays) { + writeBytes(out, a); + } + return out.toByteArray(); + } + + private static void writeBytes(final ByteArrayOutputStream out, final byte[] bytes) { + out.write(bytes, 0, bytes.length); + } + + private static void writeULong(final ByteArrayOutputStream out, final int value) { + out.write(value & 0xff); + out.write((value >> 8) & 0xff); + out.write((value >> 16) & 0xff); + out.write((value >> 24) & 0xff); + } + + private static void writeSecurityBuffer(final ByteArrayOutputStream out, final int length, final int offset) { + out.write(length & 0xff); + out.write((length >> 8) & 0xff); + out.write(length & 0xff); + out.write((length >> 8) & 0xff); + writeULong(out, offset); + } + + // --- minimal HTTP ----------------------------------------------------------- + + private static void respond( + final OutputStream out, + final int status, + final String extraHeader, + final String contentType, + final byte[] body + ) throws IOException { + final byte[] payload = body == null ? new byte[0] : body; + final StringBuilder head = new StringBuilder(); + head.append("HTTP/1.1 ").append(status).append(' ').append(status == 200 ? "OK" : "Error").append("\r\n"); + head.append("Server: FakeWsmanServer\r\n"); + if (extraHeader != null) { + head.append(extraHeader).append("\r\n"); + } + if (contentType != null) { + head.append("Content-Type: ").append(contentType).append("\r\n"); + } + head.append("Content-Length: ").append(payload.length).append("\r\n"); + head.append("\r\n"); + out.write(head.toString().getBytes(StandardCharsets.ISO_8859_1)); + out.write(payload); + out.flush(); + } + + /** One parsed HTTP request: headers (lower-cased names) and the raw body. */ + private static final class HttpRequest { + + final Map headers = new TreeMap<>(); + byte[] body = new byte[0]; + + String header(final String name) { + return headers.get(name.toLowerCase(Locale.ROOT)); + } + + static HttpRequest read(final InputStream in) throws IOException { + final String requestLine = readLine(in); + if (requestLine == null || requestLine.isEmpty()) { + return null; + } + final HttpRequest request = new HttpRequest(); + String line; + while ((line = readLine(in)) != null && !line.isEmpty()) { + final int colon = line.indexOf(':'); + if (colon > 0) { + request.headers.put( + line.substring(0, colon).trim().toLowerCase(Locale.ROOT), + line.substring(colon + 1).trim() + ); + } + } + final String contentLength = request.header("content-length"); + if (contentLength != null) { + final int length = Integer.parseInt(contentLength); + final byte[] body = new byte[length]; + int read = 0; + while (read < length) { + final int n = in.read(body, read, length - read); + if (n < 0) { + throw new IOException("EOF in request body"); + } + read += n; + } + request.body = body; + } + return request; + } + + private static String readLine(final InputStream in) throws IOException { + final ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + int b; + int prev = -1; + while ((b = in.read()) != -1) { + if (prev == '\r' && b == '\n') { + final byte[] raw = buffer.toByteArray(); + return new String(raw, 0, raw.length - 1, StandardCharsets.ISO_8859_1); + } + buffer.write(b); + prev = b; + } + return buffer.size() == 0 ? null : buffer.toString("ISO-8859-1"); + } + } +} diff --git a/src/test/java/org/metricshub/winrm/light/FallbackAuthSchemeTest.java b/src/test/java/org/metricshub/winrm/light/FallbackAuthSchemeTest.java new file mode 100644 index 0000000..f1641e9 --- /dev/null +++ b/src/test/java/org/metricshub/winrm/light/FallbackAuthSchemeTest.java @@ -0,0 +1,121 @@ +package org.metricshub.winrm.light; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import org.junit.jupiter.api.Test; + +/** Verifies the ordered-fallback state machine without a network. */ +class FallbackAuthSchemeTest { + + /** A stand-in AuthScheme whose handshake can be made to fail on/after a chosen call. */ + private static final class FakeScheme implements AuthScheme { + + private final String name; + private final int failFromCall; // fail on this 1-based authenticate() call onward; MAX_VALUE = never + private boolean authenticated; + private int authenticateCalls; + + FakeScheme(final String name, final int failFromCall) { + this.name = name; + this.failFromCall = failFromCall; + } + + @Override + public String authenticate(final HttpTransport transport) throws Exception { + authenticateCalls++; + if (authenticateCalls >= failFromCall) { + authenticated = false; + throw new IllegalStateException(name + " handshake failed"); + } + authenticated = true; + return "Negotiate " + name; + } + + @Override + public boolean isAuthenticated() { + return authenticated; + } + + @Override + public void reset() { + authenticated = false; + } + + @Override + public byte[] wrap(final byte[] soapUtf8) { + return soapUtf8; + } + + @Override + public String wrapContentType() { + return "application/soap+xml;charset=UTF-8"; + } + + @Override + public byte[] unwrap(final HttpTransport.Response response) { + return response.body; + } + } + + private static final int NEVER = Integer.MAX_VALUE; + private static final int ALWAYS = 1; + + // Never connects — the fake schemes ignore it, and FallbackAuthScheme only calls close() on it. + private static HttpTransport dummyTransport() { + return new HttpTransport("localhost", 1, 1000); + } + + @Test + void clientSideFailureFallsBackToNextScheme() throws Exception { + final FakeScheme kerberos = new FakeScheme("kerberos", ALWAYS); // fails during the handshake + final FakeScheme ntlm = new FakeScheme("ntlm", NEVER); + final FallbackAuthScheme fallback = new FallbackAuthScheme(List.of(kerberos, ntlm)); + + assertEquals("Negotiate ntlm", fallback.authenticate(dummyTransport())); + assertTrue(fallback.isAuthenticated()); + assertEquals(1, kerberos.authenticateCalls); + assertEquals(1, ntlm.authenticateCalls); + } + + @Test + void advanceMovesPastAServerRejectedScheme() throws Exception { + // Both handshakes succeed client-side; the first is "rejected server-side" via advance(). + final FakeScheme kerberos = new FakeScheme("kerberos", NEVER); + final FakeScheme ntlm = new FakeScheme("ntlm", NEVER); + final FallbackAuthScheme fallback = new FallbackAuthScheme(List.of(kerberos, ntlm)); + final HttpTransport transport = dummyTransport(); + + assertEquals("Negotiate kerberos", fallback.authenticate(transport)); + assertTrue(fallback.advance()); // server rejected kerberos -> move to ntlm + assertFalse(fallback.isAuthenticated()); // active cleared until re-authenticated + assertEquals("Negotiate ntlm", fallback.authenticate(transport)); + assertFalse(fallback.advance()); // ntlm is the last candidate + } + + @Test + void reAuthFailureOfActiveSchemeFallsThrough() throws Exception { + // Kerberos succeeds initially, then fails on the second handshake (e.g. TGT expired on reconnect). + final FakeScheme kerberos = new FakeScheme("kerberos", 2); + final FakeScheme ntlm = new FakeScheme("ntlm", NEVER); + final FallbackAuthScheme fallback = new FallbackAuthScheme(List.of(kerberos, ntlm)); + final HttpTransport transport = dummyTransport(); + + assertEquals("Negotiate kerberos", fallback.authenticate(transport)); // picks kerberos + fallback.reset(); // simulate a dropped connection + // Re-auth of kerberos now fails, so it must fall through to ntlm rather than throwing. + assertEquals("Negotiate ntlm", fallback.authenticate(transport)); + assertEquals(2, kerberos.authenticateCalls); + } + + @Test + void allSchemesFailingThrows() { + final FallbackAuthScheme fallback = new FallbackAuthScheme( + List.of(new FakeScheme("kerberos", ALWAYS), new FakeScheme("ntlm", ALWAYS)) + ); + assertThrows(IllegalStateException.class, () -> fallback.authenticate(dummyTransport())); + } +} diff --git a/src/test/java/org/metricshub/winrm/light/LightTlsTest.java b/src/test/java/org/metricshub/winrm/light/LightTlsTest.java new file mode 100644 index 0000000..233a951 --- /dev/null +++ b/src/test/java/org/metricshub/winrm/light/LightTlsTest.java @@ -0,0 +1,32 @@ +package org.metricshub.winrm.light; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +/** Verifies the light backend's TLS posture: validate by default, insecure only when opted in. */ +class LightTlsTest { + + @AfterEach + void clearInsecure() { + System.clearProperty(LightTls.INSECURE_PROPERTY); + } + + @Test + void validatesByDefault() { + assertFalse(LightTls.isInsecure()); + assertTrue(LightTls.verifyHostname()); + assertNotNull(LightTls.socketFactory()); + } + + @Test + void insecureWhenOptedIn() { + System.setProperty(LightTls.INSECURE_PROPERTY, "true"); + assertTrue(LightTls.isInsecure()); + assertFalse(LightTls.verifyHostname()); + assertNotNull(LightTls.socketFactory()); + } +} diff --git a/src/test/java/org/metricshub/winrm/light/WsmanClientParityTest.java b/src/test/java/org/metricshub/winrm/light/WsmanClientParityTest.java new file mode 100644 index 0000000..4ccda7e --- /dev/null +++ b/src/test/java/org/metricshub/winrm/light/WsmanClientParityTest.java @@ -0,0 +1,229 @@ +package org.metricshub.winrm.light; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.Test; +import org.w3c.dom.Document; + +/** + * Pins the light backend's WSMan response handling to the behavior of the legacy CXF backend + * (issue #106: feature parity and fault mapping): EndOfSequence/Items namespace variants, and + * fault summaries that carry the WSManFault detail message (with its WBEM_E_* mnemonics). + */ +class WsmanClientParityTest { + + private static final String WSEN = "http://schemas.xmlsoap.org/ws/2004/09/enumeration"; + private static final String WSMAN = "http://schemas.dmtf.org/wbem/wsman/1/wsman.xsd"; + + private static Document parse(final String xml) throws Exception { + return WsmanClient.parse(xml.getBytes(StandardCharsets.UTF_8)); + } + + // --- EndOfSequence variants --------------------------------------------- + + @Test + void endOfSequenceDetectedInWsEnumerationNamespace() throws Exception { + final Document doc = parse( + "" + + "" + + "" + + "" + ); + assertTrue(WsmanClient.hasEnumerationElement(doc, "EndOfSequence")); + } + + @Test + void endOfSequenceDetectedInWsmanNamespaceVariant() throws Exception { + // WinRM can emit the marker in its own WSMan namespace instead of WS-Enumeration; the CXF + // backend accepts both variants, so the light backend must too. + final Document doc = parse( + "" + + "" + + "" + + "" + ); + assertTrue(WsmanClient.hasEnumerationElement(doc, "EndOfSequence")); + } + + @Test + void wmiPropertyNamedEndOfSequenceIsNotMistakenForTheMarker() throws Exception { + // A WMI property that happens to be called EndOfSequence lives in the class's own namespace + // and must not terminate the enumeration. + final Document doc = parse( + "" + + "" + + "" + + "" + + "oops" + + "" + + "" + + "" + ); + assertFalse(WsmanClient.hasEnumerationElement(doc, "EndOfSequence")); + } + + // --- Items variants and row extraction ---------------------------------- + + @Test + void itemsCollectedFromBothNamespaceVariants() throws Exception { + // EnumerateResponse carries wsen:Items, PullResponse carries wsman:Items; both must yield rows. + final String instance = + "" + + "Spooler" + + "Running" + + ""; + final Document wsenItems = parse( + "" + + "" + + "" + + instance + + "" + + "" + ); + final Document wsmanItems = parse( + "" + + "" + + "" + + instance + + "" + + "" + ); + + for (final Document doc : List.of(wsenItems, wsmanItems)) { + final List> rows = new ArrayList<>(); + WsmanClient.collectItems(doc, rows); + assertEquals(1, rows.size()); + assertEquals("Spooler", rows.get(0).get("Name")); + assertEquals("Running", rows.get(0).get("State")); + } + } + + @Test + void wmiPropertyNamedItemsIsNotMistakenForTheWrapper() throws Exception { + // A structured WMI property called Items (in the class's namespace) must not be read as a + // second Items wrapper producing phantom rows. + final Document doc = parse( + "" + + "" + + "" + + "" + + "real-row" + + "phantom" + + "" + + "" + + "" + ); + final List> rows = new ArrayList<>(); + WsmanClient.collectItems(doc, rows); + assertEquals(1, rows.size()); + assertEquals("real-row", rows.get(0).get("Name")); + } + + // --- Fault summaries ------------------------------------------------------ + + @Test + void faultSummaryCarriesWsmanFaultDetailWithWbemMnemonic() throws Exception { + // MetricsHub tells an "acceptable" WMI error (bad class/namespace) from a broken connection by + // matching WBEM_E_* in the exception message — the detail Message text must surface. + final Document doc = parse( + "" + + "" + + "The WS-Management service cannot process the request." + + "" + + "" + + "The WMI service or the WMI provider returned an unknown error: WBEM_E_INVALID_CLASS" + + "" + + "" + + "" + ); + final String summary = WsmanClient.faultSummary(500, doc); + assertTrue(summary.contains("HTTP 500"), summary); + assertTrue(summary.contains("WSManFault 2150858778"), summary); + assertTrue(summary.contains("The WS-Management service cannot process the request."), summary); + assertTrue(summary.contains("WBEM_E_INVALID_CLASS"), summary); + } + + @Test + void faultSummaryDoesNotDuplicateDetailAlreadyInReason() throws Exception { + // WinRM usually repeats the same text in Reason and in the WSManFault Message; it must appear once. + final String message = "The WMI service or the WMI provider returned an unknown error: WBEM_E_INVALID_NAMESPACE"; + final Document doc = parse( + "" + + "" + + "" + + message + + " " + + "" + + "" + + "" + + message + + " " + + "" + + "" + + "" + ); + final String summary = WsmanClient.faultSummary(500, doc); + final int first = summary.indexOf("WBEM_E_INVALID_NAMESPACE"); + final int last = summary.lastIndexOf("WBEM_E_INVALID_NAMESPACE"); + assertTrue(first >= 0, summary); + assertEquals(first, last, "detail text must not be duplicated: " + summary); + } + + @Test + void faultSummaryWithoutWsmanFaultDetailStillReadable() throws Exception { + final Document doc = parse( + "" + + "" + + "Some transport-level failure" + + "" + ); + assertEquals("HTTP 500: Some transport-level failure", WsmanClient.faultSummary(500, doc)); + } + + // --- OperationTimeout format --------------------------------------------- + + @Test + void operationTimeoutFormattedLikeCxf() { + // The CXF backend formats the WSMan OperationTimeout with DecimalFormat("PT#.###S", ROOT): + // millisecond precision, '.' separator, no trailing zeros. Same bytes on the wire from light. + assertTrue(timeoutOf(30000L).contains("PT30S")); + assertTrue(timeoutOf(1500L).contains("PT1.5S")); + assertTrue(timeoutOf(1234L).contains("PT1.234S")); + assertTrue(timeoutOf(90061L).contains("PT90.061S")); + } + + private static String timeoutOf(final long timeoutMs) { + return Envelopes.enumerateWql( + "http://host:5985/wsman", + "root/cimv2", + "SELECT * FROM Win32_OperatingSystem", + timeoutMs + ); + } +} diff --git a/src/test/java/org/metricshub/winrm/light/WsmanProtocolTest.java b/src/test/java/org/metricshub/winrm/light/WsmanProtocolTest.java new file mode 100644 index 0000000..3b236cb --- /dev/null +++ b/src/test/java/org/metricshub/winrm/light/WsmanProtocolTest.java @@ -0,0 +1,388 @@ +package org.metricshub.winrm.light; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.metricshub.winrm.WinRMHttpProtocolEnum; +import org.metricshub.winrm.WindowsRemoteCommandResult; +import org.metricshub.winrm.exceptions.WinRMException; +import org.metricshub.winrm.service.WinRMEndpoint; +import org.metricshub.winrm.service.client.auth.AuthenticationEnum; + +/** + * End-to-end protocol tests against {@link FakeWsmanServer} (issue #107): the full NTLM + * handshake, message encryption, multipart framing, WQL Enumerate/Pull paging, the command + * shell lifecycle, and fault mapping — all in-process, no Windows host required. + */ +class WsmanProtocolTest { + + private static final String DOMAIN = "FAKE"; + private static final String USER = "user"; + private static final String PASSWORD = "s3cret-Passw0rd"; + private static final long TIMEOUT = 30_000L; + + private static final String SOAP_NS = "http://www.w3.org/2003/05/soap-envelope"; + private static final String WSEN = "http://schemas.xmlsoap.org/ws/2004/09/enumeration"; + private static final String WSMAN = "http://schemas.dmtf.org/wbem/wsman/1/wsman.xsd"; + private static final String RSP = "http://schemas.microsoft.com/wbem/wsman/1/windows/shell"; + private static final String FAULT_NS = "http://schemas.microsoft.com/wbem/wsman/1/wsmanfault"; + + private FakeWsmanServer server; + + @BeforeEach + void startServer() throws Exception { + server = new FakeWsmanServer(DOMAIN, USER, PASSWORD); + } + + @AfterEach + void stopServer() { + server.close(); + } + + private LightWinRMService client(final String password) throws Exception { + final WinRMEndpoint endpoint = new WinRMEndpoint( + WinRMHttpProtocolEnum.HTTP, + "127.0.0.1", + server.port(), + DOMAIN + "\\" + USER, + password.toCharArray(), + null + ); + return LightWinRMService.createInstance(endpoint, TIMEOUT, null, List.of(AuthenticationEnum.NTLM)); + } + + // --- WQL paging ----------------------------------------------------------- + + @Test + void wqlPagesAcrossEnumerateAndPullsOverEncryptedNtlm() throws Exception { + // Optimized EnumerateResponse (wsman:Items) -> Pull (wsen:Items) -> final Pull with the + // wsman:EndOfSequence variant: covers both Items and both EndOfSequence namespaces end to end. + server + .enqueue( + 200, + envelope( + "" + + "uuid:CTX-1" + + "" + + service("Spooler", "Running") + + "" + + "" + ) + ) + .enqueue( + 200, + envelope( + "" + + "uuid:CTX-2" + + "" + + service("WinRM", "Running") + + service("Wecsvc", "Stopped") + + "" + + "" + ) + ) + .enqueue( + 200, + envelope( + "" + + "" + + "" + ) + ); + + try (LightWinRMService service = client(PASSWORD)) { + final List> rows = service.executeWql("SELECT Name,State FROM Win32_Service", TIMEOUT); + + assertEquals(3, rows.size()); + assertEquals("Spooler", rows.get(0).get("Name")); + assertEquals("Running", rows.get(0).get("State")); + assertEquals("WinRM", rows.get(1).get("Name")); + assertEquals("Wecsvc", rows.get(2).get("Name")); + assertEquals("Stopped", rows.get(2).get("State")); + } + + // The decrypted request bodies pin what the client actually sends on the wire. + final List requests = server.decryptedRequests(); + assertEquals(3, requests.size(), () -> String.join("\n---\n", requests)); + final String enumerate = requests.get(0); + assertTrue(enumerate.contains("PT30S"), enumerate); + assertTrue(enumerate.contains(""), enumerate); + assertTrue(enumerate.contains("32000"), enumerate); + assertTrue(enumerate.contains("http://schemas.microsoft.com/wbem/wsman/1/wmi/ROOT/CIMV2/*"), enumerate); + assertTrue(enumerate.contains("SELECT Name,State FROM Win32_Service"), enumerate); + assertTrue(requests.get(1).contains("uuid:CTX-1"), requests.get(1)); + assertTrue(requests.get(2).contains("uuid:CTX-2"), requests.get(2)); + } + + // --- Command shell lifecycle ------------------------------------------------ + + @Test + void commandLifecycleReassemblesMultibyteOutputSplitAcrossReceives() throws Exception { + // "héllo!" in UTF-8, split in the middle of the 2-byte 'é' across two Receive responses: the + // client must accumulate raw bytes and decode once, or the boundary bytes become U+FFFD. + final byte[] utf8 = "héllo!".getBytes(StandardCharsets.UTF_8); + final byte[] chunk1 = java.util.Arrays.copyOfRange(utf8, 0, 2); // 'h' + first byte of 'é' + final byte[] chunk2 = java.util.Arrays.copyOfRange(utf8, 2, utf8.length); + + server + .enqueue(200, envelope(resourceCreated("SHELL-1"))) + .enqueue(200, envelope(commandResponse("CMD-1"))) + .enqueue(200, envelope(receiveResponse("CMD-1", stream("stdout", chunk1), null))) + .enqueue( + 200, + envelope( + receiveResponse( + "CMD-1", + stream("stdout", chunk2) + stream("stderr", "warn!".getBytes(StandardCharsets.UTF_8)), + done("CMD-1", 7) + ) + ) + ) + .enqueue(200, envelope("")); + + try (LightWinRMService service = client(PASSWORD)) { + final WindowsRemoteCommandResult result = service.executeCommand( + "echo héllo!", + null, + StandardCharsets.UTF_8, + TIMEOUT + ); + + assertEquals("héllo!", result.getStdout()); + assertEquals("warn!", result.getStderr()); + assertEquals(7, result.getStatusCode()); + } + + final List requests = server.decryptedRequests(); + // Create + Command + 2x Receive + Signal (+ the close()-time shell Delete). + assertTrue(requests.size() >= 5, () -> String.join("\n---\n", requests)); + final String create = requests.get(0); + assertTrue(create.contains("TRUE"), create); + assertTrue(create.contains("437"), create); + assertTrue(create.contains("stdout stderr"), create); + final String command = requests.get(1); + assertTrue(command.contains("echo héllo!"), command); + assertTrue(command.contains("Selector Name=\"ShellId\">SHELL-1<"), command); + final String receive = requests.get(2); + assertTrue(receive.contains("CommandId=\"CMD-1\">stdout stderr"), receive); + final String signal = requests.get(4); + assertTrue(signal.contains(RSP + "/signal/terminate"), signal); + } + + @Test + void receiveRetriesOnOperationTimeoutFault() throws Exception { + // No output before OperationTimeout: the server faults with 2150858793 and the client must + // immediately re-issue the Receive rather than fail the command. + server + .enqueue(200, envelope(resourceCreated("SHELL-1"))) + .enqueue(200, envelope(commandResponse("CMD-1"))) + .enqueue( + 500, + fault( + "2150858793", + "The WS-Management service cannot complete the operation within the time specified in OperationTimeout." + ) + ) + .enqueue( + 200, + envelope(receiveResponse("CMD-1", stream("stdout", "late".getBytes(StandardCharsets.UTF_8)), done("CMD-1", 0))) + ) + .enqueue(200, envelope("")); + + try (LightWinRMService service = client(PASSWORD)) { + final WindowsRemoteCommandResult result = service.executeCommand("slow", null, StandardCharsets.UTF_8, TIMEOUT); + assertEquals("late", result.getStdout()); + assertEquals(0, result.getStatusCode()); + } + + // Two Receive requests must have been sent: the faulted one and the retry. + final long receives = server.decryptedRequests().stream().filter(r -> r.contains("")).count(); + assertEquals(2, receives); + } + + @Test + void terminateSignalToleratesShellNotFoundFault() throws Exception { + // The command finished and the shell may already be gone: fault 2150858843 on the terminate + // Signal must not fail the (successful) command. + server + .enqueue(200, envelope(resourceCreated("SHELL-1"))) + .enqueue(200, envelope(commandResponse("CMD-1"))) + .enqueue( + 200, + envelope(receiveResponse("CMD-1", stream("stdout", "ok".getBytes(StandardCharsets.UTF_8)), done("CMD-1", 0))) + ) + .enqueue( + 500, + fault("2150858843", "The WS-Management service cannot process the request because the resource offline.") + ); + + try (LightWinRMService service = client(PASSWORD)) { + final WindowsRemoteCommandResult result = service.executeCommand("whoami", null, StandardCharsets.UTF_8, TIMEOUT); + assertEquals("ok", result.getStdout()); + assertEquals(0, result.getStatusCode()); + } + } + + // --- Fault mapping ------------------------------------------------------------ + + @Test + void wqlFaultSurfacesCodeReasonAndWbemDetail() throws Exception { + server.enqueue( + 500, + fault( + "2150858778", + "The WS-Management service cannot process the request.", + "The WMI service or the WMI provider returned an unknown error: WBEM_E_INVALID_CLASS" + ) + ); + + try (LightWinRMService service = client(PASSWORD)) { + final WinRMException e = assertThrows( + WinRMException.class, + () -> service.executeWql("SELECT Name FROM No_Such_Class", TIMEOUT) + ); + final String message = e.getMessage(); + assertTrue(message.contains("Enumerate failed"), message); + assertTrue(message.contains("WSManFault 2150858778"), message); + assertTrue(message.contains("The WS-Management service cannot process the request."), message); + // The provider-level detail carries the WBEM_E_* mnemonics MetricsHub matches on. + assertTrue(message.contains("WBEM_E_INVALID_CLASS"), message); + } + } + + @Test + void wrongPasswordSurfacesTheCxfAuthenticationErrorMessage() throws Exception { + try (LightWinRMService service = client("wrong-password")) { + final WinRMException e = assertThrows( + WinRMException.class, + () -> service.executeWql("SELECT Name FROM Win32_Service", TIMEOUT) + ); + // Exact CXF-parity message (issue #106): operators and callers match on this format. + assertEquals( + "Authentication error on http://127.0.0.1:" + server.port() + "/wsman with user name \"FAKE\\user\"", + e.getMessage() + ); + } + } + + // --- response body builders ----------------------------------------------------- + + private static String envelope(final String body) { + return "" + body + ""; + } + + private static String service(final String name, final String state) { + return ( + "" + + "" + + name + + "" + + state + + "" + ); + } + + private static String resourceCreated(final String shellId) { + return ( + "" + + "http://127.0.0.1/wsman" + + "" + + "" + + RSP + + "/cmd" + + "" + + shellId + + "" + + "" + ); + } + + private static String commandResponse(final String commandId) { + return ( + "" + + commandId + + "" + ); + } + + private static String receiveResponse(final String commandId, final String streams, final String commandState) { + return ( + "" + + streams + + (commandState == null ? "" : commandState) + + "" + ); + } + + private static String stream(final String name, final byte[] content) { + return ( + "" + + Base64.getEncoder().encodeToString(content) + + "" + ); + } + + private static String done(final String commandId, final int exitCode) { + return ( + "" + + exitCode + + "" + ); + } + + private static String fault(final String code, final String reason) { + return fault(code, reason, null); + } + + private static String fault(final String code, final String reason, final String detailMessage) { + return ( + "" + + "s:Receiver" + + "" + + reason + + "" + + "" + + "" + + (detailMessage == null ? reason : detailMessage) + + "" + + "" + ); + } +} diff --git a/src/test/java/org/metricshub/winrm/service/WinRMEndpointTest.java b/src/test/java/org/metricshub/winrm/service/WinRMEndpointTest.java index 730cd95..17122a1 100644 --- a/src/test/java/org/metricshub/winrm/service/WinRMEndpointTest.java +++ b/src/test/java/org/metricshub/winrm/service/WinRMEndpointTest.java @@ -105,6 +105,25 @@ void testGetEndPointPort() { assertEquals(5986, getEndpointPort(HTTPS, null)); } + @Test + void testGetPort() { + assertEquals(5985, new WinRMEndpoint(HTTP, HOSTNAME, null, USER, PASSWORD, null).getPort()); + assertEquals(5986, new WinRMEndpoint(HTTPS, HOSTNAME, null, USER, PASSWORD, null).getPort()); + assertEquals(PORT, new WinRMEndpoint(HTTP, HOSTNAME, PORT, USER, PASSWORD, null).getPort()); + assertEquals(PORT, new WinRMEndpoint(HTTPS, HOSTNAME, PORT, USER, PASSWORD, null).getPort()); + } + + @Test + void testUnderscoreHostnameEndpoint() { + // Regression guard for the light backend default: WinRMEndpoint accepts NetBIOS-style names with + // underscores and exposes a usable hostname/port, whereas java.net.URI cannot classify such a host + // (URI.create(endpoint).getHost() is null). The light backend must rely on these accessors. + final WinRMEndpoint winRMEndpoint = new WinRMEndpoint(HTTP, "server_name", 5999, USER, PASSWORD, null); + assertEquals("server_name", winRMEndpoint.getHostname()); + assertEquals(5999, winRMEndpoint.getPort()); + assertNull(java.net.URI.create(winRMEndpoint.getEndpoint()).getHost()); + } + @Test void testBuildNamespace() { assertEquals("ROOT/CIMV2", buildNamespace(null)); diff --git a/src/test/java/org/metricshub/winrm/service/WinRMExecutorFactoryTest.java b/src/test/java/org/metricshub/winrm/service/WinRMExecutorFactoryTest.java new file mode 100644 index 0000000..554afb5 --- /dev/null +++ b/src/test/java/org/metricshub/winrm/service/WinRMExecutorFactoryTest.java @@ -0,0 +1,197 @@ +package org.metricshub.winrm.service; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.metricshub.winrm.WinRMHttpProtocolEnum; +import org.metricshub.winrm.WindowsRemoteExecutor; +import org.metricshub.winrm.exceptions.WinRMException; +import org.metricshub.winrm.light.LightWinRMService; +import org.metricshub.winrm.service.client.auth.AuthenticationEnum; + +/** Verifies backend selection and the light backend's capability guards (no network required). */ +class WinRMExecutorFactoryTest { + + private static WinRMEndpoint endpoint(final WinRMHttpProtocolEnum protocol) { + return new WinRMEndpoint(protocol, "testhost", null, "user", "pwd".toCharArray(), null); + } + + @AfterEach + void clearBackend() { + System.clearProperty(WinRMExecutorFactory.BACKEND_PROPERTY); + } + + @Test + void lightBackendSelectedForHttpNtlm() throws Exception { + System.setProperty(WinRMExecutorFactory.BACKEND_PROPERTY, "light"); + try ( + final WindowsRemoteExecutor executor = WinRMExecutorFactory.createInstance( + endpoint(WinRMHttpProtocolEnum.HTTP), + 30000L, + null, + List.of(AuthenticationEnum.NTLM) + ) + ) { + assertInstanceOf(LightWinRMService.class, executor); + assertEquals("testhost", executor.getHostname()); + } + } + + @Test + void defaultBackendIsLight() throws Exception { + // No backend property set -> the dependency-free light backend. Building the client does not + // open a connection (that happens on the first operation), so this stays offline. + try ( + final WindowsRemoteExecutor executor = WinRMExecutorFactory.createInstance( + endpoint(WinRMHttpProtocolEnum.HTTP), + 30000L, + null, + List.of(AuthenticationEnum.NTLM) + ) + ) { + assertInstanceOf(LightWinRMService.class, executor); + } + } + + @Test + void cxfBackendRejectedWithRemovalMessage() { + // The CXF backend was removed in 2.0.0. An operator who explicitly pinned it must get a clear + // error, not be silently switched to another implementation. + System.setProperty(WinRMExecutorFactory.BACKEND_PROPERTY, "cxf"); + final WinRMException e = assertThrows( + WinRMException.class, + () -> + WinRMExecutorFactory.createInstance( + endpoint(WinRMHttpProtocolEnum.HTTP), + 30000L, + null, + List.of(AuthenticationEnum.NTLM) + ) + ); + assertTrue(e.getMessage().contains("removed in winrm-java 2.0.0"), e.getMessage()); + } + + @Test + void defaultBackendAcceptsHttps() throws Exception { + // Light now supports HTTPS (TLS + plaintext SOAP), so the default backend accepts it. Building + // the client does not open a connection (or a TLS handshake), so this stays offline. + try ( + final WindowsRemoteExecutor executor = WinRMExecutorFactory.createInstance( + endpoint(WinRMHttpProtocolEnum.HTTPS), + 30000L, + null, + List.of(AuthenticationEnum.NTLM) + ) + ) { + assertInstanceOf(LightWinRMService.class, executor); + } + } + + @Test + void lightBackendAcceptsHttps() throws Exception { + System.setProperty(WinRMExecutorFactory.BACKEND_PROPERTY, "light"); + try ( + final WindowsRemoteExecutor executor = WinRMExecutorFactory.createInstance( + endpoint(WinRMHttpProtocolEnum.HTTPS), + 30000L, + null, + List.of(AuthenticationEnum.NTLM) + ) + ) { + assertInstanceOf(LightWinRMService.class, executor); + } + } + + @Test + void kerberosOnlyOverHttpRejected() { + // Kerberos requires HTTPS (no message encryption over plain HTTP, matching CXF) and there is no + // other scheme to fall back to, so a Kerberos-only request over HTTP is rejected. + System.setProperty(WinRMExecutorFactory.BACKEND_PROPERTY, "light"); + assertThrows( + WinRMException.class, + () -> + WinRMExecutorFactory.createInstance( + endpoint(WinRMHttpProtocolEnum.HTTP), + 30000L, + null, + List.of(AuthenticationEnum.KERBEROS) + ) + ); + } + + @Test + void mixedKerberosNtlmFallsBackToNtlmOverHttp() throws Exception { + // Ordered fallback: [KERBEROS, NTLM] over HTTP cannot use Kerberos (HTTPS-only), so it falls back + // to NTLM and constructs successfully. Building the client opens no connection, so this is offline. + System.setProperty(WinRMExecutorFactory.BACKEND_PROPERTY, "light"); + try ( + final WindowsRemoteExecutor executor = WinRMExecutorFactory.createInstance( + endpoint(WinRMHttpProtocolEnum.HTTP), + 30000L, + null, + List.of(AuthenticationEnum.KERBEROS, AuthenticationEnum.NTLM) + ) + ) { + assertInstanceOf(LightWinRMService.class, executor); + } + } + + @Test + void kerberosOverHttpsAccepted() throws Exception { + // Kerberos is supported over HTTPS; the login/handshake happen on the first operation, so + // constructing the executor stays offline. + System.setProperty(WinRMExecutorFactory.BACKEND_PROPERTY, "light"); + try ( + final WindowsRemoteExecutor executor = WinRMExecutorFactory.createInstance( + endpoint(WinRMHttpProtocolEnum.HTTPS), + 30000L, + null, + List.of(AuthenticationEnum.KERBEROS) + ) + ) { + assertInstanceOf(LightWinRMService.class, executor); + } + } + + @Test + void unsupportedBackendValueRejected() { + // A typo or unknown value must fail loudly instead of silently falling through to a backend the + // operator did not request. + System.setProperty(WinRMExecutorFactory.BACKEND_PROPERTY, "cxff"); + assertThrows( + WinRMException.class, + () -> + WinRMExecutorFactory.createInstance( + endpoint(WinRMHttpProtocolEnum.HTTP), + 30000L, + null, + List.of(AuthenticationEnum.NTLM) + ) + ); + } + + @Test + void closedLightExecutorRejectsOperations() throws Exception { + // close() must release the executor for good: a later operation is rejected, not silently served + // by a fresh reconnect/handshake. + System.setProperty(WinRMExecutorFactory.BACKEND_PROPERTY, "light"); + final WindowsRemoteExecutor executor = WinRMExecutorFactory.createInstance( + endpoint(WinRMHttpProtocolEnum.HTTP), + 30000L, + null, + List.of(AuthenticationEnum.NTLM) + ); + executor.close(); + final IllegalStateException e = assertThrows( + IllegalStateException.class, + () -> executor.executeWql("SELECT Name FROM Win32_OperatingSystem", 30000L) + ); + // Same message as the CXF backend (part of the exception-surface parity, issue #106). + assertEquals("This instance has been closed and a new one must be created.", e.getMessage()); + } +} diff --git a/src/test/java/org/metricshub/winrm/service/WinRMServiceTest.java b/src/test/java/org/metricshub/winrm/service/WinRMServiceTest.java deleted file mode 100644 index 543056a..0000000 --- a/src/test/java/org/metricshub/winrm/service/WinRMServiceTest.java +++ /dev/null @@ -1,348 +0,0 @@ -package org.metricshub.winrm.service; - -import static java.nio.charset.StandardCharsets.UTF_8; -import static java.util.Collections.emptyList; -import static java.util.Collections.singletonList; -import static org.junit.jupiter.api.Assertions.assertArrayEquals; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.metricshub.winrm.Utils.EMPTY; -import static org.metricshub.winrm.service.WinRMService.createInstance; -import static org.metricshub.winrm.service.client.auth.AuthenticationEnum.NTLM; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyList; -import static org.mockito.ArgumentMatchers.anyLong; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.ArgumentMatchers.isNull; -import static org.mockito.Mockito.doNothing; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.mockStatic; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; - -import java.io.StringWriter; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.List; -import org.apache.cxf.Bus; -import org.apache.cxf.endpoint.Client; -import org.apache.cxf.endpoint.Endpoint; -import org.apache.cxf.service.model.EndpointInfo; -import org.apache.cxf.transport.http.HTTPConduitFactory; -import org.apache.cxf.transport.http.asyncclient.AsyncHTTPConduitFactory; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; -import org.metricshub.winrm.WindowsRemoteCommandResult; -import org.metricshub.winrm.exceptions.WqlQuerySyntaxException; -import org.metricshub.winrm.service.client.WinRMInvocationHandler; -import org.metricshub.winrm.service.client.auth.AuthenticationEnum; -import org.metricshub.winrm.service.enumeration.EnumerateResponse; -import org.metricshub.winrm.service.enumeration.EnumerationContextType; -import org.metricshub.winrm.service.transfer.ResourceCreated; -import org.metricshub.winrm.service.wsman.SelectorSetType; -import org.mockito.MockedStatic; - -class WinRMServiceTest { - - private static final MockedStatic MOCKED_WIN_RM_SERVICE = mockStatic(WinRMService.class); - - @BeforeAll - static void init() { - MOCKED_WIN_RM_SERVICE - .when(() -> createInstance(isNull(), anyLong(), any(Path.class), anyList())) - .thenCallRealMethod(); - - MOCKED_WIN_RM_SERVICE - .when(() -> createInstance(any(WinRMEndpoint.class), anyLong(), any(Path.class), anyList())) - .thenCallRealMethod(); - - MOCKED_WIN_RM_SERVICE - .when(() -> createInstance(any(WinRMEndpoint.class), anyLong(), isNull(), isNull())) - .thenCallRealMethod(); - - MOCKED_WIN_RM_SERVICE - .when(() -> - WinRMService.createWinRMInvocationHandlerInstance( - any(WinRMEndpoint.class), - any(Bus.class), - anyLong(), - anyString(), - isNull(), - anyList() - ) - ) - .thenReturn(mock(WinRMInvocationHandler.class)); - - MOCKED_WIN_RM_SERVICE - .when(() -> - WinRMService.createWinRMInvocationHandlerInstance( - any(WinRMEndpoint.class), - any(Bus.class), - anyLong(), - isNull(), - isNull(), - anyList() - ) - ) - .thenReturn(mock(WinRMInvocationHandler.class)); - } - - @AfterAll - static void closeMockStatics() { - MOCKED_WIN_RM_SERVICE.close(); - } - - @Test - void testCreateInstance() throws Exception { - final String hostname = "host"; - final String username = "user"; - final String rawUsername = "domain\\" + username; - final char[] password = "pwd".toCharArray(); - final WinRMEndpoint winRMEndpoint = new WinRMEndpoint(null, hostname, null, rawUsername, password, null); - final long timeout = 30 * 1000L; - final Path ticketCache = Paths.get("path"); - final List authentications = singletonList(NTLM); - - //check arguments - assertThrows(IllegalArgumentException.class, () -> createInstance(null, timeout, ticketCache, authentications)); - - assertThrows( - IllegalArgumentException.class, - () -> createInstance(winRMEndpoint, -1L, ticketCache, authentications) - ); - - assertThrows(IllegalArgumentException.class, () -> createInstance(winRMEndpoint, 0L, ticketCache, authentications)); - - final WinRMService winRMService1 = createInstance(winRMEndpoint, timeout, null, null); - assertNotNull(winRMService1); - assertEquals(1, winRMService1.getUseCount()); - assertEquals(rawUsername, winRMService1.getUsername()); - assertEquals(hostname, winRMService1.getHostname()); - assertArrayEquals(password, winRMService1.getPassword()); - assertTrue(winRMService1.isConnected()); - - final WinRMService winRMService2 = createInstance(winRMEndpoint, timeout, null, null); - assertNotNull(winRMService2); - assertEquals(2, winRMService1.getUseCount()); - assertEquals(2, winRMService2.getUseCount()); - assertEquals(rawUsername, winRMService2.getUsername()); - assertEquals(hostname, winRMService2.getHostname()); - assertArrayEquals(password, winRMService2.getPassword()); - assertTrue(winRMService1.isConnected()); - assertTrue(winRMService2.isConnected()); - - winRMService1.close(); - assertTrue(winRMService1.isConnected()); - assertTrue(winRMService2.isConnected()); - assertEquals(1, winRMService1.getUseCount()); - assertEquals(1, winRMService2.getUseCount()); - - winRMService2.close(); - assertFalse(winRMService1.isConnected()); - assertFalse(winRMService2.isConnected()); - assertEquals(0, winRMService1.getUseCount()); - assertEquals(0, winRMService2.getUseCount()); - } - - @Test - void testExecuteCommand() throws Exception { - final WinRMEndpoint winRMEndpoint = new WinRMEndpoint( - null, - "host", - null, - "domain\\user", - "pwd".toCharArray(), - null - ); - final long timeout = 30 * 1000L; - final String command = "ipconfig"; - final String workingDirectory = "dir"; - - //check arguments - try (final WinRMService winRMService = createInstance(winRMEndpoint, timeout, null, null)) { - assertThrows( - IllegalArgumentException.class, - () -> winRMService.executeCommand(null, workingDirectory, UTF_8, timeout) - ); - - assertThrows( - IllegalArgumentException.class, - () -> winRMService.executeCommand(command, workingDirectory, UTF_8, -1L) - ); - - assertThrows( - IllegalArgumentException.class, - () -> winRMService.executeCommand(command, workingDirectory, UTF_8, 0L) - ); - } - - try (final WinRMService winRMService = spy(createInstance(winRMEndpoint, timeout, null, null))) { - doNothing().when(winRMService).checkConnectedFirst(); - - doReturn(new ResourceCreated()).when(winRMService).create(null); - - doReturn(0).when(winRMService).execute(eq(command), any(StringWriter.class), any(StringWriter.class), eq(UTF_8)); - - final WindowsRemoteCommandResult actual = winRMService.executeCommand(command, null, null, timeout); - - assertEquals(EMPTY, actual.getStdout()); - assertEquals(EMPTY, actual.getStderr()); - } - - try (final WinRMService winRMService = spy(createInstance(winRMEndpoint, timeout, null, null))) { - doNothing().when(winRMService).checkConnectedFirst(); - - doReturn(new SelectorSetType()).when(winRMService).getShellSelector(); - verify(winRMService, times(0)).create(null); - - doReturn(0).when(winRMService).execute(eq(command), any(StringWriter.class), any(StringWriter.class), eq(UTF_8)); - - final WindowsRemoteCommandResult actual = winRMService.executeCommand(command, null, null, timeout); - - assertEquals(EMPTY, actual.getStdout()); - assertEquals(EMPTY, actual.getStderr()); - } - } - - @Test - void testExecuteWql() throws Exception { - final WinRMEndpoint winRMEndpoint = new WinRMEndpoint( - null, - "host", - null, - "domain\\user", - "pwd".toCharArray(), - null - ); - final long timeout = 30 * 1000L; - final String wqlQuery = "Select Name,Path from Win32_Share"; - - //check arguments - try (final WinRMService winRMService = createInstance(winRMEndpoint, timeout, null, null)) { - assertThrows(IllegalArgumentException.class, () -> winRMService.executeWql(null, timeout)); - - assertThrows(WqlQuerySyntaxException.class, () -> winRMService.executeWql(EMPTY, timeout)); - - assertThrows(WqlQuerySyntaxException.class, () -> winRMService.executeWql("Win32_Share", timeout)); - - assertThrows(IllegalArgumentException.class, () -> winRMService.executeWql(wqlQuery, -1L)); - - assertThrows(IllegalArgumentException.class, () -> winRMService.executeWql(wqlQuery, 0L)); - } - - try (final WinRMService winRMService = spy(createInstance(winRMEndpoint, timeout, null, null))) { - doNothing().when(winRMService).checkConnectedFirst(); - - final EnumerationContextType contextType = mock(EnumerationContextType.class); - final EnumerateResponse enumerateResponse = new EnumerateResponse(); - enumerateResponse.setEnumerationContext(contextType); - - doReturn(enumerateResponse).when(winRMService).enumerate(wqlQuery); - doReturn(emptyList()).when(contextType).getContent(); - - doReturn(true).when(winRMService).getItemsFrom(eq(enumerateResponse), anyList()); - - verify(winRMService, times(0)).getContextIdFrom(eq(contextType)); - verify(winRMService, times(0)).pull(anyString(), anyList()); - - assertEquals(emptyList(), winRMService.executeWql(wqlQuery, timeout)); - } - - try (final WinRMService winRMService = spy(createInstance(winRMEndpoint, timeout, null, null))) { - doNothing().when(winRMService).checkConnectedFirst(); - - final EnumerationContextType contextType = mock(EnumerationContextType.class); - final EnumerateResponse enumerateResponse = new EnumerateResponse(); - enumerateResponse.setEnumerationContext(contextType); - - doReturn(enumerateResponse).when(winRMService).enumerate(wqlQuery); - doReturn(emptyList()).when(contextType).getContent(); - - doReturn(false).when(winRMService).getItemsFrom(eq(enumerateResponse), anyList()); - - doReturn("nextContextId").when(winRMService).getContextIdFrom(eq(contextType)); - doReturn("nextContextId").when(winRMService).pull(anyString(), anyList()); - - assertEquals(emptyList(), winRMService.executeWql(wqlQuery, timeout)); - } - } - - @Test - void testCloseShutdownsConduitFactories() throws Exception { - // Use a unique endpoint to avoid interference with other test stubs - final WinRMEndpoint endpointForFactoryTest = new WinRMEndpoint( - null, - "factory-test-host", - null, - "user", - "pwd".toCharArray(), - null - ); - - // Set up a mock factory that will be stored in the client's endpoint info - final AsyncHTTPConduitFactory mockFactory = mock(AsyncHTTPConduitFactory.class); - - // Set up mock endpoint info containing the factory - final EndpointInfo mockEndpointInfo = mock(EndpointInfo.class); - doReturn(mockFactory).when(mockEndpointInfo).getProperty(HTTPConduitFactory.class.getName()); - - // Set up mock endpoint - final Endpoint mockEndpoint = mock(Endpoint.class); - doReturn(mockEndpointInfo).when(mockEndpoint).getEndpointInfo(); - - // Set up mock client - final Client mockClient = mock(Client.class); - doReturn(mockEndpoint).when(mockClient).getEndpoint(); - - // Set up mock invocation handlers that expose the configured client - final WinRMInvocationHandler cmdHandler = mock(WinRMInvocationHandler.class); - doReturn(mockClient).when(cmdHandler).getClient(); - - final WinRMInvocationHandler wqlHandler = mock(WinRMInvocationHandler.class); - doReturn(mockClient).when(wqlHandler).getClient(); - - // Override the default stubs for this specific endpoint (registered later, so they take precedence) - MOCKED_WIN_RM_SERVICE - .when(() -> - WinRMService.createWinRMInvocationHandlerInstance( - eq(endpointForFactoryTest), - any(Bus.class), - anyLong(), - isNull(), - isNull(), - anyList() - ) - ) - .thenReturn(cmdHandler); - - MOCKED_WIN_RM_SERVICE - .when(() -> - WinRMService.createWinRMInvocationHandlerInstance( - eq(endpointForFactoryTest), - any(Bus.class), - anyLong(), - anyString(), - isNull(), - anyList() - ) - ) - .thenReturn(wqlHandler); - - // Create the service and immediately close it - // (null ticketCache and authentications: the class-level createInstance stub calling the real - // method only matches isNull() for both) - final WinRMService winRMService = createInstance(endpointForFactoryTest, 30000L, null, null); - assertNotNull(winRMService); - winRMService.close(); - - // Verify that shutdown() was called on the factory for both the cmd and wql clients - verify(mockFactory, times(2)).shutdown(); - } -} diff --git a/src/test/java/org/metricshub/winrm/service/client/WinRMInvocationHandlerTest.java b/src/test/java/org/metricshub/winrm/service/client/WinRMInvocationHandlerTest.java deleted file mode 100644 index cef39eb..0000000 --- a/src/test/java/org/metricshub/winrm/service/client/WinRMInvocationHandlerTest.java +++ /dev/null @@ -1,792 +0,0 @@ -package org.metricshub.winrm.service.client; - -import static java.util.Arrays.asList; -import static java.util.Collections.singletonList; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.metricshub.winrm.Utils.EMPTY; -import static org.metricshub.winrm.Utils.checkNonNull; -import static org.metricshub.winrm.Utils.checkNonNullField; -import static org.metricshub.winrm.WinRMHttpProtocolEnum.HTTPS; -import static org.metricshub.winrm.service.client.WinRMInvocationHandler.computeCredentials; -import static org.metricshub.winrm.service.client.WinRMInvocationHandler.createCallInfos; -import static org.metricshub.winrm.service.client.WinRMInvocationHandler.createCredentials; -import static org.metricshub.winrm.service.client.WinRMInvocationHandler.createWinRMWebService; -import static org.metricshub.winrm.service.client.WinRMInvocationHandler.getWebServiceClient; -import static org.metricshub.winrm.service.client.auth.AuthenticationEnum.KERBEROS; -import static org.metricshub.winrm.service.client.auth.AuthenticationEnum.NTLM; -import static org.metricshub.winrm.service.client.auth.kerberos.KerberosUtils.createCredentials; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyInt; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.ArgumentMatchers.isNull; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.doThrow; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.mockStatic; -import static org.mockito.Mockito.reset; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; - -import jakarta.xml.soap.SOAPFactory; -import jakarta.xml.ws.WebServiceException; -import jakarta.xml.ws.soap.SOAPFaultException; -import java.io.IOException; -import java.lang.reflect.Method; -import java.nio.file.Path; -import java.util.LinkedList; -import java.util.List; -import java.util.Queue; -import java.util.stream.Collectors; -import org.apache.cxf.Bus; -import org.apache.cxf.endpoint.Client; -import org.apache.http.auth.Credentials; -import org.apache.http.auth.KerberosCredentials; -import org.apache.http.auth.NTCredentials; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; -import org.metricshub.winrm.Utils; -import org.metricshub.winrm.exceptions.KerberosCredentialsException; -import org.metricshub.winrm.service.WinRMEndpoint; -import org.metricshub.winrm.service.WinRMWebService; -import org.metricshub.winrm.service.client.WinRMInvocationHandler.AuthCredentials; -import org.metricshub.winrm.service.client.WinRMInvocationHandler.RetryAuthenticationException; -import org.metricshub.winrm.service.client.WinRMInvocationHandler.RetryTgtExpirationException; -import org.metricshub.winrm.service.client.auth.AuthenticationEnum; -import org.metricshub.winrm.service.client.auth.kerberos.KerberosUtils; -import org.metricshub.winrm.service.client.auth.ntlm.NTCredentialsWithEncryption; -import org.metricshub.winrm.service.shell.Receive; -import org.metricshub.winrm.service.shell.ReceiveResponse; -import org.metricshub.winrm.service.wsman.Locale; -import org.metricshub.winrm.service.wsman.SelectorSetType; -import org.mockito.MockedStatic; - -class WinRMInvocationHandlerTest { - - private static final WinRMEndpoint WIN_RM_ENDPOINT = new WinRMEndpoint( - null, - "host", - null, - "JohnDoe", - "pwd".toCharArray(), - null - ); - - private static final WinRMEndpoint WIN_RM_ENDPOINT_2 = new WinRMEndpoint( - null, - "host2", - null, - "JohnDoe2", - "pwd".toCharArray(), - null - ); - - private static final WinRMEndpoint WIN_RM_ENDPOINT_3 = new WinRMEndpoint( - null, - "host3", - null, - "JohnDoe3", - "pwd".toCharArray(), - null - ); - - private static final Bus BUS = mock(Bus.class); - private static final long TIMEOUT = 120L; - private static final WinRMWebService WIN_RM_WS = mock(WinRMWebService.class); - private static final Client WS_CLIENT = mock(Client.class); - private static final KerberosCredentials KERBEROS_CREDENTIALS = mock(KerberosCredentials.class); - private static final NTCredentials NTC_CREDENTIALS = mock(NTCredentialsWithEncryption.class); - private static final Method RECEIVE_METHOD; - private static final Object[] RECEIVE_ARGS; - private static final List AUTHENTICATIONS = singletonList(NTLM); - - static { - try { - RECEIVE_METHOD = - WinRMWebService.class.getMethod( - "receive", - Receive.class, - String.class, - int.class, - String.class, - Locale.class, - SelectorSetType.class - ); - RECEIVE_ARGS = - new Object[] { new Receive(), "resourceURI", 512000, "PT60S", new Locale(), new SelectorSetType() }; - } catch (ReflectiveOperationException e) { - throw new ExceptionInInitializerError(e); - } - } - - private static final Queue AUTHENTICATIONS_KERBEROS_NTLM = asList(KERBEROS, NTLM) - .stream() - .collect(Collectors.toCollection(LinkedList::new)); - - private static final Queue AUTHENTICATIONS_NTLM = AUTHENTICATIONS - .stream() - .collect(Collectors.toCollection(LinkedList::new)); - - private static final Queue AUTHENTICATIONS_KERBEROS = singletonList(KERBEROS) - .stream() - .collect(Collectors.toCollection(LinkedList::new)); - - private static final AuthCredentials AUTH_CREDENTIALS_NTLM = new AuthCredentials(NTLM, NTC_CREDENTIALS); - - private static final AuthCredentials AUTH_CREDENTIALS_KERBEROS = new AuthCredentials(KERBEROS, KERBEROS_CREDENTIALS); - - private static final Object PROXY = new Object(); - private static final Object[] ARGS = {}; - private static final ReceiveResponse RESULT = mock(ReceiveResponse.class); - - /** to neutralize Utils.sleep */ - private static final MockedStatic MOCKED_UTILS = mockStatic(Utils.class); - - private static final MockedStatic MOCKED_WIN_RM_INVOCATION_HANDLER = mockStatic( - WinRMInvocationHandler.class - ); - - @BeforeAll - static void init() { - MOCKED_UTILS.when(() -> checkNonNull(isNull(), anyString())).thenCallRealMethod(); - MOCKED_UTILS.when(() -> checkNonNull(any(), anyString())).thenCallRealMethod(); - - MOCKED_UTILS.when(() -> checkNonNullField(isNull(), anyString())).thenCallRealMethod(); - MOCKED_UTILS.when(() -> checkNonNullField(any(), anyString())).thenCallRealMethod(); - - MOCKED_WIN_RM_INVOCATION_HANDLER.when(() -> createWinRMWebService(WIN_RM_ENDPOINT, BUS)).thenReturn(WIN_RM_WS); - - MOCKED_WIN_RM_INVOCATION_HANDLER - .when(() -> createCredentials(any(WinRMEndpoint.class), eq(NTLM), isNull())) - .thenCallRealMethod(); - - MOCKED_WIN_RM_INVOCATION_HANDLER - .when(() -> createCredentials(WIN_RM_ENDPOINT, KERBEROS, null)) - .thenCallRealMethod(); - - MOCKED_WIN_RM_INVOCATION_HANDLER - .when(() -> createCredentials(WIN_RM_ENDPOINT_2, KERBEROS, null)) - .thenThrow(KerberosCredentialsException.class); - - MOCKED_WIN_RM_INVOCATION_HANDLER - .when(() -> createCredentials(WIN_RM_ENDPOINT_2, NTLM, null)) - .thenReturn(NTC_CREDENTIALS); - - MOCKED_WIN_RM_INVOCATION_HANDLER - .when(() -> createCredentials(WIN_RM_ENDPOINT_3, NTLM, null)) - .thenThrow(IllegalStateException.class); - - MOCKED_WIN_RM_INVOCATION_HANDLER - .when(() -> createCredentials(WIN_RM_ENDPOINT_3, KERBEROS, null)) - .thenReturn(KERBEROS_CREDENTIALS); - - MOCKED_WIN_RM_INVOCATION_HANDLER - .when(() -> computeCredentials(WIN_RM_ENDPOINT, null, AUTHENTICATIONS_NTLM)) - .thenReturn(AUTH_CREDENTIALS_NTLM); - - MOCKED_WIN_RM_INVOCATION_HANDLER - .when(() -> computeCredentials(WIN_RM_ENDPOINT, null, AUTHENTICATIONS_KERBEROS_NTLM)) - .thenReturn(AUTH_CREDENTIALS_NTLM); - - MOCKED_WIN_RM_INVOCATION_HANDLER - .when(() -> computeCredentials(WIN_RM_ENDPOINT, null, AUTHENTICATIONS_KERBEROS)) - .thenReturn(AUTH_CREDENTIALS_KERBEROS); - - MOCKED_WIN_RM_INVOCATION_HANDLER - .when(() -> computeCredentials(eq(WIN_RM_ENDPOINT_2), isNull(), any())) - .thenCallRealMethod(); - - MOCKED_WIN_RM_INVOCATION_HANDLER - .when(() -> computeCredentials(eq(WIN_RM_ENDPOINT_3), isNull(), any())) - .thenCallRealMethod(); - - MOCKED_WIN_RM_INVOCATION_HANDLER - .when(() -> - getWebServiceClient( - eq(WIN_RM_ENDPOINT), - eq(TIMEOUT), - isNull(), - any(WinRMWebService.class), - any(Credentials.class) - ) - ) - .thenReturn(WS_CLIENT); - - MOCKED_WIN_RM_INVOCATION_HANDLER.when(() -> createCallInfos(isNull(), isNull())).thenCallRealMethod(); - - MOCKED_WIN_RM_INVOCATION_HANDLER.when(() -> createCallInfos(isNull(), any(Object[].class))).thenCallRealMethod(); - - MOCKED_WIN_RM_INVOCATION_HANDLER.when(() -> createCallInfos(any(Method.class), isNull())).thenCallRealMethod(); - - MOCKED_WIN_RM_INVOCATION_HANDLER - .when(() -> createCallInfos(any(Method.class), any(Object[].class))) - .thenCallRealMethod(); - } - - @AfterAll - static void closeMockStatics() { - MOCKED_UTILS.close(); - MOCKED_WIN_RM_INVOCATION_HANDLER.close(); - } - - @AfterEach - void resetMocks() { - reset(WIN_RM_WS); - } - - private static void verifyReceiveInvoked(final int times) { - verify(WIN_RM_WS, times(times)) - .receive(any(Receive.class), anyString(), anyInt(), anyString(), any(Locale.class), any(SelectorSetType.class)); - } - - @Test - void testNewWinRMInvocationHandlerInstance() { - final String resourceUri = "resourceURI"; - final Path ticketCache = mock(Path.class); - - final WinRMInvocationHandler winRMInvocationHandler = new WinRMInvocationHandler( - WIN_RM_ENDPOINT, - BUS, - TIMEOUT, - null, - null, - AUTHENTICATIONS - ); - - // check arguments - assertThrows( - IllegalArgumentException.class, - () -> new WinRMInvocationHandler(null, BUS, TIMEOUT, resourceUri, ticketCache, AUTHENTICATIONS) - ); - - assertThrows( - IllegalArgumentException.class, - () -> new WinRMInvocationHandler(WIN_RM_ENDPOINT, null, TIMEOUT, resourceUri, ticketCache, AUTHENTICATIONS) - ); - - assertThrows( - IllegalArgumentException.class, - () -> new WinRMInvocationHandler(WIN_RM_ENDPOINT, BUS, TIMEOUT, resourceUri, ticketCache, null) - ); - - assertNotNull(winRMInvocationHandler); - assertEquals(WS_CLIENT, winRMInvocationHandler.getClient()); - } - - @Test - void testInvoke() throws Throwable { - // check argument method null - { - final WinRMInvocationHandler winRMInvocationHandler = new WinRMInvocationHandler( - WIN_RM_ENDPOINT, - BUS, - TIMEOUT, - null, - null, - AUTHENTICATIONS - ); - - assertThrows(IllegalArgumentException.class, () -> winRMInvocationHandler.invoke(PROXY, null, ARGS)); - } - - // check KERBEROS authentication failure like a ticket validity expiration - try (final MockedStatic mockedKerberosUtils = mockStatic(KerberosUtils.class)) { - final List authentications = singletonList(KERBEROS); - - final WinRMInvocationHandler winRMInvocationHandler = spy( - new WinRMInvocationHandler(WIN_RM_ENDPOINT, BUS, TIMEOUT, null, null, authentications) - ); - - doThrow( - new RetryTgtExpirationException( - new RuntimeException("Authentication error on HTTP://host:5985 with user name \"JohnDoe\"") - ) - ) - .doReturn(RESULT) - .when(winRMInvocationHandler) - .invokeMethod(RECEIVE_METHOD, RECEIVE_ARGS); - - mockedKerberosUtils - .when(() -> KerberosUtils.createCredentials("JohnDoe", "pwd".toCharArray(), null)) - .thenReturn(KERBEROS_CREDENTIALS); - - assertEquals(RESULT, winRMInvocationHandler.invoke(PROXY, RECEIVE_METHOD, RECEIVE_ARGS)); - verify(winRMInvocationHandler, times(2)).invoke(PROXY, RECEIVE_METHOD, RECEIVE_ARGS); - } - - // check KERBEROS authentication failure like a ticket validity expiration with an exception and no more retry - try (final MockedStatic mockedKerberosUtils = mockStatic(KerberosUtils.class)) { - final List authentications = asList(KERBEROS, NTLM); - - final WinRMInvocationHandler winRMInvocationHandler = spy( - new WinRMInvocationHandler(WIN_RM_ENDPOINT, BUS, TIMEOUT, null, null, authentications) - ); - - doThrow( - new RetryTgtExpirationException( - new RuntimeException("Authentication error on HTTP://host:5985 with user name \"JohnDoe\"") - ) - ) - .doReturn(RESULT) - .when(winRMInvocationHandler) - .invokeMethod(RECEIVE_METHOD, RECEIVE_ARGS); - - doReturn(false).when(winRMInvocationHandler).continueToRetry(); - - mockedKerberosUtils - .when(() -> KerberosUtils.createCredentials("JohnDoe", "pwd".toCharArray(), null)) - .thenThrow(KerberosCredentialsException.class); - - assertThrows( - KerberosCredentialsException.class, - () -> winRMInvocationHandler.invoke(PROXY, RECEIVE_METHOD, RECEIVE_ARGS) - ); - verify(winRMInvocationHandler, times(1)).invoke(PROXY, RECEIVE_METHOD, RECEIVE_ARGS); - } - - // check KERBEROS authentication failure like a ticket validity expiration with an exception and a retry - try (final MockedStatic mockedKerberosUtils = mockStatic(KerberosUtils.class)) { - final List authentications = asList(KERBEROS, NTLM); - - final WinRMInvocationHandler winRMInvocationHandler = spy( - new WinRMInvocationHandler(WIN_RM_ENDPOINT, BUS, TIMEOUT, null, null, authentications) - ); - - doThrow( - new RetryTgtExpirationException( - new RuntimeException("Authentication error on HTTP://host:5985 with user name \"JohnDoe\"") - ) - ) - .doReturn(RESULT) - .when(winRMInvocationHandler) - .invokeMethod(RECEIVE_METHOD, RECEIVE_ARGS); - - doReturn(true).when(winRMInvocationHandler).continueToRetry(); - - mockedKerberosUtils - .when(() -> KerberosUtils.createCredentials("JohnDoe", "pwd".toCharArray(), null)) - .thenThrow(KerberosCredentialsException.class); - - assertEquals(RESULT, winRMInvocationHandler.invoke(PROXY, RECEIVE_METHOD, RECEIVE_ARGS)); - verify(winRMInvocationHandler, times(2)).invoke(PROXY, RECEIVE_METHOD, RECEIVE_ARGS); - } - - // check retry - { - final List authentications = asList(KERBEROS, NTLM); - - final WinRMInvocationHandler winRMInvocationHandler = spy( - new WinRMInvocationHandler(WIN_RM_ENDPOINT, BUS, TIMEOUT, null, null, authentications) - ); - - doThrow(new RetryAuthenticationException(new SOAPFaultException(SOAPFactory.newInstance().createFault()))) - .doReturn(RESULT) - .when(winRMInvocationHandler) - .invokeMethod(RECEIVE_METHOD, RECEIVE_ARGS); - - doReturn(true, false).when(winRMInvocationHandler).continueToRetry(); - - assertEquals(RESULT, winRMInvocationHandler.invoke(PROXY, RECEIVE_METHOD, RECEIVE_ARGS)); - verify(winRMInvocationHandler, times(2)).invoke(PROXY, RECEIVE_METHOD, RECEIVE_ARGS); - } - - // check KO no retries KERBEROS over HTTP - { - final List authentications = singletonList(KERBEROS); - - final WinRMInvocationHandler winRMInvocationHandler = spy( - new WinRMInvocationHandler(WIN_RM_ENDPOINT, BUS, TIMEOUT, null, null, authentications) - ); - - doThrow(new RetryAuthenticationException(new SOAPFaultException(SOAPFactory.newInstance().createFault()))) - .when(winRMInvocationHandler) - .invokeMethod(RECEIVE_METHOD, RECEIVE_ARGS); - - doReturn(false).when(winRMInvocationHandler).continueToRetry(); - - final RuntimeException exception = assertThrows( - RuntimeException.class, - () -> winRMInvocationHandler.invoke(PROXY, RECEIVE_METHOD, RECEIVE_ARGS) - ); - assertEquals("KERBEROS with encryption over HTTP is not implemented.", exception.getMessage()); - verify(winRMInvocationHandler, times(1)).invoke(PROXY, RECEIVE_METHOD, RECEIVE_ARGS); - } - - // check KO no retries - { - final List authentications = singletonList(NTLM); - - final WinRMInvocationHandler winRMInvocationHandler = spy( - new WinRMInvocationHandler(WIN_RM_ENDPOINT, BUS, TIMEOUT, null, null, authentications) - ); - - doThrow( - new RetryAuthenticationException( - new RuntimeException("Authentication error on HTTP://host:5985 with user name \"JohnDoe\"") - ) - ) - .when(winRMInvocationHandler) - .invokeMethod(RECEIVE_METHOD, RECEIVE_ARGS); - - doReturn(false).when(winRMInvocationHandler).continueToRetry(); - - assertThrows(RuntimeException.class, () -> winRMInvocationHandler.invoke(PROXY, RECEIVE_METHOD, RECEIVE_ARGS)); - verify(winRMInvocationHandler, times(1)).invoke(PROXY, RECEIVE_METHOD, RECEIVE_ARGS); - } - - // check OK without retry - { - final WinRMInvocationHandler winRMInvocationHandler = spy( - new WinRMInvocationHandler(WIN_RM_ENDPOINT, BUS, TIMEOUT, null, null, AUTHENTICATIONS) - ); - - doReturn(RESULT).when(winRMInvocationHandler).invokeMethod(RECEIVE_METHOD, RECEIVE_ARGS); - - assertEquals(RESULT, winRMInvocationHandler.invoke(PROXY, RECEIVE_METHOD, RECEIVE_ARGS)); - verify(winRMInvocationHandler, times(1)).invoke(PROXY, RECEIVE_METHOD, RECEIVE_ARGS); - } - } - - @Test - void testComputeCredentials() throws Exception { - // check KerberosCredentialsException no retry - { - final Queue authenticationsQueue = singletonList(KERBEROS) - .stream() - .collect(Collectors.toCollection(LinkedList::new)); - - assertThrows( - KerberosCredentialsException.class, - () -> computeCredentials(WIN_RM_ENDPOINT_2, null, authenticationsQueue) - ); - } - - // check KerberosCredentialsException OK with retry - { - final Queue authenticationsQueue = asList(KERBEROS, NTLM) - .stream() - .collect(Collectors.toCollection(LinkedList::new)); - - assertEquals(AUTH_CREDENTIALS_NTLM, computeCredentials(WIN_RM_ENDPOINT_2, null, authenticationsQueue)); - } - - // check IllegalStateException on NTLM no retry - { - final Queue authenticationsQueue = singletonList(NTLM) - .stream() - .collect(Collectors.toCollection(LinkedList::new)); - - assertThrows( - IllegalStateException.class, - () -> computeCredentials(WIN_RM_ENDPOINT_3, null, authenticationsQueue) - ); - } - - // check IllegalStateException on NTLM OK with retry - { - final Queue authenticationsQueue = asList(NTLM, KERBEROS) - .stream() - .collect(Collectors.toCollection(LinkedList::new)); - - assertEquals(AUTH_CREDENTIALS_KERBEROS, computeCredentials(WIN_RM_ENDPOINT_3, null, authenticationsQueue)); - } - - // check OK without Exception - { - final Queue authenticationsQueue = singletonList(NTLM) - .stream() - .collect(Collectors.toCollection(LinkedList::new)); - - assertEquals(AUTH_CREDENTIALS_NTLM, computeCredentials(WIN_RM_ENDPOINT_2, null, authenticationsQueue)); - } - } - - @Test - void testInvokeMethodSOAPFaultExceptionKerberosRetry() throws Exception { - final List authentications = singletonList(KERBEROS); - - final WinRMInvocationHandler winRMInvocationHandler = new WinRMInvocationHandler( - WIN_RM_ENDPOINT, - BUS, - TIMEOUT, - null, - null, - authentications - ); - - doThrow(new SOAPFaultException(SOAPFactory.newInstance().createFault())) - .when(WIN_RM_WS) - .receive(any(Receive.class), anyString(), anyInt(), anyString(), any(Locale.class), any(SelectorSetType.class)); - - assertThrows( - RetryAuthenticationException.class, - () -> winRMInvocationHandler.invokeMethod(RECEIVE_METHOD, RECEIVE_ARGS) - ); - - verifyReceiveInvoked(1); - } - - @Test - void testInvokeMethodSOAPFaultExceptionKO() throws Exception { - final WinRMInvocationHandler winRMInvocationHandler = new WinRMInvocationHandler( - WIN_RM_ENDPOINT, - BUS, - TIMEOUT, - null, - null, - AUTHENTICATIONS - ); - - doThrow(new SOAPFaultException(SOAPFactory.newInstance().createFault())) - .when(WIN_RM_WS) - .receive(any(Receive.class), anyString(), anyInt(), anyString(), any(Locale.class), any(SelectorSetType.class)); - - assertThrows(SOAPFaultException.class, () -> winRMInvocationHandler.invokeMethod(RECEIVE_METHOD, RECEIVE_ARGS)); - - verifyReceiveInvoked(1); - } - - @Test - void testInvokeMethodNotWebServiceExceptionNoRetryKO() throws Exception { - final WinRMInvocationHandler winRMInvocationHandler = new WinRMInvocationHandler( - WIN_RM_ENDPOINT, - BUS, - TIMEOUT, - null, - null, - AUTHENTICATIONS - ); - - doThrow(new IllegalArgumentException()) - .when(WIN_RM_WS) - .receive(any(Receive.class), anyString(), anyInt(), anyString(), any(Locale.class), any(SelectorSetType.class)); - - final IllegalStateException exception = assertThrows( - IllegalStateException.class, - () -> winRMInvocationHandler.invokeMethod(RECEIVE_METHOD, RECEIVE_ARGS) - ); - - assertTrue(exception.getMessage().startsWith("Failure when calling receive")); - - verifyReceiveInvoked(1); - } - - @Test - void testInvokeMethodWebServiceExceptionNotIOExceptionNoRetryKO() throws Exception { - final WinRMInvocationHandler winRMInvocationHandler = new WinRMInvocationHandler( - WIN_RM_ENDPOINT, - BUS, - TIMEOUT, - null, - null, - AUTHENTICATIONS - ); - - doThrow(new WebServiceException()) - .when(WIN_RM_WS) - .receive(any(Receive.class), anyString(), anyInt(), anyString(), any(Locale.class), any(SelectorSetType.class)); - - final RuntimeException exception = assertThrows( - RuntimeException.class, - () -> winRMInvocationHandler.invokeMethod(RECEIVE_METHOD, RECEIVE_ARGS) - ); - - assertTrue(exception.getMessage().startsWith("Exception occurred while making WinRM WebService call receive")); - - verifyReceiveInvoked(1); - } - - @Test - void testInvokeMethodAuthorizationLoopRetryKerberos() throws Exception { - final List authentications = singletonList(KERBEROS); - - final WinRMInvocationHandler winRMInvocationHandler = new WinRMInvocationHandler( - WIN_RM_ENDPOINT, - BUS, - TIMEOUT, - null, - null, - authentications - ); - - doThrow(new WebServiceException(new IOException("Authorization loop detected on Conduit"))) - .when(WIN_RM_WS) - .receive(any(Receive.class), anyString(), anyInt(), anyString(), any(Locale.class), any(SelectorSetType.class)); - - final RetryTgtExpirationException exception = assertThrows( - RetryTgtExpirationException.class, - () -> winRMInvocationHandler.invokeMethod(RECEIVE_METHOD, RECEIVE_ARGS) - ); - - final Throwable cause = exception.getCause(); - assertTrue(cause instanceof RuntimeException); - assertEquals("Authentication error on http://host:5985/wsman with user name \"JohnDoe\"", cause.getMessage()); - - verifyReceiveInvoked(1); - } - - @Test - void testInvokeMethodAuthorizationLoopRetryCredentials() throws Exception { - final WinRMInvocationHandler winRMInvocationHandler = new WinRMInvocationHandler( - WIN_RM_ENDPOINT, - BUS, - TIMEOUT, - null, - null, - AUTHENTICATIONS - ); - - doThrow(new WebServiceException(new IOException("Authorization loop detected on Conduit"))) - .when(WIN_RM_WS) - .receive(any(Receive.class), anyString(), anyInt(), anyString(), any(Locale.class), any(SelectorSetType.class)); - - final RetryAuthenticationException exception = assertThrows( - RetryAuthenticationException.class, - () -> winRMInvocationHandler.invokeMethod(RECEIVE_METHOD, RECEIVE_ARGS) - ); - - final Throwable cause = exception.getCause(); - assertTrue(cause instanceof RuntimeException); - assertEquals("Authentication error on http://host:5985/wsman with user name \"JohnDoe\"", cause.getMessage()); - - verifyReceiveInvoked(1); - } - - @Test - void testInvokeMethod3RetriesKO() throws Exception { - final WinRMInvocationHandler winRMInvocationHandler = new WinRMInvocationHandler( - WIN_RM_ENDPOINT, - BUS, - TIMEOUT, - null, - null, - AUTHENTICATIONS - ); - - doThrow(new WebServiceException(new IOException())) - .doThrow(new WebServiceException(new IOException())) - .doThrow(new WebServiceException(new IOException())) - .doReturn(RESULT) - .when(WIN_RM_WS) - .receive(any(Receive.class), anyString(), anyInt(), anyString(), any(Locale.class), any(SelectorSetType.class)); - - final RuntimeException exception = assertThrows( - RuntimeException.class, - () -> winRMInvocationHandler.invokeMethod(RECEIVE_METHOD, RECEIVE_ARGS) - ); - - assertTrue(exception.getMessage().startsWith("failed task \"receive")); - assertTrue(exception.getMessage().endsWith("after 3 attempts")); - - verifyReceiveInvoked(3); - } - - @Test - void testInvokeMethodOK() throws Exception { - final WinRMInvocationHandler winRMInvocationHandler = new WinRMInvocationHandler( - WIN_RM_ENDPOINT, - BUS, - TIMEOUT, - null, - null, - AUTHENTICATIONS - ); - - doReturn(RESULT) - .when(WIN_RM_WS) - .receive(any(Receive.class), anyString(), anyInt(), anyString(), any(Locale.class), any(SelectorSetType.class)); - - assertEquals(RESULT, winRMInvocationHandler.invokeMethod(RECEIVE_METHOD, RECEIVE_ARGS)); - - verifyReceiveInvoked(1); - } - - @Test - void testInvokeMethod1RetryOK() throws Exception { - final WinRMInvocationHandler winRMInvocationHandler = new WinRMInvocationHandler( - WIN_RM_ENDPOINT, - BUS, - TIMEOUT, - null, - null, - AUTHENTICATIONS - ); - - doThrow(new WebServiceException(new IOException())) - .doReturn(RESULT) - .when(WIN_RM_WS) - .receive(any(Receive.class), anyString(), anyInt(), anyString(), any(Locale.class), any(SelectorSetType.class)); - - assertEquals(RESULT, winRMInvocationHandler.invokeMethod(RECEIVE_METHOD, RECEIVE_ARGS)); - - verifyReceiveInvoked(2); - } - - @Test - void testInvokeMethod2RetriesOK() throws Exception { - final WinRMInvocationHandler winRMInvocationHandler = new WinRMInvocationHandler( - WIN_RM_ENDPOINT, - BUS, - TIMEOUT, - null, - null, - AUTHENTICATIONS - ); - - doThrow(new WebServiceException(new IOException())) - .doThrow(new WebServiceException(new IOException())) - .doReturn(RESULT) - .when(WIN_RM_WS) - .receive(any(Receive.class), anyString(), anyInt(), anyString(), any(Locale.class), any(SelectorSetType.class)); - - assertEquals(RESULT, winRMInvocationHandler.invokeMethod(RECEIVE_METHOD, RECEIVE_ARGS)); - - verifyReceiveInvoked(3); - } - - @Test - void testCreateCredentials() { - // check NTLM HTTP - { - final Credentials credentials = createCredentials(WIN_RM_ENDPOINT, NTLM, null); - - assertTrue(credentials instanceof NTCredentialsWithEncryption); - } - - // check NTLM HTTPS - { - final WinRMEndpoint winRMEndpoint = new WinRMEndpoint(HTTPS, "host", null, "JohnDoe", "pwd".toCharArray(), null); - - final Credentials credentials = createCredentials(winRMEndpoint, NTLM, null); - - assertFalse(credentials instanceof NTCredentialsWithEncryption); - assertTrue(credentials instanceof NTCredentials); - } - - // check KERBEROS - try (final MockedStatic mockedKerberosUtils = mockStatic(KerberosUtils.class)) { - mockedKerberosUtils - .when(() -> createCredentials(anyString(), any(char[].class), isNull())) - .thenReturn(KERBEROS_CREDENTIALS); - - assertEquals(KERBEROS_CREDENTIALS, createCredentials(WIN_RM_ENDPOINT, KERBEROS, null)); - } - } - - @Test - void testCreateCallInfos() { - assertEquals(EMPTY, createCallInfos(null, null)); - assertEquals("receive", createCallInfos(RECEIVE_METHOD, null)); - assertEquals(EMPTY, createCallInfos(null, ARGS)); - - final Object[] args = { "arg1", 2, true }; - assertEquals("receive arg1 2 true", createCallInfos(RECEIVE_METHOD, args)); - } -} diff --git a/src/test/java/org/metricshub/winrm/service/client/auth/kerberos/KerberosUtilsTest.java b/src/test/java/org/metricshub/winrm/service/client/auth/kerberos/KerberosUtilsTest.java deleted file mode 100644 index 36a19be..0000000 --- a/src/test/java/org/metricshub/winrm/service/client/auth/kerberos/KerberosUtilsTest.java +++ /dev/null @@ -1,120 +0,0 @@ -package org.metricshub.winrm.service.client.auth.kerberos; - -import static java.nio.file.Paths.get; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.metricshub.winrm.service.client.auth.kerberos.KerberosUtils.authenticate; -import static org.metricshub.winrm.service.client.auth.kerberos.KerberosUtils.createConfigurationWithTicketCache; -import static org.metricshub.winrm.service.client.auth.kerberos.KerberosUtils.createCredentials; -import static org.metricshub.winrm.service.client.auth.kerberos.KerberosUtils.createLoginContext; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.doThrow; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.mockStatic; -import static org.mockito.Mockito.times; - -import java.nio.file.Path; -import javax.security.auth.Subject; -import javax.security.auth.callback.CallbackHandler; -import javax.security.auth.login.Configuration; -import javax.security.auth.login.LoginContext; -import javax.security.auth.login.LoginException; -import org.apache.http.auth.Credentials; -import org.apache.http.auth.KerberosCredentials; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.Test; -import org.metricshub.winrm.exceptions.KerberosCredentialsException; -import org.mockito.MockedStatic; - -class KerberosUtilsTest { - - private static final String USERNAME = "username"; - private static final char[] PASSWORD = "pwd".toCharArray(); - private static final Path TICKET_CACHE = get("path"); - - // To neutralize Subject.doAs - private static final MockedStatic MOCKED_SUBJECT = mockStatic(Subject.class); - - @AfterAll - static void closeMockStatics() { - MOCKED_SUBJECT.close(); - } - - @Test - void testCreateCredentialsArgumentsKO() throws Exception { - assertThrows(IllegalArgumentException.class, () -> createCredentials(null, PASSWORD, TICKET_CACHE)); - - assertThrows(IllegalArgumentException.class, () -> createCredentials(USERNAME, null, TICKET_CACHE)); - - try (final MockedStatic mockedKerberosUtils = mockStatic(KerberosUtils.class)) { - mockedKerberosUtils.when(() -> createCredentials(USERNAME, PASSWORD, null)).thenCallRealMethod(); - - mockedKerberosUtils - .when(() -> authenticate(anyString(), any(char[].class), any(Configuration.class))) - .thenCallRealMethod(); - - final LoginContext loginContext = mock(LoginContext.class); - - mockedKerberosUtils - .when(() -> createLoginContext(any(CallbackHandler.class), any(Configuration.class))) - .thenReturn(loginContext); - - doThrow(new LoginException("KrbException: Cannot locate default realm")).when(loginContext).login(); - - final KerberosCredentialsException exception = assertThrows( - KerberosCredentialsException.class, - () -> createCredentials(USERNAME, PASSWORD, null) - ); - - assertEquals("Kerberos Login failure. Make sure Kerberos is properly configured.", exception.getMessage()); - assertEquals(LoginException.class, exception.getCause().getClass()); - assertEquals("KrbException: Cannot locate default realm", exception.getCause().getMessage()); - } - - try (final MockedStatic mockedKerberosUtils = mockStatic(KerberosUtils.class)) { - mockedKerberosUtils.when(() -> createCredentials(USERNAME, PASSWORD, null)).thenCallRealMethod(); - - mockedKerberosUtils - .when(() -> authenticate(anyString(), any(char[].class), any(Configuration.class))) - .thenThrow(new SecurityException("Security Error")); - - final KerberosCredentialsException exception = assertThrows( - KerberosCredentialsException.class, - () -> createCredentials(USERNAME, PASSWORD, null) - ); - - assertEquals("java.lang.SecurityException: Security Error", exception.getMessage()); - assertEquals(SecurityException.class, exception.getCause().getClass()); - assertEquals("Security Error", exception.getCause().getMessage()); - } - } - - @Test - void testCreateCredentialsArgumentsWithoutTicketCacheOK() { - try (final MockedStatic mockedKerberosUtils = mockStatic(KerberosUtils.class)) { - mockedKerberosUtils.when(() -> createCredentials(USERNAME, PASSWORD, null)).thenCallRealMethod(); - - final Credentials actual = createCredentials(USERNAME, PASSWORD, null); - assertNotNull(actual); - assertTrue(actual instanceof KerberosCredentials); - - mockedKerberosUtils.verify(() -> createConfigurationWithTicketCache(any(Path.class)), times(0)); - } - } - - @Test - void testCreateCredentialsArgumentsWithTicketCacheOK() { - try (final MockedStatic mockedKerberosUtils = mockStatic(KerberosUtils.class)) { - mockedKerberosUtils.when(() -> createCredentials(USERNAME, PASSWORD, TICKET_CACHE)).thenCallRealMethod(); - - final Credentials actual = createCredentials(USERNAME, PASSWORD, TICKET_CACHE); - assertNotNull(actual); - assertTrue(actual instanceof KerberosCredentials); - - mockedKerberosUtils.verify(() -> createConfigurationWithTicketCache(any(Path.class)), times(1)); - } - } -} diff --git a/src/test/java/org/metricshub/winrm/shares/SmbTempShareTest.java b/src/test/java/org/metricshub/winrm/shares/SmbTempShareTest.java index 9058c9a..7896093 100644 --- a/src/test/java/org/metricshub/winrm/shares/SmbTempShareTest.java +++ b/src/test/java/org/metricshub/winrm/shares/SmbTempShareTest.java @@ -33,9 +33,10 @@ import java.util.List; import org.junit.jupiter.api.Test; import org.metricshub.winrm.ShareRemoteDirectoryConsumer; +import org.metricshub.winrm.WindowsRemoteExecutor; import org.metricshub.winrm.WindowsTempShare; 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.mockito.MockedStatic; @@ -63,19 +64,19 @@ void testCreateInstance() throws Exception { assertThrows(IllegalArgumentException.class, () -> createInstance(winRMEndpoint, 0L, ticketCache, authentications)); try ( - final MockedStatic mockedWinRMService = mockStatic(WinRMService.class); + final MockedStatic mockedFactory = mockStatic(WinRMExecutorFactory.class); final MockedStatic mockedSmbTempShare = mockStatic(SmbTempShare.class); final MockedStatic mockedWindowsTempShare = mockStatic(WindowsTempShare.class); final MockedStatic mockedSmbConfig = mockStatic(SmbConfig.class) ) { - final WinRMService winRMService = mock(WinRMService.class); - mockedWinRMService - .when(() -> WinRMService.createInstance(winRMEndpoint, timeout, null, null)) - .thenReturn(winRMService); + final WindowsRemoteExecutor executor = mock(WindowsRemoteExecutor.class); + mockedFactory + .when(() -> WinRMExecutorFactory.createInstance(winRMEndpoint, timeout, null, null)) + .thenReturn(executor); final WindowsTempShare windowsTempShare = mock(WindowsTempShare.class); mockedWindowsTempShare - .when(() -> getOrCreateShare(eq(winRMService), anyLong(), any(ShareRemoteDirectoryConsumer.class))) + .when(() -> getOrCreateShare(eq(executor), anyLong(), any(ShareRemoteDirectoryConsumer.class))) .thenReturn(windowsTempShare); doReturn("\\\\2001-db8--85b-3c51-f5ff-ffdb.ipv6-literal.net\\SEN_ShareFor_PC-TEST$") .when(windowsTempShare) @@ -112,14 +113,14 @@ void testCreateInstance() throws Exception { final SmbTempShare smbTempShare1 = createInstance(winRMEndpoint, timeout, null, null); assertNotNull(smbTempShare1); assertEquals(1, smbTempShare1.getUseCount()); - assertEquals(winRMService, smbTempShare1.getWindowsRemoteExecutor()); + assertEquals(executor, smbTempShare1.getWindowsRemoteExecutor()); assertTrue(smbTempShare1.isConnected()); final SmbTempShare smbTempShare2 = createInstance(winRMEndpoint, timeout, null, null); assertNotNull(smbTempShare2); assertEquals(2, smbTempShare1.getUseCount()); assertEquals(2, smbTempShare2.getUseCount()); - assertEquals(winRMService, smbTempShare2.getWindowsRemoteExecutor()); + assertEquals(executor, smbTempShare2.getWindowsRemoteExecutor()); assertTrue(smbTempShare1.isConnected()); assertTrue(smbTempShare2.isConnected()); diff --git a/src/test/java/org/metricshub/winrm/wql/WinRMWqlExecutorTest.java b/src/test/java/org/metricshub/winrm/wql/WinRMWqlExecutorTest.java index c5f13cc..5090ddd 100644 --- a/src/test/java/org/metricshub/winrm/wql/WinRMWqlExecutorTest.java +++ b/src/test/java/org/metricshub/winrm/wql/WinRMWqlExecutorTest.java @@ -6,7 +6,6 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.metricshub.winrm.WinRMHttpProtocolEnum.HTTPS; -import static org.metricshub.winrm.service.WinRMService.createInstance; import static org.metricshub.winrm.service.client.auth.AuthenticationEnum.NTLM; import static org.metricshub.winrm.wql.WinRMWqlExecutor.executeWql; import static org.mockito.ArgumentMatchers.any; @@ -23,8 +22,9 @@ import java.util.List; import java.util.Map; import org.junit.jupiter.api.Test; +import org.metricshub.winrm.WindowsRemoteExecutor; 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.mockito.MockedStatic; @@ -71,8 +71,8 @@ void testExecute() throws Exception { () -> executeWql(HTTPS, hostname, 5986, username, password, null, wqlQuery, 0L, ticketCache, authentications) ); - try (final MockedStatic mockedWinRMService = mockStatic(WinRMService.class)) { - final WinRMService winRMService = mock(WinRMService.class); + try (final MockedStatic mockedFactory = mockStatic(WinRMExecutorFactory.class)) { + final WindowsRemoteExecutor executor = mock(WindowsRemoteExecutor.class); final List> result = new ArrayList<>(); { @@ -88,11 +88,11 @@ void testExecute() throws Exception { result.add(row); } - mockedWinRMService - .when(() -> createInstance(any(WinRMEndpoint.class), anyLong(), isNull(), isNull())) - .thenReturn(winRMService); + mockedFactory + .when(() -> WinRMExecutorFactory.createInstance(any(WinRMEndpoint.class), anyLong(), isNull(), isNull())) + .thenReturn(executor); - doReturn(result).when(winRMService).executeWql(eq(wqlQuery), anyLong()); + doReturn(result).when(executor).executeWql(eq(wqlQuery), anyLong()); final WinRMWqlExecutor actual = executeWql( null,