io.cloudslang.content.services.WSManRemoteShellService.java Source code

Java tutorial

Introduction

Here is the source code for io.cloudslang.content.services.WSManRemoteShellService.java

Source

/*******************************************************************************
 * (c) Copyright 2017 Hewlett-Packard Development Company, L.P.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Apache License v2.0 which accompany this distribution.
 *
 * The Apache License is available at
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 *******************************************************************************/
package io.cloudslang.content.services;

import io.cloudslang.content.entities.EncoderDecoder;
import io.cloudslang.content.entities.OutputStream;
import io.cloudslang.content.entities.WSManRequestInputs;
import io.cloudslang.content.httpclient.HttpClientInputs;
import io.cloudslang.content.httpclient.CSHttpClient;
import io.cloudslang.content.utils.Constants;
import io.cloudslang.content.utils.ResourceLoader;
import io.cloudslang.content.utils.WSManUtils;
import io.cloudslang.content.utils.XMLUtils;
import org.apache.commons.io.Charsets;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.client.methods.HttpPost;
import org.xml.sax.SAXException;

import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.xpath.XPathExpressionException;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.TimeoutException;

import static io.cloudslang.content.utils.Constants.OutputNames.RETURN_RESULT;

/**
 * Created by giloan on 3/27/2016.
 */
public class WSManRemoteShellService {

    private static final String SERVICE_ADDRESS_PLACEHOLDER_NAME = "$PLACEHOLDER_FOR_SERVICE_NETWORK_ADDRESS";
    private static final String SHELL_ID_PLACEHOLDER_NAME = "$PLACEHOLDER_FOR_SHELL_ID";
    private static final String MESSAGE_ID_PLACEHOLDER_NAME = "$PLACEHOLDER_FOR_MESSAGE_ID";
    private static final String COMMAND_ID_PLACEHOLDER_NAME = "$PLACEHOLDER_FOR_COMMAND_ID";
    private static final String COMMAND_PLACEHOLDER_NAME = "$PLACEHOLDER_FOR_COMMAND";
    private static final String MAX_ENVELOPE_SIZE_PLACEHOLDER_NAME = "$PLACEHOLDER_FOR_MAX_ENVELOPE_SIZE";
    private static final String OPERATION_TIMEOUT_PLACEHOLDER_NAME = "$PLACEHOLDER_FOR_OPERATION_TIMEOUT";
    private static final String WINRM_LOCALE_PLACEHOLDER_NAME = "$PLACEHOLDER_FOR_LOCALE";

    private static final String CREATE_RESPONSE_SHELL_ID_XPATH = "/Envelope/Body/ResourceCreated/ReferenceParameters/SelectorSet/Selector[@Name='ShellId']/text()";
    private static final String COMMAND_RESULT_COMMAND_ID_XPATH = "/Envelope/Body/CommandResponse/CommandId";
    private static final String RECEIVE_RESPONSE_XPATH = "/Envelope/Body/ReceiveResponse/Stream[@Name='%s'][%d]/text()";

    private static final String CREATE_RESPONSE_ACTION = "http://schemas.xmlsoap.org/ws/2004/09/transfer/CreateResponse";
    private static final String COMMAND_RESPONSE_ACTION = "http://schemas.microsoft.com/wbem/wsman/1/windows/shell/CommandResponse";
    private static final String RECEIVE_RESPONSE_ACTION = "http://schemas.microsoft.com/wbem/wsman/1/windows/shell/ReceiveResponse";
    private static final String DELETE_RESPONSE_ACTION = "http://schemas.xmlsoap.org/ws/2004/09/transfer/DeleteResponse";
    private static final String CONTENT_TYPE_HEADER = "Content-Type:application/soap+xml;charset=UTF-8";
    private static final String STATUS_CODE = "statusCode";

