Java tutorial
/************************************************************************* * * * SignServer: The OpenSource Automated Signing Server * * * * 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.signserver.client.cli.defaultimpl; import java.io.*; import java.net.MalformedURLException; import java.net.URL; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; import java.security.cert.CertificateException; import java.text.MessageFormat; import java.util.ArrayList; import java.util.HashMap; import java.util.Map; import java.util.ResourceBundle; import java.util.concurrent.TimeUnit; import javax.xml.ws.soap.SOAPFaultException; import org.apache.commons.cli.*; import org.apache.log4j.Logger; import org.ejbca.ui.cli.util.ConsolePasswordReader; import org.signserver.cli.spi.AbstractCommand; import org.signserver.cli.spi.CommandFailureException; import org.signserver.cli.spi.IllegalCommandArgumentsException; import org.signserver.common.AccessDeniedException; import org.signserver.common.AuthorizationRequiredException; import org.signserver.common.CryptoTokenOfflineException; import org.signserver.common.IllegalRequestException; import org.signserver.common.SignServerException; import org.signserver.protocol.ws.client.SignServerWSClientFactory; /** * Command Line Interface (CLI) for signing documents. * * @author Markus Kils * @version $Id: SignDocumentCommand.java 6027 2015-05-11 11:53:12Z netmackan $ */ public class SignDocumentCommand extends AbstractCommand implements ConsolePasswordProvider { /** Logger for this class. */ private static final Logger LOG = Logger.getLogger(SignDocumentCommand.class); /** ResourceBundle with internationalized StringS. */ private static final ResourceBundle TEXTS = ResourceBundle .getBundle("org/signserver/client/cli/defaultimpl/ResourceBundle"); private static final String DEFAULT_CLIENTWS_WSDL_URL = "/signserver/ClientWSService/ClientWS?wsdl"; /** System-specific new line characters. **/ private static final String NL = System.getProperty("line.separator"); /** The name of this command. */ private static final String COMMAND = "signdocument"; /** Option WORKERID. */ public static final String WORKERID = "workerid"; /** Option WORKERNAME. */ public static final String WORKERNAME = "workername"; /** Option DATA. */ public static final String DATA = "data"; /** Option HOST. */ public static final String HOST = "host"; /** Option INFILE. */ public static final String INFILE = "infile"; /** Option OUTFILE. */ public static final String OUTFILE = "outfile"; /** Option INDIR. */ public static final String INDIR = "indir"; /** Option OUTDIR. */ public static final String OUTDIR = "outdir"; /** Option THREADS. */ public static final String THREADS = "threads"; /** Option REMOVEFROMINDIR. */ public static final String REMOVEFROMINDIR = "removefromindir"; /** Option ONEFIRST. */ public static final String ONEFIRST = "onefirst"; /** Option STARTALL. */ public static final String STARTALL = "startall"; /** Option PORT. */ public static final String PORT = "port"; public static final String SERVLET = "servlet"; /** Option PROTOCOL. */ public static final String PROTOCOL = "protocol"; /** Option USERNAME. */ public static final String USERNAME = "username"; /** Option PASSWORD. */ public static final String PASSWORD = "password"; /** Option PDFPASSWORD. */ public static final String PDFPASSWORD = "pdfpassword"; /** Option METADATA. */ public static final String METADATA = "metadata"; /** The command line options. */ private static final Options OPTIONS; private static final int DEFAULT_THREADS = 1; /** * Protocols that can be used for accessing SignServer. */ public static enum Protocol { /** The SignServerWS interface. */ WEBSERVICES, /** The ClientWS interface. */ CLIENTWS, /** The HTTP interface. */ HTTP } static { OPTIONS = new Options(); OPTIONS.addOption(WORKERID, true, TEXTS.getString("WORKERID_DESCRIPTION")); OPTIONS.addOption(WORKERNAME, true, TEXTS.getString("WORKERNAME_DESCRIPTION")); OPTIONS.addOption(DATA, true, TEXTS.getString("DATA_DESCRIPTION")); OPTIONS.addOption(INFILE, true, TEXTS.getString("INFILE_DESCRIPTION")); OPTIONS.addOption(OUTFILE, true, TEXTS.getString("OUTFILE_DESCRIPTION")); OPTIONS.addOption(HOST, true, TEXTS.getString("HOST_DESCRIPTION")); OPTIONS.addOption(PORT, true, TEXTS.getString("PORT_DESCRIPTION")); OPTIONS.addOption(SERVLET, true, TEXTS.getString("SERVLET_DESCRIPTION")); OPTIONS.addOption(PROTOCOL, true, TEXTS.getString("PROTOCOL_DESCRIPTION")); OPTIONS.addOption(USERNAME, true, TEXTS.getString("USERNAME_DESCRIPTION")); OPTIONS.addOption(PASSWORD, true, TEXTS.getString("PASSWORD_DESCRIPTION")); OPTIONS.addOption(PDFPASSWORD, true, TEXTS.getString("PDFPASSWORD_DESCRIPTION")); OPTIONS.addOption(METADATA, true, TEXTS.getString("METADATA_DESCRIPTION")); OPTIONS.addOption(INDIR, true, TEXTS.getString("INDIR_DESCRIPTION")); OPTIONS.addOption(OUTDIR, true, TEXTS.getString("OUTDIR_DESCRIPTION")); OPTIONS.addOption(THREADS, true, TEXTS.getString("THREADS_DESCRIPTION")); OPTIONS.addOption(REMOVEFROMINDIR, false, TEXTS.getString("REMOVEFROMINDIR_DESCRIPTION")); OPTIONS.addOption(ONEFIRST, false, TEXTS.getString("ONEFIRST_DESCRIPTION")); OPTIONS.addOption(STARTALL, false, TEXTS.getString("STARTALL_DESCRIPTION")); for (Option option : KeyStoreOptions.getKeyStoreOptions()) { OPTIONS.addOption(option); } } /** ID of worker who should perform the operation. */ private int workerId; /** Name of worker who should perform the operation. */ private String workerName; /** Data to sign. */ private String data; /** Hostname or IP address of the SignServer host. */ private String host; /** TCP port number of the SignServer host. */ private Integer port; private String servlet = "/signserver/process"; /** File to read the data from. */ private File inFile; /** File to read the signed data to. */ private File outFile; /** Directory to read files from. */ private File inDir; /** Directory to write files to. */ private File outDir; /** Number of threads to use when running in batch mode. */ private Integer threads; /** If the successfully processed files should be removed from indir. */ private boolean removeFromIndir; /** If one request should be set first before starting the remaining threads. */ private boolean oneFirst; /** If all should be started directly (ie not oneFirst). */ private boolean startAll; /** Protocol to use for contacting SignServer. */ private Protocol protocol = Protocol.HTTP; private String username; private String password; private boolean promptForPassword; private String pdfPassword; private final KeyStoreOptions keyStoreOptions = new KeyStoreOptions(); /** Meta data parameters passed in */ private Map<String, String> metadata; @Override public String getDescription() { return "Request a document to be signed by SignServer"; } @Override public String getUsages() { StringBuilder footer = new StringBuilder(); footer.append(NL).append("Sample usages:").append(NL).append("a) ").append(COMMAND) .append(" -workername XMLSigner -data \"<root/>\"").append(NL).append("b) ").append(COMMAND) .append(" -workername XMLSigner -infile /tmp/document.xml").append(NL).append("c) ").append(COMMAND) .append(" -workerid 2 -data \"<root/>\" -truststore truststore.jks -truststorepwd changeit") .append(NL).append("d) ").append(COMMAND) .append(" -workerid 2 -data \"<root/>\" -keystore superadmin.jks -keystorepwd foo123").append(NL) .append("e) ").append(COMMAND) .append(" -workerid 2 -data \"<root/>\" -metadata param1=value1 -metadata param2=value2").append(NL) .append("f) ").append(COMMAND) .append(" -workerid 3 -indir ./input/ -removefromindir -outdir ./output/ -threads 5").append(NL); ByteArrayOutputStream bout = new ByteArrayOutputStream(); final HelpFormatter formatter = new HelpFormatter(); PrintWriter pw = new PrintWriter(bout); formatter.printHelp(pw, HelpFormatter.DEFAULT_WIDTH, "signdocument <-workername WORKERNAME | -workerid WORKERID> [options]", getDescription(), OPTIONS, HelpFormatter.DEFAULT_LEFT_PAD, HelpFormatter.DEFAULT_DESC_PAD, footer.toString()); pw.close(); return bout.toString(); } /** * Reads all the options from the command line. * * @param line The command line to read from */ private void parseCommandLine(final CommandLine line) throws IllegalCommandArgumentsException { if (line.hasOption(WORKERNAME)) { workerName = line.getOptionValue(WORKERNAME, null); } if (line.hasOption(WORKERID)) { workerId = Integer.parseInt(line.getOptionValue(WORKERID, null)); } host = line.getOptionValue(HOST, KeyStoreOptions.DEFAULT_HOST); if (line.hasOption(PORT)) { port = Integer.parseInt(line.getOptionValue(PORT)); } if (line.hasOption(SERVLET)) { servlet = line.getOptionValue(SERVLET, null); } if (line.hasOption(DATA)) { data = line.getOptionValue(DATA, null); } if (line.hasOption(INFILE)) { inFile = new File(line.getOptionValue(INFILE, null)); } if (line.hasOption(OUTFILE)) { outFile = new File(line.getOptionValue(OUTFILE, null)); } if (line.hasOption(INDIR)) { inDir = new File(line.getOptionValue(INDIR, null)); } if (line.hasOption(OUTDIR)) { outDir = new File(line.getOptionValue(OUTDIR, null)); } if (line.hasOption(THREADS)) { threads = Integer.parseInt(line.getOptionValue(THREADS, null)); } if (line.hasOption(REMOVEFROMINDIR)) { removeFromIndir = true; } if (line.hasOption(ONEFIRST)) { oneFirst = true; } if (line.hasOption(STARTALL)) { startAll = true; } if (line.hasOption(PROTOCOL)) { protocol = Protocol.valueOf(line.getOptionValue(PROTOCOL, null)); // if the protocol is WS and -servlet is not set, override the servlet URL // with the default one for the WS servlet if (Protocol.WEBSERVICES.equals(protocol) && !line.hasOption(SERVLET)) { servlet = SignServerWSClientFactory.DEFAULT_WSDL_URL; } if ((Protocol.CLIENTWS.equals(protocol)) && !line.hasOption(SERVLET)) { servlet = DEFAULT_CLIENTWS_WSDL_URL; } } if (line.hasOption(USERNAME)) { username = line.getOptionValue(USERNAME, null); } if (line.hasOption(PASSWORD)) { password = line.getOptionValue(PASSWORD, null); } if (line.hasOption(PDFPASSWORD)) { pdfPassword = line.getOptionValue(PDFPASSWORD, null); } if (line.hasOption(METADATA)) { metadata = MetadataParser.parseMetadata(line.getOptionValues(METADATA)); } try { final ConsolePasswordReader passwordReader = createConsolePasswordReader(); keyStoreOptions.parseCommandLine(line, passwordReader, out); // Prompt for user password if not given if (username != null && password == null) { promptForPassword = true; out.print("Password for user '" + username + "': "); out.flush(); password = new String(passwordReader.readPassword()); } } catch (IOException ex) { throw new IllegalCommandArgumentsException("Failed to read password: " + ex.getLocalizedMessage()); } catch (NoSuchAlgorithmException ex) { throw new IllegalCommandArgumentsException("Failure setting up keystores: " + ex.getMessage()); } catch (CertificateException ex) { throw new IllegalCommandArgumentsException("Failure setting up keystores: " + ex.getMessage()); } catch (KeyStoreException ex) { throw new IllegalCommandArgumentsException("Failure setting up keystores: " + ex.getMessage()); } } /** * @return a ConsolePasswordReader that can be used to read passwords */ @Override public ConsolePasswordReader createConsolePasswordReader() { return new ConsolePasswordReader(); } /** * Checks that all mandatory options are given. */ private void validateOptions() throws IllegalCommandArgumentsException { if (workerName == null && workerId == 0) { throw new IllegalCommandArgumentsException("Missing -workername or -workerid"); } else if (data == null && inFile == null && inDir == null && outDir == null) { throw new IllegalCommandArgumentsException("Missing -data, -infile or -indir"); } if (inDir != null && outDir == null) { throw new IllegalCommandArgumentsException("Missing -outdir"); } if (data != null && inFile != null) { throw new IllegalCommandArgumentsException("Can not specify both -data and -infile"); } if (data != null && inDir != null) { throw new IllegalCommandArgumentsException("Can not specify both -data and -indir"); } if (inFile != null && inDir != null) { throw new IllegalCommandArgumentsException("Can not specify both -infile and -indir"); } if (inDir != null && inDir.equals(outDir)) { throw new IllegalCommandArgumentsException("Can not specify the same directory as -indir and -outdir"); } if (inDir == null & threads != null) { throw new IllegalCommandArgumentsException("Can not specify -threads unless -indir"); } if (threads != null && threads < 1) { throw new IllegalCommandArgumentsException("Number of threads must be > 0"); } if (startAll && oneFirst) { throw new IllegalCommandArgumentsException("Can not specify both -onefirst and -startall"); } if ((startAll || oneFirst) && (inDir == null)) { throw new IllegalCommandArgumentsException( "The options -onefirst and -startall only supported in batch mode. Specify -indir."); } // Default to use oneFirst if username is specified and not startall if (!startAll && username != null) { oneFirst = true; } keyStoreOptions.validateOptions(); } /** * Creates a DocumentSigner using the choosen protocol. * * @return a DocumentSigner using the choosen protocol * @throws MalformedURLException in case an URL can not be constructed * using the given host and port */ private DocumentSigner createSigner(final String currentPassword) throws MalformedURLException { final DocumentSigner signer; keyStoreOptions.setupHTTPS(); // TODO: Should be done earlier and only once (not for each signer) if (port == null) { if (keyStoreOptions.isUsePrivateHTTPS()) { port = KeyStoreOptions.DEFAULT_PRIVATE_HTTPS_PORT; } else if (keyStoreOptions.isUseHTTPS()) { port = KeyStoreOptions.DEFAULT_PUBLIC_HTTPS_PORT; } else { port = KeyStoreOptions.DEFAULT_HTTP_PORT; } } switch (protocol) { case WEBSERVICES: { LOG.debug("Using SignServerWS as procotol"); final String workerIdOrName; if (workerId == 0) { workerIdOrName = workerName; } else { workerIdOrName = String.valueOf(workerId); } signer = new WebServicesDocumentSigner(host, port, servlet, workerIdOrName, keyStoreOptions.isUseHTTPS(), username, currentPassword, pdfPassword, metadata); break; } case CLIENTWS: { LOG.debug("Using ClientWS as procotol"); final String workerIdOrName; if (workerId == 0) { workerIdOrName = workerName; } else { workerIdOrName = String.valueOf(workerId); } signer = new ClientWSDocumentSigner(host, port, servlet, workerIdOrName, keyStoreOptions.isUseHTTPS(), username, currentPassword, pdfPassword, metadata); break; } case HTTP: default: { LOG.debug("Using HTTP as procotol"); final URL url = new URL(keyStoreOptions.isUseHTTPS() ? "https" : "http", host, port, servlet); if (workerId == 0) { signer = new HTTPDocumentSigner(url, workerName, username, currentPassword, pdfPassword, metadata); } else { signer = new HTTPDocumentSigner(url, workerId, username, currentPassword, pdfPassword, metadata); } } } return signer; } /** * Execute the signing operation. * @param manager for managing the threads * @param inFile directory * @param outFile directory */ protected void runBatch(TransferManager manager, final File inFile, final File outFile) { FileInputStream fin = null; try { final byte[] bytes; Map<String, Object> requestContext = new HashMap<String, Object>(); if (inFile == null) { bytes = data.getBytes(); } else { requestContext.put("FILENAME", inFile.getName()); fin = new FileInputStream(inFile); bytes = new byte[(int) inFile.length()]; fin.read(bytes); } runFile(manager, requestContext, inFile, bytes, outFile); } catch (FileNotFoundException ex) { LOG.error(MessageFormat.format(TEXTS.getString("FILE_NOT_FOUND:"), ex.getLocalizedMessage())); } catch (IOException ex) { LOG.error(ex); } finally { if (fin != null) { try { fin.close(); } catch (IOException ex) { LOG.error("Error closing file", ex); } } } } /** * Runs the signing operation for one file. * * @param manager for the threads * @param requestContext for the request * @param inFile directory * @param bytes to sign * @param outFile directory */ private void runFile(TransferManager manager, Map<String, Object> requestContext, final File inFile, final byte[] bytes, final File outFile) { // TODO: merge with runBatch ?, inFile here is only used when removing the file try { OutputStream outStream = null; try { if (outFile == null) { outStream = System.out; } else { outStream = new FileOutputStream(outFile); } final DocumentSigner signer = createSigner(manager == null ? password : manager.getPassword()); // Take start time final long startTime = System.nanoTime(); // Get the data signed signer.sign(bytes, outStream, requestContext); // Take stop time final long estimatedTime = System.nanoTime() - startTime; if (LOG.isInfoEnabled()) { LOG.info("Wrote " + outFile + "."); LOG.info("Processing " + (inFile == null ? "" : inFile.getName()) + " took " + TimeUnit.NANOSECONDS.toMillis(estimatedTime) + " ms."); } } finally { if (outStream != null && outStream != System.out) { outStream.close(); } } if (removeFromIndir && inFile != null && inFile.exists()) { if (inFile.delete()) { LOG.info("Removed " + inFile); } else { LOG.error("Could not remove " + inFile); if (manager != null) { manager.registerFailure(); } } } if (manager != null) { manager.registerSuccess(); // Login must have worked } } catch (FileNotFoundException ex) { LOG.error("Failure for " + (inFile == null ? "" : inFile.getName()) + ": " + MessageFormat.format(TEXTS.getString("FILE_NOT_FOUND:"), ex.getLocalizedMessage())); if (manager != null) { manager.registerFailure(); } } catch (IllegalRequestException ex) { LOG.error("Failure for " + (inFile == null ? "" : inFile.getName()) + ": " + ex.getMessage()); if (manager != null) { manager.registerFailure(); } } catch (CryptoTokenOfflineException ex) { LOG.error("Failure for " + (inFile == null ? "" : inFile.getName()) + ": " + ex.getMessage()); if (manager != null) { manager.registerFailure(); } } catch (SignServerException ex) { LOG.error("Failure for " + (inFile == null ? "" : inFile.getName()) + ": " + ex.getMessage()); if (manager != null) { manager.registerFailure(); } } catch (SOAPFaultException ex) { if (ex.getCause() instanceof AuthorizationRequiredException) { final AuthorizationRequiredException authEx = (AuthorizationRequiredException) ex.getCause(); LOG.error("Authorization failure for " + (inFile == null ? "" : inFile.getName()) + ": " + authEx.getMessage()); } else if (ex.getCause() instanceof AccessDeniedException) { final AccessDeniedException authEx = (AccessDeniedException) ex.getCause(); LOG.error("Access defined failure for " + (inFile == null ? "" : inFile.getName()) + ": " + authEx.getMessage()); } LOG.error(ex); } catch (HTTPException ex) { LOG.error("Failure for " + (inFile == null ? "" : inFile.getName()) + ": HTTP Error " + ex.getResponseCode() + ": " + ex.getResponseMessage()); if (manager != null) { if (ex.getResponseCode() == 401) { // Only abort for authentication failure if (promptForPassword) { // If password was not specified at command line, ask again for it manager.tryAgainWithNewPassword(inFile); } else { manager.abort(); } } else { manager.registerFailure(); } } } catch (IOException ex) { LOG.error("Failure for " + (inFile == null ? "" : inFile.getName()) + ": " + ex.getMessage()); if (manager != null) { manager.registerFailure(); } } } @Override public int execute(String[] args) throws IllegalCommandArgumentsException, CommandFailureException { try { // Parse the command line parseCommandLine(new GnuParser().parse(OPTIONS, args)); validateOptions(); if (inFile != null) { LOG.debug("Will request for single file " + inFile); runBatch(null, inFile, outFile); } else if (inDir != null) { LOG.debug("Will request for each file in directory " + inDir); File[] inFiles = inDir.listFiles(); if (inFiles == null || inFiles.length == 0) { LOG.error("No input files"); return 1; } TransferManager producer = new TransferManager(inFiles, username, password, this, out, oneFirst); if (threads == null) { threads = DEFAULT_THREADS; } final int threadCount = threads > inFiles.length ? inFiles.length : threads; final ArrayList<TransferThread> consumers = new ArrayList<TransferThread>(); for (int i = 0; i < threadCount; i++) { consumers.add(new TransferThread(i, producer)); } // Start the threads for (TransferThread consumer : consumers) { consumer.start(); } // Wait for the threads to finish try { for (TransferThread w : consumers) { if (LOG.isDebugEnabled()) { LOG.debug("Waiting for thread " + w.getName()); } w.join(); if (LOG.isDebugEnabled()) { LOG.debug("Thread " + w.getName() + " stopped"); } } } catch (InterruptedException ex) { if (LOG.isDebugEnabled()) { LOG.debug("Interupted when waiting for thread: " + ex.getMessage()); } } if (producer.isAborted()) { throw new CommandFailureException("Aborted due to failure."); } if (producer.hasFailures()) { throw new CommandFailureException("At least one file failed."); } } else { LOG.debug("Will requst for the specified data"); runBatch(null, null, outFile); } return 0; } catch (ParseException ex) { throw new IllegalCommandArgumentsException(ex.getMessage()); } } /** * Thread for running the upload/download of the data. */ @SuppressWarnings("PMD.DoNotUseThreads") // Not an JEE application private class TransferThread extends Thread { private final int id; private final TransferManager producer; public TransferThread(int id, TransferManager producer) { super("transfer-" + id); this.id = id; this.producer = producer; } @Override public void run() { if (LOG.isTraceEnabled()) { LOG.trace("Starting " + getName() + "..."); } File file; while ((file = producer.nextFile()) != null) { if (LOG.isInfoEnabled()) { LOG.info("Sending " + file + "..."); } runBatch(producer, file, new File(outDir, file.getName())); } if (LOG.isTraceEnabled()) { LOG.trace(id + ": No more work."); } } } }