Java tutorial
/* * Copyright (c) 2008-2014, XebiaLabs B.V., All rights reserved. * * * Overthere is licensed under the terms of the GPLv2 * <http://www.gnu.org/licenses/old-licenses/gpl-2.0.html>, like most XebiaLabs Libraries. * There are special exceptions to the terms and conditions of the GPLv2 as it is applied to * this software, see the FLOSS License Exception * <http://github.com/xebialabs/overthere/blob/master/LICENSE>. * * This program is free software; you can redistribute it and/or modify it under the terms * of the GNU General Public License as published by the Free Software Foundation; version 2 * of the License. * * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * See the GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along with this * program; if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth * Floor, Boston, MA 02110-1301 USA */ package com.xebialabs.overthere.cifs.winrm; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.io.Reader; import java.io.StringWriter; import java.io.Writer; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; import java.security.GeneralSecurityException; import java.security.KeyManagementException; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; import java.security.Principal; import java.security.PrivilegedActionException; import java.security.PrivilegedExceptionAction; import java.security.UnrecoverableKeyException; import java.util.Iterator; import java.util.List; import java.util.UUID; import javax.security.auth.Subject; import javax.security.auth.callback.CallbackHandler; import javax.security.auth.kerberos.KerberosPrincipal; import javax.security.auth.login.LoginContext; import javax.security.auth.login.LoginException; import org.apache.commons.codec.binary.Base64; import org.apache.http.Header; import org.apache.http.HttpEntity; import org.apache.http.HttpResponse; import org.apache.http.auth.AuthScope; import org.apache.http.auth.BasicUserPrincipal; import org.apache.http.auth.Credentials; import org.apache.http.client.methods.HttpPost; import org.apache.http.conn.scheme.Scheme; import org.apache.http.conn.ssl.SSLSocketFactory; import org.apache.http.conn.ssl.TrustStrategy; import org.apache.http.conn.ssl.X509HostnameVerifier; import org.apache.http.entity.ContentType; import org.apache.http.entity.StringEntity; import org.apache.http.impl.client.DefaultHttpClient; import org.apache.http.protocol.BasicHttpContext; import org.apache.http.protocol.HttpContext; import org.dom4j.Document; import org.dom4j.DocumentHelper; import org.dom4j.Element; import org.dom4j.QName; import org.dom4j.io.OutputFormat; import org.dom4j.io.XMLWriter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.xebialabs.overthere.cifs.WinrmHttpsCertificateTrustStrategy; import com.xebialabs.overthere.cifs.WinrmHttpsHostnameVerificationStrategy; import com.xebialabs.overthere.cifs.winrm.soap.Action; import com.xebialabs.overthere.cifs.winrm.soap.BodyBuilder; import com.xebialabs.overthere.cifs.winrm.soap.HeaderBuilder; import com.xebialabs.overthere.cifs.winrm.soap.OptionSet; import com.xebialabs.overthere.cifs.winrm.soap.ResourceURI; import com.xebialabs.overthere.cifs.winrm.soap.SoapAction; import com.xebialabs.overthere.cifs.winrm.soap.SoapMessageBuilder; import com.xebialabs.overthere.cifs.winrm.soap.Soapy; import static com.xebialabs.overthere.util.OverthereUtils.closeQuietly; import static org.apache.http.auth.AuthScope.ANY_HOST; import static org.apache.http.auth.AuthScope.ANY_PORT; import static org.apache.http.auth.AuthScope.ANY_REALM; import static org.apache.http.client.params.AuthPolicy.BASIC; import static org.apache.http.client.params.AuthPolicy.KERBEROS; import static org.apache.http.client.params.AuthPolicy.SPNEGO; import static org.apache.http.client.params.ClientPNames.HANDLE_AUTHENTICATION; import static org.apache.http.util.EntityUtils.consume; /** * See http://msdn.microsoft.com/en-us/library/cc251731(v=prot.10).aspx for some examples of how the WS-MAN protocol works on Windows */ public class WinRmClient { private final String username; private final boolean enableKerberos; private final String password; private final URL targetURL; private final String unmappedAddress; private final int unmappedPort; private String winRmTimeout; private int winRmEnvelopSize; private String winRmLocale; private WinrmHttpsCertificateTrustStrategy httpsCertTrustStrategy; private WinrmHttpsHostnameVerificationStrategy httpsHostnameVerifyStrategy; private boolean kerberosUseHttpSpn; private boolean kerberosAddPortToSpn; private boolean kerberosDebug; private String shellId; private String commandId; private int exitValue = -1; private int chunk = 0; public WinRmClient(final String username, final String password, final URL targetURL, final String unmappedAddress, final int unmappedPort) { int posOfAtSign = username.indexOf('@'); if (posOfAtSign >= 0) { String u = username.substring(0, posOfAtSign); String d = username.substring(posOfAtSign + 1); if (d.toUpperCase().equals(d)) { this.username = username; } else { this.username = u + "@" + d.toUpperCase(); logger.warn("Fixing username [{}] to have an upper case domain name [{}]", username, this.username); } this.enableKerberos = true; } else { this.username = username; this.enableKerberos = false; } this.password = password; this.targetURL = targetURL; this.unmappedAddress = unmappedAddress; this.unmappedPort = unmappedPort; } public String createShell() { logger.debug("Sending WinRM Create Shell request"); final Element bodyContent = DocumentHelper.createElement(QName.get("Shell", Namespaces.NS_WIN_SHELL)); bodyContent.addElement(QName.get("InputStreams", Namespaces.NS_WIN_SHELL)).addText("stdin"); bodyContent.addElement(QName.get("OutputStreams", Namespaces.NS_WIN_SHELL)).addText("stdout stderr"); final Document requestDocument = getRequestDocument(Action.WS_ACTION, ResourceURI.RESOURCE_URI_CMD, OptionSet.OPEN_SHELL, bodyContent); Document responseDocument = sendRequest(requestDocument, SoapAction.SHELL); shellId = getFirstElement(responseDocument, ResponseExtractor.SHELL_ID); logger.debug("Received WinRM Create Shell response: shell with ID {} start created", shellId); return shellId; } public String executeCommand(String command) { logger.debug("Sending WinRM Execute Command request to shell {}", shellId); final Element bodyContent = DocumentHelper.createElement(QName.get("CommandLine", Namespaces.NS_WIN_SHELL)); String encoded = "\"" + command + "\""; bodyContent.addElement(QName.get("Command", Namespaces.NS_WIN_SHELL)).addText(encoded); final Document requestDocument = getRequestDocument(Action.WS_COMMAND, ResourceURI.RESOURCE_URI_CMD, OptionSet.RUN_COMMAND, bodyContent); Document responseDocument = sendRequest(requestDocument, SoapAction.COMMAND_LINE); commandId = getFirstElement(responseDocument, ResponseExtractor.COMMAND_ID); logger.debug("Received WinRM Execute Command response to shell {}: command with ID {} was started", shellId, commandId); return commandId; } public boolean receiveOutput(OutputStream stdout, OutputStream stderr) throws IOException { logger.debug("Sending WinRM Receive Output request for command {} in shell {}", commandId, shellId); final Element bodyContent = DocumentHelper.createElement(QName.get("Receive", Namespaces.NS_WIN_SHELL)); bodyContent.addElement(QName.get("DesiredStream", Namespaces.NS_WIN_SHELL)) .addAttribute("CommandId", commandId).addText("stdout stderr"); final Document requestDocument = getRequestDocument(Action.WS_RECEIVE, ResourceURI.RESOURCE_URI_CMD, null, bodyContent); Document responseDocument = sendRequest(requestDocument, SoapAction.RECEIVE); logger.debug("Received WinRM Receive Output response for command {} in shell {}", commandId, shellId); handleStream(responseDocument, ResponseExtractor.STDOUT, stdout); handleStream(responseDocument, ResponseExtractor.STDERR, stderr); if (chunk == 0) { parseExitCode(responseDocument); } chunk++; /* * We may need to get additional output if the stream has not finished. The CommandState will change from * Running to Done like so: * * @example * * from... <rsp:CommandState CommandId="..." * State="http://schemas.microsoft.com/wbem/wsman/1/windows/shell/CommandState/Running"/> to... * <rsp:CommandState CommandId="..." * State="http://schemas.microsoft.com/wbem/wsman/1/windows/shell/CommandState/Done"> * <rsp:ExitCode>0</rsp:ExitCode> </rsp:CommandState> */ final List<?> list = ResponseExtractor.STREAM_DONE.getXPath().selectNodes(responseDocument); if (!list.isEmpty()) { logger.trace("Found CommandState element with State=Done, parsing exit code and returning false."); parseExitCode(responseDocument); return false; } else { logger.trace("Did not find CommandState element with State=Done, returning true."); return true; } } public void sendInput(byte[] buf) throws IOException { logger.debug("Sending WinRM Send Input request for command {} in shell {}", commandId, shellId); final Element bodyContent = DocumentHelper.createElement(QName.get("Send", Namespaces.NS_WIN_SHELL)); final Base64 base64 = new Base64(); bodyContent.addElement(QName.get("Stream", Namespaces.NS_WIN_SHELL)).addAttribute("Name", "stdin") .addAttribute("CommandId", commandId).addText(base64.encodeAsString(buf)); final Document requestDocument = getRequestDocument(Action.WS_SEND, ResourceURI.RESOURCE_URI_CMD, null, bodyContent); sendRequest(requestDocument, SoapAction.SEND); logger.debug("Sent WinRM Send Input request for command {} in shell {}", commandId, shellId); } public void signal() { if (commandId == null) { logger.warn("Not sending WinRM Signal request in shell {} because there is no running command", shellId); return; } logger.debug("Sending WinRM Signal request for command {} in shell {}", commandId, shellId); final Element bodyContent = DocumentHelper.createElement(QName.get("Signal", Namespaces.NS_WIN_SHELL)) .addAttribute("CommandId", commandId); bodyContent.addElement(QName.get("Code", Namespaces.NS_WIN_SHELL)) .addText("http://schemas.microsoft.com/wbem/wsman/1/windows/shell/signal/terminate"); final Document requestDocument = getRequestDocument(Action.WS_SIGNAL, ResourceURI.RESOURCE_URI_CMD, null, bodyContent); sendRequest(requestDocument, SoapAction.SIGNAL); logger.debug("Sent WinRM Signal request for command {} in shell {}", commandId, shellId); } public void deleteShell() { if (shellId == null) { logger.warn("Not sending WinRM Delete Shell request because there is no shell"); return; } logger.debug("Sending WinRM Delete Shell request for shell {}", shellId); final Document requestDocument = getRequestDocument(Action.WS_DELETE, ResourceURI.RESOURCE_URI_CMD, null, null); sendRequest(requestDocument, null); logger.debug("Sent WinRM Delete Shell request for shell {}", shellId); } public int exitValue() { return exitValue; } private void parseExitCode(Document responseDocument) { try { logger.trace("Parsing exit code"); String exitCode = getFirstElement(responseDocument, ResponseExtractor.EXIT_CODE); logger.trace("Found exit code {}", exitCode); try { exitValue = Integer.parseInt(exitCode); } catch (NumberFormatException exc) { logger.error("Cannot parse exit code {}, setting it to -1", exc); exitValue = -1; } } catch (Exception exc) { logger.trace("Exit code not found,"); } } private static void handleStream(Document responseDocument, ResponseExtractor stream, OutputStream out) throws IOException { @SuppressWarnings("unchecked") final List<Element> streams = stream.getXPath().selectNodes(responseDocument); if (!streams.isEmpty()) { final Base64 base64 = new Base64(); Iterator<Element> itStreams = streams.iterator(); while (itStreams.hasNext()) { Element e = itStreams.next(); // TODO check performance with http://www.iharder.net/current/java/base64/ final byte[] decode = base64.decode(e.getText()); out.write(decode); } } } private static String getFirstElement(Document doc, ResponseExtractor extractor) { @SuppressWarnings("unchecked") final List<Element> nodes = extractor.getXPath().selectNodes(doc); if (nodes.isEmpty()) throw new RuntimeException("Cannot find " + extractor.getXPath() + " in " + toString(doc)); final Element next = nodes.iterator().next(); return next.getText(); } private Document getRequestDocument(Action action, ResourceURI resourceURI, OptionSet optionSet, Element bodyContent) { SoapMessageBuilder message = Soapy.newMessage(); SoapMessageBuilder.EnvelopeBuilder envelope = message.envelope(); try { addHeaders(envelope, action, resourceURI, optionSet); } catch (URISyntaxException e) { throw new IllegalArgumentException(e); } BodyBuilder body = envelope.body(); if (bodyContent != null) body.setContent(bodyContent); return message.getDocument(); } private void addHeaders(SoapMessageBuilder.EnvelopeBuilder envelope, Action action, ResourceURI resourceURI, OptionSet optionSet) throws URISyntaxException { HeaderBuilder header = envelope.header(); header.to(targetURL.toURI()) .replyTo(new URI("http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous")); header.maxEnvelopeSize(winRmEnvelopSize); header.withId(getUUID()); header.withLocale(winRmLocale); header.withTimeout(winRmTimeout); header.withAction(action.getUri()); if (shellId != null) { header.withShellId(shellId); } header.withResourceURI(resourceURI.getUri()); if (optionSet != null) { header.withOptionSet(optionSet.getKeyValuePairs()); } } private static String getUUID() { return "uuid:" + UUID.randomUUID().toString().toUpperCase(); } private Document sendRequest(final Document requestDocument, final SoapAction soapAction) { if (enableKerberos) { return runPrivileged(new PrivilegedSendMessage(requestDocument, soapAction)); } else { return doSendRequest(requestDocument, soapAction); } } /** * Performs the JAAS login and run the sendRequest method within a privileged scope. */ private Document runPrivileged(final PrivilegedSendMessage privilegedSendMessage) { final CallbackHandler handler = new ProvidedAuthCallback(username, password); Document result; try { final LoginContext lc = new LoginContext("", null, handler, new KerberosJaasConfiguration(kerberosDebug)); lc.login(); result = Subject.doAs(lc.getSubject(), privilegedSendMessage); } catch (LoginException e) { throw new WinRmRuntimeIOException( "Login failure sending message on " + targetURL + " error: " + e.getMessage(), privilegedSendMessage.getRequestDocument(), null, e); } catch (PrivilegedActionException e) { throw new WinRmRuntimeIOException( "Failure sending message on " + targetURL + " error: " + e.getMessage(), privilegedSendMessage.getRequestDocument(), null, e.getException()); } return result; } /** * PrivilegedExceptionAction that wraps the internal sendRequest */ private class PrivilegedSendMessage implements PrivilegedExceptionAction<Document> { private Document requestDocument; private SoapAction soapAction; private PrivilegedSendMessage(final Document requestDocument, final SoapAction soapAction) { this.requestDocument = requestDocument; this.soapAction = soapAction; } @Override public Document run() throws Exception { return WinRmClient.this.doSendRequest(requestDocument, soapAction); } public Document getRequestDocument() { return requestDocument; } } /** * Internal sendRequest, performs the HTTP request and returns the result document. */ private Document doSendRequest(final Document requestDocument, final SoapAction soapAction) { final DefaultHttpClient client = new DefaultHttpClient(); try { configureHttpClient(client); final HttpContext context = new BasicHttpContext(); final HttpPost request = new HttpPost(targetURL.toURI()); if (soapAction != null) { request.setHeader("SOAPAction", soapAction.getValue()); } final String requestBody = toString(requestDocument); logger.trace("Request:\nPOST {}\n{}", targetURL, requestBody); final HttpEntity entity = createEntity(requestBody); request.setEntity(entity); final HttpResponse response = client.execute(request, context); logResponseHeaders(response); if (response.getStatusLine().getStatusCode() != 200) { throw new WinRmRuntimeIOException(String.format("Unexpected HTTP response on %s: %s (%s)", targetURL, response.getStatusLine().getReasonPhrase(), response.getStatusLine().getStatusCode())); } final String responseBody = handleResponse(response, context); Document responseDocument = DocumentHelper.parseText(responseBody); logDocument("Response body:", responseDocument); return responseDocument; } catch (WinRmRuntimeIOException exc) { throw exc; } catch (Exception exc) { throw new WinRmRuntimeIOException("Error when sending request to " + targetURL, requestDocument, null, exc); } finally { client.getConnectionManager().shutdown(); } } private void configureHttpClient(final DefaultHttpClient httpclient) throws GeneralSecurityException { configureTrust(httpclient); configureAuthentication(httpclient, BASIC, new BasicUserPrincipal(username)); if (enableKerberos) { String spnServiceClass = kerberosUseHttpSpn ? "HTTP" : "WSMAN"; httpclient.getAuthSchemes().register(KERBEROS, new WsmanKerberosSchemeFactory(!kerberosAddPortToSpn, spnServiceClass, unmappedAddress, unmappedPort)); httpclient.getAuthSchemes().register(SPNEGO, new WsmanSPNegoSchemeFactory(!kerberosAddPortToSpn, spnServiceClass, unmappedAddress, unmappedPort)); configureAuthentication(httpclient, KERBEROS, new KerberosPrincipal(username)); configureAuthentication(httpclient, SPNEGO, new KerberosPrincipal(username)); } httpclient.getParams().setBooleanParameter(HANDLE_AUTHENTICATION, true); } private void configureTrust(final DefaultHttpClient httpclient) throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException { if (!"https".equalsIgnoreCase(targetURL.getProtocol())) { return; } final TrustStrategy trustStrategy = httpsCertTrustStrategy.getStrategy(); final X509HostnameVerifier hostnameVerifier = httpsHostnameVerifyStrategy.getVerifier(); final SSLSocketFactory socketFactory = new SSLSocketFactory(trustStrategy, hostnameVerifier); final Scheme sch = new Scheme("https", 443, socketFactory); httpclient.getConnectionManager().getSchemeRegistry().register(sch); } private void configureAuthentication(final DefaultHttpClient httpclient, final String scheme, final Principal principal) { httpclient.getCredentialsProvider().setCredentials(new AuthScope(ANY_HOST, ANY_PORT, ANY_REALM, scheme), new Credentials() { public Principal getUserPrincipal() { return principal; } public String getPassword() { return password; } }); } private static void logResponseHeaders(final HttpResponse response) { if (!logger.isTraceEnabled()) { return; } StringBuilder headers = new StringBuilder(); for (final Header header : response.getAllHeaders()) { headers.append(header.getName()).append(": ").append(header.getValue()).append("\n"); } logger.trace("Response headers:\n{}", headers); } private static void logDocument(String caption, final Document document) { if (!logger.isTraceEnabled()) { return; } StringWriter text = new StringWriter(); try { XMLWriter writer = new XMLWriter(text, OutputFormat.createPrettyPrint()); writer.write(document); writer.close(); } catch (IOException e) { logger.trace("{}\n{}", caption, e); } logger.trace("{}\n{}", caption, text); } /** * Handle the httpResponse and return the SOAP XML String. */ protected String handleResponse(final HttpResponse response, final HttpContext context) throws IOException { final HttpEntity entity = response.getEntity(); if (null == entity.getContentType() || !entity.getContentType().getValue().startsWith("application/soap+xml")) { throw new WinRmRuntimeIOException("Error when sending request to " + targetURL + "; Unexpected content-type: " + entity.getContentType()); } final InputStream is = entity.getContent(); final Writer writer = new StringWriter(); final Reader reader = new BufferedReader(new InputStreamReader(is, "UTF-8")); try { int n; final char[] buffer = new char[1024]; while ((n = reader.read(buffer)) != -1) { writer.write(buffer, 0, n); } } finally { closeQuietly(reader); closeQuietly(is); consume(response.getEntity()); } return writer.toString(); } private static String toString(Document doc) { StringWriter stringWriter = new StringWriter(); XMLWriter xmlWriter = new XMLWriter(stringWriter, OutputFormat.createPrettyPrint()); try { xmlWriter.write(doc); xmlWriter.close(); } catch (IOException exc) { throw new WinRmRuntimeIOException("Cannnot convert XML to String ", exc); } return stringWriter.toString(); } /** * Create the HttpEntity to send in the request. */ protected HttpEntity createEntity(final String requestDocAsString) { return new StringEntity(requestDocAsString, ContentType.create("application/soap+xml", "UTF-8")); } public void setWinRmTimeout(String timeout) { this.winRmTimeout = timeout; } public void setWinRmEnvelopSize(int envelopSize) { this.winRmEnvelopSize = envelopSize; } public void setWinRmLocale(String locale) { this.winRmLocale = locale; } public void setHttpsCertTrustStrategy(WinrmHttpsCertificateTrustStrategy httpsCertTrustStrategy) { this.httpsCertTrustStrategy = httpsCertTrustStrategy; } public void setHttpsHostnameVerifyStrategy(WinrmHttpsHostnameVerificationStrategy httpsHostnameVerifyStrategy) { this.httpsHostnameVerifyStrategy = httpsHostnameVerifyStrategy; } public void setKerberosUseHttpSpn(boolean kerberosUseHttpSpn) { this.kerberosUseHttpSpn = kerberosUseHttpSpn; } public void setKerberosAddPortToSpn(boolean kerberosAddPortToSpn) { this.kerberosAddPortToSpn = kerberosAddPortToSpn; } public void setKerberosDebug(boolean kerberosDebug) { this.kerberosDebug = kerberosDebug; } private static Logger logger = LoggerFactory.getLogger(WinRmClient.class); }