    private static final String UNAUTHORIZED_STATUS_CODE = "401";
    private static final String WSMAN_RESOURCE_URI = "/wsman";
    private static final String NEW_LINE_SEPARATOR = "\\n";
    private static final String UUID_LABEL = "uuid:";
    private static final String PLACEHOLDER_NOT_FOUND = "Resource does not contain the expected placeholder name: ";
    private static final String CREATE_SHELL_REQUEST_XML = "templates/CreateShell.xml";
    private static final String EXECUTE_COMMAND_REQUEST_XML = "templates/ExecuteCommand.xml";
    private static final String RECEIVE_REQUEST_XML = "templates/Receive.xml";
    private static final String DELETE_SHELL_REQUEST_XML = "templates/DeleteShell.xml";
    private static final String UNEXPECTED_SERVICE_RESPONSE = "Unexpected service response: ";
    private static final String SHELL_ID = "shellId";
    private static final String COMMAND_ID = "commandId";
    private static final String EXECUTION_TIMED_OUT = "The script execution timed out!";
    private static final String COMMAND_ID_NOT_RETRIEVED = "The command id could not be retrieved.";
    private static final String SHELL_ID_NOT_RETRIEVED = "The shell id could not be retrieved.";
    private static final String POWERSHELL_SCRIPT_PREFIX = "PowerShell -NonInteractive -EncodedCommand";
    private static final String UNAUTHORIZED_EXCEPTION_MESSAGE = "Unauthorized! Service responded with 401 status code!";

    private long commandExecutionStartTime;

    /**
     * Executes a command on a remote shell by communicating with the WinRM server from the remote host.
     * Method creates a shell, runs a command on the shell, waits for the command execution to finnish, retrieves the result then deletes the shell.
     *
     * @param wsManRequestInputs
     * @return a map with the result of the command and the exit code of the command execution.
     * @throws RuntimeException
     * @throws IOException
     * @throws InterruptedException
     * @throws ParserConfigurationException
     * @throws TransformerException
     * @throws XPathExpressionException
     * @throws TimeoutException
     * @throws URISyntaxException
     * @throws SAXException
     */
    public Map<String, String> runCommand(WSManRequestInputs wsManRequestInputs)
            throws RuntimeException, IOException, InterruptedException, ParserConfigurationException,
            TransformerException, XPathExpressionException, TimeoutException, URISyntaxException, SAXException {
        CSHttpClient csHttpClient = new CSHttpClient();
        HttpClientInputs httpClientInputs = new HttpClientInputs();
        URL url = buildURL(wsManRequestInputs, WSMAN_RESOURCE_URI);
        httpClientInputs = setCommonHttpInputs(httpClientInputs, url, wsManRequestInputs);
        String shellId = createShell(csHttpClient, httpClientInputs, wsManRequestInputs);
        WSManUtils.validateUUID(shellId, SHELL_ID);
        String commandStr = POWERSHELL_SCRIPT_PREFIX + " "
                + EncoderDecoder.encodeStringInBase64(wsManRequestInputs.getScript(), Charsets.UTF_16LE);
        String commandId = executeCommand(csHttpClient, httpClientInputs, shellId, wsManRequestInputs, commandStr);
        WSManUtils.validateUUID(commandId, COMMAND_ID);
        Map<String, String> scriptResults = receiveCommandResult(csHttpClient, httpClientInputs, shellId, commandId,
                wsManRequestInputs);
        deleteShell(csHttpClient, httpClientInputs, shellId, wsManRequestInputs);
        return scriptResults;
    }

