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.io.IOException;
24  import java.nio.charset.Charset;
25  import java.nio.charset.StandardCharsets;
26  import java.nio.file.Files;
27  import java.nio.file.Path;
28  import java.nio.file.Paths;
29  import java.nio.file.StandardCopyOption;
30  import java.nio.file.attribute.FileTime;
31  import java.util.Collections;
32  import java.util.HashMap;
33  import java.util.List;
34  import java.util.Map;
35  import java.util.Objects;
36  import java.util.concurrent.TimeoutException;
37  import java.util.regex.Matcher;
38  import java.util.regex.Pattern;
39  import org.metricshub.winrm.exceptions.WindowsRemoteException;
40  import org.metricshub.winrm.exceptions.WqlQuerySyntaxException;
41  
42  public class WindowsRemoteProcessUtils {
43  
44  	private WindowsRemoteProcessUtils() {}
45  
46  	private static final String DEFAULT_CODESET = "1252";
47  	private static final Charset DEFAULT_CHARSET = Charset.forName("windows-1252");
48  
49  	/**
50  	 * Windows CodeSet to java.nio.charset Charset Code map.
51  	 *
52  	 * @see <a href="https://en.wikipedia.org/wiki/Windows_code_page">Windows code page</a>
53  	 * @see <a href="https://docs.oracle.com/javase/8/docs/technotes/guides/intl/encoding.doc.html">
54  	 * Supported Encodings</a>
55  	 *
56  	 */
57  	private static final Map<String, Charset> CODESET_MAP;
58  
59  	static {
60  		final Map<String, Charset> map = new HashMap<>();
61  		map.put("1250", Charset.forName("windows-1250"));
62  		map.put("1251", Charset.forName("windows-1251"));
63  		map.put("1252", DEFAULT_CHARSET);
64  		map.put("1253", Charset.forName("windows-1253"));
65  		map.put("1254", Charset.forName("windows-1254"));
66  		map.put("1255", Charset.forName("windows-1255"));
67  		map.put("1256", Charset.forName("windows-1256"));
68  		map.put("1257", Charset.forName("windows-1257"));
69  		map.put("1258", Charset.forName("windows-1258"));
70  		map.put("874", Charset.forName("x-windows-874"));
71  		map.put("932", Charset.forName("Shift_JIS"));
72  		map.put("936", Charset.forName("GBK"));
73  		map.put("949", Charset.forName("EUC-KR"));
74  		map.put("950", Charset.forName("Big5"));
75  		map.put("951", Charset.forName("Big5-HKSCS"));
76  		map.put("28591", StandardCharsets.ISO_8859_1);
77  		map.put("20127", StandardCharsets.US_ASCII);
78  		map.put("65001", StandardCharsets.UTF_8);
79  		map.put("1200", StandardCharsets.UTF_16LE);
80  		map.put("1201", StandardCharsets.UTF_16BE);
81  
82  		CODESET_MAP = Collections.unmodifiableMap(map);
83  	}
84  
85  	/**
86  	 * Get the CharSet from the Win32_OperatingSystem CodeSet. (if not found by default Latin-1 windows-1252)
87  	 *
88  	 * @param windowsRemoteExecutor WindowsRemoteExecutor instance
89  	 * @param timeout Timeout in milliseconds.
90  	 *
91  	 * @return the encoding charset from Win32_OperatingSystem
92  	 *
93  	 * @throws TimeoutException To notify userName of timeout
94  	 * @throws WqlQuerySyntaxException On WQL syntax errors
95  	 * @throws WindowsRemoteException For any problem encountered on remote
96  	 *
97  	 * @see <a href="https://docs.microsoft.com/en-us/windows/win32/cimwin32prov/win32-operatingsystem">
98  	 * Win32_OperatingSystem class</a>
99  	 *
100 	 */
101 	public static Charset getWindowsEncodingCharset(
102 		final WindowsRemoteExecutor windowsRemoteExecutor,
103 		final long timeout
104 	) throws TimeoutException, WqlQuerySyntaxException, WindowsRemoteException {
105 		if (windowsRemoteExecutor == null || timeout < 1) {
106 			return DEFAULT_CHARSET;
107 		}
108 
109 		final List<Map<String, Object>> result = windowsRemoteExecutor.executeWql(
110 			"SELECT CodeSet FROM Win32_OperatingSystem",
111 			timeout
112 		);
113 
114 		final String codeSet = result
115 			.stream()
116 			.map(row -> (String) row.get("CodeSet"))
117 			.filter(Objects::nonNull)
118 			.findFirst()
119 			.orElse(DEFAULT_CODESET);
120 
121 		return CODESET_MAP.getOrDefault(codeSet, DEFAULT_CHARSET);
122 	}
123 
124 	/**
125 	 * Builds a new output file name, with 99.9999999% chances of being unique
126 	 * on the remote system
127 	 *
128 	 * @return file name
129 	 */
130 	public static String buildNewOutputFileName() {
131 		return String.format(
132 			"SEN_%s_%d_%d",
133 			Utils.getComputerName(),
134 			Utils.getCurrentTimeMillis(),
135 			(long) (Math.random() * 1000000)
136 		);
137 	}
138 
139 	/**
140 	 * Copy the local files to the share and update the command with their path as seen in the remote system.
141 	 *
142 	 * @param command The command (mandatory)
143 	 * @param localFiles The local files to copy list
144 	 * @param uncSharePath The UNC path of the share
145 	 * @param remotePath The remote path
146 	 *
147 	 * @return The updated command.
148 	 *
149 	 * @throws IOException If an I/O error occurs.
150 	 */
151 	public static String copyLocalFilesToShare(
152 		final String command,
153 		final List<String> localFiles,
154 		final String uncSharePath,
155 		final String remotePath
156 	) throws IOException {
157 		Utils.checkNonNull(command, "command");
158 
159 		if (localFiles == null || localFiles.isEmpty()) {
160 			return command;
161 		}
162 
163 		Utils.checkNonNull(uncSharePath, "uncSharePath");
164 		Utils.checkNonNull(remotePath, "remotePath");
165 
166 		try {
167 			return localFiles
168 				.stream()
169 				.reduce(
170 					command,
171 					(cmd, localFile) -> {
172 						try {
173 							final Path localFilePath = Paths.get(localFile);
174 							final Path remoteFilePath = copyToShare(localFilePath, uncSharePath, remotePath);
175 
176 							return caseInsensitiveReplace(cmd, localFile, remoteFilePath.toString());
177 						} catch (final IOException e) {
178 							throw new RuntimeException(e);
179 						}
180 					}
181 				);
182 		} catch (final Exception e) {
183 			if (e.getCause() instanceof IOException) {
184 				throw (IOException) e.getCause();
185 			}
186 			throw e;
187 		}
188 	}
189 
190 	/**
191 	 * Copy a file to the share.
192 	 *
193 	 * If the same file is already present on the share, the copy is not performed.
194 	 * The "last-modified" time is used to determine whether the file needs to be
195 	 * copied or not.
196 	 *
197 	 * @param localFilePath The path to the file to copy
198 	 * @param uncSharePath The UNC path of the share
199 	 * @param remotePath The remote path
200 	 *
201 	 * @return the path to the copied file, as seen in the remote system
202 	 *
203 	 * @throws IOException If an I/O error occurs.
204 	 */
205 	static Path copyToShare(final Path localFilePath, final String uncSharePath, final String remotePath)
206 		throws IOException {
207 		final Path targetUncPath = Paths.get(uncSharePath, localFilePath.getFileName().toString());
208 		final Path targetRemotePath = Paths.get(remotePath, localFilePath.getFileName().toString());
209 
210 		if (Files.exists(targetUncPath)) {
211 			final FileTime sourceFileTime = Files.getLastModifiedTime(localFilePath);
212 			final FileTime targetFileTime = Files.getLastModifiedTime(targetUncPath);
213 			if (sourceFileTime.compareTo(targetFileTime) <= 0) {
214 				// File is already present on the target, simply skip the copy operation
215 				return targetRemotePath;
216 			}
217 		}
218 
219 		// Copy
220 		Files.copy(localFilePath, targetUncPath, StandardCopyOption.COPY_ATTRIBUTES, StandardCopyOption.REPLACE_EXISTING);
221 
222 		// Return the path to the copied file, as seen in the remote system
223 		return targetRemotePath;
224 	}
225 
226 	/**
227 	 * Perform a case-insensitive replace of all occurrences of <em>target</em> string with
228 	 * specified <em>replacement</em>
229 	 *
230 	 * Similar to <code>String.replace(target, replacement)</code>
231 	 *
232 	 * @param string The string to parse
233 	 * @param target The string to replace
234 	 * @param replacement The replacement string
235 	 *
236 	 * @return updated string
237 	 */
238 	static String caseInsensitiveReplace(final String string, final String target, final String replacement) {
239 		return string == null || target == null
240 			? string
241 			: Pattern
242 				.compile(target, Pattern.LITERAL | Pattern.CASE_INSENSITIVE)
243 				.matcher(string)
244 				.replaceAll(Matcher.quoteReplacement(replacement == null ? Utils.EMPTY : replacement));
245 	}
246 }