/************************************************************************* * * * CESeCore: CE Security Core * * * * This software is free software; you can redistribute it and/or * * modify it under the terms of the GNU Lesser General Public * * License as published by the Free Software Foundation; either * * version 2.1 of the License, or any later version. * * * * See terms of license at gnu.org. * * * *************************************************************************/ package org.cesecore.util; import java.io.BufferedReader; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStreamReader; import java.security.cert.Certificate; import java.security.cert.CertificateEncodingException; import java.security.cert.CertificateParsingException; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import org.apache.commons.collections.CollectionUtils; import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.SystemUtils; import org.apache.log4j.Logger; import org.cesecore.internal.InternalResources; /** * Tools to handle calls with Java Process API ({@link https://docs.oracle.com/javase/8/docs/api/java/lang/Process.html}. * * @version $Id: ExternalProcessTools.java 27126 2017-12-16 09:28:54Z anjakobs $ */ public final class ExternalProcessTools { /** Class logger. */ private static final Logger log = Logger.getLogger(ExternalProcessTools.class); /** Internal localization of logs and errors. */ private static final InternalResources intres = InternalResources.getInstance(); /** Literal for the (platform dependent) line separator. */ private static final String LINE_SEPARATOR = System.getProperty("line.separator"); /** Literal for place holder for the certificate issued */ public static final String PLACE_HOLDER_CERTIFICATE = "%cert%"; /** Literal for default MS Windows shell. */ public static final String WINDOWS_SHELL = "cmd.exe"; /** Literal for default MS Windows shell options. */ public static final String WINDOWS_SHELL_OPTIONS = "/c"; /** Literal for default Unix shell. */ public static final String UNIX_SHELL = "/bin/sh"; /** Literal for default Unix shell options. */ public static final String UNIX_SHELL_OPTIONS = "-c"; /** Literal for exit code label / prefix. */ public static final String EXIT_CODE_PREFIX = "Exit code: "; /** Literal for STDOUT label to log the external out streams . */ public static final String STDOUT_PREFIX = "STDOUT: "; /** Literal for ERROUT label to log the external out streams . */ public static final String ERROUT_PREFIX = "ERROUT: "; /** * Builds the platform dependent external command array: * - field at index 0 is the interpreter, * - field at index 1 is the one and only parameter of the interpreter, * - field at index 2 must contain the complete external command, including pipes, chains, sub-shells, etc. and is appended later. * * @param the external command or script * @return the command array as list. */ protected static final List buildShellCommand(final String cmd) { final List result = new ArrayList(); if (SystemUtils.IS_OS_WINDOWS) { result.add(WINDOWS_SHELL); result.add(WINDOWS_SHELL_OPTIONS); } else { result.add(UNIX_SHELL); result.add(UNIX_SHELL_OPTIONS); } if (log.isDebugEnabled()) { log.debug("Use platform shell command for " + SystemUtils.OS_NAME + " : " + result); } if (result.size() == 2) { result.add(cmd); } return result; } /** * Writes the byte-array to a temporary file or pipes the PEM certificate into the command, and launches the given external command, * see {@link ExternalProcessTools#launchExternalCommand(String, byte[], boolean, boolean, boolean, boolean, List, String)}. * * @param cmd The command to run. * @param bytes The buffer with content to write to the file. * @param failOnCode Determines if the method should fail on a non-zero exit code. * @param failOnOutput Determines if the method should fail on output to standard error. * @param args Added to the command after the temporary files name * @throws ExternalProcessException if the temporary file could not be written or the external process fails. */ public static final List launchExternalCommand(final String cmd, final byte[] bytes, final boolean failOnCode, final boolean failOnOutput, final List args, final String filePrefix) throws ExternalProcessException { return launchExternalCommand(cmd, bytes, failOnCode, failOnOutput, false, false, args, filePrefix); } /** * Writes the byte-array to a temporary file and launches the given external command with the file as argument at * index positional parameter index 1 or the pipes the PEM certificate into the command. The function will, depending on * its parameters, fail if output to standard error from the command was detected or the command returns with an non-zero exit code. * * @param cmd The command to run. If the parameter place holder {@link #PLACE_HOLDER_CERTIFICATE} is used, the PEM certificate is piped into the STDIN of the command (i.e. 'openssl x509 -text -noout %cert%'). * @param bytes The buffer with content to write to the file. * @param failOnCode Determines if the method should fail on a non-zero exit code. * @param failOnOutput Determines if the method should fail on output to standard error. * @param logStdOut if the scripts STDOUT should be logged as info. * @param logErrOut if the scripts ERROUT should be logged as info. * @param arguments Added to the command after the temporary files name * @throws ExternalProcessException if the temporary file could not be written or the external process fails. */ public static final List launchExternalCommand(final String cmd, final byte[] bytes, final boolean failOnCode, final boolean failOnOutput, final boolean logStdOut, final boolean logErrOut, final List arguments, final String filePrefix) throws ExternalProcessException { final long startTime = System.currentTimeMillis(); int exitStatus = -1; final List result = new ArrayList(); final boolean writeFileToDisk = !arguments.contains(PLACE_HOLDER_CERTIFICATE); File file = null; if (writeFileToDisk) { file = writeTemporaryFileToDisk(bytes, filePrefix, ".tmp"); } // Execute external script or command with PEM in STDIN or full path of temporary file as first argument. String filename = null; try { final List cmdTokens = Arrays.asList(cmd.split("\\s")); // Write file to disk or process place holder with PEM certificates and build shell command. if (writeFileToDisk) { filename = file.getCanonicalPath(); arguments.add(0, filename); } else { // Only works with PEM X.509 certificates at the time as used in ExternalCommandCertificateValidator (not by CRL publishers). final List certificates = new ArrayList(); certificates.add(CertTools.getCertfromByteArray(bytes)); final byte[] testPemBytes = CertTools.getPemFromCertificateChain(certificates); String pemString = new String(testPemBytes); pemString = pemString.substring(pemString.indexOf(LINE_SEPARATOR) + 1, pemString.length()); pemString = pemString.substring(pemString.indexOf(LINE_SEPARATOR) + 1, pemString.length()); if (log.isDebugEnabled()) { log.debug("Using certificates:\n" + pemString); } arguments.remove(arguments.indexOf(PLACE_HOLDER_CERTIFICATE)); if (SystemUtils.IS_OS_WINDOWS) { // Broken. Command cannot be executed. cmdTokens.set(0, "echo \"" + pemString + "\" | " + cmdTokens.get(0)); /* * Hack needed for Windows, where Runtime.exec won't consistently encapsulate arguments, leading to arguments * containing spaces (such as Subject DNs) sometimes being parsed as multiple arguments. Bash, on the other hand, * won't parse quote surrounded arguments. */ qouteArguments(arguments); } else { cmdTokens.set(0, "echo -n \"" + pemString + "\" | " + cmdTokens.get(0)); } } List cmdArray = new ArrayList(); cmdArray.addAll(cmdTokens); cmdArray.addAll(arguments); if (!writeFileToDisk) { cmdArray = buildShellCommand(StringUtils.join(cmdArray, " ")); } if (log.isDebugEnabled()) { log.debug("Process external command for " + getPlatformString() + ": " + cmdArray); } // Launch external process. final Process externalProcess = Runtime.getRuntime().exec(cmdArray.toArray(new String[] {}), null, null); externalProcess.getOutputStream().close(); // prevent process from trying to wait for user input (e.g. prompt for overwrite, or similar) final BufferedReader stdError = new BufferedReader(new InputStreamReader(externalProcess.getErrorStream())); final BufferedReader stdOut = new BufferedReader(new InputStreamReader(externalProcess.getInputStream())); String line = null; while ((line = stdOut.readLine()) != null) { // NOPMD: Required under win32 to avoid lock if (logStdOut) { result.add(STDOUT_PREFIX + line); } } String stdErrorOutput = null; // Check error code and the external applications output to STDERR. exitStatus = externalProcess.waitFor(); result.add(0, EXIT_CODE_PREFIX + exitStatus); if (((exitStatus != 0) && failOnCode) || (stdError.ready() && failOnOutput)) { if (writeFileToDisk && file.exists()) { file.delete(); } String errTemp = null; while (stdError.ready() && (errTemp = stdError.readLine()) != null) { if (logErrOut) { result.add(ERROUT_PREFIX + errTemp); } if (stdErrorOutput == null) { stdErrorOutput = errTemp; } else { stdErrorOutput += "\n" + errTemp; } } String msg = intres.getLocalizedMessage("process.errorexternalapp", cmd); if (stdErrorOutput != null) { msg += " - " + stdErrorOutput + " - " + filename; } throw new ExternalProcessException(msg, result); } } catch (CertificateParsingException | CertificateEncodingException e) { // Should never happen (is only used for certificates not for CRL.) throw new ExternalProcessException("Certificate could not parsed or encoded." + cmd, e, result); } catch (IOException e) { // if the command could not be found result.add(0, EXIT_CODE_PREFIX + exitStatus); if (logErrOut) { result.add(ERROUT_PREFIX + e.getMessage()); } throw new ExternalProcessException(intres.getLocalizedMessage("process.errorexternalapp", cmd), e, result); } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new ExternalProcessException(intres.getLocalizedMessage("process.errorexternalapp", cmd), e, result); } finally { if (writeFileToDisk && file != null && file.exists() && !file.delete()) { // Remove temporary file or schedule for delete if delete fails. file.deleteOnExit(); log.info(intres.getLocalizedMessage("process.errordeletetempfile", filename)); } } if (log.isTraceEnabled()) { log.trace("Time spent to execute external command (writeFileToDisk=" + writeFileToDisk + "): " + (System.currentTimeMillis() - startTime) + "ms."); } return result; } public static final String getPlatformString() { return SystemUtils.OS_NAME + " / " + SystemUtils.OS_VERSION + " - " + SystemUtils.OS_ARCH; } /** * Writes the byte array into a temporary file with the prefix + "-" + System.currentTimeMillies + suffix into the user directory and returns it, or null. * * @param bytes the bytes to write. * @param filePrefix the file prefix. * @param fileSuffix the file suffix. * @return the file or null. * @throws ExternalProcessException any exception. */ public static final File writeTemporaryFileToDisk(final byte[] bytes, final String filePrefix, final String fileSuffix) throws ExternalProcessException { File file = null; try { file = File.createTempFile(filePrefix + "-" + System.currentTimeMillis(), fileSuffix); } catch (IOException e) { final String msg = intres.getLocalizedMessage("process.errortempfile"); log.error(msg, e); } if (file != null) { try (FileOutputStream fos = new FileOutputStream(file)) { fos.write(bytes); } catch (FileNotFoundException e) { final String msg = intres.getLocalizedMessage("process.errortempfile"); log.error(msg, e); throw new ExternalProcessException(msg); } catch (IOException e) { try { file.delete(); } catch (Exception e1) { // NOOP } final String msg = intres.getLocalizedMessage("process.errortempfile"); log.error(msg, e); throw new ExternalProcessException(msg); } } return file; } /** * Extracts the exit code in the list (at index 0 prefixed with #EXIT_CODE_PREFIX). * @param out the output of the external process. * * @return the exit code. */ public static final Integer extractExitCode(final List out) { Integer result = null; if (CollectionUtils.isNotEmpty(out)) { result = Integer.parseInt(out.get(0).replaceFirst(ExternalProcessTools.EXIT_CODE_PREFIX, StringUtils.EMPTY)); } return result; } /** * Checks if the list contains logging to ERROUT. * @param out the output of the external process. * @return true if the list contains logging to ERROUT. */ public static final boolean containsErrout(final List out) { if (CollectionUtils.isNotEmpty(out) && out.size() > 1) { for (int i= 1,j=out.size();i arguments) { for (int i = 0; i < arguments.size(); i++) { String argument = arguments.get(i); //Add quotes to encapsulate argument. if (!argument.startsWith("\"") && !argument.endsWith("\"")) { arguments.set(i, "\"" + argument + "\""); } } } /** * Avoid instantiation. */ private ExternalProcessTools() { } }