    /**
     * Configures the HttpClientInputs object with the most common http parameters.
     *
     * @param httpClientInputs
     * @param url
     * @param wsManRequestInputs
     * @return the configured HttpClientInputs object.
     * @throws MalformedURLException
     */
    private static HttpClientInputs setCommonHttpInputs(HttpClientInputs httpClientInputs, URL url,
            WSManRequestInputs wsManRequestInputs) throws MalformedURLException {
        httpClientInputs.setUrl(url.toString());
        httpClientInputs.setUsername(wsManRequestInputs.getUsername());
        httpClientInputs.setPassword(wsManRequestInputs.getPassword());
        httpClientInputs.setAuthType(wsManRequestInputs.getAuthType());
        httpClientInputs.setKerberosConfFile(wsManRequestInputs.getKerberosConfFile());
        httpClientInputs.setKerberosLoginConfFile(wsManRequestInputs.getKerberosLoginConfFile());
        httpClientInputs.setKerberosSkipPortCheck(wsManRequestInputs.getKerberosSkipPortForLookup());
        httpClientInputs.setTrustAllRoots(wsManRequestInputs.getTrustAllRoots());
        httpClientInputs.setX509HostnameVerifier(wsManRequestInputs.getX509HostnameVerifier());
        httpClientInputs.setProxyHost(wsManRequestInputs.getProxyHost());
        httpClientInputs.setProxyPort(wsManRequestInputs.getProxyPort());
        httpClientInputs.setProxyUsername(wsManRequestInputs.getProxyUsername());
        httpClientInputs.setProxyPassword(wsManRequestInputs.getProxyPassword());
        httpClientInputs.setKeystore(wsManRequestInputs.getKeystore());
        httpClientInputs.setKeystorePassword(wsManRequestInputs.getKeystorePassword());
        httpClientInputs.setTrustKeystore(wsManRequestInputs.getTrustKeystore());
        httpClientInputs.setTrustPassword(wsManRequestInputs.getTrustPassword());
        String headers = httpClientInputs.getHeaders();
        if (StringUtils.isEmpty(headers)) {
            httpClientInputs.setHeaders(CONTENT_TYPE_HEADER);
        } else {
            httpClientInputs.setHeaders(headers + NEW_LINE_SEPARATOR + CONTENT_TYPE_HEADER);
        }
        httpClientInputs.setMethod(HttpPost.METHOD_NAME);
        return httpClientInputs;
    }

    /**
     * This method executes a request with the given CSHttpClient, HttpClientInputs and body.
     *
     * @param csHttpClient
     * @param httpClientInputs
     * @param requestMessage
     * @return the result of the request execution.
     */
    private Map<String, String> executeRequest(CSHttpClient csHttpClient, HttpClientInputs httpClientInputs,
            String requestMessage) {
        httpClientInputs.setBody(requestMessage);
        Map<String, String> requestResponse = csHttpClient.execute(httpClientInputs);
        if (UNAUTHORIZED_STATUS_CODE.equals(requestResponse.get(STATUS_CODE))) {
            throw new RuntimeException(UNAUTHORIZED_EXCEPTION_MESSAGE);
        }
        return requestResponse;
    }

    /**
     * Creates a shell on the remote server and returns the shell id.
     *
     * @param csHttpClient
     * @param httpClientInputs
     * @param wsManRequestInputs
     * @return the id of the created shell.
     * @throws RuntimeException
     * @throws IOException
     * @throws URISyntaxException
     * @throws TransformerException
     * @throws XPathExpressionException
     * @throws SAXException
     * @throws ParserConfigurationException
     */
    private String createShell(CSHttpClient csHttpClient, HttpClientInputs httpClientInputs,
            WSManRequestInputs wsManRequestInputs) throws RuntimeException, IOException, URISyntaxException,
            TransformerException, XPathExpressionException, SAXException, ParserConfigurationException {
        String document = ResourceLoader.loadAsString(CREATE_SHELL_REQUEST_XML);
        document = createCreateShellRequestBody(document, httpClientInputs.getUrl(),
                String.valueOf(wsManRequestInputs.getMaxEnvelopeSize()), wsManRequestInputs.getWinrmLocale(),
                String.valueOf(wsManRequestInputs.getOperationTimeout()));
        Map<String, String> createShellResult = executeRequest(csHttpClient, httpClientInputs, document);
        return getResourceId(createShellResult.get(RETURN_RESULT), CREATE_RESPONSE_ACTION,
                CREATE_RESPONSE_SHELL_ID_XPATH, SHELL_ID_NOT_RETRIEVED);
    }

