1 package org.metricshub.winrm;
2
3 /*-
4 * ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲
5 * WinRM Java Client
6 * ჻჻჻჻჻჻
7 * Copyright (C) 2023 - 2026 MetricsHub
8 * ჻჻჻჻჻჻
9 * Licensed under the Apache License, Version 2.0 (the "License");
10 * you may not use this file except in compliance with the License.
11 * You may obtain a copy of the License at
12 *
13 * http://www.apache.org/licenses/LICENSE-2.0
14 *
15 * Unless required by applicable law or agreed to in writing, software
16 * distributed under the License is distributed on an "AS IS" BASIS,
17 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18 * See the License for the specific language governing permissions and
19 * limitations under the License.
20 * ╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱
21 */
22
23 import java.util.Arrays;
24 import java.util.List;
25 import java.util.Map;
26 import java.util.concurrent.TimeoutException;
27 import java.util.regex.Pattern;
28 import java.util.stream.Collectors;
29 import org.metricshub.winrm.exceptions.WindowsRemoteException;
30 import org.metricshub.winrm.exceptions.WqlQuerySyntaxException;
31
32 public abstract class WmiHelper {
33
34 /**
35 * Private constructor, as this class cannot be instantiated (it's pure static)
36 */
37 private WmiHelper() {}
38
39 public static final String DEFAULT_NAMESPACE = "ROOT\\CIMV2";
40
41 /**
42 * Pattern to detect a simple WQL select query.
43 */
44 private static final Pattern WQL_SIMPLE_SELECT_PATTERN = Pattern.compile(
45 "^\\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+.*)?$",
46 Pattern.CASE_INSENSITIVE
47 );
48
49 /**
50 * Check if the WQL Query respect the simple syntax in the form of
51 * <code>Select * from (where)</code> or <code>Select a,b,c from (where)</code>
52 * is valid.
53 *
54 * @param wqlQuery
55 * @return whether specified WQL query's syntax is valid or not
56 */
57 public static boolean isValidWql(final String wqlQuery) {
58 return WQL_SIMPLE_SELECT_PATTERN.matcher(wqlQuery).find();
59 }
60
61 /**
62 * Execute one of the library's internal housekeeping WQL queries (encoding detection, Windows
63 * directory discovery) explicitly in the {@value #DEFAULT_NAMESPACE} namespace — where the
64 * standard {@code Win32_*} classes live — regardless of the executor's configured default
65 * namespace, which the caller may have pointed at a custom namespace. Executors that do not
66 * support an explicit per-query namespace fall back to their default namespace, preserving the
67 * historical behavior.
68 *
69 * @param windowsRemoteExecutor Executor connected to the remote host
70 * @param wqlQuery The WQL query to run
71 * @param timeout Timeout in milliseconds
72 * @return the query result rows
73 * @throws TimeoutException To notify userName of timeout
74 * @throws WqlQuerySyntaxException On WQL syntax errors
75 * @throws WindowsRemoteException For any problem encountered on the remote host
76 */
77 public static List<Map<String, Object>> executeWqlInCimv2(
78 final WindowsRemoteExecutor windowsRemoteExecutor,
79 final String wqlQuery,
80 final long timeout
81 ) throws TimeoutException, WqlQuerySyntaxException, WindowsRemoteException {
82 try {
83 return windowsRemoteExecutor.executeWql(
84 DEFAULT_NAMESPACE,
85 wqlQuery,
86 timeout,
87 WindowsRemoteExecutor.DEFAULT_WQL_MAX_ELEMENTS,
88 0
89 );
90 } catch (final UnsupportedOperationException e) {
91 return windowsRemoteExecutor.executeWql(wqlQuery, timeout);
92 }
93 }
94
95 /**
96 * The "network resource" is either just the namespace (for localhost), or \\hostname\\namespace.
97 *
98 * @param hostname Host to connect to.
99 * @param namespace The Namespace.
100 * @return resource
101 */
102 public static String createNetworkResource(final String hostname, final String namespace) {
103 Utils.checkNonNull(namespace, "namespace");
104 return hostname == null || hostname.isEmpty() ? namespace : String.format("\\\\%s\\%s", hostname, namespace);
105 }
106
107 /**
108 * @param networkResource Network resource string to test
109 * @return whether specified networkResource is local or not
110 */
111 public static boolean isLocalNetworkResource(final String networkResource) {
112 Utils.checkNonNull(networkResource, "networkResource");
113 return !networkResource.startsWith("\\\\")
114 ||
115 networkResource.startsWith("\\\\localhost\\")
116 ||
117 networkResource.startsWith("\\\\127.0.0.1\\")
118 ||
119 networkResource.startsWith("\\\\0:0:0:0:0:0:0:1\\")
120 ||
121 networkResource.startsWith("\\\\::1\\")
122 ||
123 networkResource.startsWith("\\\\0000:0000:0000:0000:0000:0000:0000:0001\\")
124 ||
125 networkResource.toLowerCase().startsWith("\\\\" + Utils.getComputerName().toLowerCase() + "\\");
126 }
127
128 /**
129 * Extract the exact name of the properties from a WMI result.
130 * The interest is to retrieve the exact case of the property names, instead of
131 * the lowercase that we have at this stage.
132 *
133 * @param resultRows The result whose first row will be parsed
134 * @param wql The WQL query that was used (so we make sure to return the properties in the same order)
135 * @return a list of property names
136 * @throws IllegalStateException if the specified WQL is invalid
137 */
138 public static List<String> extractPropertiesFromResult(final List<Map<String, Object>> resultRows, final String wql) {
139 try {
140 return extractPropertiesFromResult(resultRows, WqlQuery.newInstance(wql));
141 } catch (WqlQuerySyntaxException e) {
142 throw new IllegalStateException(e);
143 }
144 }
145
146 /**
147 * Extract the exact name of the properties from a WMI result.
148 * The interest is to retrieve the exact case of the property names, instead of
149 * the lowercase that we have at this stage.
150 * Note: The exact case cannot be retrieved if result is empty, in which case all
151 * names are reported in lower case
152 *
153 * @param resultRows The result whose first row will be parsed
154 * @param wqlQuery The WQL query that was used (so we make sure to return the properties in the same order)
155 * @return a list of property names
156 */
157 public static List<String> extractPropertiesFromResult(
158 final List<Map<String, Object>> resultRows,
159 final WqlQuery wqlQuery
160 ) {
161 // If resultRows is empty, we won't be able to retrieve the actual property names
162 // with the correct case. So, we simply return the list of specified properties in the
163 // WQL query
164 if (resultRows.isEmpty()) {
165 return wqlQuery.getSelectedProperties();
166 }
167
168 // Extract the actual property names
169 final String[] resultPropertyArray = resultRows.get(0).keySet().toArray(new String[0]);
170
171 // First case: we don't have any specified properties in the WQL Query, so we just
172 // return the properties from the result set in alphabetical order
173 if (wqlQuery.getSelectedProperties().isEmpty()) {
174 Arrays.sort(resultPropertyArray, String.CASE_INSENSITIVE_ORDER);
175 return Arrays.asList(resultPropertyArray);
176 }
177
178 // Create a new list based on queryPropertyArray (with its order), but with the values
179 // from resultPropertyArray
180 final List<String> queryProperties = wqlQuery.getSelectedProperties();
181 final Map<String, String> resultProperties = Arrays
182 .asList(resultPropertyArray)
183 .stream()
184 .collect(Collectors.toMap(String::toLowerCase, property -> property));
185 return queryProperties
186 .stream()
187 .map(property -> resultProperties.getOrDefault(property.toLowerCase(), property))
188 .collect(Collectors.toList());
189 }
190 }