View Javadoc
1   package org.metricshub.winrm.service;
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 jakarta.xml.bind.JAXBElement;
24  import jakarta.xml.ws.BindingProvider;
25  import jakarta.xml.ws.soap.SOAPFaultException;
26  import java.io.IOException;
27  import java.io.StringWriter;
28  import java.io.Writer;
29  import java.lang.reflect.Proxy;
30  import java.math.BigDecimal;
31  import java.nio.charset.Charset;
32  import java.nio.charset.StandardCharsets;
33  import java.nio.file.Path;
34  import java.text.DecimalFormat;
35  import java.text.DecimalFormatSymbols;
36  import java.util.ArrayList;
37  import java.util.Collections;
38  import java.util.HashMap;
39  import java.util.List;
40  import java.util.Map;
41  import java.util.Objects;
42  import java.util.Optional;
43  import java.util.concurrent.ConcurrentHashMap;
44  import java.util.concurrent.ExecutionException;
45  import java.util.concurrent.TimeoutException;
46  import java.util.concurrent.atomic.AtomicInteger;
47  import java.util.stream.Collectors;
48  import java.util.stream.IntStream;
49  import javax.xml.namespace.QName;
50  import javax.xml.parsers.DocumentBuilderFactory;
51  import javax.xml.parsers.ParserConfigurationException;
52  import javax.xml.xpath.XPath;
53  import javax.xml.xpath.XPathExpressionException;
54  import javax.xml.xpath.XPathFactory;
55  import org.apache.cxf.Bus;
56  import org.apache.cxf.Bus.BusState;
57  import org.apache.cxf.BusFactory;
58  import org.apache.cxf.endpoint.Client;
59  import org.apache.cxf.transport.http.asyncclient.AsyncHTTPConduit;
60  import org.apache.cxf.transport.http.asyncclient.AsyncHTTPConduitFactory;
61  import org.apache.cxf.transport.http.asyncclient.AsyncHTTPConduitFactory.UseAsyncPolicy;
62  import org.metricshub.winrm.Utils;
63  import org.metricshub.winrm.WindowsRemoteCommandResult;
64  import org.metricshub.winrm.WindowsRemoteExecutor;
65  import org.metricshub.winrm.WmiHelper;
66  import org.metricshub.winrm.exceptions.WinRMException;
67  import org.metricshub.winrm.exceptions.WqlQuerySyntaxException;
68  import org.metricshub.winrm.service.client.WinRMInvocationHandler;
69  import org.metricshub.winrm.service.client.auth.AuthenticationEnum;
70  import org.metricshub.winrm.service.enumeration.Enumerate;
71  import org.metricshub.winrm.service.enumeration.EnumerateResponse;
72  import org.metricshub.winrm.service.enumeration.EnumerationContextType;
73  import org.metricshub.winrm.service.enumeration.FilterType;
74  import org.metricshub.winrm.service.enumeration.Pull;
75  import org.metricshub.winrm.service.enumeration.PullResponse;
76  import org.metricshub.winrm.service.shell.CommandLine;
77  import org.metricshub.winrm.service.shell.CommandStateType;
78  import org.metricshub.winrm.service.shell.DesiredStreamType;
79  import org.metricshub.winrm.service.shell.Receive;
80  import org.metricshub.winrm.service.shell.ReceiveResponse;
81  import org.metricshub.winrm.service.shell.Shell;
82  import org.metricshub.winrm.service.shell.StreamType;
83  import org.metricshub.winrm.service.transfer.ResourceCreated;
84  import org.metricshub.winrm.service.wsman.AnyListType;
85  import org.metricshub.winrm.service.wsman.CommandResponse;
86  import org.metricshub.winrm.service.wsman.Delete;
87  import org.metricshub.winrm.service.wsman.Locale;
88  import org.metricshub.winrm.service.wsman.MixedDataType;
89  import org.metricshub.winrm.service.wsman.OptionSetType;
90  import org.metricshub.winrm.service.wsman.OptionType;
91  import org.metricshub.winrm.service.wsman.SelectorSetType;
92  import org.metricshub.winrm.service.wsman.SelectorType;
93  import org.metricshub.winrm.service.wsman.Signal;
94  import org.w3c.dom.Document;
95  import org.w3c.dom.Element;
96  import org.w3c.dom.Node;
97  import org.w3c.dom.NodeList;
98  
99  public class WinRMService implements WindowsRemoteExecutor {
100 
101 	public static final List<AuthenticationEnum> DEFAULT_AUTHENTICATION = Collections.singletonList(
102 		AuthenticationEnum.NTLM
103 	);
104 
105 	private static final String STDERR = "stderr";
106 	private static final String STDOUT = "stdout";
107 
108 	private static final int MAX_ENVELOPE_SIZE = 153600;
109 
110 	private static final String ENUMERATION_NAMESPACE = "http://schemas.xmlsoap.org/ws/2004/09/enumeration";
111 
112 	private static final String WSMAN_URI = "http://schemas.microsoft.com/wbem/wsman/1";
113 
114 	private static final String DIALECT_WQL = WSMAN_URI + "/WQL";
115 
116 	private static final String SHELL_URI = WSMAN_URI + "/windows/shell";
117 	private static final String COMMAND_RESOURCE_URI = SHELL_URI + "/cmd";
118 	private static final String COMMAND_STATE_DONE = SHELL_URI + "/CommandState/Done";
119 	private static final String TERMINATE_CODE = SHELL_URI + "/signal/terminate";
120 
121 	private static final QName WSEN_ITEMS_QNAME = new QName(ENUMERATION_NAMESPACE, "Items");
122 
123 	private static final QName WSMAN_ITEMS_QNAME = new QName(WinRMInvocationHandler.WSMAN_SCHEMA_NAMESPACE, "Items");
124 
125 	private static final QName WSMAN_END_OF_SEQUENCE_QNAME = new QName(
126 		WinRMInvocationHandler.WSMAN_SCHEMA_NAMESPACE,
127 		"EndOfSequence"
128 	);
129 
130 	private static final QName WSEN_END_OF_SEQUENCE_QNAME = new QName(ENUMERATION_NAMESPACE, "EndOfSequence");
131 
132 	private static final QName WSMAN_XML_FRAGMENT_QNAME = new QName(
133 		WinRMInvocationHandler.WSMAN_SCHEMA_NAMESPACE,
134 		"XmlFragment"
135 	);
136 
137 	private static final DocumentBuilderFactory DOCUMENT_BUILDER_FACTORY = DocumentBuilderFactory.newInstance();
138 
139 	/**
140 	 * If no output is available before the wsman:OperationTimeout expires, the server MUST return a WSManFault with
141 	 *  the Code attribute equal to "2150858793"
142 	 * https://msdn.microsoft.com/en-us/library/cc251676.aspx
143 	 */
144 	private static final String WSMAN_FAULT_CODE_OPERATION_TIMEOUT_EXPIRED = "2150858793";
145 
146 	/**
147 	 * Example response:
148 	 *   [truncated]The request for the Windows Remote Shell with ShellId xxxx-yyyy-ccc... failed because the shell
149 	 *   was not found on the server.
150 	 *   Possible causes are: the specified ShellId is incorrect or the shell no longer exist
151 	 */
152 	private static final String WSMAN_FAULT_CODE_SHELL_WAS_NOT_FOUND = "2150858843";
153 
154 	private static final Locale LOCALE;
155 
156 	static {
157 		LOCALE = new Locale();
158 		LOCALE.setLang(java.util.Locale.US.toLanguageTag());
159 	}
160 
161 	private static final OptionSetType OPTION_SET_CREATE;
162 
163 	static {
164 		final OptionType optNoProfile = new OptionType();
165 		optNoProfile.setName("WINRS_NOPROFILE");
166 		optNoProfile.setValue("true");
167 
168 		final OptionType optCodepage = new OptionType();
169 		optCodepage.setName("WINRS_CODEPAGE");
170 		optCodepage.setValue("437");
171 
172 		OPTION_SET_CREATE = new OptionSetType();
173 		OPTION_SET_CREATE.getOption().add(optNoProfile);
174 		OPTION_SET_CREATE.getOption().add(optCodepage);
175 	}
176 
177 	private static final OptionSetType OPTION_SET_COMMAND;
178 
179 	static {
180 		final OptionType optConsoleModeStdin = new OptionType();
181 		optConsoleModeStdin.setName("WINRS_CONSOLEMODE_STDIN");
182 		optConsoleModeStdin.setValue("true");
183 
184 		final OptionType optSkipCmdShell = new OptionType();
185 		optSkipCmdShell.setName("WINRS_SKIP_CMD_SHELL");
186 		optSkipCmdShell.setValue("false");
187 
188 		OPTION_SET_COMMAND = new OptionSetType();
189 		OPTION_SET_COMMAND.getOption().add(optConsoleModeStdin);
190 		OPTION_SET_COMMAND.getOption().add(optSkipCmdShell);
191 	}
192 
193 	private static final ConcurrentHashMap<WinRMEndpoint, WinRMService> CONNECTIONS_CACHE = new ConcurrentHashMap<>();
194 
195 	private final AtomicInteger useCount = new AtomicInteger(1);
196 
197 	private final WinRMEndpoint winRMEndpoint;
198 	private final Bus bus;
199 	private final WinRMWebService cmdWS;
200 	private final WinRMWebService wqlWS;
201 	private final Client cmdClient;
202 	private final Client wqlClient;
203 	private final String strTimeout;
204 
205 	private SelectorSetType shellSelector = null;
206 
207 	/**
208 	 * The WinRMService constructor.
209 	 *
210 	 * @param winRMEndpoint Endpoint with credentials
211 	 * @param bus Apache CXF Bus
212 	 * @param cmdInvocation The WinRM web service for executing commands
213 	 * @param wqlInvocation The WinRM web service for executing WQL queries
214 	 * @param timeout Timeout in milliseconds
215 	 */
216 	private WinRMService(
217 		final WinRMEndpoint winRMEndpoint,
218 		final Bus bus,
219 		final WinRMInvocationHandler cmdInvocation,
220 		final WinRMInvocationHandler wqlInvocation,
221 		final long timeout
222 	) {
223 		this.winRMEndpoint = winRMEndpoint;
224 		this.bus = bus;
225 		this.cmdWS = createProxyService(cmdInvocation);
226 		this.wqlWS = createProxyService(wqlInvocation);
227 		this.cmdClient = cmdInvocation.getClient();
228 		this.wqlClient = wqlInvocation.getClient();
229 
230 		final BigDecimal timeoutSec = BigDecimal.valueOf(timeout).divide(BigDecimal.valueOf(1000));
231 		final DecimalFormat decimalFormat = new DecimalFormat("PT#.###S", new DecimalFormatSymbols(java.util.Locale.ROOT));
232 		this.strTimeout = decimalFormat.format(timeoutSec);
233 	}
234 
235 	/**
236 	 * Create a WinRMService instance
237 	 *
238 	 * @param winRMEndpoint Endpoint with credentials (mandatory)
239 	 * @param timeout Timeout used for Connection, Connection Request and Receive Request
240 	 * in milliseconds (throws an IllegalArgumentException if negative or zero)
241 	 * @param ticketCache The Ticket Cache path
242 	 * @param authentications List of authentications. only NTLM if absent
243 	 *
244 	 * @return WinRMService instance
245 	 *
246 	 * @throws WinRMException For any problem encountered
247 	 */
248 	public static WinRMService createInstance(
249 		final WinRMEndpoint winRMEndpoint,
250 		final long timeout,
251 		final Path ticketCache,
252 		final List<AuthenticationEnum> authentications
253 	) throws WinRMException {
254 		Utils.checkNonNull(winRMEndpoint, "winRMEndpoint");
255 		Utils.checkArgumentNotZeroOrNegative(timeout, "timeout");
256 
257 		final List<AuthenticationEnum> normalizedAuthentications = authentications == null
258 			? DEFAULT_AUTHENTICATION
259 			: authentications.stream().distinct().collect(Collectors.toList());
260 
261 		try {
262 			return CONNECTIONS_CACHE.compute(
263 				winRMEndpoint,
264 				(key, win) -> {
265 					if (win == null) {
266 						final Bus bus = BusFactory.newInstance().createBus();
267 
268 						// Needed to be async to force the use of Apache HTTP Components client.
269 						// Details at http://cxf.apache.org/docs/asynchronous-client-http-transport.html.
270 						// Apache HTTP Components needed to support NTLM authentication.
271 						bus.getProperties().put(AsyncHTTPConduit.USE_ASYNC, Boolean.TRUE);
272 						bus.getProperties().put(AsyncHTTPConduitFactory.USE_POLICY, UseAsyncPolicy.ALWAYS);
273 
274 						final WinRMInvocationHandler cmdInvocation = createWinRMInvocationHandlerInstance(
275 							winRMEndpoint,
276 							bus,
277 							timeout,
278 							null,
279 							ticketCache,
280 							normalizedAuthentications
281 						);
282 
283 						final WinRMInvocationHandler wqlInvocation = createWinRMInvocationHandlerInstance(
284 							winRMEndpoint,
285 							bus,
286 							timeout,
287 							String.format("%s/wmi/%s/*", WSMAN_URI, winRMEndpoint.getNamespace()),
288 							ticketCache,
289 							normalizedAuthentications
290 						);
291 
292 						return new WinRMService(winRMEndpoint, bus, cmdInvocation, wqlInvocation, timeout);
293 					} else {
294 						synchronized (win) {
295 							win.incrementUseCount();
296 
297 							return win;
298 						}
299 					}
300 				}
301 			);
302 		} catch (final RuntimeException e) {
303 			if (e.getCause() != null) {
304 				final String message = e.getMessage() != null
305 					? String.format(
306 						"%s\n%s: %s",
307 						e.getMessage(),
308 						e.getCause().getClass().getSimpleName(),
309 						e.getCause().getMessage()
310 					)
311 					: String.format("%s: %s", e.getCause().getClass().getSimpleName(), e.getCause().getMessage());
312 				throw new WinRMException(e.getCause(), message);
313 			}
314 
315 			throw new WinRMException(e.getMessage());
316 		}
317 	}
318 
319 	public int getUseCount() {
320 		return useCount.get();
321 	}
322 
323 	/**
324 	 * @return whether this WbemServices instance is connected and usable
325 	 */
326 	public boolean isConnected() {
327 		return getUseCount() > 0;
328 	}
329 
330 	void incrementUseCount() {
331 		useCount.incrementAndGet();
332 	}
333 
334 	/**
335 	 * Check if it's connected. If not, throw an IllegalStateException.
336 	 */
337 	public void checkConnectedFirst() {
338 		if (!isConnected()) {
339 			throw new IllegalStateException("This instance has been closed and a new one must be created.");
340 		}
341 	}
342 
343 	@Override
344 	public void close() {
345 		if (useCount.decrementAndGet() == 0) {
346 			CONNECTIONS_CACHE.remove(winRMEndpoint);
347 
348 			if (shellSelector != null) {
349 				cmdWS.delete(new Delete(), COMMAND_RESOURCE_URI, MAX_ENVELOPE_SIZE, strTimeout, LOCALE, shellSelector);
350 
351 				shellSelector = null;
352 			}
353 
354 			if (cmdClient != null) {
355 				cmdClient.destroy();
356 			}
357 
358 			if (wqlClient != null) {
359 				wqlClient.destroy();
360 			}
361 
362 			if (bus != null && bus.getState() != BusState.SHUTDOWN) {
363 				bus.shutdown(true);
364 			}
365 		}
366 	}
367 
368 	@Override
369 	public WindowsRemoteCommandResult executeCommand(
370 		final String command,
371 		final String workingDirectory,
372 		final Charset charset,
373 		final long timeout
374 	) throws WinRMException, TimeoutException {
375 		Utils.checkNonNull(command, "command");
376 		Utils.checkArgumentNotZeroOrNegative(timeout, "timeout");
377 
378 		checkConnectedFirst();
379 
380 		try {
381 			return Utils.execute(
382 				() -> {
383 					if (getShellSelector() == null) {
384 						create(workingDirectory);
385 					}
386 
387 					try {
388 						final StringWriter stdout = new StringWriter();
389 						final StringWriter stderr = new StringWriter();
390 						final Charset cs = charset != null ? charset : StandardCharsets.UTF_8;
391 
392 						final long start = Utils.getCurrentTimeMillis();
393 						final int statusCode = execute(command, stdout, stderr, cs);
394 						final float executionTime = (Utils.getCurrentTimeMillis() - start) / 1000.0f;
395 
396 						return new WindowsRemoteCommandResult(stdout.toString(), stderr.toString(), executionTime, statusCode);
397 					} catch (final WinRMException e) {
398 						throw new RuntimeException(e);
399 					}
400 				},
401 				timeout
402 			);
403 		} catch (final InterruptedException | ExecutionException e) {
404 			if (e.getCause() != null) {
405 				throw new WinRMException(e.getCause(), e.getCause().getMessage());
406 			}
407 			throw new WinRMException(e);
408 		}
409 	}
410 
411 	@Override
412 	public List<Map<String, Object>> executeWql(final String wqlQuery, final long timeout)
413 		throws WinRMException, WqlQuerySyntaxException, TimeoutException {
414 		Utils.checkNonNull(wqlQuery, "wqlQuery");
415 		if (!WmiHelper.isValidWql(wqlQuery)) {
416 			throw new WqlQuerySyntaxException(wqlQuery);
417 		}
418 		Utils.checkArgumentNotZeroOrNegative(timeout, "timeout");
419 
420 		checkConnectedFirst();
421 
422 		try {
423 			return Utils.execute(
424 				() -> {
425 					final List<Node> nodes = new ArrayList<>();
426 
427 					final EnumerateResponse enumerateResponse = enumerate(wqlQuery);
428 
429 					final boolean endOfSequence = getItemsFrom(enumerateResponse, nodes);
430 					if (!endOfSequence) {
431 						final String nextContextId = getContextIdFrom(enumerateResponse.getEnumerationContext());
432 						pull(nextContextId, nodes);
433 					}
434 
435 					return nodes.stream().map(WinRMService::convertRow).collect(Collectors.toList());
436 				},
437 				timeout
438 			);
439 		} catch (final InterruptedException | ExecutionException e) {
440 			if (e.getCause() != null) {
441 				throw new WinRMException(e.getCause(), e.getCause().getMessage());
442 			}
443 			throw new WinRMException(e);
444 		}
445 	}
446 
447 	public static WinRMInvocationHandler createWinRMInvocationHandlerInstance(
448 		final WinRMEndpoint winRMEndpoint,
449 		final Bus bus,
450 		final long timeout,
451 		final String resourceUri,
452 		final Path ticketCache,
453 		final List<AuthenticationEnum> authentications
454 	) {
455 		return new WinRMInvocationHandler(winRMEndpoint, bus, timeout, resourceUri, ticketCache, authentications);
456 	}
457 
458 	private static WinRMWebService createProxyService(final WinRMInvocationHandler winRMInvocationHandler) {
459 		return (WinRMWebService) Proxy.newProxyInstance(
460 			WinRMWebService.class.getClassLoader(),
461 			new Class[] { WinRMWebService.class, BindingProvider.class },
462 			winRMInvocationHandler
463 		);
464 	}
465 
466 	public EnumerateResponse enumerate(final String wqlQuery) {
467 		final FilterType filterType = new FilterType();
468 		filterType.setDialect(DIALECT_WQL);
469 		filterType.getContent().add(wqlQuery);
470 
471 		final Enumerate body = new Enumerate();
472 		body.setFilter(filterType);
473 
474 		return wqlWS.enumerate(body);
475 	}
476 
477 	public String pull(final String contextId, final List<Node> nodes) throws WinRMException {
478 		final EnumerationContextType enumContext = new EnumerationContextType();
479 		enumContext.getContent().add(contextId);
480 
481 		final Pull body = new Pull();
482 		body.setEnumerationContext(enumContext);
483 
484 		final PullResponse response = wqlWS.pull(body);
485 
486 		if (response == null) {
487 			throw new WinRMException(String.format("Pull failed for context id: %s", contextId));
488 		}
489 
490 		final boolean endOfSequence = getItemsFrom(response, nodes);
491 		final String nextContextId = response.getEnumerationContext() == null
492 			? null // The PullResponse will not contain an EnumerationContext if EndOfSequence is set
493 			: getContextIdFrom(response.getEnumerationContext());
494 
495 		return endOfSequence ? nextContextId : pull(nextContextId, nodes); // If we're pulling recursively, and we haven't hit the last element, continue pulling
496 	}
497 
498 	public ResourceCreated create(final String workingDirectory) {
499 		final Shell shell = new Shell();
500 		shell.getInputStreams().add("stdin");
501 		shell.getOutputStreams().add(STDOUT);
502 		shell.getOutputStreams().add(STDERR);
503 
504 		if (Utils.isNotBlank(workingDirectory)) {
505 			shell.setWorkingDirectory(workingDirectory);
506 		}
507 
508 		final ResourceCreated resourceCreated = cmdWS.create(
509 			shell,
510 			COMMAND_RESOURCE_URI,
511 			MAX_ENVELOPE_SIZE,
512 			strTimeout,
513 			LOCALE,
514 			OPTION_SET_CREATE
515 		);
516 
517 		final String shellId = getShellId(resourceCreated);
518 
519 		shellSelector = new SelectorSetType();
520 		final SelectorType selectorType = new SelectorType();
521 		selectorType.setName("ShellId");
522 		selectorType.getContent().add(shellId);
523 		shellSelector.getSelector().add(selectorType);
524 
525 		return resourceCreated;
526 	}
527 
528 	public int execute(final String command, final Writer out, final Writer err, final Charset charset)
529 		throws WinRMException {
530 		final CommandLine body = new CommandLine();
531 		body.setCommand(command);
532 
533 		final CommandResponse commandResponse = cmdWS.command(
534 			body,
535 			COMMAND_RESOURCE_URI,
536 			MAX_ENVELOPE_SIZE,
537 			strTimeout,
538 			LOCALE,
539 			shellSelector,
540 			OPTION_SET_COMMAND
541 		);
542 
543 		final String commandId = commandResponse.getCommandId();
544 
545 		try {
546 			return receiveCommand(commandId, out, err, charset);
547 		} finally {
548 			try {
549 				final Signal signal = new Signal();
550 				signal.setCommandId(commandId);
551 				signal.setCode(TERMINATE_CODE);
552 
553 				cmdWS.signal(signal, COMMAND_RESOURCE_URI, MAX_ENVELOPE_SIZE, strTimeout, LOCALE, shellSelector);
554 			} catch (final SOAPFaultException soapFault) {
555 				assertFaultCode(soapFault, WSMAN_FAULT_CODE_SHELL_WAS_NOT_FOUND, true);
556 			}
557 		}
558 	}
559 
560 	private int receiveCommand(final String commandId, final Writer out, final Writer err, final Charset charset)
561 		throws WinRMException {
562 		while (true) {
563 			final DesiredStreamType stream = new DesiredStreamType();
564 			stream.setCommandId(commandId);
565 			stream.setValue("stdout stderr");
566 
567 			final Receive receive = new Receive();
568 			receive.setDesiredStream(stream);
569 
570 			try {
571 				final ReceiveResponse receiveResponse = cmdWS.receive(
572 					receive,
573 					COMMAND_RESOURCE_URI,
574 					MAX_ENVELOPE_SIZE,
575 					strTimeout,
576 					LOCALE,
577 					shellSelector
578 				);
579 				getStreams(receiveResponse, out, err, charset);
580 
581 				final CommandStateType state = receiveResponse.getCommandState();
582 				if (COMMAND_STATE_DONE.equals(state.getState())) {
583 					return state.getExitCode().intValue();
584 				}
585 			} catch (final SOAPFaultException soapFault) {
586 				// If such Exception which has a code 2150858793 the client is expected to again trigger immediately
587 				// a receive request. https://msdn.microsoft.com/en-us/library/cc251676.aspx
588 				assertFaultCode(soapFault, WSMAN_FAULT_CODE_OPERATION_TIMEOUT_EXPIRED, false);
589 			}
590 		}
591 	}
592 
593 	private static Map<String, Object> convertRow(final Node node) {
594 		return IntStream
595 			.range(0, node.getChildNodes().getLength())
596 			.mapToObj(node.getChildNodes()::item)
597 			.filter(Objects::nonNull)
598 			.collect(HashMap::new, (map, child) -> map.put(child.getLocalName(), child.getTextContent()), HashMap::putAll);
599 	}
600 
601 	private static String getShellId(final ResourceCreated resourceCreated) {
602 		final XPath xpath = XPathFactory.newInstance().newXPath();
603 
604 		for (final Element element : resourceCreated.getAny()) {
605 			try {
606 				final String shellId = xpath.evaluate("//*[local-name()='Selector' and @Name='ShellId']", element);
607 				if (shellId != null && !shellId.isEmpty()) {
608 					return shellId;
609 				}
610 			} catch (final XPathExpressionException e) {
611 				throw new IllegalStateException(e);
612 			}
613 		}
614 		throw new IllegalStateException("Shell ID not fount in " + resourceCreated);
615 	}
616 
617 	private static void assertFaultCode(final SOAPFaultException soapFault, final String code, final boolean retry) {
618 		try {
619 			final NodeList faultDetails = soapFault.getFault().getDetail().getChildNodes();
620 
621 			for (int i = 0; i < faultDetails.getLength(); i++) {
622 				final Node item = faultDetails.item(i);
623 
624 				if ("WSManFault".equals(item.getLocalName())) {
625 					if (retry && code.equals(item.getAttributes().getNamedItem("Code").getNodeValue())) {
626 						return;
627 					}
628 					throw soapFault;
629 				}
630 			}
631 			throw soapFault;
632 		} catch (final NullPointerException e) {
633 			throw soapFault;
634 		}
635 	}
636 
637 	private void getStreams(
638 		final ReceiveResponse receiveResponse,
639 		final Writer out,
640 		final Writer err,
641 		final Charset charset
642 	) throws WinRMException {
643 		final List<StreamType> streams = receiveResponse.getStream();
644 		for (final StreamType streamType : streams) {
645 			final byte[] value = streamType.getValue();
646 			if (value == null) {
647 				continue;
648 			}
649 
650 			writeStd(out, STDOUT, streamType, value, charset);
651 			writeStd(err, STDERR, streamType, value, charset);
652 		}
653 	}
654 
655 	private void writeStd(
656 		final Writer std,
657 		final String name,
658 		final StreamType streamType,
659 		final byte[] value,
660 		final Charset charset
661 	) throws WinRMException {
662 		if (std == null || !name.equals(streamType.getName())) {
663 			return;
664 		}
665 
666 		try {
667 			if (value.length > 0) {
668 				std.write(new String(value, charset));
669 				std.flush();
670 			}
671 
672 			if (streamType.isEnd() != null && streamType.isEnd().booleanValue()) {
673 				std.close();
674 			}
675 		} catch (final IOException e) {
676 			throw new WinRMException(e);
677 		}
678 	}
679 
680 	/**
681 	 * Retrieves the list of items from the given response, adding them to the given
682 	 * list and returns true if the response contains an 'end-of-sequence' marker.
683 	 * @throws WinRMException
684 	 */
685 	public boolean getItemsFrom(final EnumerateResponse response, final List<Node> items) throws WinRMException {
686 		for (final Object object : response.getAny()) {
687 			if (object instanceof JAXBElement) {
688 				final JAXBElement<?> jaxbElement = (JAXBElement<?>) object;
689 
690 				if (WSEN_ITEMS_QNAME.equals(jaxbElement.getName()) || WSMAN_ITEMS_QNAME.equals(jaxbElement.getName())) {
691 					if (jaxbElement.isNil()) {
692 						// No items
693 					} else if (jaxbElement.getValue() instanceof AnyListType) {
694 						// some items
695 						final AnyListType itemList = (AnyListType) jaxbElement.getValue();
696 						for (final Object item : itemList.getAny()) {
697 							final Node node = toNode(item)
698 								.orElseThrow(() ->
699 									new WinRMException(
700 										"Unsupported element of type %s in EnumerateResponse: %s",
701 										object.getClass(),
702 										object
703 									)
704 								);
705 
706 							items.add(node);
707 						}
708 					} else {
709 						throw new WinRMException(
710 							"Unsupported value in EnumerateResponse Items: %s of type: %s",
711 							jaxbElement.getValue(),
712 							jaxbElement.getValue().getClass()
713 						);
714 					}
715 				} else if (
716 					WSEN_END_OF_SEQUENCE_QNAME.equals(jaxbElement.getName()) ||
717 					WSMAN_END_OF_SEQUENCE_QNAME.equals(jaxbElement.getName())
718 				) {
719 					return true;
720 				} else {
721 					throw new WinRMException(
722 						"Unsupported element in EnumerateResponse: %s with name: %s",
723 						jaxbElement,
724 						jaxbElement.getName()
725 					);
726 				}
727 			} else if (object instanceof Node) {
728 				final Node node = (Node) object;
729 
730 				if (
731 					(WSEN_END_OF_SEQUENCE_QNAME.getNamespaceURI().equals(node.getNamespaceURI()) &&
732 						WSEN_END_OF_SEQUENCE_QNAME.getLocalPart().equals(node.getLocalName())) ||
733 					(WSMAN_END_OF_SEQUENCE_QNAME.getNamespaceURI().equals(node.getNamespaceURI()) &&
734 						WSMAN_END_OF_SEQUENCE_QNAME.getLocalPart().equals(node.getLocalName()))
735 				) {
736 					return true;
737 				}
738 				throw new WinRMException(
739 					"Unsupported node in EnumerateResponse: %s with namespace: %s",
740 					node.toString(),
741 					node.getNamespaceURI()
742 				);
743 			} else {
744 				throw new WinRMException(
745 					"Unsupported element in EnumerateResponse: %s, with type: %s",
746 					object,
747 					object != null ? object.getClass() : null
748 				);
749 			}
750 		}
751 
752 		return false;
753 	}
754 
755 	private static boolean getItemsFrom(final PullResponse response, final List<Node> items) throws WinRMException {
756 		for (final Object item : response.getItems().getAny()) {
757 			final Node node = toNode(item)
758 				.orElseThrow(() ->
759 					new WinRMException(
760 						"The pull response contains an unsupported item %s of type %s",
761 						item,
762 						item != null ? item.getClass() : null
763 					)
764 				);
765 
766 			items.add(node);
767 		}
768 		return response.getEndOfSequence() != null;
769 	}
770 
771 	private static Optional<Node> toNode(final Object item) throws WinRMException {
772 		if (item instanceof Node) {
773 			return Optional.of((Node) item);
774 		}
775 
776 		if (item instanceof JAXBElement) {
777 			final JAXBElement<?> nestedElement = (JAXBElement<?>) item;
778 			if (
779 				WSMAN_XML_FRAGMENT_QNAME.equals(nestedElement.getName()) &&
780 				!nestedElement.isNil() &&
781 				nestedElement.getValue() instanceof MixedDataType
782 			) {
783 				// Create a new document/node that contains the elements within the fragment
784 				final Document document = createNewDocument();
785 				final Element rootElement = document.createElementNS(
786 					WSMAN_XML_FRAGMENT_QNAME.getNamespaceURI(),
787 					WSMAN_XML_FRAGMENT_QNAME.getLocalPart()
788 				);
789 				document.appendChild(rootElement);
790 
791 				final MixedDataType mixed = (MixedDataType) nestedElement.getValue();
792 				for (final Object nestedItem : mixed.getContent()) {
793 					if (nestedItem instanceof String) {
794 						// Skip over whitespace
795 					} else if (nestedItem instanceof Node) {
796 						// Node's can't belong to two different documents, so we need to import it first
797 						final Node nestedNode = document.importNode((Node) nestedItem, true);
798 						rootElement.appendChild(nestedNode);
799 					} else {
800 						throw new WinRMException(
801 							"Unsupported element of type %s in XmlFragment: %s",
802 							nestedItem.getClass(),
803 							nestedItem
804 						);
805 					}
806 				}
807 				return Optional.of(rootElement);
808 			}
809 		}
810 		return Optional.empty();
811 	}
812 
813 	private static Document createNewDocument() throws WinRMException {
814 		// The DocumentBuilderFactory provides no guarantees on thread safety
815 		// so we lock it in order to avoid creating new or separate instances per thread
816 		synchronized (DOCUMENT_BUILDER_FACTORY) {
817 			try {
818 				return DOCUMENT_BUILDER_FACTORY.newDocumentBuilder().newDocument();
819 			} catch (final ParserConfigurationException e) {
820 				throw new WinRMException(e);
821 			}
822 		}
823 	}
824 
825 	public String getContextIdFrom(final EnumerationContextType context) throws WinRMException {
826 		// The content of the EnumerationContext should contain a single string, the context id
827 		if (context == null || context.getContent() == null) {
828 			throw new WinRMException("EnumerationContext %s has no content.", context);
829 		}
830 
831 		if (context.getContent().isEmpty()) {
832 			// The EnumerationContext can be empty if we issue an optimized enumeration
833 			// and all of the records are immediately returned
834 			return null;
835 		}
836 
837 		if (context.getContent().size() == 1) {
838 			final Object content = context.getContent().get(0);
839 			if (content instanceof String) {
840 				return (String) content;
841 			}
842 			throw new WinRMException("Unsupported EnumerationContext content: %s", content);
843 		}
844 
845 		throw new WinRMException(
846 			"EnumerationContext contains too many elements, expected: 1 actual: %d",
847 			context.getContent().size()
848 		);
849 	}
850 
851 	public SelectorSetType getShellSelector() {
852 		return shellSelector;
853 	}
854 
855 	@Override
856 	public String getHostname() {
857 		return winRMEndpoint.getHostname();
858 	}
859 
860 	@Override
861 	public String getUsername() {
862 		return winRMEndpoint.getRawUsername();
863 	}
864 
865 	@Override
866 	public char[] getPassword() {
867 		return winRMEndpoint.getPassword();
868 	}
869 }