    /**
     * Executes a command on the given shell.
     *
     * @param csHttpClient
     * @param httpClientInputs
     * @param shellId
     * @param wsManRequestInputs
     * @return the command id.
     * @throws RuntimeException
     * @throws IOException
     * @throws URISyntaxException
     * @throws TransformerException
     * @throws XPathExpressionException
     * @throws SAXException
     * @throws ParserConfigurationException
     */
    private String executeCommand(CSHttpClient csHttpClient, HttpClientInputs httpClientInputs, String shellId,
            WSManRequestInputs wsManRequestInputs, String command)
            throws RuntimeException, IOException, URISyntaxException, TransformerException,
            XPathExpressionException, SAXException, ParserConfigurationException {
        String documentStr = ResourceLoader.loadAsString(EXECUTE_COMMAND_REQUEST_XML);
        documentStr = createExecuteCommandRequestBody(documentStr, httpClientInputs.getUrl(), shellId, command,
                String.valueOf(wsManRequestInputs.getMaxEnvelopeSize()), wsManRequestInputs.getWinrmLocale(),
                String.valueOf(wsManRequestInputs.getOperationTimeout()));
        commandExecutionStartTime = System.currentTimeMillis() / 1000;
        Map<String, String> executeCommandResult = executeRequest(csHttpClient, httpClientInputs, documentStr);
        return getResourceId(executeCommandResult.get(RETURN_RESULT), COMMAND_RESPONSE_ACTION,
                COMMAND_RESULT_COMMAND_ID_XPATH, COMMAND_ID_NOT_RETRIEVED);
    }

