View Javadoc
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 }