View Javadoc
1   package org.metricshub.winrm;
2   
3   /*-
4    * ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲
5    * WinRM Java Client
6    * ჻჻჻჻჻჻
7    * Copyright 2023 - 2024 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.Objects;
24  import java.util.Optional;
25  import java.util.concurrent.TimeoutException;
26  import org.metricshub.winrm.exceptions.WindowsRemoteException;
27  import org.metricshub.winrm.exceptions.WqlQuerySyntaxException;
28  
29  public class WindowsTempShare {
30  
31  	/** The share name */
32  	private final String shareName;
33  
34  	/** The UNC path of the share. */
35  	private final String uncSharePath;
36  
37  	/** The remote path.*/
38  	private final String remotePath;
39  
40  	/** The WindowsRemoteExecutor instance */
41  	private final WindowsRemoteExecutor windowsRemoteExecutor;
42  
43  	/**
44  	 * Constructor of WindowsTempShare
45  	 *
46  	 * @param windowsRemoteExecutor the WindowsRemoteExecutor instance
47  	 * connected to the remote host (mandatory)
48  	 * @param shareNameOrUnc The name of the share, or its full UNC path (mandatory)
49  	 * @param remotePath The path on the remote system of the directory being shared
50  	 */
51  	public WindowsTempShare(
52  		final WindowsRemoteExecutor windowsRemoteExecutor,
53  		final String shareNameOrUnc,
54  		final String remotePath
55  	) {
56  		Utils.checkNonNull(windowsRemoteExecutor, "windowsRemoteExecutor");
57  		Utils.checkNonNull(shareNameOrUnc, "shareNameOrUnc");
58  
59  		if (shareNameOrUnc.startsWith("\\\\")) {
60  			this.uncSharePath = shareNameOrUnc;
61  			final String[] uncElements = shareNameOrUnc.split("\\\\");
62  			this.shareName = uncElements[3];
63  		} else {
64  			this.uncSharePath = buildUncPath(windowsRemoteExecutor.getHostname(), shareNameOrUnc);
65  			this.shareName = shareNameOrUnc;
66  		}
67  
68  		this.remotePath = remotePath;
69  		this.windowsRemoteExecutor = windowsRemoteExecutor;
70  	}
71  
72  	/**
73  	 * Get the existing share on the host or create it if absent.
74  	 *
75  	 * @param windowsRemoteExecutor WindowsRemoteExecutor instance. (mandatory)
76  	 * @param timeout Timeout in milliseconds. (throws an IllegalArgumentException if negative or zero)
77  	 * @param shareRemoteDirectory ShareRemoteDirectoryConsumer function (mandatory)
78  	 *
79  	 * @return The remote path.
80  	 *
81  	 * @throws TimeoutException To notify userName of timeout.
82  	 * @throws WindowsRemoteException For any problem encountered.
83  	 */
84  	public static WindowsTempShare getOrCreateShare(
85  		final WindowsRemoteExecutor windowsRemoteExecutor,
86  		final long timeout,
87  		final ShareRemoteDirectoryConsumer<WindowsRemoteExecutor, String, String, Long> shareRemoteDirectory
88  	) throws TimeoutException, WindowsRemoteException {
89  		Utils.checkNonNull(windowsRemoteExecutor, "windowsRemoteExecutor");
90  		Utils.checkArgumentNotZeroOrNegative(timeout, "timeout");
91  		Utils.checkNonNull(shareRemoteDirectory, "shareRemoteDirectory");
92  
93  		final long start = Utils.getCurrentTimeMillis();
94  
95  		// Are we targeting a Windows cluster?
96  		// (in which case, things are a bit different)
97  
98  		// Try to get a clustered share
99  		final Optional<WindowsTempShare> clusterShare = getClusterShare(
100 			windowsRemoteExecutor,
101 			TimeoutHelper.getRemainingTime(timeout, start, "No time left to check for cluster share"),
102 			start
103 		);
104 
105 		if (clusterShare.isPresent()) {
106 			return clusterShare.get();
107 		}
108 
109 		// Normal case (non-cluster)
110 		final String shareName = buildShareName();
111 		final Optional<WindowsTempShare> share = getShare(
112 			windowsRemoteExecutor,
113 			shareName,
114 			TimeoutHelper.getRemainingTime(timeout, start, "No time left to get a normal temporary share")
115 		);
116 		if (share.isPresent()) {
117 			return share.get();
118 		}
119 
120 		return createTempShare(
121 			windowsRemoteExecutor,
122 			shareName,
123 			TimeoutHelper.getRemainingTime(timeout, start, "No time left to create the temporary share"),
124 			shareRemoteDirectory
125 		);
126 	}
127 
128 	/**
129 	 * Get the Windows directory.
130 	 *
131 	 * @param windowsRemoteExecutor WindowsRemoteExecutor instance. (mandatory)
132 	 * @param timeout Timeout in milliseconds. (throws an IllegalArgumentException if negative or zero)
133 	 *
134 	 * @return The Windows directory.
135 	 *
136 	 * @throws WindowsRemoteException For any problem encountered.
137 	 * @throws TimeoutException To notify userName of timeout.
138 	 *
139 	 * @see <a href="https://docs.microsoft.com/en-us/windows/win32/cimwin32prov/win32-operatingsystem">
140 	 * Win32_OperatingSystem class</a>
141 	 *
142 	 */
143 	public static String getWindowsDirectory(final WindowsRemoteExecutor windowsRemoteExecutor, final long timeout)
144 		throws WindowsRemoteException, TimeoutException {
145 		Utils.checkNonNull(windowsRemoteExecutor, "windowsRemoteExecutor");
146 		Utils.checkArgumentNotZeroOrNegative(timeout, "timeout");
147 
148 		try {
149 			// Extract the WindowsDirectory property from the first instance and return it (or throw an exception)
150 			return windowsRemoteExecutor
151 				.executeWql("SELECT WindowsDirectory FROM Win32_OperatingSystem", timeout)
152 				.stream()
153 				.limit(1)
154 				.map(row -> (String) row.get("WindowsDirectory"))
155 				.filter(Objects::nonNull)
156 				.findFirst()
157 				.orElseThrow(() ->
158 					new WindowsRemoteException(
159 						String.format("Couldn't identify the Windows root directory on %s.", windowsRemoteExecutor.getHostname())
160 					)
161 				);
162 		} catch (final WqlQuerySyntaxException e) {
163 			throw new WindowsRemoteException(e); // Impossible
164 		}
165 	}
166 
167 	/**
168 	 * Create the remote directory.
169 	 *
170 	 * @param windowsRemoteExecutor WindowsRemoteExecutor instance. (mandatory)
171 	 * @param remotePath The remote path to create
172 	 * @param timeout Timeout in milliseconds
173 	 * @param start start time in milliseconds.
174 	 *
175 	 * @throws WindowsRemoteException For any problem encountered.
176 	 * @throws TimeoutException To notify userName of timeout.
177 	 */
178 	public static void createRemoteDirectory(
179 		final WindowsRemoteExecutor windowsRemoteExecutor,
180 		final String remotePath,
181 		final long timeout,
182 		final long start
183 	) throws WindowsRemoteException, TimeoutException {
184 		Utils.checkNonNull(windowsRemoteExecutor, "windowsRemoteExecutor");
185 
186 		windowsRemoteExecutor.executeCommand(buildCreateRemoteDirectoryCommand(remotePath), null, null, timeout);
187 	}
188 
189 	/**
190 	 * Build a UNC path from hostname and share name.
191 	 *
192 	 * Note: This method ensures compatibility with IPv6 hosts
193 	 *
194 	 * @param hostname Host to connect to. (mandatory)
195 	 * @param share The share
196 	 *
197 	 * @return The UNC path to the share (\\unc(host)\share)
198 	 */
199 	static String buildUncPath(final String hostname, final String share) {
200 		Utils.checkNonNull(hostname, "hostname");
201 
202 		return hostname.contains(":")
203 			? String.format("\\\\%s.ipv6-literal.net\\%s", hostname.replace(":", "-").replace("%", "s"), share)
204 			: String.format("\\\\%s\\%s", hostname, share);
205 	}
206 
207 	/**
208 	 * Create the Windows command for a directory creation.
209 	 *
210 	 * @param remotePath The remote path to create. (mandatory)
211 	 *
212 	 * @return The command to execute.
213 	 */
214 	static String buildCreateRemoteDirectoryCommand(final String remotePath) {
215 		Utils.checkNonBlank(remotePath, "remotePath");
216 
217 		return String.format("CMD.EXE /C IF NOT EXIST \"%s\" MKDIR %s", remotePath, remotePath);
218 	}
219 
220 	/**
221 	 * @param path Root path of the temporary directory that will be used in a cluster. (mandatory)
222 	 *
223 	 * @return Path to the temporary directory
224 	 */
225 	static String buildPathOnCluster(final String path) {
226 		Utils.checkNonNull(path, "path");
227 
228 		return String.format("%s\\Temp\\SEN_TempFor_%s", path, Utils.getComputerName());
229 	}
230 
231 	/**
232 	 * Build the remote temp path name with the folder name.
233 	 *
234 	 * @param folder The folder name. (mandatory)
235 	 * @param shareName The Share Name. (mandatory)
236 	 *
237 	 * @return The remote directory path. (folder\Temp\shareName)
238 	 */
239 	static String buildRemotePath(final String folder, final String shareName) {
240 		Utils.checkNonNull(folder, "folder");
241 		Utils.checkNonBlank(shareName, "shareName");
242 
243 		return String.format("%s\\Temp\\%s", folder, shareName);
244 	}
245 
246 	/**
247 	 * Build the Share name with the computer name.
248 	 *
249 	 * @return The share name.
250 	 */
251 	static String buildShareName() {
252 		return String.format("SEN_ShareFor_%s$", Utils.getComputerName());
253 	}
254 
255 	/**
256 	 * Retrieve an "Admin Share" (like D$, E$, etc.) that is exposed by a cluster.
257 	 *
258 	 * If the targeted system is not a cluster, returns an empty optional.
259 	 *
260 	 * @param windowsRemoteExecutor WindowsRemoteExecutor instance.
261 	 * @param timeout Timeout in milliseconds.
262 	 * @param start start time in milliseconds.
263 	 *
264 	 * @return An optional Map<String, Object> with 2 entries: "Name" and "Path"
265 	 *
266 	 * @throws TimeoutException To notify userName of timeout.
267 	 * @throws WindowsRemoteException For any problem encountered.
268 	 */
269 	static Optional<WindowsTempShare> getClusterShare(
270 		final WindowsRemoteExecutor windowsRemoteExecutor,
271 		final long timeout,
272 		final long start
273 	) throws TimeoutException, WindowsRemoteException {
274 		try {
275 			final Optional<WindowsTempShare> clusterShare = windowsRemoteExecutor
276 				.executeWql(
277 					"SELECT Name,Path FROM Win32_ClusterShare WHERE " +
278 					"ServerName <> '*' AND (Type = 2147483648 OR Type = 3221225472) AND Name LIKE '%\\\\_$'",
279 					timeout
280 				)
281 				.stream()
282 				.limit(1)
283 				.map(row -> // We return a TempShare instance pointing to a subdirectory in this share
284 					new WindowsTempShare(
285 						windowsRemoteExecutor,
286 						buildPathOnCluster((String) row.get("Name")),
287 						buildPathOnCluster((String) row.get("Path"))
288 					)
289 				)
290 				.findFirst();
291 
292 			if (clusterShare.isPresent()) {
293 				// We create the subdirectory (if necessary)
294 				createRemoteDirectory(windowsRemoteExecutor, clusterShare.get().getRemotePath(), timeout, start);
295 			}
296 
297 			return clusterShare;
298 		} catch (final WqlQuerySyntaxException e) {
299 			throw new WindowsRemoteException(e); // Impossible
300 		}
301 	}
302 
303 	/**
304 	 * Retrieve the specified share.
305 	 *
306 	 * @param windowsRemoteExecutor WindowsRemoteExecutor instance
307 	 * @param shareName The share name
308 	 * @param timeout Timeout in milliseconds
309 	 *
310 	 * @return An optional TempShare instance
311 	 *
312 	 * @throws TimeoutException To notify userName of timeout
313 	 * @throws WindowsRemoteException For any problem encountered
314 	 */
315 	static Optional<WindowsTempShare> getShare(
316 		final WindowsRemoteExecutor windowsRemoteExecutor,
317 		final String shareName,
318 		final long timeout
319 	) throws TimeoutException, WindowsRemoteException {
320 		try {
321 			return windowsRemoteExecutor
322 				.executeWql(String.format("SELECT Name,Path FROM Win32_Share WHERE Name = '%s'", shareName), timeout)
323 				.stream()
324 				.limit(1)
325 				.map(row -> new WindowsTempShare(windowsRemoteExecutor, (String) row.get("Name"), (String) row.get("Path")))
326 				.findFirst();
327 		} catch (final WqlQuerySyntaxException e) {
328 			throw new WindowsRemoteException(e); // Impossible
329 		}
330 	}
331 
332 	/**
333 	 * Create a shared temporary folder on the remote
334 	 *
335 	 * @param windowsRemoteExecutor WindowsRemoteExecutor instance.
336 	 * @param shareName The Share Name.
337 	 * @param timeout Timeout in milliseconds.
338 	 * @param shareRemoteDirectory shareRemoteDirectory function
339 	 *
340 	 * @return A TempShare instance
341 	 *
342 	 * @throws WindowsRemoteException For any problem encountered.
343 	 * @throws TimeoutException To notify userName of timeout.
344 	 */
345 	static WindowsTempShare createTempShare(
346 		final WindowsRemoteExecutor windowsRemoteExecutor,
347 		final String shareName,
348 		final long timeout,
349 		final ShareRemoteDirectoryConsumer<WindowsRemoteExecutor, String, String, Long> shareRemoteDirectory
350 	) throws WindowsRemoteException, TimeoutException {
351 		final long start = Utils.getCurrentTimeMillis();
352 
353 		// Find where Windows is installed on the remote system. We will create the share under %WINDIR%\Temp.
354 		final String folder = getWindowsDirectory(windowsRemoteExecutor, timeout);
355 
356 		// Create the folder on the remote system
357 		final String remotePath = buildRemotePath(folder, shareName);
358 		createRemoteDirectory(
359 			windowsRemoteExecutor,
360 			remotePath,
361 			TimeoutHelper.getRemainingTime(timeout, start, "No time left to create the temporary directory"),
362 			start
363 		);
364 
365 		// Create the share
366 		shareRemoteDirectory.apply(windowsRemoteExecutor, remotePath, shareName, timeout);
367 
368 		return new WindowsTempShare(windowsRemoteExecutor, shareName, remotePath);
369 	}
370 
371 	/** Get the share name */
372 	public String getShareName() {
373 		return shareName;
374 	}
375 
376 	/** Get the UNC path of the share */
377 	public String getUncSharePath() {
378 		return uncSharePath;
379 	}
380 
381 	/** Get the remote path */
382 	public String getRemotePath() {
383 		return remotePath;
384 	}
385 
386 	/** Get the WindowsRemoteExecutor instance */
387 	public WindowsRemoteExecutor getWindowsRemoteExecutor() {
388 		return windowsRemoteExecutor;
389 	}
390 }