    /**
     * Waits for a specific command that is running on a remote shell to finnish it's execution.
     *
     * @param csHttpClient
     * @param httpClientInputs
     * @param shellId
     * @param commandId
     * @param wsManRequestInputs
     * @return the command execution result and exit code.
     * @throws RuntimeException
     * @throws IOException
     * @throws URISyntaxException
     * @throws TransformerException
     * @throws TimeoutException
     * @throws XPathExpressionException
     * @throws SAXException
     * @throws ParserConfigurationException
     * @throws InterruptedException
     */
    private Map<String, String> receiveCommandResult(CSHttpClient csHttpClient, HttpClientInputs httpClientInputs,
            String shellId, String commandId, WSManRequestInputs wsManRequestInputs)
            throws RuntimeException, IOException, URISyntaxException, TransformerException, TimeoutException,
            XPathExpressionException, SAXException, ParserConfigurationException, InterruptedException {
        String documentStr = ResourceLoader.loadAsString(RECEIVE_REQUEST_XML);
        documentStr = createReceiveRequestBody(documentStr, httpClientInputs.getUrl(), shellId, commandId,
                String.valueOf(wsManRequestInputs.getMaxEnvelopeSize()), wsManRequestInputs.getWinrmLocale(),
                String.valueOf(wsManRequestInputs.getOperationTimeout()));
        Map<String, String> receiveResult;
        while (true) {
            receiveResult = executeRequest(csHttpClient, httpClientInputs, documentStr);
            if (executionIsTimedOut(commandExecutionStartTime, wsManRequestInputs.getOperationTimeout())) {
                throw new TimeoutException(EXECUTION_TIMED_OUT);
            } else if (WSManUtils.isSpecificResponseAction(receiveResult.get(RETURN_RESULT),
                    RECEIVE_RESPONSE_ACTION)
                    && WSManUtils.commandExecutionIsDone(receiveResult.get(RETURN_RESULT))) {
                return processCommandExecutionResponse(receiveResult);
            } else if (WSManUtils.isFaultResponse(receiveResult.get(RETURN_RESULT))) {
                throw new RuntimeException(WSManUtils.getResponseFault(receiveResult.get(RETURN_RESULT)));
            }

            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                throw e;
            }
        }

    }

    /**
     * This method retrieves the resource id from the create resource request response and throws the appropriate exceptions in case of failure.
     *
     * @param response
     * @param resourceResponseAction
     * @param resourceIdXpath
     * @param resourceIdExceptionMessage
     * @return
     * @throws ParserConfigurationException
     * @throws SAXException
     * @throws XPathExpressionException
     * @throws IOException
     */
    private String getResourceId(String response, String resourceResponseAction, String resourceIdXpath,
            String resourceIdExceptionMessage)
            throws ParserConfigurationException, SAXException, XPathExpressionException, IOException {
        if (WSManUtils.isSpecificResponseAction(response, resourceResponseAction)) {
            String shellId = XMLUtils.parseXml(response, resourceIdXpath);
            if (StringUtils.isNotBlank(shellId)) {
                return shellId;
            } else {
                throw new RuntimeException(resourceIdExceptionMessage);
            }
        } else if (WSManUtils.isFaultResponse(response)) {
            throw new RuntimeException(WSManUtils.getResponseFault(response));
        } else {
            throw new RuntimeException(UNEXPECTED_SERVICE_RESPONSE + response);
        }
    }

    /**
     * Deletes the remote shell.
     *
     * @param csHttpClient
     * @param httpClientInputs
     * @param shellId
     * @param wsManRequestInputs
     * @throws RuntimeException
     * @throws IOException
     * @throws URISyntaxException
     * @throws TransformerException
     * @throws XPathExpressionException
     * @throws SAXException
     * @throws ParserConfigurationException
     */
    private void deleteShell(CSHttpClient csHttpClient, HttpClientInputs httpClientInputs, String shellId,
            WSManRequestInputs wsManRequestInputs) throws RuntimeException, IOException, URISyntaxException,
            TransformerException, XPathExpressionException, SAXException, ParserConfigurationException {
        String documentStr = ResourceLoader.loadAsString(DELETE_SHELL_REQUEST_XML);
        documentStr = createDeleteShellRequestBody(documentStr, httpClientInputs.getUrl(), shellId,
                String.valueOf(wsManRequestInputs.getMaxEnvelopeSize()), wsManRequestInputs.getWinrmLocale(),
                String.valueOf(wsManRequestInputs.getOperationTimeout()));
        Map<String, String> deleteShellResult = executeRequest(csHttpClient, httpClientInputs, documentStr);
        if (WSManUtils.isSpecificResponseAction(deleteShellResult.get(RETURN_RESULT), DELETE_RESPONSE_ACTION)) {
            return;
        } else if (WSManUtils.isFaultResponse(deleteShellResult.get(RETURN_RESULT))) {
            throw new RuntimeException(WSManUtils.getResponseFault(deleteShellResult.get(RETURN_RESULT)));
        } else {
            throw new RuntimeException(UNEXPECTED_SERVICE_RESPONSE + deleteShellResult.get(RETURN_RESULT));
        }
    }

    /**
     * This method separates the stdout and stderr response streams from the received execution response.
     *
     * @param receiveResult A map containing the response from the service.
     * @return a map containing the stdout, stderr streams and the script exit code.
     * @throws ParserConfigurationException
     * @throws SAXException
     * @throws XPathExpressionException
     * @throws IOException
     */
    private Map<String, String> processCommandExecutionResponse(Map<String, String> receiveResult)
            throws ParserConfigurationException, SAXException, XPathExpressionException, IOException {
        Map<String, String> scriptResults = new HashMap<>();
        scriptResults.put(RETURN_RESULT,
                buildResultFromResponseStreams(receiveResult.get(RETURN_RESULT), OutputStream.STDOUT));
        scriptResults.put(Constants.OutputNames.STDERR,
                buildResultFromResponseStreams(receiveResult.get(RETURN_RESULT), OutputStream.STDERR));
        scriptResults.put(Constants.OutputNames.SCRIPT_EXIT_CODE,
                WSManUtils.getScriptExitCode(receiveResult.get(RETURN_RESULT)));
        return scriptResults;
    }

    /**
     * Constructs the executed command response from multiple streams of data containing the encoded result of the execution.
     *
     * @param response
     * @param outputStream
     * @return the decoded result of the command in a string.
     * @throws ParserConfigurationException
     * @throws SAXException
     * @throws XPathExpressionException
     * @throws IOException
     */
    private String buildResultFromResponseStreams(String response, OutputStream outputStream)
            throws ParserConfigurationException, SAXException, XPathExpressionException, IOException {
        StringBuilder commandResult = new StringBuilder();
        int noOfStreams = WSManUtils.countStreamElements(response);
        for (int streamNo = 0; streamNo < noOfStreams; streamNo++) {
            String stream = XMLUtils.parseXml(response,
                    String.format(RECEIVE_RESPONSE_XPATH, outputStream.getValue(), streamNo));
            if (!"DQo=".equals(stream)) {
                commandResult.append(EncoderDecoder.decodeBase64String(stream));
            }
        }
        return commandResult.toString();
    }

    /**
     * Check whether or not the command execution reach the timeout value.
     *
     * @param aStartTime A start time in seconds.
     * @param aTimeout   A timeout value in seconds.
     * @return true if it reaches timeout.
     */
    private boolean executionIsTimedOut(long aStartTime, int aTimeout) {
        if (aTimeout != 0) {
            long now = System.currentTimeMillis() / 1000;
            if ((now - aStartTime) >= aTimeout) {
                return true;
            }
        }
        return false;
    }

    private String createCreateShellRequestBody(String doc, String url, String maxEnvelopeSize, String winrmLocale,
            String operationTimeout) throws RuntimeException {
        doc = replaceCommonPlaceholders(doc, url, maxEnvelopeSize, winrmLocale, operationTimeout);
        return replacePlaceholder(doc, MESSAGE_ID_PLACEHOLDER_NAME, UUID_LABEL + UUID.randomUUID().toString());
    }

    private String createExecuteCommandRequestBody(String doc, String url, String shellId, String command,
            String maxEnvelopeSize, String winrmLocale, String operationTimeout) throws RuntimeException {
        doc = replaceCommonPlaceholders(doc, url, maxEnvelopeSize, winrmLocale, operationTimeout);
        doc = replacePlaceholder(doc, SHELL_ID_PLACEHOLDER_NAME, shellId);
        doc = replacePlaceholder(doc, MESSAGE_ID_PLACEHOLDER_NAME, UUID_LABEL + UUID.randomUUID().toString());
        return replacePlaceholder(doc, COMMAND_PLACEHOLDER_NAME, command);
    }

    private String createReceiveRequestBody(String doc, String url, String shellId, String commandId,
            String maxEnvelopeSize, String winrmLocale, String operationTimeout) throws RuntimeException {
        doc = replaceCommonPlaceholders(doc, url, maxEnvelopeSize, winrmLocale, operationTimeout);
        doc = replacePlaceholder(doc, SHELL_ID_PLACEHOLDER_NAME, shellId);
        doc = replacePlaceholder(doc, MESSAGE_ID_PLACEHOLDER_NAME, UUID_LABEL + UUID.randomUUID().toString());
        return replacePlaceholder(doc, COMMAND_ID_PLACEHOLDER_NAME, commandId);
    }

    private String createDeleteShellRequestBody(String doc, String url, String shellId, String maxEnvelopeSize,
            String winrmLocale, String operationTimeout) throws RuntimeException {
        doc = replaceCommonPlaceholders(doc, url, maxEnvelopeSize, winrmLocale, operationTimeout);
        doc = replacePlaceholder(doc, SHELL_ID_PLACEHOLDER_NAME, shellId);
        return replacePlaceholder(doc, MESSAGE_ID_PLACEHOLDER_NAME, UUID_LABEL + UUID.randomUUID().toString());
    }

    private String replaceCommonPlaceholders(String doc, String url, String maxEnvelopeSize, String winrmLocale,
            String operationTimeout) {
        doc = replacePlaceholder(doc, SERVICE_ADDRESS_PLACEHOLDER_NAME, url);
        doc = replacePlaceholder(doc, MAX_ENVELOPE_SIZE_PLACEHOLDER_NAME, maxEnvelopeSize);
        doc = replacePlaceholder(doc, WINRM_LOCALE_PLACEHOLDER_NAME, winrmLocale);
        return replacePlaceholder(doc, OPERATION_TIMEOUT_PLACEHOLDER_NAME, operationTimeout);
    }

    private String replacePlaceholder(String document, String placeholderName, String placeholderValue)
            throws RuntimeException {
        if (StringUtils.containsIgnoreCase(document, placeholderName)) {
            return StringUtils.replace(document, placeholderName, placeholderValue);
        } else {
            throw new RuntimeException(PLACEHOLDER_NOT_FOUND + placeholderName);
        }
    }

    private URL buildURL(WSManRequestInputs wsManRequestInputs, String resource) throws MalformedURLException {
        return new URL(wsManRequestInputs.getProtocol(), wsManRequestInputs.getHost(),
                Integer.parseInt(wsManRequestInputs.getPort()), resource);
    }
}