diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..dd6e652 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Normalize line endings: text files are stored with LF in the repository. +* text=auto diff --git a/.prettierrc.yaml b/.prettierrc.yaml deleted file mode 100644 index 969e79a..0000000 --- a/.prettierrc.yaml +++ /dev/null @@ -1,12 +0,0 @@ -overrides: - - files: - - "**/*.java" - options: - trailingComma: none - useTabs: true - tabWidth: 2 - semi: false - singleQuote: false - printWidth: 120 - arrowParens: avoid - endOfLine: auto \ No newline at end of file diff --git a/metricshub-eclipse-formatter.xml b/metricshub-eclipse-formatter.xml new file mode 100644 index 0000000..54662fb --- /dev/null +++ b/metricshub-eclipse-formatter.xml @@ -0,0 +1,401 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/pom.xml b/pom.xml index 456a252..0d0f597 100644 --- a/pom.xml +++ b/pom.xml @@ -121,28 +121,30 @@ - + - com.hubspot.maven.plugins - prettier-maven-plugin - 0.22 - - 2.5.0 - false - false - - src/main/java/**/*.java - src/test/java/**/*.java - - + net.revelc.code.formatter + formatter-maven-plugin + 2.29.0 validate - check + validate + + + ${project.build.sourceDirectory} + ${project.build.testSourceDirectory} + + + **/*.java + + metricshub-eclipse-formatter.xml + KEEP + diff --git a/src/main/java/org/metricshub/winrm/ShareRemoteDirectoryConsumer.java b/src/main/java/org/metricshub/winrm/ShareRemoteDirectoryConsumer.java index ca98277..cc52bae 100644 --- a/src/main/java/org/metricshub/winrm/ShareRemoteDirectoryConsumer.java +++ b/src/main/java/org/metricshub/winrm/ShareRemoteDirectoryConsumer.java @@ -1,34 +1,34 @@ -package org.metricshub.winrm; - -/*- - * ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲ - * 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. - * ╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱ - */ - -@FunctionalInterface -public interface ShareRemoteDirectoryConsumer { - /** - * Share the remote directory on the host. - * - * @param windowsRemoteExecutor WindowsRemoteExecutor instance. - * @param remotePath The remote path. - * @param shareName The Share Name. - * @param timeout Timeout in milliseconds. - */ - public void apply(final W windowsRemoteExecutor, final R remotePath, final S shareName, final T timeout); -} +package org.metricshub.winrm; + +/*- + * ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲ + * 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. + * ╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱ + */ + +@FunctionalInterface +public interface ShareRemoteDirectoryConsumer { + /** + * Share the remote directory on the host. + * + * @param windowsRemoteExecutor WindowsRemoteExecutor instance. + * @param remotePath The remote path. + * @param shareName The Share Name. + * @param timeout Timeout in milliseconds. + */ + public void apply(final W windowsRemoteExecutor, final R remotePath, final S shareName, final T timeout); +} diff --git a/src/main/java/org/metricshub/winrm/TimeoutHelper.java b/src/main/java/org/metricshub/winrm/TimeoutHelper.java index c791d1d..8fe13ef 100644 --- a/src/main/java/org/metricshub/winrm/TimeoutHelper.java +++ b/src/main/java/org/metricshub/winrm/TimeoutHelper.java @@ -1,92 +1,90 @@ -package org.metricshub.winrm; - -/*- - * ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲ - * 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.concurrent.TimeoutException; - -/** - * for stagged timeout handling. - * - */ -public class TimeoutHelper { - - private TimeoutHelper() {} - - /** - * Get the remaining time before timeout. - * - * @param timeout Timeout in milliseconds. (throws an IllegalArgumentException if negative or zero) - * @param start start time in milliseconds. (throws an IllegalArgumentException if negative or zero) - * @return The remaining time - * @throws TimeoutException if there's no time left - */ - public static long getRemainingTime(final long timeout, final long start, final String message) - throws TimeoutException { - long remaining = start + timeout - Utils.getCurrentTimeMillis(); - if (remaining < 0) { - throw new TimeoutException(message); - } - return remaining; - } - - /** - * Sub to make the thread sleep in a stepped delay. - *

Delay: - *

- * - * @param timeout Timeout in milliseconds - * @param start start time in milliseconds - * @param message The timeout error message - * - * @throws TimeoutException when times out - * - */ - public static void stagedSleep(final long timeout, final long start, final String message) throws TimeoutException { - // Check we still have some time on our hands - getRemainingTime(timeout, start, message); - - final long elapsedTime = Utils.getCurrentTimeMillis() - start; - try { - if (elapsedTime < 500) { - Utils.sleep(50); - } else if (elapsedTime < 1000) { - Utils.sleep(100); - } else if (elapsedTime < 5000) { - Utils.sleep(250); - } else if (elapsedTime < 10000) { - Utils.sleep(500); - } else if (elapsedTime < 30000) { - Utils.sleep(1000); - } else { - Utils.sleep(3000); - } - } catch (InterruptedException e) { - throw new TimeoutException(message); - } - } -} +package org.metricshub.winrm; + +/*- + * ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲ + * 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.concurrent.TimeoutException; + +/** + * for stagged timeout handling. + */ +public class TimeoutHelper { + + private TimeoutHelper() {} + + /** + * Get the remaining time before timeout. + * + * @param timeout Timeout in milliseconds. (throws an IllegalArgumentException if negative or zero) + * @param start start time in milliseconds. (throws an IllegalArgumentException if negative or zero) + * @return The remaining time + * @throws TimeoutException if there's no time left + */ + public static long getRemainingTime(final long timeout, final long start, final String message) + throws TimeoutException { + long remaining = start + timeout - Utils.getCurrentTimeMillis(); + if (remaining < 0) { + throw new TimeoutException(message); + } + return remaining; + } + + /** + * Sub to make the thread sleep in a stepped delay. + *

+ * Delay: + *

+ * + * @param timeout Timeout in milliseconds + * @param start start time in milliseconds + * @param message The timeout error message + * @throws TimeoutException when times out + */ + public static void stagedSleep(final long timeout, final long start, final String message) throws TimeoutException { + // Check we still have some time on our hands + getRemainingTime(timeout, start, message); + + final long elapsedTime = Utils.getCurrentTimeMillis() - start; + try { + if (elapsedTime < 500) { + Utils.sleep(50); + } else if (elapsedTime < 1000) { + Utils.sleep(100); + } else if (elapsedTime < 5000) { + Utils.sleep(250); + } else if (elapsedTime < 10000) { + Utils.sleep(500); + } else if (elapsedTime < 30000) { + Utils.sleep(1000); + } else { + Utils.sleep(3000); + } + } catch (InterruptedException e) { + throw new TimeoutException(message); + } + } +} diff --git a/src/main/java/org/metricshub/winrm/Utils.java b/src/main/java/org/metricshub/winrm/Utils.java index 4de0a4f..cf7e320 100644 --- a/src/main/java/org/metricshub/winrm/Utils.java +++ b/src/main/java/org/metricshub/winrm/Utils.java @@ -1,171 +1,171 @@ -package org.metricshub.winrm; - -/*- - * ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲ - * 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.concurrent.Callable; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.Future; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; - -public class Utils { - - public static final String EMPTY = ""; - public static final String NEW_LINE = "\n"; - - /** - * Check if the required argument is not null. - * - * @param argument - * @param name - * @throws IllegalArgumentException if the argument is null - */ - public static void checkNonNull(final T argument, final String name) { - if (argument == null) { - throw new IllegalArgumentException(name + " must not be null."); - } - } - - /** - * @param value The value to check - * @return whether the value is null, empty or contains only blank chars - */ - public static boolean isBlank(String value) { - return value == null || isEmpty(value); - } - - /** - * @param value The value to check - * @return whether the value is empty of non-blank chars - * @throws NullPointerException if value is null - */ - public static boolean isEmpty(String value) { - return value.trim().isEmpty(); - } - - /** - * @param value The value to check - * @return whether the value is not null, nor empty nor contains only blank chars - */ - public static boolean isNotBlank(final String value) { - return !isBlank(value); - } - - /** - * @return the name of the local computer (or "localhost" if it can't be determined) - */ - public static String getComputerName() { - final String computerName = System.getenv("COMPUTERNAME"); - if (computerName == null) { - return "localhost"; - } - return computerName; - } - - /** - * Wrapper for Thread.sleep(millis) - * - * @param millis Time to sleep (in milliseconds) - * @throws InterruptedException - */ - public static void sleep(final long millis) throws InterruptedException { - Thread.sleep(millis); - } - - /** - * Check if the required argument is not negative or zero. - * - * @param argument - * @param name - * @throws IllegalArgumentException if the argument is null - */ - public static void checkArgumentNotZeroOrNegative(final long argument, final String name) { - if (argument <= 0) { - throw new IllegalArgumentException(String.format("%s=%d must not be negative or zero.", name, argument)); - } - } - - /** - * Get the current time in Milliseconds. - * @return the current time in Milliseconds. - */ - public static long getCurrentTimeMillis() { - return System.currentTimeMillis(); - } - - /** - * Check if the required argument is not blank (null or empty). - * - * @param argument - * @param name - * @throws IllegalArgumentException if the argument is null - */ - public static void checkNonBlank(final String argument, final String name) { - if (isBlank(argument)) { - throw new IllegalArgumentException(name + " must not be null or empty."); - } - } - - /** - * Run the given {@link Callable} using the passed timeout in seconds. - * - * @param - * @param callable - * @param timeout - * @return {@link T} result returned by the callable - * - * @throws InterruptedException - * @throws ExecutionException - * @throws TimeoutException - */ - public static T execute(Callable callable, long timeout) - throws InterruptedException, ExecutionException, TimeoutException { - ExecutorService executorService = Executors.newSingleThreadExecutor(); - Future future = executorService.submit(callable); - - try { - return future.get(timeout, TimeUnit.MILLISECONDS); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - throw e; - } catch (TimeoutException e) { - future.cancel(true); - throw e; - } finally { - executorService.shutdownNow(); - } - } - - /** - * Check if the required field is not null. - * - * @param field - * @param name - * @throws IllegalStateException if the argument is null - */ - public static void checkNonNullField(final T field, final String name) { - if (field == null) { - throw new IllegalStateException(name + " must not be null."); - } - } -} +package org.metricshub.winrm; + +/*- + * ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲ + * 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.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +public class Utils { + + public static final String EMPTY = ""; + public static final String NEW_LINE = "\n"; + + /** + * Check if the required argument is not null. + * + * @param argument + * @param name + * @throws IllegalArgumentException if the argument is null + */ + public static void checkNonNull(final T argument, final String name) { + if (argument == null) { + throw new IllegalArgumentException(name + " must not be null."); + } + } + + /** + * @param value The value to check + * @return whether the value is null, empty or contains only blank chars + */ + public static boolean isBlank(String value) { + return value == null || isEmpty(value); + } + + /** + * @param value The value to check + * @return whether the value is empty of non-blank chars + * @throws NullPointerException if value is null + */ + public static boolean isEmpty(String value) { + return value.trim().isEmpty(); + } + + /** + * @param value The value to check + * @return whether the value is not null, nor empty nor contains only blank chars + */ + public static boolean isNotBlank(final String value) { + return !isBlank(value); + } + + /** + * @return the name of the local computer (or "localhost" if it can't be determined) + */ + public static String getComputerName() { + final String computerName = System.getenv("COMPUTERNAME"); + if (computerName == null) { + return "localhost"; + } + return computerName; + } + + /** + * Wrapper for Thread.sleep(millis) + * + * @param millis Time to sleep (in milliseconds) + * @throws InterruptedException + */ + public static void sleep(final long millis) throws InterruptedException { + Thread.sleep(millis); + } + + /** + * Check if the required argument is not negative or zero. + * + * @param argument + * @param name + * @throws IllegalArgumentException if the argument is null + */ + public static void checkArgumentNotZeroOrNegative(final long argument, final String name) { + if (argument <= 0) { + throw new IllegalArgumentException(String.format("%s=%d must not be negative or zero.", name, argument)); + } + } + + /** + * Get the current time in Milliseconds. + * + * @return the current time in Milliseconds. + */ + public static long getCurrentTimeMillis() { + return System.currentTimeMillis(); + } + + /** + * Check if the required argument is not blank (null or empty). + * + * @param argument + * @param name + * @throws IllegalArgumentException if the argument is null + */ + public static void checkNonBlank(final String argument, final String name) { + if (isBlank(argument)) { + throw new IllegalArgumentException(name + " must not be null or empty."); + } + } + + /** + * Run the given {@link Callable} using the passed timeout in seconds. + * + * @param + * @param callable + * @param timeout + * @return {@link T} result returned by the callable + * @throws InterruptedException + * @throws ExecutionException + * @throws TimeoutException + */ + public static T execute(Callable callable, long timeout) + throws InterruptedException, ExecutionException, TimeoutException { + ExecutorService executorService = Executors.newSingleThreadExecutor(); + Future future = executorService.submit(callable); + + try { + return future.get(timeout, TimeUnit.MILLISECONDS); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw e; + } catch (TimeoutException e) { + future.cancel(true); + throw e; + } finally { + executorService.shutdownNow(); + } + } + + /** + * Check if the required field is not null. + * + * @param field + * @param name + * @throws IllegalStateException if the argument is null + */ + public static void checkNonNullField(final T field, final String name) { + if (field == null) { + throw new IllegalStateException(name + " must not be null."); + } + } +} diff --git a/src/main/java/org/metricshub/winrm/WinRMHttpProtocolEnum.java b/src/main/java/org/metricshub/winrm/WinRMHttpProtocolEnum.java index ad517d2..3aacc55 100644 --- a/src/main/java/org/metricshub/winrm/WinRMHttpProtocolEnum.java +++ b/src/main/java/org/metricshub/winrm/WinRMHttpProtocolEnum.java @@ -1,29 +1,29 @@ -package org.metricshub.winrm; - -/*- - * ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲ - * 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. - * ╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱ - */ - -/** - * Enumeration for protocol to use. - */ -public enum WinRMHttpProtocolEnum { - HTTP, - HTTPS -} +package org.metricshub.winrm; + +/*- + * ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲ + * 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. + * ╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱ + */ + +/** + * Enumeration for protocol to use. + */ +public enum WinRMHttpProtocolEnum { + HTTP, + HTTPS +} diff --git a/src/main/java/org/metricshub/winrm/WindowsRemoteCommandResult.java b/src/main/java/org/metricshub/winrm/WindowsRemoteCommandResult.java index 33042ab..c89d7aa 100644 --- a/src/main/java/org/metricshub/winrm/WindowsRemoteCommandResult.java +++ b/src/main/java/org/metricshub/winrm/WindowsRemoteCommandResult.java @@ -1,95 +1,99 @@ -package org.metricshub.winrm; - -/*- - * ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲ - * 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 WindowsRemoteCommandResult { - - private final String stdout; - private final String stderr; - private final float executionTime; - private final int statusCode; - - /** - * Command Result constructor - * - * @param stdout The stdout of the command - * @param stderr The stderr of the command - * @param executionTime The execution time of the command in milliseconds - * @param statusCode The command return status code - */ - public WindowsRemoteCommandResult( - final String stdout, - final String stderr, - final float executionTime, - final int statusCode - ) { - this.stdout = stdout; - this.stderr = stderr; - this.executionTime = executionTime; - this.statusCode = statusCode; - } - - /** - * Get the stdout of the command. - * @return - */ - public String getStdout() { - return stdout; - } - - /** - * Get the stderr of the command. - * @return - */ - public String getStderr() { - return stderr; - } - - /** - * Get the execution time of the command in seconds. - * @return - */ - public float getExecutionTime() { - return executionTime; - } - - /** - * Get the return status code of the command - * @return - */ - public int getStatusCode() { - return statusCode; - } - - @Override - public String toString() { - return new StringBuilder() - .append("WindowsRemoteCommandResult:\nstdout:\n") - .append(stdout) - .append("\nstderr:\n") - .append(stderr) - .append("\nexecutionTime = ") - .append(executionTime) - .append("\nstatusCode = ") - .append(statusCode) - .toString(); - } -} +package org.metricshub.winrm; + +/*- + * ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲ + * 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. + * ╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱ + */ + +public class WindowsRemoteCommandResult { + + private final String stdout; + private final String stderr; + private final float executionTime; + private final int statusCode; + + /** + * Command Result constructor + * + * @param stdout The stdout of the command + * @param stderr The stderr of the command + * @param executionTime The execution time of the command in milliseconds + * @param statusCode The command return status code + */ + public WindowsRemoteCommandResult( + final String stdout, + final String stderr, + final float executionTime, + final int statusCode + ) { + this.stdout = stdout; + this.stderr = stderr; + this.executionTime = executionTime; + this.statusCode = statusCode; + } + + /** + * Get the stdout of the command. + * + * @return + */ + public String getStdout() { + return stdout; + } + + /** + * Get the stderr of the command. + * + * @return + */ + public String getStderr() { + return stderr; + } + + /** + * Get the execution time of the command in seconds. + * + * @return + */ + public float getExecutionTime() { + return executionTime; + } + + /** + * Get the return status code of the command + * + * @return + */ + public int getStatusCode() { + return statusCode; + } + + @Override + public String toString() { + return new StringBuilder() + .append("WindowsRemoteCommandResult:\nstdout:\n") + .append(stdout) + .append("\nstderr:\n") + .append(stderr) + .append("\nexecutionTime = ") + .append(executionTime) + .append("\nstatusCode = ") + .append(statusCode) + .toString(); + } +} diff --git a/src/main/java/org/metricshub/winrm/WindowsRemoteExecutor.java b/src/main/java/org/metricshub/winrm/WindowsRemoteExecutor.java index ce87c62..671e343 100644 --- a/src/main/java/org/metricshub/winrm/WindowsRemoteExecutor.java +++ b/src/main/java/org/metricshub/winrm/WindowsRemoteExecutor.java @@ -1,92 +1,93 @@ -package org.metricshub.winrm; - -/*- - * ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲ - * 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.List; -import java.util.Map; -import java.util.concurrent.TimeoutException; -import org.metricshub.winrm.exceptions.WindowsRemoteException; -import org.metricshub.winrm.exceptions.WqlQuerySyntaxException; - -public interface WindowsRemoteExecutor extends AutoCloseable { - /** - *

Execute a WQL query and process its result.

- * - * @param wqlQuery the WQL query (required) - * @param timeout Timeout in milliseconds (throws an IllegalArgumentException if negative or zero) - * - * @return a list of result rows. A result row is a Map(LinkedHashMap to preserve the query order) of - * properties/values. - * - * @throws TimeoutException to notify userName of timeout. - * @throws WqlQuerySyntaxException if WQL query syntax is invalid - * @throws WindowsRemoteException For any problem encountered - */ - public List> executeWql(final String wqlQuery, final long timeout) - throws TimeoutException, WqlQuerySyntaxException, WindowsRemoteException; - - /** - * Execute the command on the remote - * - * @param command The command to execute - * @param workingDirectory Path of the directory for the spawned process on the remote system (can be null) - * @param charset The charset - * @param timeout Timeout in milliseconds - * - * @return The command result - * - * @throws WindowsRemoteException For any problem encountered - * @throws TimeoutException To notify userName of timeout. - */ - public WindowsRemoteCommandResult executeCommand( - final String command, - final String workingDirectory, - final Charset charset, - final long timeout - ) throws WindowsRemoteException, TimeoutException; - - /** - * Get the hostname. - * @return - */ - public String getHostname(); - - /** - * Get the username. - * @return - */ - public String getUsername(); - - /** - * Get the password. - * @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(); -} +package org.metricshub.winrm; + +/*- + * ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲ + * WinRM Java Client + * ჻჻჻჻჻჻ + * Copyright (C) 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.List; +import java.util.Map; +import java.util.concurrent.TimeoutException; +import org.metricshub.winrm.exceptions.WindowsRemoteException; +import org.metricshub.winrm.exceptions.WqlQuerySyntaxException; + +public interface WindowsRemoteExecutor extends AutoCloseable { + /** + *

+ * Execute a WQL query and process its result. + *

+ * + * @param wqlQuery the WQL query (required) + * @param timeout Timeout in milliseconds (throws an IllegalArgumentException if negative or zero) + * @return a list of result rows. A result row is a Map(LinkedHashMap to preserve the query order) of + * properties/values. + * @throws TimeoutException to notify userName of timeout. + * @throws WqlQuerySyntaxException if WQL query syntax is invalid + * @throws WindowsRemoteException For any problem encountered + */ + public List> executeWql(final String wqlQuery, final long timeout) + throws TimeoutException, WqlQuerySyntaxException, WindowsRemoteException; + + /** + * Execute the command on the remote + * + * @param command The command to execute + * @param workingDirectory Path of the directory for the spawned process on the remote system (can be null) + * @param charset The charset + * @param timeout Timeout in milliseconds + * @return The command result + * @throws WindowsRemoteException For any problem encountered + * @throws TimeoutException To notify userName of timeout. + */ + public WindowsRemoteCommandResult executeCommand( + final String command, + final String workingDirectory, + final Charset charset, + final long timeout + ) throws WindowsRemoteException, TimeoutException; + + /** + * Get the hostname. + * + * @return + */ + public String getHostname(); + + /** + * Get the username. + * + * @return + */ + public String getUsername(); + + /** + * Get the password. + * + * @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/WindowsRemoteProcessUtils.java b/src/main/java/org/metricshub/winrm/WindowsRemoteProcessUtils.java index 6587f71..9420f79 100644 --- a/src/main/java/org/metricshub/winrm/WindowsRemoteProcessUtils.java +++ b/src/main/java/org/metricshub/winrm/WindowsRemoteProcessUtils.java @@ -1,246 +1,234 @@ -package org.metricshub.winrm; - -/*- - * ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲ - * 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.nio.charset.Charset; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.nio.file.StandardCopyOption; -import java.nio.file.attribute.FileTime; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.concurrent.TimeoutException; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import org.metricshub.winrm.exceptions.WindowsRemoteException; -import org.metricshub.winrm.exceptions.WqlQuerySyntaxException; - -public class WindowsRemoteProcessUtils { - - private WindowsRemoteProcessUtils() {} - - private static final String DEFAULT_CODESET = "1252"; - private static final Charset DEFAULT_CHARSET = Charset.forName("windows-1252"); - - /** - * Windows CodeSet to java.nio.charset Charset Code map. - * - * @see Windows code page - * @see - * Supported Encodings - * - */ - private static final Map CODESET_MAP; - - static { - final Map map = new HashMap<>(); - map.put("1250", Charset.forName("windows-1250")); - map.put("1251", Charset.forName("windows-1251")); - map.put("1252", DEFAULT_CHARSET); - map.put("1253", Charset.forName("windows-1253")); - map.put("1254", Charset.forName("windows-1254")); - map.put("1255", Charset.forName("windows-1255")); - map.put("1256", Charset.forName("windows-1256")); - map.put("1257", Charset.forName("windows-1257")); - map.put("1258", Charset.forName("windows-1258")); - map.put("874", Charset.forName("x-windows-874")); - map.put("932", Charset.forName("Shift_JIS")); - map.put("936", Charset.forName("GBK")); - map.put("949", Charset.forName("EUC-KR")); - map.put("950", Charset.forName("Big5")); - map.put("951", Charset.forName("Big5-HKSCS")); - map.put("28591", StandardCharsets.ISO_8859_1); - map.put("20127", StandardCharsets.US_ASCII); - map.put("65001", StandardCharsets.UTF_8); - map.put("1200", StandardCharsets.UTF_16LE); - map.put("1201", StandardCharsets.UTF_16BE); - - CODESET_MAP = Collections.unmodifiableMap(map); - } - - /** - * Get the CharSet from the Win32_OperatingSystem CodeSet. (if not found by default Latin-1 windows-1252) - * - * @param windowsRemoteExecutor WindowsRemoteExecutor instance - * @param timeout Timeout in milliseconds. - * - * @return the encoding charset from Win32_OperatingSystem - * - * @throws TimeoutException To notify userName of timeout - * @throws WqlQuerySyntaxException On WQL syntax errors - * @throws WindowsRemoteException For any problem encountered on remote - * - * @see - * Win32_OperatingSystem class - * - */ - public static Charset getWindowsEncodingCharset( - final WindowsRemoteExecutor windowsRemoteExecutor, - final long timeout - ) throws TimeoutException, WqlQuerySyntaxException, WindowsRemoteException { - if (windowsRemoteExecutor == null || timeout < 1) { - return DEFAULT_CHARSET; - } - - final List> result = windowsRemoteExecutor.executeWql( - "SELECT CodeSet FROM Win32_OperatingSystem", - timeout - ); - - final String codeSet = result - .stream() - .map(row -> (String) row.get("CodeSet")) - .filter(Objects::nonNull) - .findFirst() - .orElse(DEFAULT_CODESET); - - return CODESET_MAP.getOrDefault(codeSet, DEFAULT_CHARSET); - } - - /** - * Builds a new output file name, with 99.9999999% chances of being unique - * on the remote system - * - * @return file name - */ - public static String buildNewOutputFileName() { - return String.format( - "SEN_%s_%d_%d", - Utils.getComputerName(), - Utils.getCurrentTimeMillis(), - (long) (Math.random() * 1000000) - ); - } - - /** - * Copy the local files to the share and update the command with their path as seen in the remote system. - * - * @param command The command (mandatory) - * @param localFiles The local files to copy list - * @param uncSharePath The UNC path of the share - * @param remotePath The remote path - * - * @return The updated command. - * - * @throws IOException If an I/O error occurs. - */ - public static String copyLocalFilesToShare( - final String command, - final List localFiles, - final String uncSharePath, - final String remotePath - ) throws IOException { - Utils.checkNonNull(command, "command"); - - if (localFiles == null || localFiles.isEmpty()) { - return command; - } - - Utils.checkNonNull(uncSharePath, "uncSharePath"); - Utils.checkNonNull(remotePath, "remotePath"); - - try { - return localFiles - .stream() - .reduce( - command, - (cmd, localFile) -> { - try { - final Path localFilePath = Paths.get(localFile); - final Path remoteFilePath = copyToShare(localFilePath, uncSharePath, remotePath); - - return caseInsensitiveReplace(cmd, localFile, remoteFilePath.toString()); - } catch (final IOException e) { - throw new RuntimeException(e); - } - } - ); - } catch (final Exception e) { - if (e.getCause() instanceof IOException) { - throw (IOException) e.getCause(); - } - throw e; - } - } - - /** - * Copy a file to the share. - * - * If the same file is already present on the share, the copy is not performed. - * The "last-modified" time is used to determine whether the file needs to be - * copied or not. - * - * @param localFilePath The path to the file to copy - * @param uncSharePath The UNC path of the share - * @param remotePath The remote path - * - * @return the path to the copied file, as seen in the remote system - * - * @throws IOException If an I/O error occurs. - */ - static Path copyToShare(final Path localFilePath, final String uncSharePath, final String remotePath) - throws IOException { - final Path targetUncPath = Paths.get(uncSharePath, localFilePath.getFileName().toString()); - final Path targetRemotePath = Paths.get(remotePath, localFilePath.getFileName().toString()); - - if (Files.exists(targetUncPath)) { - final FileTime sourceFileTime = Files.getLastModifiedTime(localFilePath); - final FileTime targetFileTime = Files.getLastModifiedTime(targetUncPath); - if (sourceFileTime.compareTo(targetFileTime) <= 0) { - // File is already present on the target, simply skip the copy operation - return targetRemotePath; - } - } - - // Copy - Files.copy(localFilePath, targetUncPath, StandardCopyOption.COPY_ATTRIBUTES, StandardCopyOption.REPLACE_EXISTING); - - // Return the path to the copied file, as seen in the remote system - return targetRemotePath; - } - - /** - * Perform a case-insensitive replace of all occurrences of target string with - * specified replacement - * - * Similar to String.replace(target, replacement) - * - * @param string The string to parse - * @param target The string to replace - * @param replacement The replacement string - * - * @return updated string - */ - static String caseInsensitiveReplace(final String string, final String target, final String replacement) { - return string == null || target == null - ? string - : Pattern - .compile(target, Pattern.LITERAL | Pattern.CASE_INSENSITIVE) - .matcher(string) - .replaceAll(Matcher.quoteReplacement(replacement == null ? Utils.EMPTY : replacement)); - } -} +package org.metricshub.winrm; + +/*- + * ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲ + * 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.IOException; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; +import java.nio.file.attribute.FileTime; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.TimeoutException; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import org.metricshub.winrm.exceptions.WindowsRemoteException; +import org.metricshub.winrm.exceptions.WqlQuerySyntaxException; + +public class WindowsRemoteProcessUtils { + + private WindowsRemoteProcessUtils() {} + + private static final String DEFAULT_CODESET = "1252"; + private static final Charset DEFAULT_CHARSET = Charset.forName("windows-1252"); + + /** + * Windows CodeSet to java.nio.charset Charset Code map. + * + * @see Windows code page + * @see + * Supported Encodings + */ + private static final Map CODESET_MAP; + + static { + final Map map = new HashMap<>(); + map.put("1250", Charset.forName("windows-1250")); + map.put("1251", Charset.forName("windows-1251")); + map.put("1252", DEFAULT_CHARSET); + map.put("1253", Charset.forName("windows-1253")); + map.put("1254", Charset.forName("windows-1254")); + map.put("1255", Charset.forName("windows-1255")); + map.put("1256", Charset.forName("windows-1256")); + map.put("1257", Charset.forName("windows-1257")); + map.put("1258", Charset.forName("windows-1258")); + map.put("874", Charset.forName("x-windows-874")); + map.put("932", Charset.forName("Shift_JIS")); + map.put("936", Charset.forName("GBK")); + map.put("949", Charset.forName("EUC-KR")); + map.put("950", Charset.forName("Big5")); + map.put("951", Charset.forName("Big5-HKSCS")); + map.put("28591", StandardCharsets.ISO_8859_1); + map.put("20127", StandardCharsets.US_ASCII); + map.put("65001", StandardCharsets.UTF_8); + map.put("1200", StandardCharsets.UTF_16LE); + map.put("1201", StandardCharsets.UTF_16BE); + + CODESET_MAP = Collections.unmodifiableMap(map); + } + + /** + * Get the CharSet from the Win32_OperatingSystem CodeSet. (if not found by default Latin-1 windows-1252) + * + * @param windowsRemoteExecutor WindowsRemoteExecutor instance + * @param timeout Timeout in milliseconds. + * @return the encoding charset from Win32_OperatingSystem + * @throws TimeoutException To notify userName of timeout + * @throws WqlQuerySyntaxException On WQL syntax errors + * @throws WindowsRemoteException For any problem encountered on remote + * @see + * Win32_OperatingSystem class + */ + public static Charset getWindowsEncodingCharset( + final WindowsRemoteExecutor windowsRemoteExecutor, + final long timeout + ) throws TimeoutException, WqlQuerySyntaxException, WindowsRemoteException { + if (windowsRemoteExecutor == null || timeout < 1) { + return DEFAULT_CHARSET; + } + + final List> result = windowsRemoteExecutor.executeWql( + "SELECT CodeSet FROM Win32_OperatingSystem", + timeout + ); + + final String codeSet = result + .stream() + .map(row -> (String) row.get("CodeSet")) + .filter(Objects::nonNull) + .findFirst() + .orElse(DEFAULT_CODESET); + + return CODESET_MAP.getOrDefault(codeSet, DEFAULT_CHARSET); + } + + /** + * Builds a new output file name, with 99.9999999% chances of being unique + * on the remote system + * + * @return file name + */ + public static String buildNewOutputFileName() { + return String.format( + "SEN_%s_%d_%d", + Utils.getComputerName(), + Utils.getCurrentTimeMillis(), + (long) (Math.random() * 1000000) + ); + } + + /** + * Copy the local files to the share and update the command with their path as seen in the remote system. + * + * @param command The command (mandatory) + * @param localFiles The local files to copy list + * @param uncSharePath The UNC path of the share + * @param remotePath The remote path + * @return The updated command. + * @throws IOException If an I/O error occurs. + */ + public static String copyLocalFilesToShare( + final String command, + final List localFiles, + final String uncSharePath, + final String remotePath + ) throws IOException { + Utils.checkNonNull(command, "command"); + + if (localFiles == null || localFiles.isEmpty()) { + return command; + } + + Utils.checkNonNull(uncSharePath, "uncSharePath"); + Utils.checkNonNull(remotePath, "remotePath"); + + try { + return localFiles + .stream() + .reduce( + command, + (cmd, localFile) -> { + try { + final Path localFilePath = Paths.get(localFile); + final Path remoteFilePath = copyToShare(localFilePath, uncSharePath, remotePath); + + return caseInsensitiveReplace(cmd, localFile, remoteFilePath.toString()); + } catch (final IOException e) { + throw new RuntimeException(e); + } + } + ); + } catch (final Exception e) { + if (e.getCause() instanceof IOException) { + throw (IOException) e.getCause(); + } + throw e; + } + } + + /** + * Copy a file to the share. + * If the same file is already present on the share, the copy is not performed. + * The "last-modified" time is used to determine whether the file needs to be + * copied or not. + * + * @param localFilePath The path to the file to copy + * @param uncSharePath The UNC path of the share + * @param remotePath The remote path + * @return the path to the copied file, as seen in the remote system + * @throws IOException If an I/O error occurs. + */ + static Path copyToShare(final Path localFilePath, final String uncSharePath, final String remotePath) + throws IOException { + final Path targetUncPath = Paths.get(uncSharePath, localFilePath.getFileName().toString()); + final Path targetRemotePath = Paths.get(remotePath, localFilePath.getFileName().toString()); + + if (Files.exists(targetUncPath)) { + final FileTime sourceFileTime = Files.getLastModifiedTime(localFilePath); + final FileTime targetFileTime = Files.getLastModifiedTime(targetUncPath); + if (sourceFileTime.compareTo(targetFileTime) <= 0) { + // File is already present on the target, simply skip the copy operation + return targetRemotePath; + } + } + + // Copy + Files.copy(localFilePath, targetUncPath, StandardCopyOption.COPY_ATTRIBUTES, StandardCopyOption.REPLACE_EXISTING); + + // Return the path to the copied file, as seen in the remote system + return targetRemotePath; + } + + /** + * Perform a case-insensitive replace of all occurrences of target string with + * specified replacement + * Similar to String.replace(target, replacement) + * + * @param string The string to parse + * @param target The string to replace + * @param replacement The replacement string + * @return updated string + */ + static String caseInsensitiveReplace(final String string, final String target, final String replacement) { + return string == null || target == null + ? string + : Pattern + .compile(target, Pattern.LITERAL | Pattern.CASE_INSENSITIVE) + .matcher(string) + .replaceAll(Matcher.quoteReplacement(replacement == null ? Utils.EMPTY : replacement)); + } +} diff --git a/src/main/java/org/metricshub/winrm/WindowsTempShare.java b/src/main/java/org/metricshub/winrm/WindowsTempShare.java index 09358f3..5bb74d4 100644 --- a/src/main/java/org/metricshub/winrm/WindowsTempShare.java +++ b/src/main/java/org/metricshub/winrm/WindowsTempShare.java @@ -1,390 +1,371 @@ -package org.metricshub.winrm; - -/*- - * ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲ - * 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.Objects; -import java.util.Optional; -import java.util.concurrent.TimeoutException; -import org.metricshub.winrm.exceptions.WindowsRemoteException; -import org.metricshub.winrm.exceptions.WqlQuerySyntaxException; - -public class WindowsTempShare { - - /** The share name */ - private final String shareName; - - /** The UNC path of the share. */ - private final String uncSharePath; - - /** The remote path.*/ - private final String remotePath; - - /** The WindowsRemoteExecutor instance */ - private final WindowsRemoteExecutor windowsRemoteExecutor; - - /** - * Constructor of WindowsTempShare - * - * @param windowsRemoteExecutor the WindowsRemoteExecutor instance - * connected to the remote host (mandatory) - * @param shareNameOrUnc The name of the share, or its full UNC path (mandatory) - * @param remotePath The path on the remote system of the directory being shared - */ - public WindowsTempShare( - final WindowsRemoteExecutor windowsRemoteExecutor, - final String shareNameOrUnc, - final String remotePath - ) { - Utils.checkNonNull(windowsRemoteExecutor, "windowsRemoteExecutor"); - Utils.checkNonNull(shareNameOrUnc, "shareNameOrUnc"); - - if (shareNameOrUnc.startsWith("\\\\")) { - this.uncSharePath = shareNameOrUnc; - final String[] uncElements = shareNameOrUnc.split("\\\\"); - this.shareName = uncElements[3]; - } else { - this.uncSharePath = buildUncPath(windowsRemoteExecutor.getHostname(), shareNameOrUnc); - this.shareName = shareNameOrUnc; - } - - this.remotePath = remotePath; - this.windowsRemoteExecutor = windowsRemoteExecutor; - } - - /** - * Get the existing share on the host or create it if absent. - * - * @param windowsRemoteExecutor WindowsRemoteExecutor instance. (mandatory) - * @param timeout Timeout in milliseconds. (throws an IllegalArgumentException if negative or zero) - * @param shareRemoteDirectory ShareRemoteDirectoryConsumer function (mandatory) - * - * @return The remote path. - * - * @throws TimeoutException To notify userName of timeout. - * @throws WindowsRemoteException For any problem encountered. - */ - public static WindowsTempShare getOrCreateShare( - final WindowsRemoteExecutor windowsRemoteExecutor, - final long timeout, - final ShareRemoteDirectoryConsumer shareRemoteDirectory - ) throws TimeoutException, WindowsRemoteException { - Utils.checkNonNull(windowsRemoteExecutor, "windowsRemoteExecutor"); - Utils.checkArgumentNotZeroOrNegative(timeout, "timeout"); - Utils.checkNonNull(shareRemoteDirectory, "shareRemoteDirectory"); - - final long start = Utils.getCurrentTimeMillis(); - - // Are we targeting a Windows cluster? - // (in which case, things are a bit different) - - // Try to get a clustered share - final Optional clusterShare = getClusterShare( - windowsRemoteExecutor, - TimeoutHelper.getRemainingTime(timeout, start, "No time left to check for cluster share"), - start - ); - - if (clusterShare.isPresent()) { - return clusterShare.get(); - } - - // Normal case (non-cluster) - final String shareName = buildShareName(); - final Optional share = getShare( - windowsRemoteExecutor, - shareName, - TimeoutHelper.getRemainingTime(timeout, start, "No time left to get a normal temporary share") - ); - if (share.isPresent()) { - return share.get(); - } - - return createTempShare( - windowsRemoteExecutor, - shareName, - TimeoutHelper.getRemainingTime(timeout, start, "No time left to create the temporary share"), - shareRemoteDirectory - ); - } - - /** - * Get the Windows directory. - * - * @param windowsRemoteExecutor WindowsRemoteExecutor instance. (mandatory) - * @param timeout Timeout in milliseconds. (throws an IllegalArgumentException if negative or zero) - * - * @return The Windows directory. - * - * @throws WindowsRemoteException For any problem encountered. - * @throws TimeoutException To notify userName of timeout. - * - * @see - * Win32_OperatingSystem class - * - */ - public static String getWindowsDirectory(final WindowsRemoteExecutor windowsRemoteExecutor, final long timeout) - throws WindowsRemoteException, TimeoutException { - Utils.checkNonNull(windowsRemoteExecutor, "windowsRemoteExecutor"); - Utils.checkArgumentNotZeroOrNegative(timeout, "timeout"); - - try { - // Extract the WindowsDirectory property from the first instance and return it (or throw an exception) - return windowsRemoteExecutor - .executeWql("SELECT WindowsDirectory FROM Win32_OperatingSystem", timeout) - .stream() - .limit(1) - .map(row -> (String) row.get("WindowsDirectory")) - .filter(Objects::nonNull) - .findFirst() - .orElseThrow(() -> - new WindowsRemoteException( - String.format("Couldn't identify the Windows root directory on %s.", windowsRemoteExecutor.getHostname()) - ) - ); - } catch (final WqlQuerySyntaxException e) { - throw new WindowsRemoteException(e); // Impossible - } - } - - /** - * Create the remote directory. - * - * @param windowsRemoteExecutor WindowsRemoteExecutor instance. (mandatory) - * @param remotePath The remote path to create - * @param timeout Timeout in milliseconds - * @param start start time in milliseconds. - * - * @throws WindowsRemoteException For any problem encountered. - * @throws TimeoutException To notify userName of timeout. - */ - public static void createRemoteDirectory( - final WindowsRemoteExecutor windowsRemoteExecutor, - final String remotePath, - final long timeout, - final long start - ) throws WindowsRemoteException, TimeoutException { - Utils.checkNonNull(windowsRemoteExecutor, "windowsRemoteExecutor"); - - windowsRemoteExecutor.executeCommand(buildCreateRemoteDirectoryCommand(remotePath), null, null, timeout); - } - - /** - * Build a UNC path from hostname and share name. - * - * Note: This method ensures compatibility with IPv6 hosts - * - * @param hostname Host to connect to. (mandatory) - * @param share The share - * - * @return The UNC path to the share (\\unc(host)\share) - */ - static String buildUncPath(final String hostname, final String share) { - Utils.checkNonNull(hostname, "hostname"); - - return hostname.contains(":") - ? String.format("\\\\%s.ipv6-literal.net\\%s", hostname.replace(":", "-").replace("%", "s"), share) - : String.format("\\\\%s\\%s", hostname, share); - } - - /** - * Create the Windows command for a directory creation. - * - * @param remotePath The remote path to create. (mandatory) - * - * @return The command to execute. - */ - static String buildCreateRemoteDirectoryCommand(final String remotePath) { - Utils.checkNonBlank(remotePath, "remotePath"); - - return String.format("CMD.EXE /C IF NOT EXIST \"%s\" MKDIR %s", remotePath, remotePath); - } - - /** - * @param path Root path of the temporary directory that will be used in a cluster. (mandatory) - * - * @return Path to the temporary directory - */ - static String buildPathOnCluster(final String path) { - Utils.checkNonNull(path, "path"); - - return String.format("%s\\Temp\\SEN_TempFor_%s", path, Utils.getComputerName()); - } - - /** - * Build the remote temp path name with the folder name. - * - * @param folder The folder name. (mandatory) - * @param shareName The Share Name. (mandatory) - * - * @return The remote directory path. (folder\Temp\shareName) - */ - static String buildRemotePath(final String folder, final String shareName) { - Utils.checkNonNull(folder, "folder"); - Utils.checkNonBlank(shareName, "shareName"); - - return String.format("%s\\Temp\\%s", folder, shareName); - } - - /** - * Build the Share name with the computer name. - * - * @return The share name. - */ - static String buildShareName() { - return String.format("SEN_ShareFor_%s$", Utils.getComputerName()); - } - - /** - * Retrieve an "Admin Share" (like D$, E$, etc.) that is exposed by a cluster. - * - * If the targeted system is not a cluster, returns an empty optional. - * - * @param windowsRemoteExecutor WindowsRemoteExecutor instance. - * @param timeout Timeout in milliseconds. - * @param start start time in milliseconds. - * - * @return An optional Map with 2 entries: "Name" and "Path" - * - * @throws TimeoutException To notify userName of timeout. - * @throws WindowsRemoteException For any problem encountered. - */ - static Optional getClusterShare( - final WindowsRemoteExecutor windowsRemoteExecutor, - final long timeout, - final long start - ) throws TimeoutException, WindowsRemoteException { - try { - final Optional clusterShare = windowsRemoteExecutor - .executeWql( - "SELECT Name,Path FROM Win32_ClusterShare WHERE " + - "ServerName <> '*' AND (Type = 2147483648 OR Type = 3221225472) AND Name LIKE '%\\\\_$'", - timeout - ) - .stream() - .limit(1) - .map(row -> // We return a TempShare instance pointing to a subdirectory in this share - new WindowsTempShare( - windowsRemoteExecutor, - buildPathOnCluster((String) row.get("Name")), - buildPathOnCluster((String) row.get("Path")) - ) - ) - .findFirst(); - - if (clusterShare.isPresent()) { - // We create the subdirectory (if necessary) - createRemoteDirectory(windowsRemoteExecutor, clusterShare.get().getRemotePath(), timeout, start); - } - - return clusterShare; - } catch (final WqlQuerySyntaxException e) { - throw new WindowsRemoteException(e); // Impossible - } - } - - /** - * Retrieve the specified share. - * - * @param windowsRemoteExecutor WindowsRemoteExecutor instance - * @param shareName The share name - * @param timeout Timeout in milliseconds - * - * @return An optional TempShare instance - * - * @throws TimeoutException To notify userName of timeout - * @throws WindowsRemoteException For any problem encountered - */ - static Optional getShare( - final WindowsRemoteExecutor windowsRemoteExecutor, - final String shareName, - final long timeout - ) throws TimeoutException, WindowsRemoteException { - try { - return windowsRemoteExecutor - .executeWql(String.format("SELECT Name,Path FROM Win32_Share WHERE Name = '%s'", shareName), timeout) - .stream() - .limit(1) - .map(row -> new WindowsTempShare(windowsRemoteExecutor, (String) row.get("Name"), (String) row.get("Path"))) - .findFirst(); - } catch (final WqlQuerySyntaxException e) { - throw new WindowsRemoteException(e); // Impossible - } - } - - /** - * Create a shared temporary folder on the remote - * - * @param windowsRemoteExecutor WindowsRemoteExecutor instance. - * @param shareName The Share Name. - * @param timeout Timeout in milliseconds. - * @param shareRemoteDirectory shareRemoteDirectory function - * - * @return A TempShare instance - * - * @throws WindowsRemoteException For any problem encountered. - * @throws TimeoutException To notify userName of timeout. - */ - static WindowsTempShare createTempShare( - final WindowsRemoteExecutor windowsRemoteExecutor, - final String shareName, - final long timeout, - final ShareRemoteDirectoryConsumer shareRemoteDirectory - ) throws WindowsRemoteException, TimeoutException { - final long start = Utils.getCurrentTimeMillis(); - - // Find where Windows is installed on the remote system. We will create the share under %WINDIR%\Temp. - final String folder = getWindowsDirectory(windowsRemoteExecutor, timeout); - - // Create the folder on the remote system - final String remotePath = buildRemotePath(folder, shareName); - createRemoteDirectory( - windowsRemoteExecutor, - remotePath, - TimeoutHelper.getRemainingTime(timeout, start, "No time left to create the temporary directory"), - start - ); - - // Create the share - shareRemoteDirectory.apply(windowsRemoteExecutor, remotePath, shareName, timeout); - - return new WindowsTempShare(windowsRemoteExecutor, shareName, remotePath); - } - - /** Get the share name */ - public String getShareName() { - return shareName; - } - - /** Get the UNC path of the share */ - public String getUncSharePath() { - return uncSharePath; - } - - /** Get the remote path */ - public String getRemotePath() { - return remotePath; - } - - /** Get the WindowsRemoteExecutor instance */ - public WindowsRemoteExecutor getWindowsRemoteExecutor() { - return windowsRemoteExecutor; - } -} +package org.metricshub.winrm; + +/*- + * ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲ + * 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.Objects; +import java.util.Optional; +import java.util.concurrent.TimeoutException; +import org.metricshub.winrm.exceptions.WindowsRemoteException; +import org.metricshub.winrm.exceptions.WqlQuerySyntaxException; + +public class WindowsTempShare { + + /** The share name */ + private final String shareName; + + /** The UNC path of the share. */ + private final String uncSharePath; + + /** The remote path. */ + private final String remotePath; + + /** The WindowsRemoteExecutor instance */ + private final WindowsRemoteExecutor windowsRemoteExecutor; + + /** + * Constructor of WindowsTempShare + * + * @param windowsRemoteExecutor the WindowsRemoteExecutor instance + * connected to the remote host (mandatory) + * @param shareNameOrUnc The name of the share, or its full UNC path (mandatory) + * @param remotePath The path on the remote system of the directory being shared + */ + public WindowsTempShare( + final WindowsRemoteExecutor windowsRemoteExecutor, + final String shareNameOrUnc, + final String remotePath + ) { + Utils.checkNonNull(windowsRemoteExecutor, "windowsRemoteExecutor"); + Utils.checkNonNull(shareNameOrUnc, "shareNameOrUnc"); + + if (shareNameOrUnc.startsWith("\\\\")) { + this.uncSharePath = shareNameOrUnc; + final String[] uncElements = shareNameOrUnc.split("\\\\"); + this.shareName = uncElements[3]; + } else { + this.uncSharePath = buildUncPath(windowsRemoteExecutor.getHostname(), shareNameOrUnc); + this.shareName = shareNameOrUnc; + } + + this.remotePath = remotePath; + this.windowsRemoteExecutor = windowsRemoteExecutor; + } + + /** + * Get the existing share on the host or create it if absent. + * + * @param windowsRemoteExecutor WindowsRemoteExecutor instance. (mandatory) + * @param timeout Timeout in milliseconds. (throws an IllegalArgumentException if negative or zero) + * @param shareRemoteDirectory ShareRemoteDirectoryConsumer function (mandatory) + * @return The remote path. + * @throws TimeoutException To notify userName of timeout. + * @throws WindowsRemoteException For any problem encountered. + */ + public static WindowsTempShare getOrCreateShare( + final WindowsRemoteExecutor windowsRemoteExecutor, + final long timeout, + final ShareRemoteDirectoryConsumer shareRemoteDirectory + ) throws TimeoutException, WindowsRemoteException { + Utils.checkNonNull(windowsRemoteExecutor, "windowsRemoteExecutor"); + Utils.checkArgumentNotZeroOrNegative(timeout, "timeout"); + Utils.checkNonNull(shareRemoteDirectory, "shareRemoteDirectory"); + + final long start = Utils.getCurrentTimeMillis(); + + // Are we targeting a Windows cluster? + // (in which case, things are a bit different) + + // Try to get a clustered share + final Optional clusterShare = getClusterShare( + windowsRemoteExecutor, + TimeoutHelper.getRemainingTime(timeout, start, "No time left to check for cluster share"), + start + ); + + if (clusterShare.isPresent()) { + return clusterShare.get(); + } + + // Normal case (non-cluster) + final String shareName = buildShareName(); + final Optional share = getShare( + windowsRemoteExecutor, + shareName, + TimeoutHelper.getRemainingTime(timeout, start, "No time left to get a normal temporary share") + ); + if (share.isPresent()) { + return share.get(); + } + + return createTempShare( + windowsRemoteExecutor, + shareName, + TimeoutHelper.getRemainingTime(timeout, start, "No time left to create the temporary share"), + shareRemoteDirectory + ); + } + + /** + * Get the Windows directory. + * + * @param windowsRemoteExecutor WindowsRemoteExecutor instance. (mandatory) + * @param timeout Timeout in milliseconds. (throws an IllegalArgumentException if negative or zero) + * @return The Windows directory. + * @throws WindowsRemoteException For any problem encountered. + * @throws TimeoutException To notify userName of timeout. + * @see + * Win32_OperatingSystem class + */ + public static String getWindowsDirectory(final WindowsRemoteExecutor windowsRemoteExecutor, final long timeout) + throws WindowsRemoteException, TimeoutException { + Utils.checkNonNull(windowsRemoteExecutor, "windowsRemoteExecutor"); + Utils.checkArgumentNotZeroOrNegative(timeout, "timeout"); + + try { + // Extract the WindowsDirectory property from the first instance and return it (or throw an exception) + return windowsRemoteExecutor + .executeWql("SELECT WindowsDirectory FROM Win32_OperatingSystem", timeout) + .stream() + .limit(1) + .map(row -> (String) row.get("WindowsDirectory")) + .filter(Objects::nonNull) + .findFirst() + .orElseThrow( + () -> new WindowsRemoteException( + String.format("Couldn't identify the Windows root directory on %s.", windowsRemoteExecutor.getHostname()) + ) + ); + } catch (final WqlQuerySyntaxException e) { + throw new WindowsRemoteException(e); // Impossible + } + } + + /** + * Create the remote directory. + * + * @param windowsRemoteExecutor WindowsRemoteExecutor instance. (mandatory) + * @param remotePath The remote path to create + * @param timeout Timeout in milliseconds + * @param start start time in milliseconds. + * @throws WindowsRemoteException For any problem encountered. + * @throws TimeoutException To notify userName of timeout. + */ + public static void createRemoteDirectory( + final WindowsRemoteExecutor windowsRemoteExecutor, + final String remotePath, + final long timeout, + final long start + ) throws WindowsRemoteException, TimeoutException { + Utils.checkNonNull(windowsRemoteExecutor, "windowsRemoteExecutor"); + + windowsRemoteExecutor.executeCommand(buildCreateRemoteDirectoryCommand(remotePath), null, null, timeout); + } + + /** + * Build a UNC path from hostname and share name. + * Note: This method ensures compatibility with IPv6 hosts + * + * @param hostname Host to connect to. (mandatory) + * @param share The share + * @return The UNC path to the share (\\unc(host)\share) + */ + static String buildUncPath(final String hostname, final String share) { + Utils.checkNonNull(hostname, "hostname"); + + return hostname.contains(":") + ? String.format("\\\\%s.ipv6-literal.net\\%s", hostname.replace(":", "-").replace("%", "s"), share) + : String.format("\\\\%s\\%s", hostname, share); + } + + /** + * Create the Windows command for a directory creation. + * + * @param remotePath The remote path to create. (mandatory) + * @return The command to execute. + */ + static String buildCreateRemoteDirectoryCommand(final String remotePath) { + Utils.checkNonBlank(remotePath, "remotePath"); + + return String.format("CMD.EXE /C IF NOT EXIST \"%s\" MKDIR %s", remotePath, remotePath); + } + + /** + * @param path Root path of the temporary directory that will be used in a cluster. (mandatory) + * @return Path to the temporary directory + */ + static String buildPathOnCluster(final String path) { + Utils.checkNonNull(path, "path"); + + return String.format("%s\\Temp\\SEN_TempFor_%s", path, Utils.getComputerName()); + } + + /** + * Build the remote temp path name with the folder name. + * + * @param folder The folder name. (mandatory) + * @param shareName The Share Name. (mandatory) + * @return The remote directory path. (folder\Temp\shareName) + */ + static String buildRemotePath(final String folder, final String shareName) { + Utils.checkNonNull(folder, "folder"); + Utils.checkNonBlank(shareName, "shareName"); + + return String.format("%s\\Temp\\%s", folder, shareName); + } + + /** + * Build the Share name with the computer name. + * + * @return The share name. + */ + static String buildShareName() { + return String.format("SEN_ShareFor_%s$", Utils.getComputerName()); + } + + /** + * Retrieve an "Admin Share" (like D$, E$, etc.) that is exposed by a cluster. + * If the targeted system is not a cluster, returns an empty optional. + * + * @param windowsRemoteExecutor WindowsRemoteExecutor instance. + * @param timeout Timeout in milliseconds. + * @param start start time in milliseconds. + * @return An optional Map with 2 entries: "Name" and "Path" + * @throws TimeoutException To notify userName of timeout. + * @throws WindowsRemoteException For any problem encountered. + */ + static Optional getClusterShare( + final WindowsRemoteExecutor windowsRemoteExecutor, + final long timeout, + final long start + ) throws TimeoutException, WindowsRemoteException { + try { + final Optional clusterShare = windowsRemoteExecutor + .executeWql( + "SELECT Name,Path FROM Win32_ClusterShare WHERE " + + "ServerName <> '*' AND (Type = 2147483648 OR Type = 3221225472) AND Name LIKE '%\\\\_$'", + timeout + ) + .stream() + .limit(1) + .map(row -> // We return a TempShare instance pointing to a subdirectory in this share + new WindowsTempShare( + windowsRemoteExecutor, + buildPathOnCluster((String) row.get("Name")), + buildPathOnCluster((String) row.get("Path")) + ) + ) + .findFirst(); + + if (clusterShare.isPresent()) { + // We create the subdirectory (if necessary) + createRemoteDirectory(windowsRemoteExecutor, clusterShare.get().getRemotePath(), timeout, start); + } + + return clusterShare; + } catch (final WqlQuerySyntaxException e) { + throw new WindowsRemoteException(e); // Impossible + } + } + + /** + * Retrieve the specified share. + * + * @param windowsRemoteExecutor WindowsRemoteExecutor instance + * @param shareName The share name + * @param timeout Timeout in milliseconds + * @return An optional TempShare instance + * @throws TimeoutException To notify userName of timeout + * @throws WindowsRemoteException For any problem encountered + */ + static Optional getShare( + final WindowsRemoteExecutor windowsRemoteExecutor, + final String shareName, + final long timeout + ) throws TimeoutException, WindowsRemoteException { + try { + return windowsRemoteExecutor + .executeWql(String.format("SELECT Name,Path FROM Win32_Share WHERE Name = '%s'", shareName), timeout) + .stream() + .limit(1) + .map(row -> new WindowsTempShare(windowsRemoteExecutor, (String) row.get("Name"), (String) row.get("Path"))) + .findFirst(); + } catch (final WqlQuerySyntaxException e) { + throw new WindowsRemoteException(e); // Impossible + } + } + + /** + * Create a shared temporary folder on the remote + * + * @param windowsRemoteExecutor WindowsRemoteExecutor instance. + * @param shareName The Share Name. + * @param timeout Timeout in milliseconds. + * @param shareRemoteDirectory shareRemoteDirectory function + * @return A TempShare instance + * @throws WindowsRemoteException For any problem encountered. + * @throws TimeoutException To notify userName of timeout. + */ + static WindowsTempShare createTempShare( + final WindowsRemoteExecutor windowsRemoteExecutor, + final String shareName, + final long timeout, + final ShareRemoteDirectoryConsumer shareRemoteDirectory + ) throws WindowsRemoteException, TimeoutException { + final long start = Utils.getCurrentTimeMillis(); + + // Find where Windows is installed on the remote system. We will create the share under %WINDIR%\Temp. + final String folder = getWindowsDirectory(windowsRemoteExecutor, timeout); + + // Create the folder on the remote system + final String remotePath = buildRemotePath(folder, shareName); + createRemoteDirectory( + windowsRemoteExecutor, + remotePath, + TimeoutHelper.getRemainingTime(timeout, start, "No time left to create the temporary directory"), + start + ); + + // Create the share + shareRemoteDirectory.apply(windowsRemoteExecutor, remotePath, shareName, timeout); + + return new WindowsTempShare(windowsRemoteExecutor, shareName, remotePath); + } + + /** Get the share name */ + public String getShareName() { + return shareName; + } + + /** Get the UNC path of the share */ + public String getUncSharePath() { + return uncSharePath; + } + + /** Get the remote path */ + public String getRemotePath() { + return remotePath; + } + + /** Get the WindowsRemoteExecutor instance */ + public WindowsRemoteExecutor getWindowsRemoteExecutor() { + return windowsRemoteExecutor; + } +} diff --git a/src/main/java/org/metricshub/winrm/WmiHelper.java b/src/main/java/org/metricshub/winrm/WmiHelper.java index 91ac4d3..7db501c 100644 --- a/src/main/java/org/metricshub/winrm/WmiHelper.java +++ b/src/main/java/org/metricshub/winrm/WmiHelper.java @@ -1,153 +1,154 @@ -package org.metricshub.winrm; - -/*- - * ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲ - * 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.List; -import java.util.Map; -import java.util.regex.Pattern; -import java.util.stream.Collectors; -import org.metricshub.winrm.exceptions.WqlQuerySyntaxException; - -public abstract class WmiHelper { - - /** - * Private constructor, as this class cannot be instantiated (it's pure static) - */ - private WmiHelper() {} - - public static final String DEFAULT_NAMESPACE = "ROOT\\CIMV2"; - - /** - * Pattern to detect a simple WQL select query. - */ - private static final Pattern WQL_SIMPLE_SELECT_PATTERN = Pattern.compile( - "^\\s*SELECT\\s+(\\*|(?!SELECT|FROM|WHERE)[a-z0-9._]+|((?!SELECT|FROM|WHERE)[a-z0-9._]+\\s*,\\s*)+((?!SELECT|FROM|WHERE)[a-z0-9._]+))\\s+FROM\\s+((?!WHERE|FROM)\\w+)\\s*(WHERE\\s+.*)?$", - Pattern.CASE_INSENSITIVE - ); - - /** - * Check if the WQL Query respect the simple syntax in the form of - * Select * from (where) or Select a,b,c from (where) - * is valid. - * - * @param wqlQuery - * @return whether specified WQL query's syntax is valid or not - */ - public static boolean isValidWql(final String wqlQuery) { - return WQL_SIMPLE_SELECT_PATTERN.matcher(wqlQuery).find(); - } - - /** - * The "network resource" is either just the namespace (for localhost), or \\hostname\\namespace. - * - * @param hostname Host to connect to. - * @param namespace The Namespace. - * @return resource - */ - public static String createNetworkResource(final String hostname, final String namespace) { - Utils.checkNonNull(namespace, "namespace"); - return hostname == null || hostname.isEmpty() ? namespace : String.format("\\\\%s\\%s", hostname, namespace); - } - - /** - * @param networkResource Network resource string to test - * @return whether specified networkResource is local or not - */ - public static boolean isLocalNetworkResource(final String networkResource) { - Utils.checkNonNull(networkResource, "networkResource"); - return ( - !networkResource.startsWith("\\\\") || - networkResource.startsWith("\\\\localhost\\") || - networkResource.startsWith("\\\\127.0.0.1\\") || - networkResource.startsWith("\\\\0:0:0:0:0:0:0:1\\") || - networkResource.startsWith("\\\\::1\\") || - networkResource.startsWith("\\\\0000:0000:0000:0000:0000:0000:0000:0001\\") || - networkResource.toLowerCase().startsWith("\\\\" + Utils.getComputerName().toLowerCase() + "\\") - ); - } - - /** - * Extract the exact name of the properties from a WMI result. - * - * The interest is to retrieve the exact case of the property names, instead of - * the lowercase that we have at this stage. - * - * @param resultRows The result whose first row will be parsed - * @param wql The WQL query that was used (so we make sure to return the properties in the same order) - * @return a list of property names - * @throws IllegalStateException if the specified WQL is invalid - */ - public static List extractPropertiesFromResult(final List> resultRows, final String wql) { - try { - return extractPropertiesFromResult(resultRows, WqlQuery.newInstance(wql)); - } catch (WqlQuerySyntaxException e) { - throw new IllegalStateException(e); - } - } - - /** - * Extract the exact name of the properties from a WMI result. - * - * The interest is to retrieve the exact case of the property names, instead of - * the lowercase that we have at this stage. - * - * Note: The exact case cannot be retrieved if result is empty, in which case all - * names are reported in lower case - * - * @param resultRows The result whose first row will be parsed - * @param wqlQuery The WQL query that was used (so we make sure to return the properties in the same order) - * @return a list of property names - */ - public static List extractPropertiesFromResult( - final List> resultRows, - final WqlQuery wqlQuery - ) { - // If resultRows is empty, we won't be able to retrieve the actual property names - // with the correct case. So, we simply return the list of specified properties in the - // WQL query - if (resultRows.isEmpty()) { - return wqlQuery.getSelectedProperties(); - } - - // Extract the actual property names - final String[] resultPropertyArray = resultRows.get(0).keySet().toArray(new String[0]); - - // First case: we don't have any specified properties in the WQL Query, so we just - // return the properties from the result set in alphabetical order - if (wqlQuery.getSelectedProperties().isEmpty()) { - Arrays.sort(resultPropertyArray, String.CASE_INSENSITIVE_ORDER); - return Arrays.asList(resultPropertyArray); - } - - // Create a new list based on queryPropertyArray (with its order), but with the values - // from resultPropertyArray - final List queryProperties = wqlQuery.getSelectedProperties(); - final Map resultProperties = Arrays - .asList(resultPropertyArray) - .stream() - .collect(Collectors.toMap(String::toLowerCase, property -> property)); - return queryProperties - .stream() - .map(property -> resultProperties.getOrDefault(property.toLowerCase(), property)) - .collect(Collectors.toList()); - } -} +package org.metricshub.winrm; + +/*- + * ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲ + * 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.Arrays; +import java.util.List; +import java.util.Map; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import org.metricshub.winrm.exceptions.WqlQuerySyntaxException; + +public abstract class WmiHelper { + + /** + * Private constructor, as this class cannot be instantiated (it's pure static) + */ + private WmiHelper() {} + + public static final String DEFAULT_NAMESPACE = "ROOT\\CIMV2"; + + /** + * Pattern to detect a simple WQL select query. + */ + private static final Pattern WQL_SIMPLE_SELECT_PATTERN = Pattern.compile( + "^\\s*SELECT\\s+(\\*|(?!SELECT|FROM|WHERE)[a-z0-9._]+|((?!SELECT|FROM|WHERE)[a-z0-9._]+\\s*,\\s*)+((?!SELECT|FROM|WHERE)[a-z0-9._]+))\\s+FROM\\s+((?!WHERE|FROM)\\w+)\\s*(WHERE\\s+.*)?$", + Pattern.CASE_INSENSITIVE + ); + + /** + * Check if the WQL Query respect the simple syntax in the form of + * Select * from (where) or Select a,b,c from (where) + * is valid. + * + * @param wqlQuery + * @return whether specified WQL query's syntax is valid or not + */ + public static boolean isValidWql(final String wqlQuery) { + return WQL_SIMPLE_SELECT_PATTERN.matcher(wqlQuery).find(); + } + + /** + * The "network resource" is either just the namespace (for localhost), or \\hostname\\namespace. + * + * @param hostname Host to connect to. + * @param namespace The Namespace. + * @return resource + */ + public static String createNetworkResource(final String hostname, final String namespace) { + Utils.checkNonNull(namespace, "namespace"); + return hostname == null || hostname.isEmpty() ? namespace : String.format("\\\\%s\\%s", hostname, namespace); + } + + /** + * @param networkResource Network resource string to test + * @return whether specified networkResource is local or not + */ + public static boolean isLocalNetworkResource(final String networkResource) { + Utils.checkNonNull(networkResource, "networkResource"); + return (!networkResource.startsWith("\\\\") + || + networkResource.startsWith("\\\\localhost\\") + || + networkResource.startsWith("\\\\127.0.0.1\\") + || + networkResource.startsWith("\\\\0:0:0:0:0:0:0:1\\") + || + networkResource.startsWith("\\\\::1\\") + || + networkResource.startsWith("\\\\0000:0000:0000:0000:0000:0000:0000:0001\\") + || + networkResource.toLowerCase().startsWith("\\\\" + Utils.getComputerName().toLowerCase() + "\\")); + } + + /** + * Extract the exact name of the properties from a WMI result. + * The interest is to retrieve the exact case of the property names, instead of + * the lowercase that we have at this stage. + * + * @param resultRows The result whose first row will be parsed + * @param wql The WQL query that was used (so we make sure to return the properties in the same order) + * @return a list of property names + * @throws IllegalStateException if the specified WQL is invalid + */ + public static List extractPropertiesFromResult(final List> resultRows, final String wql) { + try { + return extractPropertiesFromResult(resultRows, WqlQuery.newInstance(wql)); + } catch (WqlQuerySyntaxException e) { + throw new IllegalStateException(e); + } + } + + /** + * Extract the exact name of the properties from a WMI result. + * The interest is to retrieve the exact case of the property names, instead of + * the lowercase that we have at this stage. + * Note: The exact case cannot be retrieved if result is empty, in which case all + * names are reported in lower case + * + * @param resultRows The result whose first row will be parsed + * @param wqlQuery The WQL query that was used (so we make sure to return the properties in the same order) + * @return a list of property names + */ + public static List extractPropertiesFromResult( + final List> resultRows, + final WqlQuery wqlQuery + ) { + // If resultRows is empty, we won't be able to retrieve the actual property names + // with the correct case. So, we simply return the list of specified properties in the + // WQL query + if (resultRows.isEmpty()) { + return wqlQuery.getSelectedProperties(); + } + + // Extract the actual property names + final String[] resultPropertyArray = resultRows.get(0).keySet().toArray(new String[0]); + + // First case: we don't have any specified properties in the WQL Query, so we just + // return the properties from the result set in alphabetical order + if (wqlQuery.getSelectedProperties().isEmpty()) { + Arrays.sort(resultPropertyArray, String.CASE_INSENSITIVE_ORDER); + return Arrays.asList(resultPropertyArray); + } + + // Create a new list based on queryPropertyArray (with its order), but with the values + // from resultPropertyArray + final List queryProperties = wqlQuery.getSelectedProperties(); + final Map resultProperties = Arrays + .asList(resultPropertyArray) + .stream() + .collect(Collectors.toMap(String::toLowerCase, property -> property)); + return queryProperties + .stream() + .map(property -> resultProperties.getOrDefault(property.toLowerCase(), property)) + .collect(Collectors.toList()); + } +} diff --git a/src/main/java/org/metricshub/winrm/WqlQuery.java b/src/main/java/org/metricshub/winrm/WqlQuery.java index df96159..bbc8bbc 100644 --- a/src/main/java/org/metricshub/winrm/WqlQuery.java +++ b/src/main/java/org/metricshub/winrm/WqlQuery.java @@ -1,245 +1,241 @@ -package org.metricshub.winrm; - -/*- - * ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲ - * 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.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.HashSet; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import java.util.stream.Collectors; -import org.metricshub.winrm.exceptions.WqlQuerySyntaxException; - -public class WqlQuery { - - /** - * Pattern to parse a WQL query - *
    - *
  • group(1) = SELECT ... FROM - *
  • group(2) = Properties in the SELECT statement (but not '*') - *
  • group(3) = ASSOCIATORS OF { object ID } - *
  • group(4) = class in the FROM statement - *
  • group(5) = Rest of the WQL statement (WHERE, etc.) - */ - private static final Pattern WQL_PATTERN = Pattern.compile( - "^\\s*(SELECT\\s+(?:\\*|([a-z0-9._]+(?:\\s*,\\s*[a-z0-9._]+)*))\\s+FROM\\s+)?(?:((?:ASSOCIATORS|REFERENCES)\\s+OF\\s+\\{.*\\})|([a-z0-9_]+))(\\s+WHERE\\s*+.+)?\\s*$", - Pattern.CASE_INSENSITIVE | Pattern.DOTALL - ); - - private String wql; - private List selectedProperties; - private Map> subPropertiesMap; - private String cleanWql; - - private WqlQuery( - String wql, - List selectedProperties, - Map> subPropertiesMap, - String cleanWql - ) { - this.wql = wql; - this.selectedProperties = selectedProperties; - this.subPropertiesMap = subPropertiesMap; - this.cleanWql = cleanWql; - } - - /** - * Parses the specified WQL query and returns a new instance of WqlQuery - * - * Supported WQL syntaxes: - *
      - *
    • SELECT * FROM Class - *
    • SELECT PropA, PropB FROM Class - *
    • SELECT PropA, PropB FROM Class WHERE condition - *
    • ASSOCIATORS OF { objectId } - *
    • ASSOCIATORS OF { objectId } WHERE condition - *
    • SELECT * FROM ASSOCIATORS OF { objectId } WHERE condition - *
    • SELECT PropA, PropB FROM ASSOCIATORS OF { objectId } WHERE condition - *
    - * @param wql The WQL query to parse - * @return a new WqlQuery instance - * @throws WqlQuerySyntaxException when the specified WQL is invalid and cannot be parsed - * @throws IllegalArgumentException if wql is null - */ - public static WqlQuery newInstance(CharSequence wql) throws WqlQuerySyntaxException { - Utils.checkNonNull(wql, "wql"); - - Matcher wqlMatcher = WQL_PATTERN.matcher(wql); - - // No match: invalid WQL - if (!wqlMatcher.find()) { - throw new WqlQuerySyntaxException(wql.toString()); - } - - // Extract the different fragments of the parsed WQL - String selectFragment = wqlMatcher.group(1); - String propertiesFragment = wqlMatcher.group(2); - String associatorsFragment = wqlMatcher.group(3); - String classFragment = wqlMatcher.group(4); - String restFragment = wqlMatcher.group(5); - - // If there is no `SELECT` and no `ASSOCIATORS OF`, it's no valid WQL - if (selectFragment == null && associatorsFragment == null) { - throw new WqlQuerySyntaxException(wql.toString()); - } - - List properties = buildSelectedProperties(propertiesFragment); - Map> subPropertiesMap = buildSupPropertiesMap(properties); - String cleanWql = buildCleanWql(associatorsFragment, subPropertiesMap, classFragment, restFragment); - - return new WqlQuery(wql.toString(), properties, subPropertiesMap, cleanWql); - } - - /** - * Note: All properties are converted to lower case - * @param propertiesFragment Comma-separated list of properties - * @return a cleaned-up array of the properties - */ - static List buildSelectedProperties(String propertiesFragment) { - if (Utils.isNotBlank(propertiesFragment)) { - return Arrays.asList(propertiesFragment.trim().toLowerCase().split("\\s*,\\s*")); - } - return new ArrayList<>(); - } - - /** - * Build a Map of subproperties to retrieve inside properties - * - * Example: - * - * Input: - * PropA, PropB.Sub1, PropB.Sub2 - * - * Output: - *
      - *
    • PropA => emptySet() - *
    • PropB => { "Sub1", "Sub2" } - *
    - * - * @param properties Selected properties (that may include subproperties) - * @return The map as described above - */ - static Map> buildSupPropertiesMap(final List properties) { - // Empty or null? - if (properties == null || properties.isEmpty()) { - return new HashMap<>(); - } - - Map> subPropertiesMap = new LinkedHashMap<>(); - properties - .stream() - .filter(Utils::isNotBlank) - .forEachOrdered(property -> { - // Split the property into fragments: - // propA => ["propA"] - // propA.subProp => ["propA", "subProp"] - String[] propertyFragmentArray = property.toLowerCase().split("\\.", 2); - String mainProperty = propertyFragmentArray[0]; - String subProperty = propertyFragmentArray.length == 2 ? propertyFragmentArray[1] : null; - - // Add this entry to the map - subPropertiesMap.compute( - mainProperty, - (key, subPropertiesSet) -> { - if (subPropertiesSet == null) { - subPropertiesSet = new HashSet<>(); - } - if (subProperty != null) { - subPropertiesSet.add(subProperty); - } - return subPropertiesSet; - } - ); - }); - - return subPropertiesMap; - } - - /** - * Build a strict WQL query from the "dirty" one we have - * - * By strict we mean a syntax that can be executed by the WMI provider.
    - * By dirty we mean the extra sugar-coated syntax we're allowing in Metricshub products, - * like subproperties, and SELECT prop FROM ASSOCIATORS OF... - * - * Examples: - *
      - *
    • SELECT PropA.Name FROM Win32_Class
      - * => SELECT PropA FROM Win32_Class - *
    • SELECT Temperature FROM ASSOCIATORS OF { Win32_Class.Id=1 }
      - * => ASSOCIATORS OF { Win32_Class.Id=1 } - *
    - * @param associatorsFragment The extracted ASSOCIATORS OF... fragment - * @param subPropertiesMap The map built with {@link WqlQuery#buildSupPropertiesMap(String[])} - * @param classFragment The extracted class fragment - * @param restFragment The rest (WHERE...) - * @return a clean and strict WQL statement - */ - static String buildCleanWql( - String associatorsFragment, - Map> subPropertiesMap, - String classFragment, - String restFragment - ) { - String cleanWql; - - if (associatorsFragment == null) { - if (subPropertiesMap.keySet().isEmpty()) { - cleanWql = "SELECT * FROM " + classFragment; - } else { - cleanWql = - String.format( - "SELECT %s FROM %s", - subPropertiesMap.keySet().stream().collect(Collectors.joining(",")), - classFragment - ); - } - } else { - cleanWql = associatorsFragment; - } - if (restFragment != null) { - cleanWql = cleanWql + restFragment; - } - return cleanWql; - } - - public List getSelectedProperties() { - return selectedProperties; - } - - public Map> getSubPropertiesMap() { - return subPropertiesMap; - } - - public String getCleanWql() { - return cleanWql; - } - - @Override - public String toString() { - return wql; - } -} +package org.metricshub.winrm; + +/*- + * ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲ + * 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.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import org.metricshub.winrm.exceptions.WqlQuerySyntaxException; + +public class WqlQuery { + + /** + * Pattern to parse a WQL query + *
      + *
    • group(1) = SELECT ... FROM + *
    • group(2) = Properties in the SELECT statement (but not '*') + *
    • group(3) = ASSOCIATORS OF { object ID } + *
    • group(4) = class in the FROM statement + *
    • group(5) = Rest of the WQL statement (WHERE, etc.) + */ + private static final Pattern WQL_PATTERN = Pattern.compile( + "^\\s*(SELECT\\s+(?:\\*|([a-z0-9._]+(?:\\s*,\\s*[a-z0-9._]+)*))\\s+FROM\\s+)?(?:((?:ASSOCIATORS|REFERENCES)\\s+OF\\s+\\{.*\\})|([a-z0-9_]+))(\\s+WHERE\\s*+.+)?\\s*$", + Pattern.CASE_INSENSITIVE | Pattern.DOTALL + ); + + private String wql; + private List selectedProperties; + private Map> subPropertiesMap; + private String cleanWql; + + private WqlQuery( + String wql, + List selectedProperties, + Map> subPropertiesMap, + String cleanWql + ) { + this.wql = wql; + this.selectedProperties = selectedProperties; + this.subPropertiesMap = subPropertiesMap; + this.cleanWql = cleanWql; + } + + /** + * Parses the specified WQL query and returns a new instance of WqlQuery + * Supported WQL syntaxes: + *
        + *
      • SELECT * FROM Class + *
      • SELECT PropA, PropB FROM Class + *
      • SELECT PropA, PropB FROM Class WHERE condition + *
      • ASSOCIATORS OF { objectId } + *
      • ASSOCIATORS OF { objectId } WHERE condition + *
      • SELECT * FROM ASSOCIATORS OF { objectId } WHERE condition + *
      • SELECT PropA, PropB FROM ASSOCIATORS OF { objectId } WHERE condition + *
      + * + * @param wql The WQL query to parse + * @return a new WqlQuery instance + * @throws WqlQuerySyntaxException when the specified WQL is invalid and cannot be parsed + * @throws IllegalArgumentException if wql is null + */ + public static WqlQuery newInstance(CharSequence wql) throws WqlQuerySyntaxException { + Utils.checkNonNull(wql, "wql"); + + Matcher wqlMatcher = WQL_PATTERN.matcher(wql); + + // No match: invalid WQL + if (!wqlMatcher.find()) { + throw new WqlQuerySyntaxException(wql.toString()); + } + + // Extract the different fragments of the parsed WQL + String selectFragment = wqlMatcher.group(1); + String propertiesFragment = wqlMatcher.group(2); + String associatorsFragment = wqlMatcher.group(3); + String classFragment = wqlMatcher.group(4); + String restFragment = wqlMatcher.group(5); + + // If there is no `SELECT` and no `ASSOCIATORS OF`, it's no valid WQL + if (selectFragment == null && associatorsFragment == null) { + throw new WqlQuerySyntaxException(wql.toString()); + } + + List properties = buildSelectedProperties(propertiesFragment); + Map> subPropertiesMap = buildSupPropertiesMap(properties); + String cleanWql = buildCleanWql(associatorsFragment, subPropertiesMap, classFragment, restFragment); + + return new WqlQuery(wql.toString(), properties, subPropertiesMap, cleanWql); + } + + /** + * Note: All properties are converted to lower case + * + * @param propertiesFragment Comma-separated list of properties + * @return a cleaned-up array of the properties + */ + static List buildSelectedProperties(String propertiesFragment) { + if (Utils.isNotBlank(propertiesFragment)) { + return Arrays.asList(propertiesFragment.trim().toLowerCase().split("\\s*,\\s*")); + } + return new ArrayList<>(); + } + + /** + * Build a Map of subproperties to retrieve inside properties + * Example: + * Input: + * PropA, PropB.Sub1, PropB.Sub2 + * Output: + *
        + *
      • PropA => emptySet() + *
      • PropB => { "Sub1", "Sub2" } + *
      + * + * @param properties Selected properties (that may include subproperties) + * @return The map as described above + */ + static Map> buildSupPropertiesMap(final List properties) { + // Empty or null? + if (properties == null || properties.isEmpty()) { + return new HashMap<>(); + } + + Map> subPropertiesMap = new LinkedHashMap<>(); + properties + .stream() + .filter(Utils::isNotBlank) + .forEachOrdered(property -> { + // Split the property into fragments: + // propA => ["propA"] + // propA.subProp => ["propA", "subProp"] + String[] propertyFragmentArray = property.toLowerCase().split("\\.", 2); + String mainProperty = propertyFragmentArray[0]; + String subProperty = propertyFragmentArray.length == 2 ? propertyFragmentArray[1] : null; + + // Add this entry to the map + subPropertiesMap.compute( + mainProperty, + (key, subPropertiesSet) -> { + if (subPropertiesSet == null) { + subPropertiesSet = new HashSet<>(); + } + if (subProperty != null) { + subPropertiesSet.add(subProperty); + } + return subPropertiesSet; + } + ); + }); + + return subPropertiesMap; + } + + /** + * Build a strict WQL query from the "dirty" one we have + * By strict we mean a syntax that can be executed by the WMI provider.
      + * By dirty we mean the extra sugar-coated syntax we're allowing in Metricshub products, + * like subproperties, and SELECT prop FROM ASSOCIATORS OF... + * Examples: + *
        + *
      • SELECT PropA.Name FROM Win32_Class
        + * => SELECT PropA FROM Win32_Class + *
      • SELECT Temperature FROM ASSOCIATORS OF { Win32_Class.Id=1 }
        + * => ASSOCIATORS OF { Win32_Class.Id=1 } + *
      + * + * @param associatorsFragment The extracted ASSOCIATORS OF... fragment + * @param subPropertiesMap The map built with {@link WqlQuery#buildSupPropertiesMap(String[])} + * @param classFragment The extracted class fragment + * @param restFragment The rest (WHERE...) + * @return a clean and strict WQL statement + */ + static String buildCleanWql( + String associatorsFragment, + Map> subPropertiesMap, + String classFragment, + String restFragment + ) { + String cleanWql; + + if (associatorsFragment == null) { + if (subPropertiesMap.keySet().isEmpty()) { + cleanWql = "SELECT * FROM " + classFragment; + } else { + cleanWql = String.format( + "SELECT %s FROM %s", + subPropertiesMap.keySet().stream().collect(Collectors.joining(",")), + classFragment + ); + } + } else { + cleanWql = associatorsFragment; + } + if (restFragment != null) { + cleanWql = cleanWql + restFragment; + } + return cleanWql; + } + + public List getSelectedProperties() { + return selectedProperties; + } + + public Map> getSubPropertiesMap() { + return subPropertiesMap; + } + + public String getCleanWql() { + return cleanWql; + } + + @Override + public String toString() { + return wql; + } +} diff --git a/src/main/java/org/metricshub/winrm/command/WinRMCommandExecutor.java b/src/main/java/org/metricshub/winrm/command/WinRMCommandExecutor.java index 25deae4..a7f8585 100644 --- a/src/main/java/org/metricshub/winrm/command/WinRMCommandExecutor.java +++ b/src/main/java/org/metricshub/winrm/command/WinRMCommandExecutor.java @@ -1,165 +1,156 @@ -package org.metricshub.winrm.command; - -/*- - * ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲ - * 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.IOException; -import java.nio.charset.Charset; -import java.nio.file.Path; -import java.util.List; -import java.util.concurrent.TimeoutException; -import java.util.stream.Collectors; -import org.metricshub.winrm.TimeoutHelper; -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.WinRMExecutorFactory; -import org.metricshub.winrm.service.client.auth.AuthenticationEnum; -import org.metricshub.winrm.shares.SmbTempShare; - -public class WinRMCommandExecutor { - - private WinRMCommandExecutor() {} - - /** - * Execute a command on a remote Windows system and return an object with - * the output of the command. - * - * You can specify local files to be copied to the remote system before executing the command. - * If the command contains references to these local files, it will be updated to reference the - * path on the remote system where the files have been copied. - * - * Example: - * - * - * WinRemoteCommandExecutor.execute( - * "CSCRIPT c:\\MyScript.vbs", null, "remote-srv", null, null, null, 30000, Arrays.asList("c:\\MyScript.vbs"), false); - * - * - * This will copy c:\\MyScript.vbs to remote-srv, typically in - * C:\\Windows\\Temp\\SEN_ShareFor_MYHOST and the command that is executed will therefore - * become: - * - * CSCRIPT "C:\\Windows\\Temp\\SEN_ShareFor_MYHOST\\MyScript.vbs" - * - * @param command The command to execute. (Mandatory) - * @param protocol The HTTP protocol (HTTP by default) - * @param hostname Host to connect to. (Mandatory) - * @param port The port (5985 for HTPP or 5986 for HTTPS by default) - * @param username The username name. (Mandatory) - * @param password The password. - * @param workingDirectory Path of the directory for the spawned process on the remote system (can be null) - * @param timeout The timeout in milliseconds (throws an IllegalArgumentException if negative or zero) - * @param localFileToCopyList List of local files to copy to the remote before the execution - * @param ticketCache The Ticket Cache path - * @param authentications List of authentications. only NTLM if absent - * - * @return an instance of WindowsRemoteCommandResult with the result of the command - * - * @throws IOException If an I/O error occurs. - * @throws TimeoutException To notify userName of timeout - * @throws WindowsRemoteException For any problem encountered on remote - */ - public static WindowsRemoteCommandResult execute( - final String command, - final WinRMHttpProtocolEnum protocol, - final String hostname, - final Integer port, - final String username, - final char[] password, - final String workingDirectory, - final long timeout, - final List localFileToCopyList, - final Path ticketCache, - final List authentications - ) throws IOException, TimeoutException, WindowsRemoteException { - Utils.checkNonNull(command, "command"); - Utils.checkArgumentNotZeroOrNegative(timeout, "timeout"); - - final long start = System.currentTimeMillis(); - - final WinRMEndpoint winRMEndpoint = new WinRMEndpoint(protocol, hostname, port, username, password, null); - - if (localFileToCopyList == null || localFileToCopyList.isEmpty()) { - try ( - final WindowsRemoteExecutor winRMService = WinRMExecutorFactory.createInstance( - winRMEndpoint, - timeout, - ticketCache, - authentications - ) - ) { - final Charset charset = WindowsRemoteProcessUtils.getWindowsEncodingCharset( - winRMService, - TimeoutHelper.getRemainingTime(timeout, start, "No time left to retrieve the code set") - ); - - return winRMService.executeCommand(command, workingDirectory, charset, timeout); - } catch (final WqlQuerySyntaxException e) { - throw new IOException(e); - } - } - - try ( - final SmbTempShare smbTempShare = SmbTempShare.createInstance( - winRMEndpoint, - timeout, - ticketCache, - authentications - ) - ) { - smbTempShare.checkConnectedFirst(); - - final List localFiles = localFileToCopyList - .stream() - .filter(Utils::isNotBlank) - .collect(Collectors.toList()); - - // Copy the list specified list of files, and update the command accordingly - final String localFilesUpdatedCommand = WindowsRemoteProcessUtils.copyLocalFilesToShare( - command, - localFiles, - smbTempShare.getUncSharePath(), - smbTempShare.getRemotePath() - ); - - final Charset charset = WindowsRemoteProcessUtils.getWindowsEncodingCharset( - smbTempShare.getWindowsRemoteExecutor(), - TimeoutHelper.getRemainingTime(timeout, start, "No time left to retrieve the code set") - ); - - return smbTempShare - .getWindowsRemoteExecutor() - .executeCommand( - String.format("CMD.EXE /C (%s)", localFilesUpdatedCommand), - null, - charset, - TimeoutHelper.getRemainingTime(timeout, start, "No time left to execute command") - ); - } catch (final WqlQuerySyntaxException e) { - throw new IOException(e); - } - } -} +package org.metricshub.winrm.command; + +/*- + * ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲ + * WinRM Java Client + * ჻჻჻჻჻჻ + * Copyright (C) 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.IOException; +import java.nio.charset.Charset; +import java.nio.file.Path; +import java.util.List; +import java.util.concurrent.TimeoutException; +import java.util.stream.Collectors; +import org.metricshub.winrm.TimeoutHelper; +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.WinRMExecutorFactory; +import org.metricshub.winrm.service.client.auth.AuthenticationEnum; +import org.metricshub.winrm.shares.SmbTempShare; + +public class WinRMCommandExecutor { + + private WinRMCommandExecutor() {} + + /** + * Execute a command on a remote Windows system and return an object with + * the output of the command. + * You can specify local files to be copied to the remote system before executing the command. + * If the command contains references to these local files, it will be updated to reference the + * path on the remote system where the files have been copied. + * Example: + * + * WinRemoteCommandExecutor.execute( + * "CSCRIPT c:\\MyScript.vbs", null, "remote-srv", null, null, null, 30000, Arrays.asList("c:\\MyScript.vbs"), false); + * + * This will copy c:\\MyScript.vbs to remote-srv, typically in + * C:\\Windows\\Temp\\SEN_ShareFor_MYHOST and the command that is executed will therefore + * become: + * CSCRIPT "C:\\Windows\\Temp\\SEN_ShareFor_MYHOST\\MyScript.vbs" + * + * @param command The command to execute. (Mandatory) + * @param protocol The HTTP protocol (HTTP by default) + * @param hostname Host to connect to. (Mandatory) + * @param port The port (5985 for HTPP or 5986 for HTTPS by default) + * @param username The username name. (Mandatory) + * @param password The password. + * @param workingDirectory Path of the directory for the spawned process on the remote system (can be null) + * @param timeout The timeout in milliseconds (throws an IllegalArgumentException if negative or zero) + * @param localFileToCopyList List of local files to copy to the remote before the execution + * @param ticketCache The Ticket Cache path + * @param authentications List of authentications. only NTLM if absent + * @return an instance of WindowsRemoteCommandResult with the result of the command + * @throws IOException If an I/O error occurs. + * @throws TimeoutException To notify userName of timeout + * @throws WindowsRemoteException For any problem encountered on remote + */ + public static WindowsRemoteCommandResult execute( + final String command, + final WinRMHttpProtocolEnum protocol, + final String hostname, + final Integer port, + final String username, + final char[] password, + final String workingDirectory, + final long timeout, + final List localFileToCopyList, + final Path ticketCache, + final List authentications + ) throws IOException, TimeoutException, WindowsRemoteException { + Utils.checkNonNull(command, "command"); + Utils.checkArgumentNotZeroOrNegative(timeout, "timeout"); + + final long start = System.currentTimeMillis(); + + final WinRMEndpoint winRMEndpoint = new WinRMEndpoint(protocol, hostname, port, username, password, null); + + if (localFileToCopyList == null || localFileToCopyList.isEmpty()) { + try ( + final WindowsRemoteExecutor winRMService = WinRMExecutorFactory.createInstance( + winRMEndpoint, + timeout, + ticketCache, + authentications + )) { + final Charset charset = WindowsRemoteProcessUtils.getWindowsEncodingCharset( + winRMService, + TimeoutHelper.getRemainingTime(timeout, start, "No time left to retrieve the code set") + ); + + return winRMService.executeCommand(command, workingDirectory, charset, timeout); + } catch (final WqlQuerySyntaxException e) { + throw new IOException(e); + } + } + + try ( + final SmbTempShare smbTempShare = SmbTempShare.createInstance( + winRMEndpoint, + timeout, + ticketCache, + authentications + )) { + smbTempShare.checkConnectedFirst(); + + final List localFiles = localFileToCopyList + .stream() + .filter(Utils::isNotBlank) + .collect(Collectors.toList()); + + // Copy the list specified list of files, and update the command accordingly + final String localFilesUpdatedCommand = WindowsRemoteProcessUtils.copyLocalFilesToShare( + command, + localFiles, + smbTempShare.getUncSharePath(), + smbTempShare.getRemotePath() + ); + + final Charset charset = WindowsRemoteProcessUtils.getWindowsEncodingCharset( + smbTempShare.getWindowsRemoteExecutor(), + TimeoutHelper.getRemainingTime(timeout, start, "No time left to retrieve the code set") + ); + + return smbTempShare + .getWindowsRemoteExecutor() + .executeCommand( + String.format("CMD.EXE /C (%s)", localFilesUpdatedCommand), + null, + charset, + TimeoutHelper.getRemainingTime(timeout, start, "No time left to execute command") + ); + } catch (final WqlQuerySyntaxException e) { + throw new IOException(e); + } + } +} diff --git a/src/main/java/org/metricshub/winrm/exceptions/WinRMException.java b/src/main/java/org/metricshub/winrm/exceptions/WinRMException.java index 72f5985..d3b32a3 100644 --- a/src/main/java/org/metricshub/winrm/exceptions/WinRMException.java +++ b/src/main/java/org/metricshub/winrm/exceptions/WinRMException.java @@ -1,42 +1,42 @@ -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 WinRMException extends WindowsRemoteException { - - private static final long serialVersionUID = 1L; - - public WinRMException(final String message) { - super(message); - } - - public WinRMException(final String messageFromat, final Object... args) { - super(messageFromat, args); - } - - public WinRMException(final Throwable cause, final String message) { - super(cause, message); - } - - public WinRMException(final Throwable cause) { - super(cause); - } -} +package org.metricshub.winrm.exceptions; + +/*- + * ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲ + * 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. + * ╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱ + */ + +public class WinRMException extends WindowsRemoteException { + + private static final long serialVersionUID = 1L; + + public WinRMException(final String message) { + super(message); + } + + public WinRMException(final String messageFromat, final Object... args) { + super(messageFromat, args); + } + + public WinRMException(final Throwable cause, final String message) { + super(cause, message); + } + + public WinRMException(final Throwable cause) { + super(cause); + } +} diff --git a/src/main/java/org/metricshub/winrm/exceptions/WindowsRemoteException.java b/src/main/java/org/metricshub/winrm/exceptions/WindowsRemoteException.java index a734176..36eedd2 100644 --- a/src/main/java/org/metricshub/winrm/exceptions/WindowsRemoteException.java +++ b/src/main/java/org/metricshub/winrm/exceptions/WindowsRemoteException.java @@ -1,46 +1,46 @@ -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 WindowsRemoteException extends Exception { - - private static final long serialVersionUID = 1L; - - public WindowsRemoteException(final String message) { - super(message); - } - - public WindowsRemoteException(final String messageFromat, final Object... args) { - super(String.format(messageFromat, args)); - } - - public WindowsRemoteException(final Throwable cause, final String message) { - super(message, cause); - } - - public WindowsRemoteException(final Throwable cause, final String messageFromat, final Object... args) { - super(String.format(messageFromat, args), cause); - } - - public WindowsRemoteException(final Throwable cause) { - super(cause); - } -} +package org.metricshub.winrm.exceptions; + +/*- + * ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲ + * 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. + * ╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱ + */ + +public class WindowsRemoteException extends Exception { + + private static final long serialVersionUID = 1L; + + public WindowsRemoteException(final String message) { + super(message); + } + + public WindowsRemoteException(final String messageFromat, final Object... args) { + super(String.format(messageFromat, args)); + } + + public WindowsRemoteException(final Throwable cause, final String message) { + super(message, cause); + } + + public WindowsRemoteException(final Throwable cause, final String messageFromat, final Object... args) { + super(String.format(messageFromat, args), cause); + } + + public WindowsRemoteException(final Throwable cause) { + super(cause); + } +} diff --git a/src/main/java/org/metricshub/winrm/exceptions/WqlQuerySyntaxException.java b/src/main/java/org/metricshub/winrm/exceptions/WqlQuerySyntaxException.java index 23d978a..eeca9d3 100644 --- a/src/main/java/org/metricshub/winrm/exceptions/WqlQuerySyntaxException.java +++ b/src/main/java/org/metricshub/winrm/exceptions/WqlQuerySyntaxException.java @@ -1,38 +1,37 @@ -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. - * ╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱ - */ - -/** - * Exception for a syntax error in the WQL Query parameter. - * - */ -public class WqlQuerySyntaxException extends Exception { - - private static final long serialVersionUID = 1L; - - public WqlQuerySyntaxException(final String wqlQuery) { - super("Syntax error in WQL Query: " + wqlQuery); - } - - public WqlQuerySyntaxException(final Throwable cause) { - super(cause); - } -} +package org.metricshub.winrm.exceptions; + +/*- + * ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲ + * 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. + * ╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱ + */ + +/** + * Exception for a syntax error in the WQL Query parameter. + */ +public class WqlQuerySyntaxException extends Exception { + + private static final long serialVersionUID = 1L; + + public WqlQuerySyntaxException(final String wqlQuery) { + super("Syntax error in WQL Query: " + wqlQuery); + } + + public WqlQuerySyntaxException(final Throwable cause) { + super(cause); + } +} diff --git a/src/main/java/org/metricshub/winrm/light/AuthScheme.java b/src/main/java/org/metricshub/winrm/light/AuthScheme.java index 6562785..105fae8 100644 --- a/src/main/java/org/metricshub/winrm/light/AuthScheme.java +++ b/src/main/java/org/metricshub/winrm/light/AuthScheme.java @@ -4,7 +4,7 @@ * ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲ * WinRM Java Client * ჻჻჻჻჻჻ - * Copyright 2023 - 2026 MetricsHub + * Copyright (C) 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. @@ -26,8 +26,8 @@ * 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 + *

      + * All methods are called while {@code WsmanClient} holds its operation lock, so implementations * need no internal synchronization. */ interface AuthScheme { diff --git a/src/main/java/org/metricshub/winrm/light/ByteArrayUtils.java b/src/main/java/org/metricshub/winrm/light/ByteArrayUtils.java index d468a14..54345a7 100644 --- a/src/main/java/org/metricshub/winrm/light/ByteArrayUtils.java +++ b/src/main/java/org/metricshub/winrm/light/ByteArrayUtils.java @@ -4,7 +4,7 @@ * ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲ * WinRM Java Client * ჻჻჻჻჻჻ - * Copyright 2023 - 2026 MetricsHub + * Copyright (C) 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. @@ -66,7 +66,8 @@ public static String formatHexDump(final byte[] array) { final byte c = array[rowOffset + index]; builder.append((c >= 20 && c < 127) ? (char) c : '.'); - if (index % 8 == 7) builder.append(" "); + if (index % 8 == 7) + builder.append(" "); } } } diff --git a/src/main/java/org/metricshub/winrm/light/CipherGen.java b/src/main/java/org/metricshub/winrm/light/CipherGen.java index 378670a..2bd0dc0 100644 --- a/src/main/java/org/metricshub/winrm/light/CipherGen.java +++ b/src/main/java/org/metricshub/winrm/light/CipherGen.java @@ -4,7 +4,7 @@ * ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲ * WinRM Java Client * ჻჻჻჻჻჻ - * Copyright 2023 - 2026 MetricsHub + * Copyright (C) 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. @@ -189,10 +189,9 @@ private byte[] getNTLMv2Blob() { * client challenge. * * @param targetInformation - * The target information block from the Type 2 message. + * The target information block from the Type 2 message. * @param clientChallenge - * The random 8-byte client challenge. - * + * The random 8-byte client challenge. * @return The blob, used in the calculation of the NTLMv2 Response. */ private static byte[] createBlob( @@ -205,12 +204,12 @@ private static byte[] createBlob( 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]; + 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; @@ -261,8 +260,8 @@ public byte[] getNTLM2SessionResponse() throws NtlmException { * @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. + * 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, @@ -289,10 +288,9 @@ private static byte[] ntlm2SessionResponse( * Creates the LM Response from the given hash and Type 2 challenge. * * @param hash - * The LM or NTLM Hash. + * The LM or NTLM Hash. * @param challenge - * The server challenge from the Type 2 message. - * + * 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 { @@ -323,10 +321,9 @@ private static byte[] lmResponse(final byte[] hash, final byte[] challenge) thro * Creates the LM Hash of the user's password. * * @param password - * The password. - * + * The password. * @return The LM Hash of the given password, used in the calculation of the - * LM Response. + * LM Response. */ private static byte[] lmHash(final String password) throws NtlmException { try { @@ -356,13 +353,12 @@ private static byte[] lmHash(final String password) throws NtlmException { * Creates a DES encryption key from the given key material. * * @param bytes - * A byte array containing the DES key material. + * 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. - * + * 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. + * the specified offset in the given byte array. */ private static Key createDESKey(final byte[] bytes, final int offset) { final byte[] keyBytes = new byte[7]; @@ -384,13 +380,13 @@ private static Key createDESKey(final byte[] bytes, final int offset) { * Applies odd parity to the given byte array. * * @param bytes - * The data whose parity bits are to be adjusted for odd parity. + * 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; + 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 { @@ -484,10 +480,9 @@ public byte[] getLanManagerSessionKey() throws NtlmException { * Creates the NTLM Hash of the user's password. * * @param password - * The 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. + * the NTLM Response and the NTLMv2 and LMv2 Hashes. */ private static byte[] ntlmHash(final String password) throws NtlmException { if (NTLMEngineUtils.UNICODE_LITTLE_UNMARKED == null) { @@ -503,7 +498,7 @@ private static byte[] ntlmHash(final String password) throws NtlmException { * Creates the LMv2 Hash of the user's password. * * @return The LMv2 Hash, used in the calculation of the NTLMv2 and LMv2 - * Responses. + * Responses. */ private static byte[] lmv2Hash(final String domain, final String user, final byte[] ntlmHash) throws NtlmException { if (NTLMEngineUtils.UNICODE_LITTLE_UNMARKED == null) { @@ -522,7 +517,7 @@ private static byte[] lmv2Hash(final String domain, final String user, final byt * Creates the NTLMv2 Hash of the user's password. * * @return The NTLMv2 Hash, used in the calculation of the NTLMv2 and LMv2 - * Responses. + * Responses. */ private static byte[] ntlmv2Hash(final String domain, final String user, final byte[] ntlmHash) throws NtlmException { if (NTLMEngineUtils.UNICODE_LITTLE_UNMARKED == null) { @@ -542,14 +537,13 @@ private static byte[] ntlmv2Hash(final String domain, final String user, final b * challenge. * * @param hash - * The NTLMv2 Hash. + * The NTLMv2 Hash. * @param clientData - * The client data (blob or client challenge). + * The client data (blob or client challenge). * @param challenge - * The server challenge from the Type 2 message. - * + * The server challenge from the Type 2 message. * @return The response (either NTLMv2 or LMv2, depending on the client - * data). + * data). */ private static byte[] lmv2Response(final byte[] hash, final byte[] challenge, final byte[] clientData) { final HMACMD5 hmacMD5 = new HMACMD5(hash); diff --git a/src/main/java/org/metricshub/winrm/light/EncryptionUtils.java b/src/main/java/org/metricshub/winrm/light/EncryptionUtils.java index 9108c14..315c070 100644 --- a/src/main/java/org/metricshub/winrm/light/EncryptionUtils.java +++ b/src/main/java/org/metricshub/winrm/light/EncryptionUtils.java @@ -4,7 +4,7 @@ * ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲ * WinRM Java Client * ჻჻჻჻჻჻ - * Copyright 2023 - 2026 MetricsHub + * Copyright (C) 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. diff --git a/src/main/java/org/metricshub/winrm/light/Envelopes.java b/src/main/java/org/metricshub/winrm/light/Envelopes.java index 001e924..00b3483 100644 --- a/src/main/java/org/metricshub/winrm/light/Envelopes.java +++ b/src/main/java/org/metricshub/winrm/light/Envelopes.java @@ -4,7 +4,7 @@ * ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲ * WinRM Java Client * ჻჻჻჻჻჻ - * Copyright 2023 - 2026 MetricsHub + * Copyright (C) 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. @@ -59,8 +59,7 @@ private Envelopes() {} // --- WQL --------------------------------------------------------------- static String enumerateWql(final String url, final String namespace, final String wql, final long timeoutMs) { - return ( - envelopeOpen(false) + + return (envelopeOpen(false) + header(url, wmiResourceUri(namespace), ACTION_ENUMERATE, timeoutMs, null, null) + "" + "" + @@ -68,88 +67,73 @@ static String enumerateWql(final String url, final String namespace, final Strin "" + escape(wql) + "" + - "" - ); + ""); } static String pull(final String url, final String namespace, final String context, final long timeoutMs) { - return ( - envelopeOpen(false) + + 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 = - "" + + final String optionSet = "" + "TRUE" + "437" + ""; final String workingDir = (workingDirectory == null || workingDirectory.trim().isEmpty()) ? "" : "" + escape(workingDirectory) + ""; - return ( - envelopeOpen(true) + + 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 = - "" + + final String optionSet = "" + "TRUE" + "FALSE" + ""; - return ( - envelopeOpen(true) + + 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) + + return (envelopeOpen(true) + header(url, SHELL_RESOURCE_URI, ACTION_RECEIVE, timeoutMs, shellSelector(shellId), null) + "stdout stderr" - ); + "\">stdout stderr"); } static String signal(final String url, final String shellId, final String commandId, final long timeoutMs) { - return ( - envelopeOpen(true) + + 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) + + return (envelopeOpen(true) + header(url, SHELL_RESOURCE_URI, ACTION_DELETE, timeoutMs, shellSelector(shellId), null) + - "" - ); + ""); } // --- helpers ----------------------------------------------------------- @@ -159,9 +143,8 @@ private static String wmiResourceUri(final String namespace) { } private static String shellSelector(final String shellId) { - return ( - "" + escape(shellId) + "" - ); + return ("" + escape(shellId) + + ""); } /** @@ -182,8 +165,7 @@ private static String header( final String selectorSet, final String optionSet ) { - return ( - "" + + return ("" + "" + url + "" + @@ -208,13 +190,11 @@ private static String header( "" + operationTimeout(timeoutMs) + "" + - "" - ); + ""); } private static String envelopeOpen(final boolean shell) { - return ( - "" - ); + ">"); } private static String escape(final String s) { diff --git a/src/main/java/org/metricshub/winrm/light/FallbackAuthScheme.java b/src/main/java/org/metricshub/winrm/light/FallbackAuthScheme.java index efe7039..ac3b7e0 100644 --- a/src/main/java/org/metricshub/winrm/light/FallbackAuthScheme.java +++ b/src/main/java/org/metricshub/winrm/light/FallbackAuthScheme.java @@ -4,7 +4,7 @@ * ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲ * WinRM Java Client * ჻჻჻჻჻჻ - * Copyright 2023 - 2026 MetricsHub + * Copyright (C) 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. @@ -26,8 +26,8 @@ * 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, + *

      + * 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 { @@ -70,7 +70,7 @@ public String authenticate(final HttpTransport transport) throws Exception { } throw new IllegalStateException( "All requested authentication schemes failed" + - (lastFailure == null ? "" : " (last: " + lastFailure.getMessage() + ")"), + (lastFailure == null ? "" : " (last: " + lastFailure.getMessage() + ")"), lastFailure ); } diff --git a/src/main/java/org/metricshub/winrm/light/HMACMD5.java b/src/main/java/org/metricshub/winrm/light/HMACMD5.java index 319dbf4..1722299 100644 --- a/src/main/java/org/metricshub/winrm/light/HMACMD5.java +++ b/src/main/java/org/metricshub/winrm/light/HMACMD5.java @@ -4,7 +4,7 @@ * ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲ * WinRM Java Client * ჻჻჻჻჻჻ - * Copyright 2023 - 2026 MetricsHub + * Copyright (C) 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. @@ -25,7 +25,6 @@ /** * 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 diff --git a/src/main/java/org/metricshub/winrm/light/HttpTransport.java b/src/main/java/org/metricshub/winrm/light/HttpTransport.java index 8cfd8d4..580dc98 100644 --- a/src/main/java/org/metricshub/winrm/light/HttpTransport.java +++ b/src/main/java/org/metricshub/winrm/light/HttpTransport.java @@ -4,7 +4,7 @@ * ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲ * WinRM Java Client * ჻჻჻჻჻჻ - * Copyright 2023 - 2026 MetricsHub + * Copyright (C) 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. diff --git a/src/main/java/org/metricshub/winrm/light/KerberosAuthScheme.java b/src/main/java/org/metricshub/winrm/light/KerberosAuthScheme.java index ac3f30f..06de531 100644 --- a/src/main/java/org/metricshub/winrm/light/KerberosAuthScheme.java +++ b/src/main/java/org/metricshub/winrm/light/KerberosAuthScheme.java @@ -4,7 +4,7 @@ * ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲ * WinRM Java Client * ჻჻჻჻჻჻ - * Copyright 2023 - 2026 MetricsHub + * Copyright (C) 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. @@ -45,11 +45,11 @@ * 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 + *

      + * 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 + *

      + * 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. */ @@ -69,11 +69,11 @@ final class KerberosAuthScheme implements AuthScheme { /** * @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 + * 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, @@ -182,11 +182,11 @@ public AppConfigurationEntry[] getAppConfigurationEntry(final String name) { options.put("doNotPrompt", "false"); } return new AppConfigurationEntry[] { - new AppConfigurationEntry( - "com.sun.security.auth.module.Krb5LoginModule", - AppConfigurationEntry.LoginModuleControlFlag.REQUIRED, - options - ) + 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 index a42413e..5315687 100644 --- a/src/main/java/org/metricshub/winrm/light/LightTls.java +++ b/src/main/java/org/metricshub/winrm/light/LightTls.java @@ -4,7 +4,7 @@ * ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲ * WinRM Java Client * ჻჻჻჻჻჻ - * Copyright 2023 - 2026 MetricsHub + * Copyright (C) 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. @@ -29,8 +29,8 @@ /** * TLS setup for the light backend's HTTPS transport. - * - *

      Unlike the legacy CXF path (which trusts every certificate), the light backend validates by + *

      + * 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 — diff --git a/src/main/java/org/metricshub/winrm/light/LightWinRMService.java b/src/main/java/org/metricshub/winrm/light/LightWinRMService.java index 7ced0fb..458f12c 100644 --- a/src/main/java/org/metricshub/winrm/light/LightWinRMService.java +++ b/src/main/java/org/metricshub/winrm/light/LightWinRMService.java @@ -4,7 +4,7 @@ * ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲ * WinRM Java Client * ჻჻჻჻჻჻ - * Copyright 2023 - 2026 MetricsHub + * Copyright (C) 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. @@ -45,8 +45,8 @@ * 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, + *

      + * 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. @@ -65,12 +65,12 @@ private LightWinRMService(final WinRMEndpoint winRMEndpoint, final WsmanClient c /** * 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 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 + * {@code null}/empty means NTLM only * @return a new {@code LightWinRMService} * @throws WinRMException on invalid arguments or an unsupported authentication request */ @@ -148,8 +148,8 @@ private static AuthScheme resolveAuthScheme( // 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." + 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); diff --git a/src/main/java/org/metricshub/winrm/light/MD4.java b/src/main/java/org/metricshub/winrm/light/MD4.java index e323247..a1dd746 100644 --- a/src/main/java/org/metricshub/winrm/light/MD4.java +++ b/src/main/java/org/metricshub/winrm/light/MD4.java @@ -4,7 +4,7 @@ * ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲ * WinRM Java Client * ჻჻჻჻჻჻ - * Copyright 2023 - 2026 MetricsHub + * Copyright (C) 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. @@ -26,7 +26,6 @@ * 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 @@ -106,8 +105,7 @@ private void processBuffer() { final int[] d = new int[16]; for (int i = 0; i < 16; i++) { - d[i] = - (dataBuffer[i * 4] & 0xff) + + d[i] = (dataBuffer[i * 4] & 0xff) + ((dataBuffer[i * 4 + 1] & 0xff) << 8) + ((dataBuffer[i * 4 + 2] & 0xff) << 16) + ((dataBuffer[i * 4 + 3] & 0xff) << 24); diff --git a/src/main/java/org/metricshub/winrm/light/NTLMEngineUtils.java b/src/main/java/org/metricshub/winrm/light/NTLMEngineUtils.java index 3eb5b5e..3d8d8da 100644 --- a/src/main/java/org/metricshub/winrm/light/NTLMEngineUtils.java +++ b/src/main/java/org/metricshub/winrm/light/NTLMEngineUtils.java @@ -4,7 +4,7 @@ * ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲ * WinRM Java Client * ჻჻჻჻჻჻ - * Copyright 2023 - 2026 MetricsHub + * Copyright (C) 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. @@ -62,13 +62,18 @@ static String convertDomain(final String domain) { // 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_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_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_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 @@ -88,6 +93,7 @@ static String convertDomain(final String domain) { /** * Find the character set based on the flags. + * * @param flags is the flags. * @return the character set. */ diff --git a/src/main/java/org/metricshub/winrm/light/NTLMMessage.java b/src/main/java/org/metricshub/winrm/light/NTLMMessage.java index 10bdd54..b96f426 100644 --- a/src/main/java/org/metricshub/winrm/light/NTLMMessage.java +++ b/src/main/java/org/metricshub/winrm/light/NTLMMessage.java @@ -4,7 +4,7 @@ * ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲ * WinRM Java Client * ჻჻჻჻჻჻ - * Copyright 2023 - 2026 MetricsHub + * Copyright (C) 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. @@ -22,7 +22,6 @@ /** * 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 @@ -83,21 +82,22 @@ 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) - ); + 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). + * 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]; @@ -110,7 +110,7 @@ void prepareResponse(final int maxlength, final int messageType) { * Adds the given byte to the response. * * @param b - * the byte to add. + * the byte to add. */ private void addByte(final byte b) { messageContents[currentOutputPosition] = b; @@ -121,7 +121,7 @@ private void addByte(final byte b) { * Adds the given bytes to the response. * * @param bytes - * the bytes to add. + * the bytes to add. */ void addBytes(final byte[] bytes) { if (bytes == null) { diff --git a/src/main/java/org/metricshub/winrm/light/NtlmAuthScheme.java b/src/main/java/org/metricshub/winrm/light/NtlmAuthScheme.java index a81ca80..1983677 100644 --- a/src/main/java/org/metricshub/winrm/light/NtlmAuthScheme.java +++ b/src/main/java/org/metricshub/winrm/light/NtlmAuthScheme.java @@ -4,7 +4,7 @@ * ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲ * WinRM Java Client * ჻჻჻჻჻჻ - * Copyright 2023 - 2026 MetricsHub + * Copyright (C) 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. @@ -32,9 +32,12 @@ 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 | + 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 diff --git a/src/main/java/org/metricshub/winrm/light/NtlmCrypto.java b/src/main/java/org/metricshub/winrm/light/NtlmCrypto.java index f32c5ee..ba83041 100644 --- a/src/main/java/org/metricshub/winrm/light/NtlmCrypto.java +++ b/src/main/java/org/metricshub/winrm/light/NtlmCrypto.java @@ -4,7 +4,7 @@ * ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲ * WinRM Java Client * ჻჻჻჻჻჻ - * Copyright 2023 - 2026 MetricsHub + * Copyright (C) 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. @@ -33,8 +33,7 @@ */ final class NtlmCrypto { - static final String ENCRYPTED_CONTENT_TYPE = - "multipart/encrypted;protocol=\"application/HTTP-SPNEGO-session-encrypted\";boundary=\"Encrypted Boundary\""; + 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"; @@ -94,9 +93,9 @@ private static void verify(final WinRMSession session, final byte[] unsealed, fi if (!Arrays.equals(checksum, expectedChecksum)) { throw new IllegalStateException( "Checksum mismatch\n" + - ByteArrayUtils.formatHexDump(checksum) + - "--\n" + - ByteArrayUtils.formatHexDump(expectedChecksum) + ByteArrayUtils.formatHexDump(checksum) + + "--\n" + + ByteArrayUtils.formatHexDump(expectedChecksum) ); } if (expectedSeqNum != seqNum) { @@ -107,7 +106,7 @@ private static void verify(final WinRMSession session, final byte[] unsealed, fi /** * @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). + * false to verify an incoming one (server signing key + server sealing stream). */ private static void calculateSignature( final WinRMSession session, @@ -182,7 +181,7 @@ void skipOver(final String s) { void skipUntil(final String s) { final byte[] expected = s.getBytes(StandardCharsets.US_ASCII); int next = index; - outer:while (true) { + 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"); diff --git a/src/main/java/org/metricshub/winrm/light/NtlmException.java b/src/main/java/org/metricshub/winrm/light/NtlmException.java index 5b15d10..e00cb04 100644 --- a/src/main/java/org/metricshub/winrm/light/NtlmException.java +++ b/src/main/java/org/metricshub/winrm/light/NtlmException.java @@ -4,7 +4,7 @@ * ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲ * WinRM Java Client * ჻჻჻჻჻჻ - * Copyright 2023 - 2026 MetricsHub + * Copyright (C) 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. diff --git a/src/main/java/org/metricshub/winrm/light/Type1Message.java b/src/main/java/org/metricshub/winrm/light/Type1Message.java index cfa3115..e74f48d 100644 --- a/src/main/java/org/metricshub/winrm/light/Type1Message.java +++ b/src/main/java/org/metricshub/winrm/light/Type1Message.java @@ -4,7 +4,7 @@ * ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲ * WinRM Java Client * ჻჻჻჻჻჻ - * Copyright 2023 - 2026 MetricsHub + * Copyright (C) 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. @@ -24,7 +24,6 @@ /** * 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 @@ -45,22 +44,26 @@ class Type1Message extends NTLMMessage { 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; + 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 - ); + 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); } /** diff --git a/src/main/java/org/metricshub/winrm/light/Type2Message.java b/src/main/java/org/metricshub/winrm/light/Type2Message.java index 904f8d1..5f069c3 100644 --- a/src/main/java/org/metricshub/winrm/light/Type2Message.java +++ b/src/main/java/org/metricshub/winrm/light/Type2Message.java @@ -4,7 +4,7 @@ * ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲ * WinRM Java Client * ჻჻჻჻჻჻ - * Copyright 2023 - 2026 MetricsHub + * Copyright (C) 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. @@ -22,7 +22,6 @@ /** * 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 diff --git a/src/main/java/org/metricshub/winrm/light/Type3Message.java b/src/main/java/org/metricshub/winrm/light/Type3Message.java index bcaca9c..0af4499 100644 --- a/src/main/java/org/metricshub/winrm/light/Type3Message.java +++ b/src/main/java/org/metricshub/winrm/light/Type3Message.java @@ -4,7 +4,7 @@ * ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲ * WinRM Java Client * ჻჻჻჻჻჻ - * Copyright 2023 - 2026 MetricsHub + * Copyright (C) 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. @@ -26,7 +26,6 @@ /** * 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 @@ -56,7 +55,8 @@ public class Type3Message extends NTLMMessage { private final byte[] sessionKey; private final byte[] exportedSessionKey; - /** More primitive constructor: don't include cert or previous messages. + /** + * More primitive constructor: don't include cert or previous messages. */ Type3Message( final String domain, @@ -85,7 +85,7 @@ public class Type3Message extends NTLMMessage { byte[] responseTargetInformation = targetInformation; - // Create a cipher generator class. Use domain BEFORE it gets modified! + // Create a cipher generator class. Use domain BEFORE it gets modified! final CipherGen gen = new CipherGen( random, currentTime, @@ -103,9 +103,9 @@ public class Type3Message extends NTLMMessage { 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 - ) { + if (((type2Flags & NTLMEngineUtils.FLAG_TARGETINFO_PRESENT) != 0) + && targetInformation != null + && target != null) { // NTLMv2 ntResp = gen.getNTLMv2Response(); lmResp = gen.getLMv2Response(); @@ -250,31 +250,26 @@ protected void buildMessage() { // 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) - */ + * //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 ); diff --git a/src/main/java/org/metricshub/winrm/light/WinRMSession.java b/src/main/java/org/metricshub/winrm/light/WinRMSession.java index 6f61260..ab165a7 100644 --- a/src/main/java/org/metricshub/winrm/light/WinRMSession.java +++ b/src/main/java/org/metricshub/winrm/light/WinRMSession.java @@ -4,7 +4,7 @@ * ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲ * WinRM Java Client * ჻჻჻჻჻჻ - * Copyright 2023 - 2026 MetricsHub + * Copyright (C) 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. @@ -35,14 +35,14 @@ 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 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; diff --git a/src/main/java/org/metricshub/winrm/light/WsmanClient.java b/src/main/java/org/metricshub/winrm/light/WsmanClient.java index 3a8b399..587584a 100644 --- a/src/main/java/org/metricshub/winrm/light/WsmanClient.java +++ b/src/main/java/org/metricshub/winrm/light/WsmanClient.java @@ -4,7 +4,7 @@ * ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲ * WinRM Java Client * ჻჻჻჻჻჻ - * Copyright 2023 - 2026 MetricsHub + * Copyright (C) 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. @@ -350,10 +350,9 @@ private static String text(final Document doc, final String localName) { * 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 - ); + 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. */ diff --git a/src/main/java/org/metricshub/winrm/service/WinRMEndpoint.java b/src/main/java/org/metricshub/winrm/service/WinRMEndpoint.java index 81aa82f..e4d9b55 100644 --- a/src/main/java/org/metricshub/winrm/service/WinRMEndpoint.java +++ b/src/main/java/org/metricshub/winrm/service/WinRMEndpoint.java @@ -1,248 +1,247 @@ -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 java.net.MalformedURLException; -import java.net.URL; -import java.util.Arrays; -import java.util.Objects; -import org.metricshub.winrm.Utils; -import org.metricshub.winrm.WinRMHttpProtocolEnum; -import org.metricshub.winrm.WmiHelper; - -public class WinRMEndpoint { - - private static final int DEFAULT_WIN_RM_HTTP_PORT = 5985; - private static final int DEFAULT_WIN_RM_HTTPS_PORT = 5986; - - private final String hostname; - private final String endpoint; - private final String domain; - private final String username; - private final char[] password; - private final String namespace; - private final String rawUsername; - private final WinRMHttpProtocolEnum protocol; - private final int port; - - /** - * Constructor of the endpoint and credentials for WinRM. - * - * @param protocol The protocol - * @param hostname The host name (mandatory) - * @param port The port - * @param username The user name (mandatory) - * @param password The password (mandatory) - * @param namespace The namespace default value: {@value WmiHelper#DEFAULT_NAMESPACE} - */ - public WinRMEndpoint( - final WinRMHttpProtocolEnum protocol, - final String hostname, - final Integer port, - final String username, - final char[] password, - final String namespace - ) { - Utils.checkNonNull(hostname, "hostname"); - Utils.checkNonNull(username, "username"); - Utils.checkNonNull(password, "password"); - - this.hostname = hostname.replaceAll("\\s", Utils.EMPTY); - this.password = password; - rawUsername = username; - - this.namespace = buildNamespace(namespace); - - final String user = username.replaceAll("\\s", Utils.EMPTY); - if (user.contains("\\")) { - final String[] array = user.split("\\\\"); - domain = array[0]; - this.username = array[1]; - } else { - domain = null; - this.username = user; - } - - 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); - } - - /** Get the hostname */ - public String getHostname() { - return hostname; - } - - /** Get the endpoint in the form of http(s)://host:port/wsman */ - public String getEndpoint() { - return endpoint; - } - - /** Get the domain from the raw username */ - public String getDomain() { - return domain; - } - - /** get the username as indicated in the constructor (could be in domain\\user form) */ - public String getRawUsername() { - return rawUsername; - } - - /** Get the username part of the raw username */ - public String getUsername() { - return username; - } - - /** Get the password */ - public char[] getPassword() { - return password; - } - - /** Get the namespace */ - public String getNamespace() { - return namespace; - } - - /** Get the protocol */ - 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. - * - * @param protocol The HTTP protocol - * @param hostname The host name - * @param port The port - * - * @return the endpoint URL in form of protocol://hostname:port (with protocol in HTTP or HTTPS). - */ - public static String buildEndpointUrl( - final WinRMHttpProtocolEnum protocol, - final String hostname, - final Integer port - ) { - final int endpointPort = getEndpointPort(protocol, port); - - return String.format("%s://%s:%d", protocol.toString(), hostname, endpointPort); - } - - /** - * Get the endpoint port: - *

        - *
      • The port if provided
      • - *
      • 5986 if the protocol is HTTPS
      • - *
      • 5985 otherwise
      • - *
      - * @param protocol The HTTP protocol - * @param port The port - * - * @return The endpoint port - */ - public static int getEndpointPort(final WinRMHttpProtocolEnum protocol, final Integer port) { - if (port != null) { - return port; - } - return protocol == WinRMHttpProtocolEnum.HTTPS ? DEFAULT_WIN_RM_HTTPS_PORT : DEFAULT_WIN_RM_HTTP_PORT; - } - - /** - * Get the namespace and replace '\' by '/' if necessary - * default: {@value WmiHelper#DEFAULT_NAMESPACE} if null - * @param namespace the provided namespace - * - * @return - */ - public static String buildNamespace(final String namespace) { - final String cleanNamespace = namespace != null ? namespace.replaceAll("\\s", Utils.EMPTY) : Utils.EMPTY; - - final String usedNamespace = Utils.isNotBlank(cleanNamespace) ? cleanNamespace : WmiHelper.DEFAULT_NAMESPACE; - - return usedNamespace.replace('\\', '/'); - } - - /** - * Build the WSMan URL endpoint. - * - * @param endpoint the endpoint in form of protocol://hostname:port (with protocol in HTTP or HTTPS). - * - * @return The external form of the endpoint URL - */ - private static String buildWSManEndpoint(final String endpoint) { - try { - return new URL(String.format("%s/wsman", endpoint)).toExternalForm(); - } catch (final MalformedURLException e) { - throw new IllegalArgumentException(String.format("endpoint %s is invalid.", endpoint), e); - } - } - - @Override - public int hashCode() { - final int prime = 31; - int result = 1; - result = prime * result + Arrays.hashCode(password); - result = prime * result + Objects.hash(endpoint, namespace, rawUsername); - return result; - } - - @Override - public boolean equals(final Object obj) { - if (this == obj) { - return true; - } - if (obj == null) { - return false; - } - if (!(obj instanceof WinRMEndpoint)) { - return false; - } - final WinRMEndpoint other = (WinRMEndpoint) obj; - return ( - Objects.equals(endpoint, other.endpoint) && - Objects.equals(namespace, other.namespace) && - Arrays.equals(password, other.password) && - Objects.equals(rawUsername, other.rawUsername) - ); - } - - @Override - public String toString() { - return new StringBuilder() - .append("WinRMEndpoint [") - .append("endpoint=") - .append(endpoint) - .append(", domain=") - .append(domain) - .append(", username=") - .append(username) - .append(", namespace=") - .append(namespace) - .append("]") - .toString(); - } -} +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.net.MalformedURLException; +import java.net.URL; +import java.util.Arrays; +import java.util.Objects; +import org.metricshub.winrm.Utils; +import org.metricshub.winrm.WinRMHttpProtocolEnum; +import org.metricshub.winrm.WmiHelper; + +public class WinRMEndpoint { + + private static final int DEFAULT_WIN_RM_HTTP_PORT = 5985; + private static final int DEFAULT_WIN_RM_HTTPS_PORT = 5986; + + private final String hostname; + private final String endpoint; + private final String domain; + private final String username; + private final char[] password; + private final String namespace; + private final String rawUsername; + private final WinRMHttpProtocolEnum protocol; + private final int port; + + /** + * Constructor of the endpoint and credentials for WinRM. + * + * @param protocol The protocol + * @param hostname The host name (mandatory) + * @param port The port + * @param username The user name (mandatory) + * @param password The password (mandatory) + * @param namespace The namespace default value: {@value WmiHelper#DEFAULT_NAMESPACE} + */ + public WinRMEndpoint( + final WinRMHttpProtocolEnum protocol, + final String hostname, + final Integer port, + final String username, + final char[] password, + final String namespace + ) { + Utils.checkNonNull(hostname, "hostname"); + Utils.checkNonNull(username, "username"); + Utils.checkNonNull(password, "password"); + + this.hostname = hostname.replaceAll("\\s", Utils.EMPTY); + this.password = password; + rawUsername = username; + + this.namespace = buildNamespace(namespace); + + final String user = username.replaceAll("\\s", Utils.EMPTY); + if (user.contains("\\")) { + final String[] array = user.split("\\\\"); + domain = array[0]; + this.username = array[1]; + } else { + domain = null; + this.username = user; + } + + 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); + } + + /** Get the hostname */ + public String getHostname() { + return hostname; + } + + /** Get the endpoint in the form of http(s)://host:port/wsman */ + public String getEndpoint() { + return endpoint; + } + + /** Get the domain from the raw username */ + public String getDomain() { + return domain; + } + + /** get the username as indicated in the constructor (could be in domain\\user form) */ + public String getRawUsername() { + return rawUsername; + } + + /** Get the username part of the raw username */ + public String getUsername() { + return username; + } + + /** Get the password */ + public char[] getPassword() { + return password; + } + + /** Get the namespace */ + public String getNamespace() { + return namespace; + } + + /** Get the protocol */ + 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. + * + * @param protocol The HTTP protocol + * @param hostname The host name + * @param port The port + * @return the endpoint URL in form of protocol://hostname:port (with protocol in HTTP or HTTPS). + */ + public static String buildEndpointUrl( + final WinRMHttpProtocolEnum protocol, + final String hostname, + final Integer port + ) { + final int endpointPort = getEndpointPort(protocol, port); + + return String.format("%s://%s:%d", protocol.toString(), hostname, endpointPort); + } + + /** + * Get the endpoint port: + *
        + *
      • The port if provided
      • + *
      • 5986 if the protocol is HTTPS
      • + *
      • 5985 otherwise
      • + *
      + * + * @param protocol The HTTP protocol + * @param port The port + * @return The endpoint port + */ + public static int getEndpointPort(final WinRMHttpProtocolEnum protocol, final Integer port) { + if (port != null) { + return port; + } + return protocol == WinRMHttpProtocolEnum.HTTPS ? DEFAULT_WIN_RM_HTTPS_PORT : DEFAULT_WIN_RM_HTTP_PORT; + } + + /** + * Get the namespace and replace '\' by '/' if necessary + * default: {@value WmiHelper#DEFAULT_NAMESPACE} if null + * + * @param namespace the provided namespace + * @return + */ + public static String buildNamespace(final String namespace) { + final String cleanNamespace = namespace != null ? namespace.replaceAll("\\s", Utils.EMPTY) : Utils.EMPTY; + + final String usedNamespace = Utils.isNotBlank(cleanNamespace) ? cleanNamespace : WmiHelper.DEFAULT_NAMESPACE; + + return usedNamespace.replace('\\', '/'); + } + + /** + * Build the WSMan URL endpoint. + * + * @param endpoint the endpoint in form of protocol://hostname:port (with protocol in HTTP or HTTPS). + * @return The external form of the endpoint URL + */ + private static String buildWSManEndpoint(final String endpoint) { + try { + return new URL(String.format("%s/wsman", endpoint)).toExternalForm(); + } catch (final MalformedURLException e) { + throw new IllegalArgumentException(String.format("endpoint %s is invalid.", endpoint), e); + } + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + Arrays.hashCode(password); + result = prime * result + Objects.hash(endpoint, namespace, rawUsername); + return result; + } + + @Override + public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (!(obj instanceof WinRMEndpoint)) { + return false; + } + final WinRMEndpoint other = (WinRMEndpoint) obj; + return (Objects.equals(endpoint, other.endpoint) + && + Objects.equals(namespace, other.namespace) + && + Arrays.equals(password, other.password) + && + Objects.equals(rawUsername, other.rawUsername)); + } + + @Override + public String toString() { + return new StringBuilder() + .append("WinRMEndpoint [") + .append("endpoint=") + .append(endpoint) + .append(", domain=") + .append(domain) + .append(", username=") + .append(username) + .append(", namespace=") + .append(namespace) + .append("]") + .toString(); + } +} diff --git a/src/main/java/org/metricshub/winrm/service/WinRMExecutorFactory.java b/src/main/java/org/metricshub/winrm/service/WinRMExecutorFactory.java index d63f227..3639be6 100644 --- a/src/main/java/org/metricshub/winrm/service/WinRMExecutorFactory.java +++ b/src/main/java/org/metricshub/winrm/service/WinRMExecutorFactory.java @@ -4,7 +4,7 @@ * ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲ * WinRM Java Client * ჻჻჻჻჻჻ - * Copyright 2023 - 2026 MetricsHub + * Copyright (C) 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. @@ -47,13 +47,13 @@ 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 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 + * requests the removed CXF backend or an unknown value */ public static WindowsRemoteExecutor createInstance( final WinRMEndpoint winRMEndpoint, @@ -70,8 +70,8 @@ public static WindowsRemoteExecutor createInstance( // 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)." + BACKEND_PROPERTY + + " system property to use the light backend (or stay on winrm-java 1.x)." ); } throw new WinRMException( diff --git a/src/main/java/org/metricshub/winrm/service/client/auth/AuthenticationEnum.java b/src/main/java/org/metricshub/winrm/service/client/auth/AuthenticationEnum.java index c1a5533..51b63e3 100644 --- a/src/main/java/org/metricshub/winrm/service/client/auth/AuthenticationEnum.java +++ b/src/main/java/org/metricshub/winrm/service/client/auth/AuthenticationEnum.java @@ -1,47 +1,46 @@ -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.util.Map; -import java.util.Optional; -import java.util.function.Function; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -public enum AuthenticationEnum { - NTLM, - KERBEROS; - - private static final Map VALUES_OF = Stream - .of(values()) - .collect(Collectors.toMap(AuthenticationEnum::name, Function.identity())); - - /** - * Get the value of the enum Name regardless the cardinality. - * - * @param name The enum value name - * - * @return An optional with the enum value if found empty otherwise - */ - public static Optional getValueOf(final String name) { - return name != null ? Optional.ofNullable(VALUES_OF.get(name.trim().toUpperCase())) : Optional.empty(); - } -} +package org.metricshub.winrm.service.client.auth; + +/*- + * ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲ + * 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.Map; +import java.util.Optional; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public enum AuthenticationEnum { + NTLM, + KERBEROS; + + private static final Map VALUES_OF = Stream + .of(values()) + .collect(Collectors.toMap(AuthenticationEnum::name, Function.identity())); + + /** + * Get the value of the enum Name regardless the cardinality. + * + * @param name The enum value name + * @return An optional with the enum value if found empty otherwise + */ + public static Optional getValueOf(final String name) { + return name != null ? Optional.ofNullable(VALUES_OF.get(name.trim().toUpperCase())) : Optional.empty(); + } +} diff --git a/src/main/java/org/metricshub/winrm/shares/SmbTempShare.java b/src/main/java/org/metricshub/winrm/shares/SmbTempShare.java index ee153ef..4327db5 100644 --- a/src/main/java/org/metricshub/winrm/shares/SmbTempShare.java +++ b/src/main/java/org/metricshub/winrm/shares/SmbTempShare.java @@ -1,318 +1,314 @@ -package org.metricshub.winrm.shares; - -/*- - * ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲ - * 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 com.hierynomus.security.bc.BCSecurityProvider; -import com.hierynomus.smbj.SMBClient; -import com.hierynomus.smbj.SmbConfig; -import com.hierynomus.smbj.auth.AuthenticationContext; -import com.hierynomus.smbj.connection.Connection; -import com.hierynomus.smbj.session.Session; -import com.hierynomus.smbj.share.DiskShare; -import java.io.IOException; -import java.nio.file.Path; -import java.util.List; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; -import java.util.concurrent.atomic.AtomicInteger; -import org.metricshub.winrm.Utils; -import org.metricshub.winrm.WindowsRemoteExecutor; -import org.metricshub.winrm.WindowsTempShare; -import org.metricshub.winrm.exceptions.WinRMException; -import org.metricshub.winrm.exceptions.WindowsRemoteException; -import org.metricshub.winrm.service.WinRMEndpoint; -import org.metricshub.winrm.service.WinRMExecutorFactory; -import org.metricshub.winrm.service.client.auth.AuthenticationEnum; - -public class SmbTempShare extends WindowsTempShare implements AutoCloseable { - - private final WinRMEndpoint winRMEndpoint; - private final SMBClient smbClient; - private final Connection connection; - private final Session session; - private final DiskShare diskShare; - - /** - * The SmbTempShare constructor. - * - * @param windowsRemoteExecutor WinRM executor (CXF or light backend) - * @param winRMEndpoint Endpoint with credentials - * @param smbClient The SMB client - * @param connection The SMB connection - * @param session The SMB session - * @param diskShare The SMB disk share - * @param shareNameOrUnc The name of the share, or its full UNC path - * @param remotePath The path on the remote system of the directory being shared - */ - private SmbTempShare( - final WindowsRemoteExecutor windowsRemoteExecutor, - final WinRMEndpoint winRMEndpoint, - final SMBClient smbClient, - final Connection connection, - final Session session, - final DiskShare diskShare, - final String shareNameOrUnc, - final String remotePath - ) { - super(windowsRemoteExecutor, shareNameOrUnc, remotePath); - this.winRMEndpoint = winRMEndpoint; - this.smbClient = smbClient; - this.connection = connection; - this.session = session; - this.diskShare = diskShare; - } - - private static final ConcurrentHashMap CONNECTIONS_CACHE = new ConcurrentHashMap<>(); - - private final AtomicInteger useCount = new AtomicInteger(1); - - /** - * Create a SmbTempShare instance. - * Get or create a temp share and connect to it with SMB. - * - * @param winRMEndpoint Endpoint with credentials (mandatory) - * @param timeout Timeout 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 SmbTempShare instance - * - * @throws IOException If an I/O error occurred - * @throws WinRMException For any problem encountered - * @throws TimeoutException To notify userName of timeout. - */ - public static SmbTempShare createInstance( - final WinRMEndpoint winRMEndpoint, - final long timeout, - final Path ticketCache, - final List authentications - ) throws IOException, WinRMException, TimeoutException { - Utils.checkNonNull(winRMEndpoint, "winRMEndpoint"); - Utils.checkNonNull(winRMEndpoint.getPassword(), "password"); - Utils.checkArgumentNotZeroOrNegative(timeout, "timeout"); - - try { - return CONNECTIONS_CACHE.compute( - winRMEndpoint, - (key, smb) -> { - if (smb == null) { - WindowsRemoteExecutor windowsRemoteExecutor = null; - SMBClient smbClient = null; - Connection connection = null; - Session session = null; - DiskShare diskShare = null; - - try { - // 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( - windowsRemoteExecutor, - timeout, - (w, r, s, t) -> { - try { - shareRemoteDirectory(w, r, s, t); - } catch (final TimeoutException | WindowsRemoteException e) { - throw new RuntimeException(e); - } - } - ); - - final SmbConfig smbConfig = SmbConfig - .builder() - .withSecurityProvider(new BCSecurityProvider()) - .withTimeout(timeout, TimeUnit.SECONDS) - .build(); - - final AuthenticationContext authenticationContext = new AuthenticationContext( - winRMEndpoint.getUsername(), - winRMEndpoint.getPassword(), - winRMEndpoint.getDomain() - ); - - smbClient = createSmbClient(smbConfig); - connection = smbClient.connect(winRMEndpoint.getHostname()); - session = connection.authenticate(authenticationContext); - diskShare = (DiskShare) session.connectShare(windowsTempShare.getShareName()); - - return new SmbTempShare( - windowsRemoteExecutor, - winRMEndpoint, - smbClient, - connection, - session, - diskShare, - windowsTempShare.getUncSharePath(), - windowsTempShare.getRemotePath() - ); - } catch (final RuntimeException e) { - closeResources(windowsRemoteExecutor, smbClient, connection, session, diskShare); - - throw e; - } catch (final Exception e) { - closeResources(windowsRemoteExecutor, smbClient, connection, session, diskShare); - - throw new RuntimeException(e); - } - } else { - synchronized (smb) { - smb.incrementUseCount(); - - return smb; - } - } - } - ); - } catch (final RuntimeException e) { - final Throwable cause = e.getCause(); - - if (cause instanceof IOException) { - throw (IOException) cause; - } - - if (cause instanceof TimeoutException) { - throw (TimeoutException) cause; - } - - if (cause instanceof WindowsRemoteException) { - throw (WinRMException) cause; - } - - throw e; - } - } - - private static void closeResources( - final WindowsRemoteExecutor windowsRemoteExecutor, - final SMBClient smbClient, - final Connection connection, - final Session session, - final DiskShare diskShare - ) { - try { - if (diskShare != null) { - diskShare.close(); - } - - if (session != null) { - session.close(); - } - - if (connection != null) { - connection.close(); - } - } catch (final IOException ioe) { - throw new RuntimeException(ioe); - } - - if (smbClient != null) { - smbClient.close(); - } - - if (windowsRemoteExecutor != null) { - windowsRemoteExecutor.close(); - } - } - - int getUseCount() { - return useCount.get(); - } - - void incrementUseCount() { - useCount.incrementAndGet(); - } - - /** - * @return whether this WbemServices instance is connected and usable - */ - boolean isConnected() { - return getUseCount() > 0; - } - - /** - * 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 synchronized void close() throws IOException { - if (useCount.decrementAndGet() == 0) { - CONNECTIONS_CACHE.remove(winRMEndpoint); - - if (diskShare != null) { - diskShare.close(); - } - - if (session != null) { - session.close(); - } - - if (connection != null) { - connection.close(); - } - - if (smbClient != null) { - smbClient.close(); - } - - getWindowsRemoteExecutor().close(); - } - } - - /** - * Share the remote directory on the host. - * - * @param windowsRemoteExecutor WinRM executor (CXF or light backend). - * @param remotePath The remote path. - * @param shareName The Share Name. - * @param timeout Timeout in milliseconds. - * - * @throws TimeoutException To notify userName of timeout. - * @throws WindowsRemoteException For any problem encountered - * - */ - private static void shareRemoteDirectory( - final WindowsRemoteExecutor windowsRemoteExecutor, - final String remotePath, - final String shareName, - final long timeout - ) throws TimeoutException, WindowsRemoteException { - final String command = String.format( - "net share %s=%s /grant:%s,Full", - shareName, - remotePath, - windowsRemoteExecutor.getUsername() - ); - - windowsRemoteExecutor.executeCommand(command, null, null, timeout); - } - - static SMBClient createSmbClient(final SmbConfig smbConfig) { - return new SMBClient(smbConfig); - } -} +package org.metricshub.winrm.shares; + +/*- + * ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲ + * 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 com.hierynomus.security.bc.BCSecurityProvider; +import com.hierynomus.smbj.SMBClient; +import com.hierynomus.smbj.SmbConfig; +import com.hierynomus.smbj.auth.AuthenticationContext; +import com.hierynomus.smbj.connection.Connection; +import com.hierynomus.smbj.session.Session; +import com.hierynomus.smbj.share.DiskShare; +import java.io.IOException; +import java.nio.file.Path; +import java.util.List; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicInteger; +import org.metricshub.winrm.Utils; +import org.metricshub.winrm.WindowsRemoteExecutor; +import org.metricshub.winrm.WindowsTempShare; +import org.metricshub.winrm.exceptions.WinRMException; +import org.metricshub.winrm.exceptions.WindowsRemoteException; +import org.metricshub.winrm.service.WinRMEndpoint; +import org.metricshub.winrm.service.WinRMExecutorFactory; +import org.metricshub.winrm.service.client.auth.AuthenticationEnum; + +public class SmbTempShare extends WindowsTempShare implements AutoCloseable { + + private final WinRMEndpoint winRMEndpoint; + private final SMBClient smbClient; + private final Connection connection; + private final Session session; + private final DiskShare diskShare; + + /** + * The SmbTempShare constructor. + * + * @param windowsRemoteExecutor WinRM executor (CXF or light backend) + * @param winRMEndpoint Endpoint with credentials + * @param smbClient The SMB client + * @param connection The SMB connection + * @param session The SMB session + * @param diskShare The SMB disk share + * @param shareNameOrUnc The name of the share, or its full UNC path + * @param remotePath The path on the remote system of the directory being shared + */ + private SmbTempShare( + final WindowsRemoteExecutor windowsRemoteExecutor, + final WinRMEndpoint winRMEndpoint, + final SMBClient smbClient, + final Connection connection, + final Session session, + final DiskShare diskShare, + final String shareNameOrUnc, + final String remotePath + ) { + super(windowsRemoteExecutor, shareNameOrUnc, remotePath); + this.winRMEndpoint = winRMEndpoint; + this.smbClient = smbClient; + this.connection = connection; + this.session = session; + this.diskShare = diskShare; + } + + private static final ConcurrentHashMap CONNECTIONS_CACHE = new ConcurrentHashMap<>(); + + private final AtomicInteger useCount = new AtomicInteger(1); + + /** + * Create a SmbTempShare instance. + * Get or create a temp share and connect to it with SMB. + * + * @param winRMEndpoint Endpoint with credentials (mandatory) + * @param timeout Timeout 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 SmbTempShare instance + * @throws IOException If an I/O error occurred + * @throws WinRMException For any problem encountered + * @throws TimeoutException To notify userName of timeout. + */ + public static SmbTempShare createInstance( + final WinRMEndpoint winRMEndpoint, + final long timeout, + final Path ticketCache, + final List authentications + ) throws IOException, WinRMException, TimeoutException { + Utils.checkNonNull(winRMEndpoint, "winRMEndpoint"); + Utils.checkNonNull(winRMEndpoint.getPassword(), "password"); + Utils.checkArgumentNotZeroOrNegative(timeout, "timeout"); + + try { + return CONNECTIONS_CACHE.compute( + winRMEndpoint, + (key, smb) -> { + if (smb == null) { + WindowsRemoteExecutor windowsRemoteExecutor = null; + SMBClient smbClient = null; + Connection connection = null; + Session session = null; + DiskShare diskShare = null; + + try { + // 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( + windowsRemoteExecutor, + timeout, + (w, r, s, t) -> { + try { + shareRemoteDirectory(w, r, s, t); + } catch (final TimeoutException | WindowsRemoteException e) { + throw new RuntimeException(e); + } + } + ); + + final SmbConfig smbConfig = SmbConfig + .builder() + .withSecurityProvider(new BCSecurityProvider()) + .withTimeout(timeout, TimeUnit.SECONDS) + .build(); + + final AuthenticationContext authenticationContext = new AuthenticationContext( + winRMEndpoint.getUsername(), + winRMEndpoint.getPassword(), + winRMEndpoint.getDomain() + ); + + smbClient = createSmbClient(smbConfig); + connection = smbClient.connect(winRMEndpoint.getHostname()); + session = connection.authenticate(authenticationContext); + diskShare = (DiskShare) session.connectShare(windowsTempShare.getShareName()); + + return new SmbTempShare( + windowsRemoteExecutor, + winRMEndpoint, + smbClient, + connection, + session, + diskShare, + windowsTempShare.getUncSharePath(), + windowsTempShare.getRemotePath() + ); + } catch (final RuntimeException e) { + closeResources(windowsRemoteExecutor, smbClient, connection, session, diskShare); + + throw e; + } catch (final Exception e) { + closeResources(windowsRemoteExecutor, smbClient, connection, session, diskShare); + + throw new RuntimeException(e); + } + } else { + synchronized (smb) { + smb.incrementUseCount(); + + return smb; + } + } + } + ); + } catch (final RuntimeException e) { + final Throwable cause = e.getCause(); + + if (cause instanceof IOException) { + throw (IOException) cause; + } + + if (cause instanceof TimeoutException) { + throw (TimeoutException) cause; + } + + if (cause instanceof WindowsRemoteException) { + throw (WinRMException) cause; + } + + throw e; + } + } + + private static void closeResources( + final WindowsRemoteExecutor windowsRemoteExecutor, + final SMBClient smbClient, + final Connection connection, + final Session session, + final DiskShare diskShare + ) { + try { + if (diskShare != null) { + diskShare.close(); + } + + if (session != null) { + session.close(); + } + + if (connection != null) { + connection.close(); + } + } catch (final IOException ioe) { + throw new RuntimeException(ioe); + } + + if (smbClient != null) { + smbClient.close(); + } + + if (windowsRemoteExecutor != null) { + windowsRemoteExecutor.close(); + } + } + + int getUseCount() { + return useCount.get(); + } + + void incrementUseCount() { + useCount.incrementAndGet(); + } + + /** + * @return whether this WbemServices instance is connected and usable + */ + boolean isConnected() { + return getUseCount() > 0; + } + + /** + * 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 synchronized void close() throws IOException { + if (useCount.decrementAndGet() == 0) { + CONNECTIONS_CACHE.remove(winRMEndpoint); + + if (diskShare != null) { + diskShare.close(); + } + + if (session != null) { + session.close(); + } + + if (connection != null) { + connection.close(); + } + + if (smbClient != null) { + smbClient.close(); + } + + getWindowsRemoteExecutor().close(); + } + } + + /** + * Share the remote directory on the host. + * + * @param windowsRemoteExecutor WinRM executor (CXF or light backend). + * @param remotePath The remote path. + * @param shareName The Share Name. + * @param timeout Timeout in milliseconds. + * @throws TimeoutException To notify userName of timeout. + * @throws WindowsRemoteException For any problem encountered + */ + private static void shareRemoteDirectory( + final WindowsRemoteExecutor windowsRemoteExecutor, + final String remotePath, + final String shareName, + final long timeout + ) throws TimeoutException, WindowsRemoteException { + final String command = String.format( + "net share %s=%s /grant:%s,Full", + shareName, + remotePath, + windowsRemoteExecutor.getUsername() + ); + + windowsRemoteExecutor.executeCommand(command, null, null, timeout); + } + + static SMBClient createSmbClient(final SmbConfig smbConfig) { + return new SMBClient(smbConfig); + } +} diff --git a/src/main/java/org/metricshub/winrm/wql/WinRMWqlExecutor.java b/src/main/java/org/metricshub/winrm/wql/WinRMWqlExecutor.java index e16e8e3..f2f9896 100644 --- a/src/main/java/org/metricshub/winrm/wql/WinRMWqlExecutor.java +++ b/src/main/java/org/metricshub/winrm/wql/WinRMWqlExecutor.java @@ -1,148 +1,148 @@ -package org.metricshub.winrm.wql; - -/*- - * ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲ - * 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.Map; -import java.util.concurrent.TimeoutException; -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.WinRMExecutorFactory; -import org.metricshub.winrm.service.client.auth.AuthenticationEnum; - -public class WinRMWqlExecutor { - - private final long executionTime; - private final List headers; - private final List> rows; - - /** - * The WinRMWqlExecutor constructor - * - * @param executionTime The execution time in milliseconds - * @param headers The headers list - * @param rows The value rows list - */ - public WinRMWqlExecutor(final long executionTime, final List headers, final List> rows) { - this.executionTime = executionTime; - this.headers = headers; - this.rows = rows; - } - - /** - * Get the execution time of the query. - * @return - */ - public long getExecutionTime() { - return executionTime; - } - - /** - * Get the headers of the query. - * @return - */ - public List getHeaders() { - return headers; - } - - /** - * Get the result rows of the query. - * @return - */ - public List> getRows() { - return rows; - } - - /** - * Execute a WQL query and process its result. - * - * @param protocol The HTTP protocol (HTTP by default) - * @param hostname Host to connect to. (Mandatory) - * @param port The port (5985 for HTPP or 5986 for HTTPS by default) - * @param username The username name. (Mandatory) - * @param password The password - * @param namespace The namespace default value: {@value WmiHelper#DEFAULT_NAMESPACE} - * @param wqlQuery The WQL query (Mandatory) - * @param timeout The timeout 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 WinRMWqlExecutor result instance with header, rows and execution time. - * - * @throws WinRMException For any problem encountered on remote - * @throws WqlQuerySyntaxException On WQL syntax errors - * @throws TimeoutException To notify userName of timeout - */ - public static WinRMWqlExecutor executeWql( - final WinRMHttpProtocolEnum protocol, - final String hostname, - final Integer port, - final String username, - final char[] password, - final String namespace, - final String wqlQuery, - final long timeout, - final Path ticketCache, - final List authentications - ) throws WinRMException, WqlQuerySyntaxException, TimeoutException { - Utils.checkNonNull(wqlQuery, "wqlQuery"); - Utils.checkArgumentNotZeroOrNegative(timeout, "timeout"); - - final long start = Utils.getCurrentTimeMillis(); - - final WinRMEndpoint winRMEndpoint = new WinRMEndpoint(protocol, hostname, port, username, password, namespace); - - try ( - final WindowsRemoteExecutor winRMService = WinRMExecutorFactory.createInstance( - winRMEndpoint, - timeout, - ticketCache, - authentications - ) - ) { - final List> result = winRMService.executeWql(wqlQuery, timeout); - - // Extract the list of properties from the result, with same order as in the WQL query - final List headers = WmiHelper.extractPropertiesFromResult(result, wqlQuery); - - final List> rows = result - .stream() - .map(row -> headers.stream().map(header -> (String) row.get(header)).collect(Collectors.toList())) - .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()); - } - } -} +package org.metricshub.winrm.wql; + +/*- + * ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲ + * WinRM Java Client + * ჻჻჻჻჻჻ + * Copyright (C) 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.Map; +import java.util.concurrent.TimeoutException; +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.WinRMExecutorFactory; +import org.metricshub.winrm.service.client.auth.AuthenticationEnum; + +public class WinRMWqlExecutor { + + private final long executionTime; + private final List headers; + private final List> rows; + + /** + * The WinRMWqlExecutor constructor + * + * @param executionTime The execution time in milliseconds + * @param headers The headers list + * @param rows The value rows list + */ + public WinRMWqlExecutor(final long executionTime, final List headers, final List> rows) { + this.executionTime = executionTime; + this.headers = headers; + this.rows = rows; + } + + /** + * Get the execution time of the query. + * + * @return + */ + public long getExecutionTime() { + return executionTime; + } + + /** + * Get the headers of the query. + * + * @return + */ + public List getHeaders() { + return headers; + } + + /** + * Get the result rows of the query. + * + * @return + */ + public List> getRows() { + return rows; + } + + /** + * Execute a WQL query and process its result. + * + * @param protocol The HTTP protocol (HTTP by default) + * @param hostname Host to connect to. (Mandatory) + * @param port The port (5985 for HTPP or 5986 for HTTPS by default) + * @param username The username name. (Mandatory) + * @param password The password + * @param namespace The namespace default value: {@value WmiHelper#DEFAULT_NAMESPACE} + * @param wqlQuery The WQL query (Mandatory) + * @param timeout The timeout 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 WinRMWqlExecutor result instance with header, rows and execution time. + * @throws WinRMException For any problem encountered on remote + * @throws WqlQuerySyntaxException On WQL syntax errors + * @throws TimeoutException To notify userName of timeout + */ + public static WinRMWqlExecutor executeWql( + final WinRMHttpProtocolEnum protocol, + final String hostname, + final Integer port, + final String username, + final char[] password, + final String namespace, + final String wqlQuery, + final long timeout, + final Path ticketCache, + final List authentications + ) throws WinRMException, WqlQuerySyntaxException, TimeoutException { + Utils.checkNonNull(wqlQuery, "wqlQuery"); + Utils.checkArgumentNotZeroOrNegative(timeout, "timeout"); + + final long start = Utils.getCurrentTimeMillis(); + + final WinRMEndpoint winRMEndpoint = new WinRMEndpoint(protocol, hostname, port, username, password, namespace); + + try ( + final WindowsRemoteExecutor winRMService = WinRMExecutorFactory.createInstance( + winRMEndpoint, + timeout, + ticketCache, + authentications + )) { + final List> result = winRMService.executeWql(wqlQuery, timeout); + + // Extract the list of properties from the result, with same order as in the WQL query + final List headers = WmiHelper.extractPropertiesFromResult(result, wqlQuery); + + final List> rows = result + .stream() + .map(row -> headers.stream().map(header -> (String) row.get(header)).collect(Collectors.toList())) + .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/test/java/org/metricshub/winrm/WinRMLiveTest.java b/src/test/java/org/metricshub/winrm/WinRMLiveTest.java index b2c7984..9ca595a 100644 --- a/src/test/java/org/metricshub/winrm/WinRMLiveTest.java +++ b/src/test/java/org/metricshub/winrm/WinRMLiveTest.java @@ -19,8 +19,8 @@ * 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: + *

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

        * mvn test -Dtest=WinRMLiveTest \
      @@ -29,8 +29,8 @@
        *   -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), + *

      + * 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. @@ -50,10 +50,9 @@ class WinRMLiveTest { @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; + 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"); diff --git a/src/test/java/org/metricshub/winrm/command/WinRMCommandExecutorTest.java b/src/test/java/org/metricshub/winrm/command/WinRMCommandExecutorTest.java index ef9f08e..be69dda 100644 --- a/src/test/java/org/metricshub/winrm/command/WinRMCommandExecutorTest.java +++ b/src/test/java/org/metricshub/winrm/command/WinRMCommandExecutorTest.java @@ -1,224 +1,216 @@ -package org.metricshub.winrm.command; - -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.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.metricshub.winrm.WinRMHttpProtocolEnum.HTTPS; -import static org.metricshub.winrm.WindowsRemoteProcessUtils.copyLocalFilesToShare; -import static org.metricshub.winrm.WindowsRemoteProcessUtils.getWindowsEncodingCharset; -import static org.metricshub.winrm.command.WinRMCommandExecutor.execute; -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 java.nio.file.Path; -import java.nio.file.Paths; -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.WinRMExecutorFactory; -import org.metricshub.winrm.service.client.auth.AuthenticationEnum; -import org.metricshub.winrm.shares.SmbTempShare; -import org.mockito.MockedStatic; - -class WinRMCommandExecutorTest { - - @Test - void testExecute() throws Exception { - final String command = "launch"; - final String hostname = "host"; - final String username = "domain\\user"; - final char[] password = "pass".toCharArray(); - final String workingDirectory = " \t\r\n dir \t\r\n "; - final long timeout = 30 * 1000L; - final List localFileToCopyList = singletonList(" \r\t\n localFile \t\r\n "); - final WindowsRemoteCommandResult expected = new WindowsRemoteCommandResult("stdout", "stderr", 1.0f, 0); - final Path ticketCache = Paths.get("path"); - final List authentications = singletonList(NTLM); - - // check arguments - assertThrows( - IllegalArgumentException.class, - () -> - execute( - null, - HTTPS, - hostname, - 5986, - username, - password, - workingDirectory, - timeout, - localFileToCopyList, - ticketCache, - authentications - ) - ); - - assertThrows( - IllegalArgumentException.class, - () -> - execute( - command, - HTTPS, - null, - 5986, - username, - password, - workingDirectory, - timeout, - localFileToCopyList, - ticketCache, - authentications - ) - ); - - assertThrows( - IllegalArgumentException.class, - () -> - execute( - command, - HTTPS, - hostname, - 5986, - null, - password, - workingDirectory, - timeout, - localFileToCopyList, - ticketCache, - authentications - ) - ); - - assertThrows( - IllegalArgumentException.class, - () -> - execute( - command, - HTTPS, - hostname, - 5986, - username, - null, - workingDirectory, - timeout, - localFileToCopyList, - ticketCache, - authentications - ) - ); - - assertThrows( - IllegalArgumentException.class, - () -> - execute( - command, - HTTPS, - hostname, - 5986, - username, - password, - workingDirectory, - -1L, - localFileToCopyList, - ticketCache, - authentications - ) - ); - - assertThrows( - IllegalArgumentException.class, - () -> - execute( - command, - HTTPS, - hostname, - 5986, - username, - password, - workingDirectory, - 0L, - localFileToCopyList, - ticketCache, - authentications - ) - ); - - // case localFileToCopyList null or empty - try ( - final MockedStatic mockedWindowsRemoteProcessUtils = mockStatic( - WindowsRemoteProcessUtils.class - ); - final MockedStatic mockedFactory = mockStatic(WinRMExecutorFactory.class) - ) { - mockedWindowsRemoteProcessUtils.when(() -> getWindowsEncodingCharset(any(), anyLong())).thenReturn(UTF_8); - - final WindowsRemoteExecutor executor = mock(WindowsRemoteExecutor.class); - - mockedFactory - .when(() -> WinRMExecutorFactory.createInstance(any(WinRMEndpoint.class), anyLong(), isNull(), isNull())) - .thenReturn(executor); - - doReturn(expected).when(executor).executeCommand(eq(command), isNull(), eq(UTF_8), anyLong()); - - assertEquals( - expected, - execute(command, null, hostname, null, username, password, null, timeout, null, null, null) - ); - - assertEquals( - expected, - execute(command, null, hostname, null, username, password, null, timeout, emptyList(), null, null) - ); - } - - // Case with localFileToCopyList - try ( - final MockedStatic mockedWindowsRemoteProcessUtils = mockStatic( - WindowsRemoteProcessUtils.class - ); - final MockedStatic mockedSmbTempShare = mockStatic(SmbTempShare.class) - ) { - mockedWindowsRemoteProcessUtils.when(() -> getWindowsEncodingCharset(any(), anyLong())).thenReturn(UTF_8); - - mockedWindowsRemoteProcessUtils - .when(() -> copyLocalFilesToShare(anyString(), anyList(), anyString(), anyString())) - .thenReturn("launch remote/localFile"); - - final SmbTempShare smbTempShare = mock(SmbTempShare.class); - final WindowsRemoteExecutor winRMService = mock(WindowsRemoteExecutor.class); - - mockedSmbTempShare - .when(() -> SmbTempShare.createInstance(any(WinRMEndpoint.class), anyLong(), isNull(), isNull())) - .thenReturn(smbTempShare); - - doNothing().when(smbTempShare).checkConnectedFirst(); - doReturn(winRMService).when(smbTempShare).getWindowsRemoteExecutor(); - doReturn("\\\\2001-db8--85b-3c51-f5ff-ffdb.ipv6-literal.net\\SEN_ShareFor_PC-TEST$") - .when(smbTempShare) - .getUncSharePath(); - doReturn("Windows\\Temp\\SEN_ShareFor_TEST$").when(smbTempShare).getRemotePath(); - - doReturn(expected).when(winRMService).executeCommand(anyString(), isNull(), eq(UTF_8), anyLong()); - - assertEquals( - expected, - execute(command, null, hostname, null, username, password, null, timeout, localFileToCopyList, null, null) - ); - } - } -} +package org.metricshub.winrm.command; + +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.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.metricshub.winrm.WinRMHttpProtocolEnum.HTTPS; +import static org.metricshub.winrm.WindowsRemoteProcessUtils.copyLocalFilesToShare; +import static org.metricshub.winrm.WindowsRemoteProcessUtils.getWindowsEncodingCharset; +import static org.metricshub.winrm.command.WinRMCommandExecutor.execute; +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 java.nio.file.Path; +import java.nio.file.Paths; +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.WinRMExecutorFactory; +import org.metricshub.winrm.service.client.auth.AuthenticationEnum; +import org.metricshub.winrm.shares.SmbTempShare; +import org.mockito.MockedStatic; + +class WinRMCommandExecutorTest { + + @Test + void testExecute() throws Exception { + final String command = "launch"; + final String hostname = "host"; + final String username = "domain\\user"; + final char[] password = "pass".toCharArray(); + final String workingDirectory = " \t\r\n dir \t\r\n "; + final long timeout = 30 * 1000L; + final List localFileToCopyList = singletonList(" \r\t\n localFile \t\r\n "); + final WindowsRemoteCommandResult expected = new WindowsRemoteCommandResult("stdout", "stderr", 1.0f, 0); + final Path ticketCache = Paths.get("path"); + final List authentications = singletonList(NTLM); + + // check arguments + assertThrows( + IllegalArgumentException.class, + () -> execute( + null, + HTTPS, + hostname, + 5986, + username, + password, + workingDirectory, + timeout, + localFileToCopyList, + ticketCache, + authentications + ) + ); + + assertThrows( + IllegalArgumentException.class, + () -> execute( + command, + HTTPS, + null, + 5986, + username, + password, + workingDirectory, + timeout, + localFileToCopyList, + ticketCache, + authentications + ) + ); + + assertThrows( + IllegalArgumentException.class, + () -> execute( + command, + HTTPS, + hostname, + 5986, + null, + password, + workingDirectory, + timeout, + localFileToCopyList, + ticketCache, + authentications + ) + ); + + assertThrows( + IllegalArgumentException.class, + () -> execute( + command, + HTTPS, + hostname, + 5986, + username, + null, + workingDirectory, + timeout, + localFileToCopyList, + ticketCache, + authentications + ) + ); + + assertThrows( + IllegalArgumentException.class, + () -> execute( + command, + HTTPS, + hostname, + 5986, + username, + password, + workingDirectory, + -1L, + localFileToCopyList, + ticketCache, + authentications + ) + ); + + assertThrows( + IllegalArgumentException.class, + () -> execute( + command, + HTTPS, + hostname, + 5986, + username, + password, + workingDirectory, + 0L, + localFileToCopyList, + ticketCache, + authentications + ) + ); + + // case localFileToCopyList null or empty + try ( + final MockedStatic mockedWindowsRemoteProcessUtils = mockStatic( + WindowsRemoteProcessUtils.class + ); + final MockedStatic mockedFactory = mockStatic(WinRMExecutorFactory.class)) { + mockedWindowsRemoteProcessUtils.when(() -> getWindowsEncodingCharset(any(), anyLong())).thenReturn(UTF_8); + + final WindowsRemoteExecutor executor = mock(WindowsRemoteExecutor.class); + + mockedFactory + .when(() -> WinRMExecutorFactory.createInstance(any(WinRMEndpoint.class), anyLong(), isNull(), isNull())) + .thenReturn(executor); + + doReturn(expected).when(executor).executeCommand(eq(command), isNull(), eq(UTF_8), anyLong()); + + assertEquals( + expected, + execute(command, null, hostname, null, username, password, null, timeout, null, null, null) + ); + + assertEquals( + expected, + execute(command, null, hostname, null, username, password, null, timeout, emptyList(), null, null) + ); + } + + // Case with localFileToCopyList + try ( + final MockedStatic mockedWindowsRemoteProcessUtils = mockStatic( + WindowsRemoteProcessUtils.class + ); + final MockedStatic mockedSmbTempShare = mockStatic(SmbTempShare.class)) { + mockedWindowsRemoteProcessUtils.when(() -> getWindowsEncodingCharset(any(), anyLong())).thenReturn(UTF_8); + + mockedWindowsRemoteProcessUtils + .when(() -> copyLocalFilesToShare(anyString(), anyList(), anyString(), anyString())) + .thenReturn("launch remote/localFile"); + + final SmbTempShare smbTempShare = mock(SmbTempShare.class); + final WindowsRemoteExecutor winRMService = mock(WindowsRemoteExecutor.class); + + mockedSmbTempShare + .when(() -> SmbTempShare.createInstance(any(WinRMEndpoint.class), anyLong(), isNull(), isNull())) + .thenReturn(smbTempShare); + + doNothing().when(smbTempShare).checkConnectedFirst(); + doReturn(winRMService).when(smbTempShare).getWindowsRemoteExecutor(); + doReturn("\\\\2001-db8--85b-3c51-f5ff-ffdb.ipv6-literal.net\\SEN_ShareFor_PC-TEST$") + .when(smbTempShare) + .getUncSharePath(); + doReturn("Windows\\Temp\\SEN_ShareFor_TEST$").when(smbTempShare).getRemotePath(); + + doReturn(expected).when(winRMService).executeCommand(anyString(), isNull(), eq(UTF_8), anyLong()); + + assertEquals( + expected, + execute(command, null, hostname, null, username, password, null, timeout, localFileToCopyList, null, null) + ); + } + } +} diff --git a/src/test/java/org/metricshub/winrm/light/FakeWsmanServer.java b/src/test/java/org/metricshub/winrm/light/FakeWsmanServer.java index bc4d62d..3b50799 100644 --- a/src/test/java/org/metricshub/winrm/light/FakeWsmanServer.java +++ b/src/test/java/org/metricshub/winrm/light/FakeWsmanServer.java @@ -27,8 +27,8 @@ * 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 + *

      + * 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 { @@ -49,21 +49,26 @@ static final class Scripted { // 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 + 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 static final int TYPE2_FLAGS = 0x00000001 + | 0x00000010 + | 0x00000020 + | 0x00080000 + | 0x00800000 + | 0x20000000 + | 0x40000000; private final String expectedDomain; private final String expectedUser; @@ -193,13 +198,12 @@ private void serveScripted(final OutputStream out, final WinRMSession session, f } if (next == null) { // Loud, decryptable failure so an over-consuming test fails on an assertion, not a hang. - next = - new Scripted( - 500, - "" + + 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); @@ -220,7 +224,8 @@ private static byte[] buildType2() { final byte[] targetInfo = concat( avPair(2, "FAKE"), // NetBIOS domain avPair(1, "FAKESRV"), // NetBIOS computer - new byte[] { 0, 0, 0, 0 } // terminator + new byte[] + { 0, 0, 0, 0 } // terminator ); final ByteArrayOutputStream msg = new ByteArrayOutputStream(); writeBytes(msg, "NTLMSSP\0".getBytes(StandardCharsets.US_ASCII)); diff --git a/src/test/java/org/metricshub/winrm/light/WsmanClientParityTest.java b/src/test/java/org/metricshub/winrm/light/WsmanClientParityTest.java index 4ccda7e..66d1f09 100644 --- a/src/test/java/org/metricshub/winrm/light/WsmanClientParityTest.java +++ b/src/test/java/org/metricshub/winrm/light/WsmanClientParityTest.java @@ -31,11 +31,11 @@ private static Document parse(final String xml) throws Exception { void endOfSequenceDetectedInWsEnumerationNamespace() throws Exception { final Document doc = parse( "" + - "" + - "" + - "" + "" + + "" + + "" ); assertTrue(WsmanClient.hasEnumerationElement(doc, "EndOfSequence")); } @@ -46,13 +46,13 @@ void endOfSequenceDetectedInWsmanNamespaceVariant() throws Exception { // backend accepts both variants, so the light backend must too. final Document doc = parse( "" + - "" + - "" + - "" + "" + + "" + + "" ); assertTrue(WsmanClient.hasEnumerationElement(doc, "EndOfSequence")); } @@ -63,17 +63,17 @@ void wmiPropertyNamedEndOfSequenceIsNotMistakenForTheMarker() throws Exception { // and must not terminate the enumeration. final Document doc = parse( "" + - "" + - "" + - "" + - "oops" + - "" + - "" + - "" + "" + + "" + + "" + + "oops" + + "" + + "" + + "" ); assertFalse(WsmanClient.hasEnumerationElement(doc, "EndOfSequence")); } @@ -83,32 +83,32 @@ void wmiPropertyNamedEndOfSequenceIsNotMistakenForTheMarker() throws Exception { @Test void itemsCollectedFromBothNamespaceVariants() throws Exception { // EnumerateResponse carries wsen:Items, PullResponse carries wsman:Items; both must yield rows. - final String instance = - "" + + final String instance = "" + + "Spooler" + "Running" + ""; final Document wsenItems = parse( "" + - "" + - "" + - instance + - "" + - "" + "" + + "" + + instance + + "" + + "" ); final Document wsmanItems = parse( "" + - "" + - "" + - instance + - "" + - "" + "" + + "" + + instance + + "" + + "" ); for (final Document doc : List.of(wsenItems, wsmanItems)) { @@ -126,18 +126,18 @@ void wmiPropertyNamedItemsIsNotMistakenForTheWrapper() throws Exception { // second Items wrapper producing phantom rows. final Document doc = parse( "" + - "" + - "" + - "" + - "real-row" + - "phantom" + - "" + - "" + - "" + "" + + "" + + "" + + "real-row" + + "phantom" + + "" + + "" + + "" ); final List> rows = new ArrayList<>(); WsmanClient.collectItems(doc, rows); @@ -153,14 +153,15 @@ void faultSummaryCarriesWsmanFaultDetailWithWbemMnemonic() throws Exception { // 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" + - "" + - "" + - "" + "" + + "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); @@ -175,18 +176,18 @@ void faultSummaryDoesNotDuplicateDetailAlreadyInReason() throws Exception { final String message = "The WMI service or the WMI provider returned an unknown error: WBEM_E_INVALID_NAMESPACE"; final Document doc = parse( "" + - "" + - "" + - message + - " " + - "" + - "" + - "" + - message + - " " + - "" + - "" + - "" + "" + + "" + + message + + " " + + "" + + "" + + "" + + message + + " " + + "" + + "" + + "" ); final String summary = WsmanClient.faultSummary(500, doc); final int first = summary.indexOf("WBEM_E_INVALID_NAMESPACE"); @@ -199,9 +200,9 @@ void faultSummaryDoesNotDuplicateDetailAlreadyInReason() throws Exception { void faultSummaryWithoutWsmanFaultDetailStillReadable() throws Exception { final Document doc = parse( "" + - "" + - "Some transport-level failure" + - "" + "" + + "Some transport-level failure" + + "" ); assertEquals("HTTP 500: Some transport-level failure", WsmanClient.faultSummary(500, doc)); } diff --git a/src/test/java/org/metricshub/winrm/light/WsmanProtocolTest.java b/src/test/java/org/metricshub/winrm/light/WsmanProtocolTest.java index 3b236cb..9a275ba 100644 --- a/src/test/java/org/metricshub/winrm/light/WsmanProtocolTest.java +++ b/src/test/java/org/metricshub/winrm/light/WsmanProtocolTest.java @@ -70,41 +70,41 @@ void wqlPagesAcrossEnumerateAndPullsOverEncryptedNtlm() throws Exception { 200, envelope( "" + - "uuid:CTX-1" + - "" + - service("Spooler", "Running") + - "" + - "" + WSEN + + "\" xmlns:wsman=\"" + + WSMAN + + "\">" + + "uuid:CTX-1" + + "" + + service("Spooler", "Running") + + "" + + "" ) ) .enqueue( 200, envelope( "" + - "uuid:CTX-2" + - "" + - service("WinRM", "Running") + - service("Wecsvc", "Stopped") + - "" + - "" + WSEN + + "\">" + + "uuid:CTX-2" + + "" + + service("WinRM", "Running") + + service("Wecsvc", "Stopped") + + "" + + "" ) ) .enqueue( 200, envelope( "" + - "" + - "" + WSEN + + "\" xmlns:wsman=\"" + + WSMAN + + "\">" + + "" + + "" ) ); @@ -290,19 +290,16 @@ private static String envelope(final String body) { } private static String service(final String name, final String state) { - return ( - "" + + return ("" + "" + name + "" + state + - "" - ); + ""); } private static String resourceCreated(final String shellId) { - return ( - "" + @@ -314,51 +311,42 @@ private static String resourceCreated(final String shellId) { "" + 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) { @@ -366,8 +354,7 @@ private static String fault(final String code, final String reason) { } private static String fault(final String code, final String reason, final String detailMessage) { - return ( - "" + "s:Receiver" + @@ -382,7 +369,6 @@ private static String fault(final String code, final String reason, final String "" + (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 17122a1..80d913e 100644 --- a/src/test/java/org/metricshub/winrm/service/WinRMEndpointTest.java +++ b/src/test/java/org/metricshub/winrm/service/WinRMEndpointTest.java @@ -1,138 +1,138 @@ -package org.metricshub.winrm.service; - -import static org.junit.jupiter.api.Assertions.assertArrayEquals; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.metricshub.winrm.Utils.EMPTY; -import static org.metricshub.winrm.WinRMHttpProtocolEnum.HTTP; -import static org.metricshub.winrm.WinRMHttpProtocolEnum.HTTPS; -import static org.metricshub.winrm.service.WinRMEndpoint.buildEndpointUrl; -import static org.metricshub.winrm.service.WinRMEndpoint.buildNamespace; -import static org.metricshub.winrm.service.WinRMEndpoint.getEndpointPort; - -import org.junit.jupiter.api.Test; - -class WinRMEndpointTest { - - private static final String HOSTNAME = "host"; - private static final int PORT = 443; - private static final String USER = "user"; - private static final char[] PASSWORD = { 'p', 'a', 's', 's' }; - - @Test - void testWinRMEndpoint() { - //check arguments - assertThrows(IllegalArgumentException.class, () -> new WinRMEndpoint(HTTP, null, 5985, USER, PASSWORD, null)); - - assertThrows(IllegalArgumentException.class, () -> new WinRMEndpoint(HTTP, HOSTNAME, 5985, null, PASSWORD, null)); - - assertThrows(IllegalArgumentException.class, () -> new WinRMEndpoint(HTTP, HOSTNAME, 5985, USER, null, null)); - - // Case user name in form of domain\\user - { - final WinRMEndpoint winRMEndpoint = new WinRMEndpoint( - null, - " \t\r\n host \t\r\n ", - null, - " \t\r\n domain \t\r\n \\ \t\r\n user \t\r\n ", - PASSWORD, - null - ); - - assertEquals(HOSTNAME, winRMEndpoint.getHostname()); - assertEquals("http://host:5985/wsman", winRMEndpoint.getEndpoint()); - assertEquals("domain", winRMEndpoint.getDomain()); - assertEquals(USER, winRMEndpoint.getUsername()); - assertArrayEquals(PASSWORD, winRMEndpoint.getPassword()); - assertEquals("ROOT/CIMV2", winRMEndpoint.getNamespace()); - assertEquals(HTTP, winRMEndpoint.getProtocol()); - } - { - final WinRMEndpoint winRMEndpoint = new WinRMEndpoint(HTTPS, HOSTNAME, PORT, "domain\\user", PASSWORD, "root"); - - assertEquals(HOSTNAME, winRMEndpoint.getHostname()); - assertEquals("https://host:443/wsman", winRMEndpoint.getEndpoint()); - assertEquals("domain", winRMEndpoint.getDomain()); - assertEquals(USER, winRMEndpoint.getUsername()); - assertEquals("root", winRMEndpoint.getNamespace()); - assertArrayEquals(PASSWORD, winRMEndpoint.getPassword()); - assertEquals(HTTPS, winRMEndpoint.getProtocol()); - } - - // Case user name without domain - { - final WinRMEndpoint winRMEndpoint = new WinRMEndpoint(null, HOSTNAME, null, USER, PASSWORD, "root/cimv2"); - - assertEquals(HOSTNAME, winRMEndpoint.getHostname()); - assertEquals("http://host:5985/wsman", winRMEndpoint.getEndpoint()); - assertNull(winRMEndpoint.getDomain()); - assertEquals(USER, winRMEndpoint.getUsername()); - assertEquals("root/cimv2", winRMEndpoint.getNamespace()); - assertArrayEquals(PASSWORD, winRMEndpoint.getPassword()); - assertEquals(HTTP, winRMEndpoint.getProtocol()); - } - { - final WinRMEndpoint winRMEndpoint = new WinRMEndpoint(HTTP, HOSTNAME, PORT, USER, PASSWORD, "SRV\\Microsoft"); - - assertEquals(HOSTNAME, winRMEndpoint.getHostname()); - assertEquals("http://host:443/wsman", winRMEndpoint.getEndpoint()); - assertNull(winRMEndpoint.getDomain()); - assertEquals(USER, winRMEndpoint.getUsername()); - assertEquals("SRV/Microsoft", winRMEndpoint.getNamespace()); - assertArrayEquals(PASSWORD, winRMEndpoint.getPassword()); - assertEquals(HTTP, winRMEndpoint.getProtocol()); - } - } - - @Test - void testBuildEndpointUrl() { - assertEquals("HTTP://host:5985", buildEndpointUrl(HTTP, HOSTNAME, null)); - assertEquals("HTTPS://host:5986", buildEndpointUrl(HTTPS, HOSTNAME, null)); - assertEquals("HTTP://host:443", buildEndpointUrl(HTTP, HOSTNAME, PORT)); - assertEquals("HTTPS://host:443", buildEndpointUrl(HTTPS, HOSTNAME, PORT)); - } - - @Test - void testGetEndPointPort() { - // case port provided - assertEquals(PORT, getEndpointPort(null, PORT)); - assertEquals(PORT, getEndpointPort(HTTPS, PORT)); - assertEquals(PORT, getEndpointPort(HTTP, PORT)); - - assertEquals(5985, getEndpointPort(null, null)); - assertEquals(5985, getEndpointPort(HTTP, null)); - 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)); - assertEquals("ROOT/CIMV2", buildNamespace(EMPTY)); - assertEquals("ROOT/CIMV2", buildNamespace(" \t\r\n ")); - - //case namespace provided - assertEquals("root", buildNamespace("root")); - assertEquals("root/cimv2", buildNamespace(" \t\r\n root \t\r\n / \t\r\n cimv2 \t\r\n ")); - assertEquals("Srv/StandardCimv2", buildNamespace("Srv\\StandardCimv2")); - } -} +package org.metricshub.winrm.service; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.metricshub.winrm.Utils.EMPTY; +import static org.metricshub.winrm.WinRMHttpProtocolEnum.HTTP; +import static org.metricshub.winrm.WinRMHttpProtocolEnum.HTTPS; +import static org.metricshub.winrm.service.WinRMEndpoint.buildEndpointUrl; +import static org.metricshub.winrm.service.WinRMEndpoint.buildNamespace; +import static org.metricshub.winrm.service.WinRMEndpoint.getEndpointPort; + +import org.junit.jupiter.api.Test; + +class WinRMEndpointTest { + + private static final String HOSTNAME = "host"; + private static final int PORT = 443; + private static final String USER = "user"; + private static final char[] PASSWORD = { 'p', 'a', 's', 's' }; + + @Test + void testWinRMEndpoint() { + // check arguments + assertThrows(IllegalArgumentException.class, () -> new WinRMEndpoint(HTTP, null, 5985, USER, PASSWORD, null)); + + assertThrows(IllegalArgumentException.class, () -> new WinRMEndpoint(HTTP, HOSTNAME, 5985, null, PASSWORD, null)); + + assertThrows(IllegalArgumentException.class, () -> new WinRMEndpoint(HTTP, HOSTNAME, 5985, USER, null, null)); + + // Case user name in form of domain\\user + { + final WinRMEndpoint winRMEndpoint = new WinRMEndpoint( + null, + " \t\r\n host \t\r\n ", + null, + " \t\r\n domain \t\r\n \\ \t\r\n user \t\r\n ", + PASSWORD, + null + ); + + assertEquals(HOSTNAME, winRMEndpoint.getHostname()); + assertEquals("http://host:5985/wsman", winRMEndpoint.getEndpoint()); + assertEquals("domain", winRMEndpoint.getDomain()); + assertEquals(USER, winRMEndpoint.getUsername()); + assertArrayEquals(PASSWORD, winRMEndpoint.getPassword()); + assertEquals("ROOT/CIMV2", winRMEndpoint.getNamespace()); + assertEquals(HTTP, winRMEndpoint.getProtocol()); + } + { + final WinRMEndpoint winRMEndpoint = new WinRMEndpoint(HTTPS, HOSTNAME, PORT, "domain\\user", PASSWORD, "root"); + + assertEquals(HOSTNAME, winRMEndpoint.getHostname()); + assertEquals("https://host:443/wsman", winRMEndpoint.getEndpoint()); + assertEquals("domain", winRMEndpoint.getDomain()); + assertEquals(USER, winRMEndpoint.getUsername()); + assertEquals("root", winRMEndpoint.getNamespace()); + assertArrayEquals(PASSWORD, winRMEndpoint.getPassword()); + assertEquals(HTTPS, winRMEndpoint.getProtocol()); + } + + // Case user name without domain + { + final WinRMEndpoint winRMEndpoint = new WinRMEndpoint(null, HOSTNAME, null, USER, PASSWORD, "root/cimv2"); + + assertEquals(HOSTNAME, winRMEndpoint.getHostname()); + assertEquals("http://host:5985/wsman", winRMEndpoint.getEndpoint()); + assertNull(winRMEndpoint.getDomain()); + assertEquals(USER, winRMEndpoint.getUsername()); + assertEquals("root/cimv2", winRMEndpoint.getNamespace()); + assertArrayEquals(PASSWORD, winRMEndpoint.getPassword()); + assertEquals(HTTP, winRMEndpoint.getProtocol()); + } + { + final WinRMEndpoint winRMEndpoint = new WinRMEndpoint(HTTP, HOSTNAME, PORT, USER, PASSWORD, "SRV\\Microsoft"); + + assertEquals(HOSTNAME, winRMEndpoint.getHostname()); + assertEquals("http://host:443/wsman", winRMEndpoint.getEndpoint()); + assertNull(winRMEndpoint.getDomain()); + assertEquals(USER, winRMEndpoint.getUsername()); + assertEquals("SRV/Microsoft", winRMEndpoint.getNamespace()); + assertArrayEquals(PASSWORD, winRMEndpoint.getPassword()); + assertEquals(HTTP, winRMEndpoint.getProtocol()); + } + } + + @Test + void testBuildEndpointUrl() { + assertEquals("HTTP://host:5985", buildEndpointUrl(HTTP, HOSTNAME, null)); + assertEquals("HTTPS://host:5986", buildEndpointUrl(HTTPS, HOSTNAME, null)); + assertEquals("HTTP://host:443", buildEndpointUrl(HTTP, HOSTNAME, PORT)); + assertEquals("HTTPS://host:443", buildEndpointUrl(HTTPS, HOSTNAME, PORT)); + } + + @Test + void testGetEndPointPort() { + // case port provided + assertEquals(PORT, getEndpointPort(null, PORT)); + assertEquals(PORT, getEndpointPort(HTTPS, PORT)); + assertEquals(PORT, getEndpointPort(HTTP, PORT)); + + assertEquals(5985, getEndpointPort(null, null)); + assertEquals(5985, getEndpointPort(HTTP, null)); + 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)); + assertEquals("ROOT/CIMV2", buildNamespace(EMPTY)); + assertEquals("ROOT/CIMV2", buildNamespace(" \t\r\n ")); + + // case namespace provided + assertEquals("root", buildNamespace("root")); + assertEquals("root/cimv2", buildNamespace(" \t\r\n root \t\r\n / \t\r\n cimv2 \t\r\n ")); + assertEquals("Srv/StandardCimv2", buildNamespace("Srv\\StandardCimv2")); + } +} diff --git a/src/test/java/org/metricshub/winrm/service/WinRMExecutorFactoryTest.java b/src/test/java/org/metricshub/winrm/service/WinRMExecutorFactoryTest.java index 554afb5..29baec9 100644 --- a/src/test/java/org/metricshub/winrm/service/WinRMExecutorFactoryTest.java +++ b/src/test/java/org/metricshub/winrm/service/WinRMExecutorFactoryTest.java @@ -35,8 +35,7 @@ void lightBackendSelectedForHttpNtlm() throws Exception { 30000L, null, List.of(AuthenticationEnum.NTLM) - ) - ) { + )) { assertInstanceOf(LightWinRMService.class, executor); assertEquals("testhost", executor.getHostname()); } @@ -52,8 +51,7 @@ void defaultBackendIsLight() throws Exception { 30000L, null, List.of(AuthenticationEnum.NTLM) - ) - ) { + )) { assertInstanceOf(LightWinRMService.class, executor); } } @@ -65,13 +63,12 @@ void cxfBackendRejectedWithRemovalMessage() { System.setProperty(WinRMExecutorFactory.BACKEND_PROPERTY, "cxf"); final WinRMException e = assertThrows( WinRMException.class, - () -> - WinRMExecutorFactory.createInstance( - endpoint(WinRMHttpProtocolEnum.HTTP), - 30000L, - null, - List.of(AuthenticationEnum.NTLM) - ) + () -> WinRMExecutorFactory.createInstance( + endpoint(WinRMHttpProtocolEnum.HTTP), + 30000L, + null, + List.of(AuthenticationEnum.NTLM) + ) ); assertTrue(e.getMessage().contains("removed in winrm-java 2.0.0"), e.getMessage()); } @@ -86,8 +83,7 @@ void defaultBackendAcceptsHttps() throws Exception { 30000L, null, List.of(AuthenticationEnum.NTLM) - ) - ) { + )) { assertInstanceOf(LightWinRMService.class, executor); } } @@ -101,8 +97,7 @@ void lightBackendAcceptsHttps() throws Exception { 30000L, null, List.of(AuthenticationEnum.NTLM) - ) - ) { + )) { assertInstanceOf(LightWinRMService.class, executor); } } @@ -114,13 +109,12 @@ void kerberosOnlyOverHttpRejected() { System.setProperty(WinRMExecutorFactory.BACKEND_PROPERTY, "light"); assertThrows( WinRMException.class, - () -> - WinRMExecutorFactory.createInstance( - endpoint(WinRMHttpProtocolEnum.HTTP), - 30000L, - null, - List.of(AuthenticationEnum.KERBEROS) - ) + () -> WinRMExecutorFactory.createInstance( + endpoint(WinRMHttpProtocolEnum.HTTP), + 30000L, + null, + List.of(AuthenticationEnum.KERBEROS) + ) ); } @@ -135,8 +129,7 @@ void mixedKerberosNtlmFallsBackToNtlmOverHttp() throws Exception { 30000L, null, List.of(AuthenticationEnum.KERBEROS, AuthenticationEnum.NTLM) - ) - ) { + )) { assertInstanceOf(LightWinRMService.class, executor); } } @@ -152,8 +145,7 @@ void kerberosOverHttpsAccepted() throws Exception { 30000L, null, List.of(AuthenticationEnum.KERBEROS) - ) - ) { + )) { assertInstanceOf(LightWinRMService.class, executor); } } @@ -165,13 +157,12 @@ void unsupportedBackendValueRejected() { System.setProperty(WinRMExecutorFactory.BACKEND_PROPERTY, "cxff"); assertThrows( WinRMException.class, - () -> - WinRMExecutorFactory.createInstance( - endpoint(WinRMHttpProtocolEnum.HTTP), - 30000L, - null, - List.of(AuthenticationEnum.NTLM) - ) + () -> WinRMExecutorFactory.createInstance( + endpoint(WinRMHttpProtocolEnum.HTTP), + 30000L, + null, + List.of(AuthenticationEnum.NTLM) + ) ); } diff --git a/src/test/java/org/metricshub/winrm/service/client/auth/AuthenticationEnumTest.java b/src/test/java/org/metricshub/winrm/service/client/auth/AuthenticationEnumTest.java index 127dae2..8dddcaa 100644 --- a/src/test/java/org/metricshub/winrm/service/client/auth/AuthenticationEnumTest.java +++ b/src/test/java/org/metricshub/winrm/service/client/auth/AuthenticationEnumTest.java @@ -1,27 +1,27 @@ -package org.metricshub.winrm.service.client.auth; - -import static java.util.Optional.empty; -import static java.util.Optional.of; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.metricshub.winrm.Utils.EMPTY; -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.AuthenticationEnum.getValueOf; - -import org.junit.jupiter.api.Test; - -class AuthenticationEnumTest { - - @Test - void testGetValueOf() { - assertEquals(empty(), getValueOf(null)); - assertEquals(empty(), getValueOf(EMPTY)); - assertEquals(empty(), getValueOf("unknown")); - assertEquals(of(NTLM), getValueOf(" ntlm ")); - assertEquals(of(NTLM), getValueOf(" Ntlm ")); - assertEquals(of(NTLM), getValueOf(" NTLM ")); - assertEquals(of(KERBEROS), getValueOf(" kerberos ")); - assertEquals(of(KERBEROS), getValueOf(" Kerberos ")); - assertEquals(of(KERBEROS), getValueOf(" KERBEROS ")); - } -} +package org.metricshub.winrm.service.client.auth; + +import static java.util.Optional.empty; +import static java.util.Optional.of; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.metricshub.winrm.Utils.EMPTY; +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.AuthenticationEnum.getValueOf; + +import org.junit.jupiter.api.Test; + +class AuthenticationEnumTest { + + @Test + void testGetValueOf() { + assertEquals(empty(), getValueOf(null)); + assertEquals(empty(), getValueOf(EMPTY)); + assertEquals(empty(), getValueOf("unknown")); + assertEquals(of(NTLM), getValueOf(" ntlm ")); + assertEquals(of(NTLM), getValueOf(" Ntlm ")); + assertEquals(of(NTLM), getValueOf(" NTLM ")); + assertEquals(of(KERBEROS), getValueOf(" kerberos ")); + assertEquals(of(KERBEROS), getValueOf(" Kerberos ")); + assertEquals(of(KERBEROS), getValueOf(" KERBEROS ")); + } +} diff --git a/src/test/java/org/metricshub/winrm/shares/SmbTempShareTest.java b/src/test/java/org/metricshub/winrm/shares/SmbTempShareTest.java index 7896093..f27b481 100644 --- a/src/test/java/org/metricshub/winrm/shares/SmbTempShareTest.java +++ b/src/test/java/org/metricshub/winrm/shares/SmbTempShareTest.java @@ -1,140 +1,139 @@ -package org.metricshub.winrm.shares; - -import static java.nio.file.Paths.get; -import static java.util.Collections.singletonList; -import static java.util.concurrent.TimeUnit.SECONDS; -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.WindowsTempShare.getOrCreateShare; -import static org.metricshub.winrm.service.client.auth.AuthenticationEnum.NTLM; -import static org.metricshub.winrm.shares.SmbTempShare.createInstance; -import static org.metricshub.winrm.shares.SmbTempShare.createSmbClient; -import static org.mockito.ArgumentMatchers.any; -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.doReturn; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.mockStatic; - -import com.hierynomus.security.bc.BCSecurityProvider; -import com.hierynomus.smbj.SMBClient; -import com.hierynomus.smbj.SmbConfig; -import com.hierynomus.smbj.SmbConfig.Builder; -import com.hierynomus.smbj.auth.AuthenticationContext; -import com.hierynomus.smbj.connection.Connection; -import com.hierynomus.smbj.session.Session; -import com.hierynomus.smbj.share.DiskShare; -import java.nio.file.Path; -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.WinRMExecutorFactory; -import org.metricshub.winrm.service.client.auth.AuthenticationEnum; -import org.mockito.MockedStatic; - -class SmbTempShareTest { - - @SuppressWarnings("unchecked") - @Test - void testCreateInstance() throws Exception { - final String hostname = "host"; - final String username = "user"; - final char[] password = "pwd".toCharArray(); - final WinRMEndpoint winRMEndpoint = new WinRMEndpoint(null, hostname, null, "domain\\" + username, password, null); - final long timeout = 30 * 1000L; - final Path ticketCache = 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)); - - try ( - 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 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(executor), anyLong(), any(ShareRemoteDirectoryConsumer.class))) - .thenReturn(windowsTempShare); - doReturn("\\\\2001-db8--85b-3c51-f5ff-ffdb.ipv6-literal.net\\SEN_ShareFor_PC-TEST$") - .when(windowsTempShare) - .getUncSharePath(); - doReturn("Windows\\Temp\\SEN_ShareFor_TEST$").when(windowsTempShare).getRemotePath(); - doReturn("SEN_ShareFor_PC-TEST$").when(windowsTempShare).getShareName(); - - final Builder smbConfigBuilder = mock(Builder.class); - mockedSmbConfig.when(SmbConfig::builder).thenReturn(smbConfigBuilder); - - doReturn(smbConfigBuilder).when(smbConfigBuilder).withSecurityProvider(any(BCSecurityProvider.class)); - - doReturn(smbConfigBuilder).when(smbConfigBuilder).withTimeout(anyLong(), eq(SECONDS)); - - final SmbConfig smbConfig = mock(SmbConfig.class); - doReturn(smbConfig).when(smbConfigBuilder).build(); - - final SMBClient smbClient = mock(SMBClient.class); - mockedSmbTempShare.when(() -> createSmbClient(smbConfig)).thenReturn(smbClient); - - final Connection connection = mock(Connection.class); - doReturn(connection).when(smbClient).connect(anyString()); - - final Session session = mock(Session.class); - doReturn(session).when(connection).authenticate(any(AuthenticationContext.class)); - - final DiskShare diskShare = mock(DiskShare.class); - doReturn(diskShare).when(session).connectShare(anyString()); - - mockedSmbTempShare - .when(() -> createInstance(any(WinRMEndpoint.class), anyLong(), isNull(), isNull())) - .thenCallRealMethod(); - - final SmbTempShare smbTempShare1 = createInstance(winRMEndpoint, timeout, null, null); - assertNotNull(smbTempShare1); - assertEquals(1, smbTempShare1.getUseCount()); - 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(executor, smbTempShare2.getWindowsRemoteExecutor()); - assertTrue(smbTempShare1.isConnected()); - assertTrue(smbTempShare2.isConnected()); - - smbTempShare1.close(); - assertTrue(smbTempShare1.isConnected()); - assertTrue(smbTempShare2.isConnected()); - assertEquals(1, smbTempShare1.getUseCount()); - assertEquals(1, smbTempShare2.getUseCount()); - - smbTempShare2.close(); - assertFalse(smbTempShare1.isConnected()); - assertFalse(smbTempShare2.isConnected()); - assertEquals(0, smbTempShare1.getUseCount()); - assertEquals(0, smbTempShare2.getUseCount()); - } - } -} +package org.metricshub.winrm.shares; + +import static java.nio.file.Paths.get; +import static java.util.Collections.singletonList; +import static java.util.concurrent.TimeUnit.SECONDS; +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.WindowsTempShare.getOrCreateShare; +import static org.metricshub.winrm.service.client.auth.AuthenticationEnum.NTLM; +import static org.metricshub.winrm.shares.SmbTempShare.createInstance; +import static org.metricshub.winrm.shares.SmbTempShare.createSmbClient; +import static org.mockito.ArgumentMatchers.any; +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.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; + +import com.hierynomus.security.bc.BCSecurityProvider; +import com.hierynomus.smbj.SMBClient; +import com.hierynomus.smbj.SmbConfig; +import com.hierynomus.smbj.SmbConfig.Builder; +import com.hierynomus.smbj.auth.AuthenticationContext; +import com.hierynomus.smbj.connection.Connection; +import com.hierynomus.smbj.session.Session; +import com.hierynomus.smbj.share.DiskShare; +import java.nio.file.Path; +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.WinRMExecutorFactory; +import org.metricshub.winrm.service.client.auth.AuthenticationEnum; +import org.mockito.MockedStatic; + +class SmbTempShareTest { + + @SuppressWarnings("unchecked") + @Test + void testCreateInstance() throws Exception { + final String hostname = "host"; + final String username = "user"; + final char[] password = "pwd".toCharArray(); + final WinRMEndpoint winRMEndpoint = new WinRMEndpoint(null, hostname, null, "domain\\" + username, password, null); + final long timeout = 30 * 1000L; + final Path ticketCache = 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)); + + try ( + 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 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(executor), anyLong(), any(ShareRemoteDirectoryConsumer.class))) + .thenReturn(windowsTempShare); + doReturn("\\\\2001-db8--85b-3c51-f5ff-ffdb.ipv6-literal.net\\SEN_ShareFor_PC-TEST$") + .when(windowsTempShare) + .getUncSharePath(); + doReturn("Windows\\Temp\\SEN_ShareFor_TEST$").when(windowsTempShare).getRemotePath(); + doReturn("SEN_ShareFor_PC-TEST$").when(windowsTempShare).getShareName(); + + final Builder smbConfigBuilder = mock(Builder.class); + mockedSmbConfig.when(SmbConfig::builder).thenReturn(smbConfigBuilder); + + doReturn(smbConfigBuilder).when(smbConfigBuilder).withSecurityProvider(any(BCSecurityProvider.class)); + + doReturn(smbConfigBuilder).when(smbConfigBuilder).withTimeout(anyLong(), eq(SECONDS)); + + final SmbConfig smbConfig = mock(SmbConfig.class); + doReturn(smbConfig).when(smbConfigBuilder).build(); + + final SMBClient smbClient = mock(SMBClient.class); + mockedSmbTempShare.when(() -> createSmbClient(smbConfig)).thenReturn(smbClient); + + final Connection connection = mock(Connection.class); + doReturn(connection).when(smbClient).connect(anyString()); + + final Session session = mock(Session.class); + doReturn(session).when(connection).authenticate(any(AuthenticationContext.class)); + + final DiskShare diskShare = mock(DiskShare.class); + doReturn(diskShare).when(session).connectShare(anyString()); + + mockedSmbTempShare + .when(() -> createInstance(any(WinRMEndpoint.class), anyLong(), isNull(), isNull())) + .thenCallRealMethod(); + + final SmbTempShare smbTempShare1 = createInstance(winRMEndpoint, timeout, null, null); + assertNotNull(smbTempShare1); + assertEquals(1, smbTempShare1.getUseCount()); + 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(executor, smbTempShare2.getWindowsRemoteExecutor()); + assertTrue(smbTempShare1.isConnected()); + assertTrue(smbTempShare2.isConnected()); + + smbTempShare1.close(); + assertTrue(smbTempShare1.isConnected()); + assertTrue(smbTempShare2.isConnected()); + assertEquals(1, smbTempShare1.getUseCount()); + assertEquals(1, smbTempShare2.getUseCount()); + + smbTempShare2.close(); + assertFalse(smbTempShare1.isConnected()); + assertFalse(smbTempShare2.isConnected()); + assertEquals(0, smbTempShare1.getUseCount()); + assertEquals(0, smbTempShare2.getUseCount()); + } + } +} diff --git a/src/test/java/org/metricshub/winrm/wql/WinRMWqlExecutorTest.java b/src/test/java/org/metricshub/winrm/wql/WinRMWqlExecutorTest.java index 5090ddd..7aa6afd 100644 --- a/src/test/java/org/metricshub/winrm/wql/WinRMWqlExecutorTest.java +++ b/src/test/java/org/metricshub/winrm/wql/WinRMWqlExecutorTest.java @@ -1,120 +1,120 @@ -package org.metricshub.winrm.wql; - -import static java.nio.file.Paths.get; -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.assertThrows; -import static org.metricshub.winrm.WinRMHttpProtocolEnum.HTTPS; -import static org.metricshub.winrm.service.client.auth.AuthenticationEnum.NTLM; -import static org.metricshub.winrm.wql.WinRMWqlExecutor.executeWql; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyLong; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.ArgumentMatchers.isNull; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.mockStatic; - -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.HashMap; -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.WinRMExecutorFactory; -import org.metricshub.winrm.service.client.auth.AuthenticationEnum; -import org.mockito.MockedStatic; - -class WinRMWqlExecutorTest { - - @Test - void testExecute() throws Exception { - final String wqlQuery = "Select Name,Path from Win32_Share"; - final String hostname = "host"; - final String username = "user"; - final char[] password = "pass".toCharArray(); - final long timeout = 30 * 1000L; - final Path ticketCache = get("path"); - final List authentications = singletonList(NTLM); - - // check arguments - assertThrows( - IllegalArgumentException.class, - () -> executeWql(HTTPS, null, 5986, username, password, null, wqlQuery, timeout, ticketCache, authentications) - ); - - assertThrows( - IllegalArgumentException.class, - () -> executeWql(HTTPS, hostname, 5986, null, password, null, wqlQuery, timeout, ticketCache, authentications) - ); - - assertThrows( - IllegalArgumentException.class, - () -> executeWql(HTTPS, hostname, 5986, username, null, null, wqlQuery, timeout, ticketCache, authentications) - ); - - assertThrows( - IllegalArgumentException.class, - () -> executeWql(HTTPS, hostname, 5986, username, password, null, null, timeout, ticketCache, authentications) - ); - - assertThrows( - IllegalArgumentException.class, - () -> executeWql(HTTPS, hostname, 5986, username, password, null, wqlQuery, -1L, ticketCache, authentications) - ); - - assertThrows( - IllegalArgumentException.class, - () -> executeWql(HTTPS, hostname, 5986, username, password, null, wqlQuery, 0L, ticketCache, authentications) - ); - - try (final MockedStatic mockedFactory = mockStatic(WinRMExecutorFactory.class)) { - final WindowsRemoteExecutor executor = mock(WindowsRemoteExecutor.class); - - final List> result = new ArrayList<>(); - { - final Map row = new HashMap<>(); - row.put("Name", "C$"); - row.put("Path", "C:\\"); - result.add(row); - } - { - final Map row = new HashMap<>(); - row.put("Name", "SEN_ShareFor_PC$"); - row.put("Path", "C:\\Windows\\Temp\\SEN_ShareFor_PC$"); - result.add(row); - } - - mockedFactory - .when(() -> WinRMExecutorFactory.createInstance(any(WinRMEndpoint.class), anyLong(), isNull(), isNull())) - .thenReturn(executor); - - doReturn(result).when(executor).executeWql(eq(wqlQuery), anyLong()); - - final WinRMWqlExecutor actual = executeWql( - null, - hostname, - null, - username, - password, - null, - wqlQuery, - timeout, - null, - null - ); - - assertEquals(asList("Name", "Path"), actual.getHeaders()); - - final List> rows = asList( - asList("C$", "C:\\"), - asList("SEN_ShareFor_PC$", "C:\\Windows\\Temp\\SEN_ShareFor_PC$") - ); - - assertEquals(rows, actual.getRows()); - } - } -} +package org.metricshub.winrm.wql; + +import static java.nio.file.Paths.get; +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.assertThrows; +import static org.metricshub.winrm.WinRMHttpProtocolEnum.HTTPS; +import static org.metricshub.winrm.service.client.auth.AuthenticationEnum.NTLM; +import static org.metricshub.winrm.wql.WinRMWqlExecutor.executeWql; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; + +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.HashMap; +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.WinRMExecutorFactory; +import org.metricshub.winrm.service.client.auth.AuthenticationEnum; +import org.mockito.MockedStatic; + +class WinRMWqlExecutorTest { + + @Test + void testExecute() throws Exception { + final String wqlQuery = "Select Name,Path from Win32_Share"; + final String hostname = "host"; + final String username = "user"; + final char[] password = "pass".toCharArray(); + final long timeout = 30 * 1000L; + final Path ticketCache = get("path"); + final List authentications = singletonList(NTLM); + + // check arguments + assertThrows( + IllegalArgumentException.class, + () -> executeWql(HTTPS, null, 5986, username, password, null, wqlQuery, timeout, ticketCache, authentications) + ); + + assertThrows( + IllegalArgumentException.class, + () -> executeWql(HTTPS, hostname, 5986, null, password, null, wqlQuery, timeout, ticketCache, authentications) + ); + + assertThrows( + IllegalArgumentException.class, + () -> executeWql(HTTPS, hostname, 5986, username, null, null, wqlQuery, timeout, ticketCache, authentications) + ); + + assertThrows( + IllegalArgumentException.class, + () -> executeWql(HTTPS, hostname, 5986, username, password, null, null, timeout, ticketCache, authentications) + ); + + assertThrows( + IllegalArgumentException.class, + () -> executeWql(HTTPS, hostname, 5986, username, password, null, wqlQuery, -1L, ticketCache, authentications) + ); + + assertThrows( + IllegalArgumentException.class, + () -> executeWql(HTTPS, hostname, 5986, username, password, null, wqlQuery, 0L, ticketCache, authentications) + ); + + try (final MockedStatic mockedFactory = mockStatic(WinRMExecutorFactory.class)) { + final WindowsRemoteExecutor executor = mock(WindowsRemoteExecutor.class); + + final List> result = new ArrayList<>(); + { + final Map row = new HashMap<>(); + row.put("Name", "C$"); + row.put("Path", "C:\\"); + result.add(row); + } + { + final Map row = new HashMap<>(); + row.put("Name", "SEN_ShareFor_PC$"); + row.put("Path", "C:\\Windows\\Temp\\SEN_ShareFor_PC$"); + result.add(row); + } + + mockedFactory + .when(() -> WinRMExecutorFactory.createInstance(any(WinRMEndpoint.class), anyLong(), isNull(), isNull())) + .thenReturn(executor); + + doReturn(result).when(executor).executeWql(eq(wqlQuery), anyLong()); + + final WinRMWqlExecutor actual = executeWql( + null, + hostname, + null, + username, + password, + null, + wqlQuery, + timeout, + null, + null + ); + + assertEquals(asList("Name", "Path"), actual.getHeaders()); + + final List> rows = asList( + asList("C$", "C:\\"), + asList("SEN_ShareFor_PC$", "C:\\Windows\\Temp\\SEN_ShareFor_PC$") + ); + + assertEquals(rows, actual.getRows()); + } + } +}