// Copyright (C) 2012 jOVAL.org. All rights reserved.
// This software is licensed under the AGPL 3.0 license available at http://www.joval.org/agpl_v3.txt
package jwsmv;
import java.io.IOException;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import javax.xml.bind.JAXBElement;
import javax.xml.bind.JAXBException;
import javax.security.auth.login.FailedLoginException;
import com.microsoft.wsman.shell.CommandLine;
import com.microsoft.wsman.shell.CommandResponse;
import com.microsoft.wsman.shell.CompressionType;
import com.microsoft.wsman.shell.EnvironmentVariable;
import com.microsoft.wsman.shell.EnvironmentVariableList;
import com.microsoft.wsman.shell.ShellType;
import org.dmtf.wsman.AnyListType;
import org.dmtf.wsman.AttributableEmpty;
import org.dmtf.wsman.AttributablePositiveInteger;
import org.dmtf.wsman.OptionSet;
import org.dmtf.wsman.OptionType;
import org.dmtf.wsman.SelectorSetType;
import org.dmtf.wsman.SelectorType;
import org.xmlsoap.ws.addressing.EndpointReferenceType;
import org.xmlsoap.ws.addressing.ReferenceParametersType;
import org.xmlsoap.ws.enumeration.Enumerate;
import org.xmlsoap.ws.enumeration.EnumerateResponse;
import org.xmlsoap.ws.enumeration.EnumerationContextType;
import org.xmlsoap.ws.enumeration.Pull;
import org.xmlsoap.ws.enumeration.PullResponse;
import org.xmlsoap.ws.transfer.AnyXmlType;
import jwsmv.wsman.FaultException;
import jwsmv.wsman.Port;
import jwsmv.wsman.operation.CreateOperation;
import jwsmv.wsman.operation.DeleteOperation;
import jwsmv.wsman.operation.EnumerateOperation;
import jwsmv.wsman.operation.PullOperation;
/**
* An implementation of MS-WSMV Shell, using WS-Management.
*
* @author David A. Solin
* @version %I% %G%
*/
public class Shell implements Constants {
/**
* Default codepage ID.
*
* @see http://msdn.microsoft.com/en-us/library/dd317756%28v=vs.85%29.aspx
*/
public static final String IBM437_CODEPAGE = "437";
/**
* Default codepage ID for UTF-8.
*/
public static final String UTF8_CODEPAGE = "65001";
/**
* Signifies MS-XCA compression.
*/
public static final String COMPRESSION_ALGORITHM = "xpress";
public static final String STDOUT = "stdout";
public static final String STDERR = "stderr";
public static final String STDIN = "stdin";
/**
* Return an Iterator of the IDs of all the remote shells available at the specified port.
*/
public static Iterable<String> enumerate(Port port)
throws JAXBException, IOException, FaultException, FailedLoginException {
//
// Build an optimized enumerate operation.
//
Enumerate enumerate = Factories.ENUMERATION.createEnumerate();
AttributableEmpty optimize = Factories.WSMAN.createAttributableEmpty();
enumerate.getAny().add(Factories.WSMAN.createOptimizeEnumeration(optimize));
AttributablePositiveInteger maxElements = Factories.WSMAN.createAttributablePositiveInteger();
maxElements.setValue(new BigInteger("4"));
enumerate.getAny().add(Factories.WSMAN.createMaxElements(maxElements));
EnumerateOperation operation = new EnumerateOperation(enumerate);
operation.addResourceURI(SHELL_BASE_URI);
//
// Capture shells listed in the enumerate response.
//
ArrayList<String> shellIds = new ArrayList<String>();
boolean endOfSequence = false;
List<Object> items = null;
EnumerateResponse response = operation.dispatch(port);
EnumerationContextType enumContext = response.getEnumerationContext();
if (response.isSetAny()) {
for (Object obj : response.getAny()) {
if (obj instanceof JAXBElement) {
JAXBElement elt = (JAXBElement)obj;
if ("EndOfSequence".equals(elt.getName().getLocalPart())) {
endOfSequence = true;
} else {
obj = ((JAXBElement)obj).getValue();
if (obj instanceof AnyListType) {
items = ((AnyListType)obj).getAny();
} else {
System.out.println("Ignoring EnumerateResponse child: " + obj.getClass().getName());
}
}
} else {
System.out.println("Ignoring EnumerateResponse child: " + obj.getClass().getName());
}
}
}
while(true) {
//
// Process items captured in the last operation.
//
if (items != null) {
for (Object obj : items) {
if (obj instanceof JAXBElement) {
obj = ((JAXBElement)obj).getValue();
}
if (obj instanceof ShellType) {
shellIds.add(((ShellType)obj).getShellId());
} else {
System.out.println("Ignoring item: " + obj.getClass().getName());
}
}
}
//
// Pull down additional shell lists until the end of the enumeration has been reached.
//
if (endOfSequence) {
break;
} else {
Pull pull = Factories.ENUMERATION.createPull();
pull.setEnumerationContext(enumContext);
pull.setMaxElements(new BigInteger("4"));
PullOperation pullOperation = new PullOperation(pull);
pullOperation.addResourceURI(SHELL_BASE_URI);
PullResponse pullResponse = pullOperation.dispatch(port);
if (pullResponse.isSetEndOfSequence()) {
endOfSequence = true;
} else if (pullResponse.isSetEnumerationContext()) {
enumContext = pullResponse.getEnumerationContext();
}
if (pullResponse.isSetItems()) {
items = pullResponse.getItems().getAny();
} else {
items = null;
}
}
}
return shellIds;
}
/**
* Attach to an existing remote shell given its ID. Keep in mind, when the returned instance is garbage collected,
* an attempt will be made to delete the remote shell.
*/
public static Shell attach(Port port, String shellId) {
return new Shell(port, shellId);
}
private String id;
private boolean disposed = false;
private HashMap<String, ShellCommand> processes;
private Thread shutdownHook;
ThreadGroup group;
Port port;
boolean compress = false;
/**
* Create a new Shell.
*
* @see http://en.wikipedia.org/wiki/Code_page_437
*
* @param port The web service port through which the WS-Management/Transfer Create request will be dispatched.
* @param compress Enable Xpress compression (currently non-functional)
* @param codepage The value of the options specifies the client's console output code page. The value is returned
* by GetConsoleOutputCP API; on the server side, this value is set as input and output code page
* to display the number of the active character set (code page) or to change the active character set.
* If null, the IBM437_CODEPAGE value "437" will be used.
* @param noProfile If set to TRUE, this option specifies that the user profile does not exist on the remote system
* and that the default profile SHOULD be used. By default, the value should be TRUE.
* @param env The desired Shell environment. Leave null for the default user environment.
* @param cwd The desired Shell working directory. If null the shell will start in the user's home directory,
* or %SystemRoot% if noProfile is set to true.
*/
public Shell(Port port, boolean compress, boolean noProfile, String codepage, String[] env, String cwd)
throws JAXBException, IOException, IllegalArgumentException, FaultException, FailedLoginException {
this.port = port;
this.compress = compress;
processes = new HashMap<String, ShellCommand>();
//
// Create the WS-Create input parameter
//
AnyXmlType arg = Factories.TRANSFER.createAnyXmlType();
ShellType shell = Factories.SHELL.createShellType();
if (env != null) {
EnvironmentVariableList envList = Factories.SHELL.createEnvironmentVariableList();
for (String pair : env) {
int ptr = pair.indexOf("=");
if (ptr == -1) {
throw new IllegalArgumentException(pair);
} else {
EnvironmentVariable var = Factories.SHELL.createEnvironmentVariable();
var.setName(pair.substring(0, ptr));
var.setValue(pair.substring(ptr+1));
envList.getVariable().add(var);
}
}
shell.setEnvironment(envList);
}
if (cwd != null) {
shell.setWorkingDirectory(cwd);
}
shell.setLifetime(Factories.XMLDT.newDuration(1800000)); // 30 min.
shell.getOutputStreams().add(STDOUT);
shell.getOutputStreams().add(STDERR);
shell.getInputStreams().add(STDIN);
arg.setAny(Factories.SHELL.createShell(shell));
//
// Create the CreateOperation and set options
//
CreateOperation createOperation = new CreateOperation(arg);
createOperation.addResourceURI(SHELL_URI);
createOperation.setTimeout(60000);
OptionType winrsNoProfile = Factories.WSMAN.createOptionType();
winrsNoProfile.setName("WINRS_NOPROFILE");
winrsNoProfile.setValue(noProfile ? "TRUE" : "FALSE");
OptionType winrsCodepage = Factories.WSMAN.createOptionType();
winrsCodepage.setName("WINRS_CODEPAGE");
winrsCodepage.setValue(codepage == null ? IBM437_CODEPAGE : codepage);
OptionSet options = Factories.WSMAN.createOptionSet();
options.getOption().add(winrsNoProfile);
options.getOption().add(winrsCodepage);
createOperation.addHeader(options);
if (compress) {
CompressionType compressionType = Factories.SHELL.createCompressionType();
compressionType.setMustUnderstand(true);
compressionType.setValue(COMPRESSION_ALGORITHM);
createOperation.addHeader(compressionType);
}
//
// Dispatch the call to the target, and get the ID of the new shell.
//
Object response = createOperation.dispatch(port);
if (response instanceof EndpointReferenceType) {
for (Object param : ((EndpointReferenceType)response).getReferenceParameters().getAny()) {
if (param instanceof JAXBElement) {
param = ((JAXBElement)param).getValue();
}
if (param instanceof SelectorSetType) {
for (SelectorType sel : ((SelectorSetType)param).getSelector()) {
if ("ShellId".equals(sel.getName())) {
id = (String)sel.getContent().get(0);
group = new ThreadGroup("Shell:" + id);
break;
}
}
}
if (id != null) break;
}
shutdownHook = new ShutdownHook();
Runtime.getRuntime().addShutdownHook(shutdownHook);
}
}
/**
* Get the ID of the shell.
*/
public String getId() {
return id;
}
/**
* Closes the remote Shell instance (idempotent).
*/
public synchronized void dispose() {
if (shutdownHook != null) {
Runtime.getRuntime().removeShutdownHook(shutdownHook);
shutdownHook = null;
}
finalize();
}
/**
* Return the number of processes being managed by this shell.
*
* NOTE: There is a defect in Microsoft's implementation, wherein the number of active processes is never decremeted,
* even after a process has terminated, so long as the shell that launched it remains open. Accordingly, the number
* returned by this method does not ever decrease. When the limit has been reached, the shell must be disposed before
* another process can be created.
*/
public int getProcessCount() {
return processes.size();
}
/**
* Returns the number of active (running) processes being managed by this shell.
*/
public int getActiveProcessCount() {
int count = 0;
for (ShellCommand process : processes.values()) {
if (process.isRunning()) {
count++;
}
}
return count;
}
/**
* Create and start a ShellCommand using this shell.
*/
public ShellCommand exec(String command) throws JAXBException, IOException, FaultException, IllegalArgumentException {
ArrayList<String> args = new ArrayList<String>();
ArgumentTokenizer tok = new ArgumentTokenizer(command);
String arg = null;
while((arg = tok.nextArg()) != null) {
args.add(arg);
}
String[] argv = new String[args.size() - 1];
for (int i=0; i < argv.length; i++) {
argv[i] = args.get(i+1);
}
ShellCommand process = new ShellCommand(this, args.get(0), argv);
process.start();
processes.put(process.getId(), process);
return process;
}
// Internal
/**
* Get a SelectorSetType with the ShellId.
*/
SelectorSetType getSelectorSet() {
SelectorSetType set = Factories.WSMAN.createSelectorSetType();
SelectorType sel = Factories.WSMAN.createSelectorType();
sel.setName("ShellId");
sel.getContent().add(id);
set.getSelector().add(sel);
return set;
}
/**
* Closes the remote Shell instance (idempotent).
*/
@Override
protected synchronized void finalize() {
if (!disposed) {
try {
for (ShellCommand process : processes.values()) {
process.finalize();
}
DeleteOperation deleteOperation = new DeleteOperation();
deleteOperation.addResourceURI(SHELL_URI);
SelectorSetType set = Factories.WSMAN.createSelectorSetType();
SelectorType sel = Factories.WSMAN.createSelectorType();
sel.setName("ShellId");
sel.getContent().add(id);
set.getSelector().add(sel);
deleteOperation.addSelectorSet(set);
deleteOperation.dispatch(port);
} catch (Exception e) {
e.printStackTrace();
} finally {
disposed = true;
}
}
}
// Private
private Shell(Port port) {
}
/**
* Grab an existing shell on the target. Uses WS-Transfer Enumerate to validate the existence of the ID.
*
* @throws NoSuchElementException if no shell with the given ID is found.
*/
private Shell(Port port, String id) {
this.port = port;
this.id = id;
group = new ThreadGroup("Shell:" + id);
processes = new HashMap<String, ShellCommand>();
}
/**
* A shutdown hook for terminating the Shell in case of an unexpected JVM exit.
*/
class ShutdownHook extends Thread {
ShutdownHook() {
super();
}
public void run() {
try {
System.err.println("Running shutdown hook for Shell " + Shell.this.getId());
finalize();
} catch (Throwable t) {
t.printStackTrace();
}
}
}
/**
* The argument tokenizer ...
*/
class ArgumentTokenizer {
String command;
int ptr;
/**
* Create a tokenizer for a command string.
*/
ArgumentTokenizer(String command) {
this.command = command;
ptr = 0;
}
/**
* Get the next argument from the command string.
*/
String nextArg() {
return nextArg(ptr);
}
// Private
/**
* Determine the next argument after ptr, starting the search for a space at the specified index.
*/
private String nextArg(int index) {
if (ptr >= command.length()) {
return null;
} else if (index == -1) {
StringBuffer sb = new StringBuffer();
int unclosed = unescapedIndexOf("\"", command, ptr);
while(unclosed-- > 0) {
sb.append(" ");
}
sb.append("^");
throw new IllegalArgumentException("Unclosed quote in command:\n " + command + "\n" +
" " + sb.toString());
}
int nextQuote = unescapedIndexOf("\"", command, index);
if (nextQuote == -1) {
nextQuote = command.length();
}
String arg = null;
int nextSpace = unescapedIndexOf(" ", command, index);
if (nextSpace == -1) {
arg = command.substring(ptr);
ptr = command.length();
} else if (nextSpace < nextQuote) {
arg = command.substring(ptr, nextSpace);
ptr = nextSpace+1;
} else {
int nextIndex = unescapedIndexOf("\"", command, nextQuote+1);
if (nextIndex == -1) {
arg = nextArg(-1);
} else {
arg = nextArg(nextIndex + 1);
}
}
arg = arg.trim();
if (arg.length() == 0) {
arg = nextArg();
}
return arg;
}
/**
* Get the index of s in target starting from fromIndex, which is not preceded by an unescaped escape character.
*/
private int unescapedIndexOf(String s, String target, int fromIndex) {
int candidate = target.indexOf(s, fromIndex);
if (candidate == -1) {
return -1;
} else if (escapedChar(candidate, target)) {
return unescapedIndexOf(s, target, candidate+1);
} else {
return candidate;
}
}
/**
* Is the character at index of s escaped?
*/
private boolean escapedChar(int index, String s) throws IllegalArgumentException {
if (index < 0) {
throw new IllegalArgumentException(Integer.toString(index));
} else if (index == 0) {
return false;
} else {
int escapes = 0;
for (int i=index-1; i >= 0; i--) {
char c = s.charAt(i);
if (c == '\\') {
escapes++;
} else {
break;
}
}
return (escapes % 2) == 1;
}
}
}
}