/******************************************************************************* * Copyright (c) 2011, 2016 Eurotech and/or its affiliates * * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Eurotech *******************************************************************************/ package org.eclipse.kura.cloud.app.command; import java.io.File; import java.io.IOException; import java.util.HashMap; import java.util.Map; import org.eclipse.kura.KuraErrorCode; import org.eclipse.kura.KuraException; import org.eclipse.kura.cloud.Cloudlet; import org.eclipse.kura.cloud.CloudletTopic; import org.eclipse.kura.command.PasswordCommandService; import org.eclipse.kura.configuration.ConfigurableComponent; import org.eclipse.kura.configuration.Password; import org.eclipse.kura.crypto.CryptoService; import org.eclipse.kura.message.KuraRequestPayload; import org.eclipse.kura.message.KuraResponsePayload; import org.osgi.service.component.ComponentContext; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class CommandCloudApp extends Cloudlet implements ConfigurableComponent, PasswordCommandService { private static final Logger s_logger = LoggerFactory.getLogger(CommandCloudApp.class); private static final String EDC_PASSWORD_METRIC_NAME = "command.password"; private static final String COMMAND_ENABLED_ID = "command.enable"; private static final String COMMAND_PASSWORD_ID = "command.password.value"; private static final String COMMAND_WORKDIR_ID = "command.working.directory"; private static final String COMMAND_TIMEOUT_ID = "command.timeout"; private static final String COMMAND_ENVIRONMENT_ID = "command.environment"; public static final String APP_ID = "CMD-V1"; private Map<String, Object> properties; private ComponentContext compCtx; private CryptoService m_cryptoService; private boolean currentStatus; /* EXEC */ public static final String RESOURCE_COMMAND = "command"; public CommandCloudApp() { super(APP_ID); } // ---------------------------------------------------------------- // // Dependencies // // ---------------------------------------------------------------- // This component inherits the required dependencies from the parent // class CloudApp. public void setCryptoService(CryptoService cryptoService) { this.m_cryptoService = cryptoService; } public void unsetCryptoService(CryptoService cryptoService) { this.m_cryptoService = null; } // ---------------------------------------------------------------- // // Activation APIs // // ---------------------------------------------------------------- // This component inherits the activation methods from the parent // class CloudApp. protected void activate(ComponentContext componentContext, Map<String, Object> properties) { s_logger.info("Bundle " + APP_ID + " has started with config!"); this.compCtx = componentContext; this.currentStatus = (Boolean) properties.get(COMMAND_ENABLED_ID); if (this.currentStatus) { super.activate(this.compCtx); } updated(properties); } public void updated(Map<String, Object> properties) { s_logger.info("updated...: " + properties); this.properties = new HashMap<String, Object>(); for (Map.Entry<String, Object> entry : properties.entrySet()) { String key = entry.getKey(); Object value = entry.getValue(); if (key.equals(COMMAND_PASSWORD_ID)) { try { Password decryptedPassword = new Password( this.m_cryptoService.decryptAes(value.toString().toCharArray())); this.properties.put(key, decryptedPassword); } catch (Exception e) { this.properties.put(key, new Password((String) value)); } } else { this.properties.put(key, value); } } boolean newStatus = (Boolean) properties.get(COMMAND_ENABLED_ID); boolean stateChanged = this.currentStatus != newStatus; if (stateChanged) { this.currentStatus = newStatus; if (!this.currentStatus && getCloudApplicationClient() != null) { super.deactivate(this.compCtx); } if (this.currentStatus) { super.activate(this.compCtx); } } } @Override protected void deactivate(ComponentContext componentContext) { s_logger.info("Bundle " + APP_ID + " is deactivating!"); if (getCloudApplicationClient() != null) { super.deactivate(this.compCtx); } } @Override protected void doExec(CloudletTopic reqTopic, KuraRequestPayload reqPayload, KuraResponsePayload respPayload) throws KuraException { String[] resources = reqTopic.getResources(); if (resources == null || resources.length != 1) { s_logger.error("Bad request topic: {}", reqTopic.toString()); s_logger.error("Expected one resource but found {}", resources != null ? resources.length : "none"); respPayload.setResponseCode(KuraResponsePayload.RESPONSE_CODE_BAD_REQUEST); return; } if (!resources[0].equals(RESOURCE_COMMAND)) { s_logger.error("Bad request topic: {}", reqTopic.toString()); s_logger.error("Cannot find resource with name: {}", resources[0]); respPayload.setResponseCode(KuraResponsePayload.RESPONSE_CODE_NOTFOUND); return; } s_logger.info("EXECuting resource: {}", RESOURCE_COMMAND); KuraCommandResponsePayload commandResp = execute(reqPayload); for (String name : commandResp.metricNames()) { Object value = commandResp.getMetric(name); respPayload.addMetric(name, value); } respPayload.setBody(commandResp.getBody()); } @Override public KuraCommandResponsePayload execute(KuraRequestPayload reqPayload) { KuraCommandRequestPayload commandReq = new KuraCommandRequestPayload(reqPayload); String receivedPassword = (String) commandReq.getMetric(EDC_PASSWORD_METRIC_NAME); Password commandPassword = (Password) this.properties.get(COMMAND_PASSWORD_ID); KuraCommandResponsePayload commandResp = new KuraCommandResponsePayload(KuraResponsePayload.RESPONSE_CODE_OK); boolean isExecutionAllowed = verifyPasswords(commandPassword, receivedPassword); if (isExecutionAllowed) { String command = commandReq.getCommand(); if (command == null || command.trim().isEmpty()) { s_logger.error("null command"); commandResp.setResponseCode(KuraResponsePayload.RESPONSE_CODE_BAD_REQUEST); return commandResp; } String[] cmdarray = prepareCommandArray(commandReq, command); String dir = getDefaultWorkDir(); String[] envp = getEnvironment(commandReq); byte[] zipBytes = commandReq.getZipBytes(); if (zipBytes != null) { try { UnZip.unZipBytes(zipBytes, dir); } catch (IOException e) { s_logger.error("Error unzipping command zip bytes", e); commandResp.setResponseCode(KuraResponsePayload.RESPONSE_CODE_ERROR); commandResp.setException(e); return commandResp; } } Process proc = null; try { proc = createExecutionProcess(dir, cmdarray, envp); } catch (Throwable t) { s_logger.error("Error executing command {}", t); commandResp.setResponseCode(KuraResponsePayload.RESPONSE_CODE_ERROR); commandResp.setException(t); return commandResp; } boolean runAsync = commandReq.isRunAsync() != null ? commandReq.isRunAsync() : false; int timeout = getTimeout(commandReq); ProcessMonitorThread pmt = new ProcessMonitorThread(proc, commandReq.getStdin(), timeout); pmt.start(); if (!runAsync) { try { pmt.join(); prepareResponseNoTimeout(commandResp, pmt); } catch (InterruptedException e) { Thread.interrupted(); pmt.interrupt(); prepareTimeoutResponse(commandResp, pmt); } } } else { s_logger.error("Password required but not correct and/or missing"); commandResp.setResponseCode(KuraResponsePayload.RESPONSE_CODE_ERROR); commandResp.setExceptionMessage("Password missing or not correct"); } return commandResp; } @Override public String execute(String cmd, String password) throws KuraException { boolean verificationEnabled = (Boolean) this.properties.get(COMMAND_ENABLED_ID); if (verificationEnabled) { Password commandPassword = (Password) this.properties.get(COMMAND_PASSWORD_ID); boolean isExecutionAllowed = verifyPasswords(commandPassword, password); if (isExecutionAllowed) { String[] cmdArray = cmd.split(" "); String defaultDir = getDefaultWorkDir(); String[] environment = getDefaultEnvironment(); try { Process proc = createExecutionProcess(defaultDir, cmdArray, environment); int timeout = getDefaultTimeout(); ProcessMonitorThread pmt = new ProcessMonitorThread(proc, null, timeout); pmt.start(); try { pmt.join(); if (pmt.getExitValue() == 0) { return pmt.getStdout(); } else { return pmt.getStderr(); } } catch (InterruptedException e) { Thread.interrupted(); pmt.interrupt(); throw KuraException.internalError(e); } } catch (IOException ex) { throw new KuraException(KuraErrorCode.INTERNAL_ERROR, ex); } } else { throw new KuraException(KuraErrorCode.CONFIGURATION_ATTRIBUTE_INVALID); } } else { throw new KuraException(KuraErrorCode.OPERATION_NOT_SUPPORTED); } } // command service defaults getters private String getDefaultWorkDir() { String workDir = (String) this.properties.get(COMMAND_WORKDIR_ID); if (workDir != null && !workDir.isEmpty()) { return workDir; } return System.getProperty("java.io.tmpdir"); } private int getDefaultTimeout() { return (Integer) this.properties.get(COMMAND_TIMEOUT_ID); } private String[] getDefaultEnvironment() { String envString = (String) this.properties.get(COMMAND_ENVIRONMENT_ID); if (envString != null) { return envString.split(" "); } return null; } private int getTimeout(KuraCommandRequestPayload req) { Integer timeout = req.getTimeout(); int defaultTimeout = getDefaultTimeout(); if (timeout != null) { return timeout; } return defaultTimeout; } private String[] getEnvironment(KuraCommandRequestPayload req) { String[] envp = req.getEnvironmentPairs(); String[] defaultEnv = getDefaultEnvironment(); if (envp != null && envp.length != 0) { return envp; } return defaultEnv; } private boolean verifyPasswords(Password commandPassword, String receivedPassword) { if (commandPassword == null && receivedPassword == null) { return true; } if (commandPassword == null && "".equals(receivedPassword)) { return true; } if (commandPassword == null) { return false; } if ("".equals(commandPassword.toString()) && receivedPassword == null) { return true; } if ("".equals(commandPassword.toString()) && "".equals(receivedPassword)) { return true; } String pwd = commandPassword.toString(); return pwd.equals(receivedPassword); } private Process createExecutionProcess(String dir, String[] cmdarray, String[] envp) throws IOException { Runtime rt = Runtime.getRuntime(); File fileDir = dir == null ? null : new File(dir); return rt.exec(cmdarray, envp, fileDir); } private String[] prepareCommandArray(KuraCommandRequestPayload req, String command) { String[] args = req.getArguments(); int argsCount = args != null ? args.length : 0; String[] cmdarray = new String[1 + argsCount]; cmdarray[0] = command; for (int i = 0; i < argsCount; i++) { cmdarray[1 + i] = args[i]; } for (String element : cmdarray) { s_logger.debug("cmdarray: {}", element); } return cmdarray; } private void prepareResponseNoTimeout(KuraCommandResponsePayload resp, ProcessMonitorThread pmt) { if (pmt.getException() != null) { resp.setResponseCode(KuraResponsePayload.RESPONSE_CODE_ERROR); resp.setException(pmt.getException()); resp.setStderr(pmt.getStderr()); resp.setStdout(pmt.getStdout()); } else { resp.setStderr(pmt.getStderr()); resp.setStdout(pmt.getStdout()); resp.setTimedout(pmt.isTimedOut()); if (!pmt.isTimedOut()) { resp.setExitCode(pmt.getExitValue()); } } } private void prepareTimeoutResponse(KuraCommandResponsePayload resp, ProcessMonitorThread pmt) { resp.setStderr(pmt.getStderr()); resp.setStdout(pmt.getStdout()); resp.setTimedout(true); } }