View Javadoc
1   package org.sentrysoftware.winrm;
2   
3   /*-
4    * ╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲╱╲
5    * WinRM Java Client
6    * ჻჻჻჻჻჻
7    * Copyright 2023 - 2024 Sentry Software
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  
40  import org.sentrysoftware.winrm.exceptions.WindowsRemoteException;
41  import org.sentrysoftware.winrm.exceptions.WqlQuerySyntaxException;
42  
43  public class WindowsRemoteProcessUtils {
44  
45  	private WindowsRemoteProcessUtils() { }
46  
47  	private static final String DEFAULT_CODESET = "1252";
48  	private static final Charset DEFAULT_CHARSET = Charset.forName("windows-1252");
49  
50  	/**
51  	 * Windows CodeSet to java.nio.charset Charset Code map.
52  	 *
53  	 * @see <a href="https://en.wikipedia.org/wiki/Windows_code_page">Windows code page</a>
54  	 * @see <a href="https://docs.oracle.com/javase/8/docs/technotes/guides/intl/encoding.doc.html">
55  	 * Supported Encodings</a>
56  	 *
57  	 */
58  	private static final Map<String, Charset> CODESET_MAP;
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) throws TimeoutException, WqlQuerySyntaxException, WindowsRemoteException {
104 
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 		final String codeSet = result.stream()
114 				.map(row -> (String) row.get("CodeSet"))
115 				.filter(Objects::nonNull)
116 				.findFirst()
117 				.orElse(DEFAULT_CODESET);
118 
119 		return CODESET_MAP.getOrDefault(codeSet, DEFAULT_CHARSET);
120 	}
121 
122 
123 	/**
124 	 * Builds a new output file name, with 99.9999999% chances of being unique
125 	 * on the remote system
126 	 * 
127 	 * @return file name
128 	 */
129 	public static String buildNewOutputFileName() {
130 		return String.format("SEN_%s_%d_%d",
131 				Utils.getComputerName(),
132 				Utils.getCurrentTimeMillis(),
133 				(long) (Math.random() * 1000000));
134 	}
135 
136 	/**
137 	 * Copy the local files to the share and update the command with their path as seen in the remote system.
138 	 *
139 	 * @param command The command (mandatory)
140 	 * @param localFiles The local files to copy list
141 	 * @param uncSharePath The UNC path of the share
142 	 * @param remotePath The remote path
143 	 * 
144 	 * @return The updated command.
145 	 * 
146 	 * @throws IOException If an I/O error occurs.
147 	 */
148 	public static String copyLocalFilesToShare(
149 			final String command,
150 			final List<String> localFiles,
151 			final String uncSharePath,
152 			final String remotePath) throws IOException {
153 
154 		Utils.checkNonNull(command, "command");
155 
156 		if (localFiles == null || localFiles.isEmpty()) {
157 			return command;
158 		}
159 
160 		Utils.checkNonNull(uncSharePath, "uncSharePath");
161 		Utils.checkNonNull(remotePath, "remotePath");
162 
163 		try {
164 			return localFiles.stream()
165 					.reduce(
166 							command,
167 							(cmd, localFile) -> {
168 								try {
169 									final Path localFilePath = Paths.get(localFile);
170 									final Path remoteFilePath = copyToShare(localFilePath, uncSharePath, remotePath);
171 
172 									return caseInsensitiveReplace(cmd, localFile, remoteFilePath.toString());
173 
174 								} catch (final IOException e) {
175 									throw new RuntimeException(e);
176 								}
177 							});
178 		} catch (final Exception e) {
179 			if (e.getCause() instanceof IOException) {
180 				throw (IOException) e.getCause();
181 			}
182 			throw e;
183 		}
184 	}
185 
186 	/**
187 	 * Copy a file to the share.
188 	 * 
189 	 * If the same file is already present on the share, the copy is not performed.
190 	 * The "last-modified" time is used to determine whether the file needs to be
191 	 * copied or not.
192 	 * 
193 	 * @param localFilePath The path to the file to copy
194 	 * @param uncSharePath The UNC path of the share
195 	 * @param remotePath The remote path
196 	 * 
197 	 * @return the path to the copied file, as seen in the remote system
198 	 * 
199 	 * @throws IOException If an I/O error occurs.
200 	 */
201 	static Path copyToShare(
202 			final Path localFilePath,
203 			final String uncSharePath,
204 			final String remotePath) throws IOException {
205 
206 		final Path targetUncPath = Paths.get(uncSharePath, localFilePath.getFileName().toString());
207 		final Path targetRemotePath = Paths.get(remotePath, localFilePath.getFileName().toString());
208 
209 		if (Files.exists(targetUncPath)) {
210 			final FileTime sourceFileTime = Files.getLastModifiedTime(localFilePath);
211 			final FileTime targetFileTime = Files.getLastModifiedTime(targetUncPath);
212 			if (sourceFileTime.compareTo(targetFileTime) <= 0) {
213 				// File is already present on the target, simply skip the copy operation
214 				return targetRemotePath;
215 			}
216 		}
217 
218 		// Copy
219 		Files.copy(
220 				localFilePath,
221 				targetUncPath,
222 				StandardCopyOption.COPY_ATTRIBUTES,
223 				StandardCopyOption.REPLACE_EXISTING);
224 
225 		// Return the path to the copied file, as seen in the remote system
226 		return targetRemotePath;
227 	}
228 
229 	/**
230 	 * Perform a case-insensitive replace of all occurrences of <em>target</em> string with
231 	 * specified <em>replacement</em>
232 	 * 
233 	 * Similar to <code>String.replace(target, replacement)</code>
234 	 * 
235 	 * @param string The string to parse
236 	 * @param target The string to replace
237 	 * @param replacement The replacement string
238 	 * 
239 	 * @return updated string
240 	 */
241 	static String caseInsensitiveReplace(final String string, final String target, final String replacement) {
242 		return string == null || target == null ? string :
243 			Pattern.compile(target, Pattern.LITERAL | Pattern.CASE_INSENSITIVE)
244 			.matcher(string)
245 			.replaceAll(Matcher.quoteReplacement(replacement == null ? Utils.EMPTY : replacement));
246 	}
247 }