/**
* Copyright (c) 2008-2016, 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.winrm;
import com.xebialabs.overthere.*;
import com.xebialabs.overthere.cifs.CifsConnectionType;
import com.xebialabs.overthere.cifs.WinrmHttpsCertificateTrustStrategy;
import com.xebialabs.overthere.cifs.WinrmHttpsHostnameVerificationStrategy;
import com.xebialabs.overthere.spi.ProcessConnection;
import com.xebialabs.overthere.spi.AddressPortMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.*;
import java.net.InetSocketAddress;
import java.net.MalformedURLException;
import java.net.URL;
import static com.xebialabs.overthere.ConnectionOptions.*;
import static com.xebialabs.overthere.cifs.CifsConnectionBuilder.*;
import static com.xebialabs.overthere.cifs.ConnectionValidator.checkIsWindowsHost;
import static com.xebialabs.overthere.cifs.ConnectionValidator.checkNotOldStyleWindowsDomain;
import static com.xebialabs.overthere.util.OverthereUtils.checkArgument;
import static com.xebialabs.overthere.util.OverthereUtils.checkNotNull;
import static com.xebialabs.overthere.util.OverthereUtils.closeQuietly;
import static java.lang.String.format;
import static java.net.InetSocketAddress.createUnresolved;
/**
* A connection to a Windows host using a Java implementation of WinRM.
*/
public class WinRmConnection implements ProcessConnection {
private int connectionTimeoutMillis;
private int socketTimeoutMillis;
private OperatingSystemFamily os;
private OverthereFile workingDirectory;
private AddressPortMapper mapper;
private String address;
private int port;
private String password;
private String username;
private String protocol;
private CifsConnectionType connectionType = CifsConnectionType.WINRM_INTERNAL;
private ConnectionOptions options;
private final String unmappedAddress;
private final int unmappedPort;
public static final int STDIN_BUF_SIZE = 4096;
public WinRmConnection(ConnectionOptions options, AddressPortMapper mapper, OverthereFile workingDirectory) {
this.workingDirectory = workingDirectory;
this.options = options;
this.mapper = mapper;
this.os = options.getEnum(OPERATING_SYSTEM, OperatingSystemFamily.class);
this.connectionTimeoutMillis = options.getInteger(CONNECTION_TIMEOUT_MILLIS, CONNECTION_TIMEOUT_MILLIS_DEFAULT);
this.socketTimeoutMillis = options.getInteger(SOCKET_TIMEOUT_MILLIS, SOCKET_TIMEOUT_MILLIS_DEFAULT);
unmappedAddress = options.get(ADDRESS);
unmappedPort = options.get(PORT, connectionType.getDefaultPort(options));
InetSocketAddress addressPort = mapper.map(createUnresolved(unmappedAddress, unmappedPort));
this.address = addressPort.getHostName();
this.port = addressPort.getPort();
this.username = options.get(USERNAME);
this.password = options.get(PASSWORD);
this.protocol = options.get(PROTOCOL);
checkIsWindowsHost(os, protocol, connectionType);
checkNotOldStyleWindowsDomain(username, protocol, connectionType);
}
@Override
public OverthereProcess startProcess(final CmdLine cmd) {
checkNotNull(cmd, "Cannot execute null command line");
checkArgument(cmd.getArguments().size() > 0, "Cannot execute empty command line");
final String obfuscatedCmd = cmd.toCommandLine(os, true);
logger.info("Starting command [{}] on [{}]", obfuscatedCmd, this);
String cmdString = cmd.toCommandLine(os, false);
if (workingDirectory != null) {
cmdString = "CD /D " + workingDirectory.getPath() + " & " + cmdString;
}
final WinRmClient winRmClient = createWinrmClient();
try {
final PipedInputStream fromCallersStdin = new PipedInputStream();
final PipedOutputStream callersStdin = new PipedOutputStream(fromCallersStdin);
final PipedInputStream callersStdout = new PipedInputStream();
final PipedOutputStream toCallersStdout = new PipedOutputStream(callersStdout);
final PipedInputStream callersStderr = new PipedInputStream();
final PipedOutputStream toCallersStderr = new PipedOutputStream(callersStderr);
winRmClient.createShell();
final String commandId = winRmClient.executeCommand(cmdString);
final Exception inputReaderTheaException[] = new Exception[1];
final Thread inputReaderThead = new Thread(format("WinRM input reader for command [%s]", commandId)) {
@Override
public void run() {
try {
byte[] buf = new byte[STDIN_BUF_SIZE];
for (; ; ) {
int n = fromCallersStdin.read(buf);
if (n == -1)
break;
if (n == 0)
continue;
byte[] bufToSend = new byte[n];
System.arraycopy(buf, 0, bufToSend, 0, n);
winRmClient.sendInput(bufToSend);
}
} catch (Exception exc) {
inputReaderTheaException[0] = exc;
} finally {
closeQuietly(fromCallersStdin);
}
}
};
inputReaderThead.setDaemon(true);
inputReaderThead.start();
final Exception outputReaderThreadException[] = new Exception[1];
final Thread outputReaderThread = new Thread(format("WinRM output reader for command [%s]", commandId)) {
@Override
public void run() {
try {
for (; ; ) {
if (!winRmClient.receiveOutput(toCallersStdout, toCallersStderr))
break;
}
} catch (Exception exc) {
outputReaderThreadException[0] = exc;
} finally {
closeQuietly(toCallersStdout);
closeQuietly(toCallersStderr);
}
}
};
outputReaderThread.setDaemon(true);
outputReaderThread.start();
return new OverthereProcess() {
boolean processTerminated = false;
@Override
public synchronized OutputStream getStdin() {
return callersStdin;
}
@Override
public synchronized InputStream getStdout() {
return callersStdout;
}
@Override
public synchronized InputStream getStderr() {
return callersStderr;
}
@Override
public synchronized int waitFor() {
if (processTerminated) {
return exitValue();
}
try {
try {
outputReaderThread.join();
} finally {
closeQuietly(callersStdin);
processTerminated = true;
try {
winRmClient.deleteShell();
}catch (Throwable t){
logger.warn("Failure while deleting winrm shell", t);
}
}
if (outputReaderThreadException[0] != null) {
if (outputReaderThreadException[0] instanceof RuntimeException) {
throw (RuntimeException) outputReaderThreadException[0];
} else {
throw new RuntimeIOException(format("Cannot execute command [%s] on [%s]", obfuscatedCmd, WinRmConnection.this), outputReaderThreadException[0]);
}
}
return exitValue();
} catch (InterruptedException exc) {
throw new RuntimeIOException(format("Cannot execute command [%s] on [%s]", obfuscatedCmd, WinRmConnection.this), exc);
}
}
@Override
public synchronized void destroy() {
if (processTerminated) {
return;
}
closeQuietly(callersStdin);
processTerminated = true;
winRmClient.signal();
winRmClient.deleteShell();
}
@Override
public synchronized int exitValue() {
if (!processTerminated) {
throw new IllegalThreadStateException(format("Process for command [%s] on [%s] is still running", obfuscatedCmd,
WinRmConnection.this));
}
return winRmClient.exitValue();
}
};
} catch (IOException exc) {
throw new RuntimeIOException("Cannot execute command " + cmd + " on " + this, exc);
}
}
@Override
public void connect() {
// no-op
}
@Override
public void close() {
// no-op
}
@Override
public void setWorkingDirectory(OverthereFile workingDirectory) {
this.workingDirectory = workingDirectory;
}
private WinRmClient createWinrmClient() {
final WinRmClient client = new WinRmClient(username, password, createWinrmURL(), unmappedAddress, unmappedPort, mapper.socketFactory());
client.setWinRmTimeout(options.get(WINRM_TIMEMOUT, DEFAULT_WINRM_TIMEOUT));
client.setWinRmEnvelopSize(options.get(WINRM_ENVELOP_SIZE, WINRM_ENVELOP_SIZE_DEFAULT));
client.setWinRmLocale(options.get(WINRM_LOCALE, WINRM_LOCALE_DEFAULT));
client.setHttpsCertTrustStrategy(options.getEnum(WINRM_HTTPS_CERTIFICATE_TRUST_STRATEGY, WinrmHttpsCertificateTrustStrategy.class, WINRM_HTTPS_CERTIFICATE_TRUST_STRATEGY_DEFAULT));
client.setHttpsHostnameVerifyStrategy(options.getEnum(WINRM_HTTPS_HOSTNAME_VERIFICATION_STRATEGY, WinrmHttpsHostnameVerificationStrategy.class, WINRM_HTTPS_HOSTNAME_VERIFICATION_STRATEGY_DEFAULT));
client.setKerberosUseHttpSpn(options.getBoolean(WINRM_KERBEROS_USE_HTTP_SPN, WINRM_KERBEROS_USE_HTTP_SPN_DEFAULT));
client.setKerberosAddPortToSpn(options.getBoolean(WINRM_KERBEROS_ADD_PORT_TO_SPN, WINRM_KERBEROS_ADD_PORT_TO_SPN_DEFAULT));
client.setKerberosDebug(options.getBoolean(WINRM_KERBEROS_DEBUG, WINRM_KERBEROS_DEBUG_DEFAULT));
client.setKerberosTicketCache(options.getBoolean(WINRM_KERBEROS_TICKET_CACHE, WINRM_KERBEROS_TICKET_CACHE_DEFAULT));
client.setConnectionTimeout(connectionTimeoutMillis);
client.setSoTimeout(socketTimeoutMillis);
return client;
}
private URL createWinrmURL() {
final String scheme = options.getBoolean(WINRM_ENABLE_HTTPS, WINRM_ENABLE_HTTPS_DEFAULT) ? "https" : "http";
final String context = options.get(WINRM_CONTEXT, WINRM_CONTEXT_DEFAULT);
try {
return new URL(scheme, address, port, context);
} catch (MalformedURLException e) {
throw new WinRmRuntimeIOException("Cannot build a new URL for " + this, e);
}
}
private static Logger logger = LoggerFactory.getLogger(WinRmConnection.class);
}