com.trsst.Command.java Source code

Java tutorial

Introduction

Here is the source code for com.trsst.Command.java

Source

/*
 * Copyright 2013 mpowers
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.trsst;

import java.io.BufferedInputStream;
import java.io.Console;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.security.InvalidKeyException;
import java.security.KeyPair;
import java.security.KeyStore;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.Security;
import java.security.SignatureException;
import java.security.cert.X509Certificate;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Date;
import java.util.LinkedList;
import java.util.List;

import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.CommandLineParser;
import org.apache.commons.cli.GnuParser;
import org.apache.commons.cli.HelpFormatter;
import org.apache.commons.cli.Option;
import org.apache.commons.cli.OptionBuilder;
import org.apache.commons.cli.Options;
import org.apache.tika.Tika;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.eclipse.jetty.servlet.ServletHolder;
import org.silvertunnel_ng.netlib.adapter.java.JvmGlobalUtil;
import org.silvertunnel_ng.netlib.api.NetFactory;
import org.silvertunnel_ng.netlib.api.NetLayer;
import org.silvertunnel_ng.netlib.api.NetLayerIDs;

import com.trsst.client.Client;
import com.trsst.client.EntryOptions;
import com.trsst.client.FeedOptions;
import com.trsst.server.Server;
import com.trsst.server.TrsstAdapter;
import com.trsst.ui.AppMain;
import com.trsst.ui.AppServlet;

/**
 * Command-line program that implements the application-level features needed to
 * use the Trsst protocol: key management and user input and output.<br/>
 * 
 * Application-level features not implemented here include syncrhonization of
 * feed subscriptions and keystores between user clients, and generation and
 * distribution of confidential public keys to groups of other users to form the
 * equivalent of "circles" or "friend lists".
 * 
 * A trsst client must to connect to a host server. If no home server is
 * specified, this client will start a temporary server on the local machine,
 * and close it when finished.
 * 
 * A client instance stores user keystores in a directory called "trsstd" in the
 * current user's home directory, or the path indicated in the
 * "com.trsst.client.storage" system property.
 * 
 * There are three basic operations:<br/>
 * <ul>
 * 
 * <li>pull: pulls the specified feed from the specified host server.<br/>
 * 
 * <li>push: pushes the specified feed from the current host server to the
 * specified remote server.<br/>
 * 
 * <li>post: posts a new entry to the specified feed on the host server,
 * creating a new feed if no feed id is specified.
 * </ul>
 * 
 * This program can alternately start a standalone server instance:
 * <ul>
 * 
 * <li>port: starts a trsst server on the specified port on this machine.
 * </ul>
 * 
 * A server instance defaults to local file persistence in a directory called
 * "trsstd" in the current user's home directory, or the path indicated in the
 * "com.trsst.server.storage" system property.
 * 
 * Application-level features not implemented here include syncrhonization of
 * feed subscriptions and keystores between user clients, and generation and
 * distribution of confidential public keys to groups of other users to form the
 * equivalent of "circles" or "friend lists".
 * 
 * 
 * @author mpowers
 */
@SuppressWarnings("deprecation")
public class Command {

    static {
        if (System.getProperty("org.slf4j.simpleLogger.defaultLogLevel") == null) {
            // if unspecified, default to error-level logging for jetty
            System.setProperty("org.slf4j.simpleLogger.defaultLogLevel", "warn");
        }
    }

    public static void main(String[] argv) {

        // during alpha period: expire after one week
        Date builtOn = Common.getBuildDate();
        if (builtOn != null) {
            long weekMillis = 1000 * 60 * 60 * 24 * 7;
            Date expiry = new Date(builtOn.getTime() + weekMillis);
            if (new Date().after(expiry)) {
                System.err.println("Build expired on: " + expiry);
                System.err.println("Please obtain a more recent build for testing.");
                System.exit(1);
            } else {
                System.err.println("Build will expire on: " + expiry);
            }
        }

        // experimental tor support
        boolean wantsTor = false;
        for (String s : argv) {
            if ("--tor".equals(s)) {
                wantsTor = true;
                break;
            }
        }
        if (wantsTor && !HAS_TOR) {
            try {
                log.info("Attempting to connect to tor network...");
                Security.addProvider(new BouncyCastleProvider());
                JvmGlobalUtil.init();
                NetLayer netLayer = NetFactory.getInstance().getNetLayerById(NetLayerIDs.TOR);
                JvmGlobalUtil.setNetLayerAndNetAddressNameService(netLayer, true);
                log.info("Connected to tor network");
                HAS_TOR = true;
            } catch (Throwable t) {
                log.error("Could not connect to tor: exiting", t);
                System.exit(1);
            }
        }

        // if unspecified, default relay to home.trsst.com
        if (System.getProperty("com.trsst.server.relays") == null) {
            System.setProperty("com.trsst.server.relays", "https://home.trsst.com/feed");
        }

        // default to user-friendlier file names
        String home = System.getProperty("user.home", ".");
        if (System.getProperty("com.trsst.client.storage") == null) {
            File client = new File(home, "Trsst Accounts");
            System.setProperty("com.trsst.client.storage", client.getAbsolutePath());
        }
        if (System.getProperty("com.trsst.server.storage") == null) {
            File server = new File(home, "Trsst System Cache");
            System.setProperty("com.trsst.server.storage", server.getAbsolutePath());
        }
        // TODO: try to detect if launching from external volume like a flash
        // drive and store on the local flash drive instead

        Console console = System.console();
        int result;
        try {
            if (console == null && argv.length == 0) {
                argv = new String[] { "serve", "--gui" };
            }
            result = new Command().doBegin(argv, System.out, System.in);

            // task queue prevents exit unless stopped
            if (TrsstAdapter.TASK_QUEUE != null) {
                TrsstAdapter.TASK_QUEUE.cancel();
            }
        } catch (Throwable t) {
            result = 1; // "general catchall error code"
            log.error("Unexpected error, exiting.", t);
        }

        // if error
        if (result != 0) {
            // force exit
            System.exit(result);
        }
    }

    private Options portOptions;
    private Options pullOptions;
    private Options mergedOptions;
    private Options postOptions;
    private Option helpOption;
    private boolean format = false;
    private static boolean HAS_TOR = false;

    @SuppressWarnings("static-access")
    private void buildOptions(String[] argv, PrintStream out, InputStream in) {

        // NOTE: OptionsBuilder is NOT thread-safe
        // which was causing us random failures.
        Option o;

        portOptions = new Options();

        o = new Option(null, "Specify port");
        o.setRequired(false);
        o.setArgs(1);
        o.setLongOpt("port");
        portOptions.addOption(o);

        o = new Option(null, "Expose client API");
        o.setRequired(false);
        o.setArgs(0);
        o.setLongOpt("api");
        portOptions.addOption(o);

        o = new Option(null, "Launch embedded GUI");
        o.setRequired(false);
        o.setArgs(0);
        o.setLongOpt("gui");
        portOptions.addOption(o);

        o = new Option(null, "Turn off SSL");
        o.setRequired(false);
        o.setArgs(0);
        o.setLongOpt("clear");
        portOptions.addOption(o);

        o = new Option(null, "Use TOR (experimental)");
        o.setRequired(false);
        o.setArgs(0);
        o.setLongOpt("tor");
        portOptions.addOption(o);

        pullOptions = new Options();

        o = new Option("h", "Set host server for this operation");
        o.setRequired(false);
        o.setArgs(1);
        o.setArgName("url");
        o.setLongOpt("host");
        pullOptions.addOption(o);

        o = new Option("d", "Decrypt entries as specified recipient id");
        o.setRequired(false);
        o.setArgs(1);
        o.setArgName("id");
        o.setLongOpt("decrypt");
        pullOptions.addOption(o);

        postOptions = new Options();

        o = new Option("a", "Attach the specified file, or - for std input");
        o.setRequired(false);
        o.setOptionalArg(true);
        o.setArgName("file");
        o.setLongOpt("attach");
        postOptions.addOption(o);

        o = new Option("b", "Set base URL for this feed");
        o.setRequired(false);
        o.setArgs(1);
        o.setArgName("url");
        o.setLongOpt("base");
        postOptions.addOption(o);

        o = new Option("p", "Specify passphrase on the command line");
        o.setRequired(false);
        o.setArgs(1);
        o.setArgName("text");
        o.setLongOpt("pass");
        postOptions.addOption(o);

        o = new Option("s", "Specify status update on command line");
        o.setRequired(false);
        o.setArgs(1);
        o.setArgName("text");
        o.setLongOpt("status");
        postOptions.addOption(o);

        o = new Option("u", "Attach the specified url to the new entry");
        o.setRequired(false);
        o.setArgs(1);
        o.setArgName("url");
        o.setLongOpt("url");
        postOptions.addOption(o);

        o = new Option("v", "Specify an activitystreams verb for this entry");
        o.setRequired(false);
        o.setArgs(1);
        o.setArgName("verb");
        o.setLongOpt("verb");
        postOptions.addOption(o);

        o = new Option("r", "Add a mention");
        o.setRequired(false);
        o.setArgs(1);
        o.setArgName("id");
        o.setLongOpt("mention");
        postOptions.addOption(o);

        o = new Option("g", "Add a tag");
        o.setRequired(false);
        o.setArgs(1);
        o.setArgName("text");
        o.setLongOpt("tag");
        postOptions.addOption(o);

        o = new Option("c", "Specify entry content on command line");
        o.setRequired(false);
        o.setArgs(1);
        o.setArgName("text");
        o.setLongOpt("content");
        postOptions.addOption(o);

        o = new Option("t", "Set this feed's title");
        o.setRequired(false);
        o.setArgs(1);
        o.setArgName("text");
        o.setLongOpt("title");
        postOptions.addOption(o);

        o = new Option(null, "Set this feed's subtitle");
        o.setRequired(false);
        o.setArgs(1);
        o.setArgName("text");
        o.setLongOpt("subtitle");
        postOptions.addOption(o);

        o = new Option("n", "Set this feed's author name");
        o.setRequired(false);
        o.setArgs(1);
        o.setArgName("text");
        o.setLongOpt("name");
        postOptions.addOption(o);

        o = new Option(null, "Set this feed's author uri");
        o.setRequired(false);
        o.setArgs(1);
        o.setArgName("uri");
        o.setLongOpt("uri");
        postOptions.addOption(o);

        o = new Option("e", "Encrypt entry for specified public key");
        o.setRequired(false);
        o.setArgs(1);
        o.setArgName("pubkey");
        o.setLongOpt("encrypt");
        postOptions.addOption(o);

        o = new Option("m", "Set this feed's author email");
        o.setRequired(false);
        o.setArgs(1);
        o.setArgName("email");
        o.setLongOpt("email");
        postOptions.addOption(o);

        o = new Option("i", "Set as this feed's icon or specify url");
        o.setRequired(false);
        o.setArgs(1);
        o.setArgName("url");
        o.setLongOpt("icon");
        postOptions.addOption(o);

        o = new Option("l", "Set as this feed's logo or specify url");
        o.setRequired(false);
        o.setArgs(1);
        o.setArgName("url");
        o.setLongOpt("logo");
        postOptions.addOption(o);

        o = new Option(null, "Generate feed id with specified prefix");
        o.setRequired(false);
        o.setArgs(1);
        o.setArgName("prefix");
        o.setLongOpt("vanity");
        postOptions.addOption(o);

        o = new Option(null, "Require SSL certs");
        o.setRequired(false);
        o.setArgs(0);
        o.setLongOpt("strict");
        postOptions.addOption(o);

        // merge options parameters
        mergedOptions = new Options();
        for (Object obj : pullOptions.getOptions()) {
            mergedOptions.addOption((Option) obj);
        }
        for (Object obj : postOptions.getOptions()) {
            mergedOptions.addOption((Option) obj);
        }
        for (Object obj : portOptions.getOptions()) {
            mergedOptions.addOption((Option) obj);
        }
        helpOption = OptionBuilder.isRequired(false).withLongOpt("help").withDescription("Display these options")
                .create('?');
        mergedOptions.addOption(helpOption);
    }

    public int doBegin(String[] argv, PrintStream out, InputStream in) {

        buildOptions(argv, out, in);

        int result = 0;
        Server server = null;
        try {

            CommandLineParser argParser = new GnuParser();
            CommandLine commands;
            try {
                commands = argParser.parse(mergedOptions, argv);
            } catch (Throwable t) {
                log.error("Unexpected error parsing arguments: " + Arrays.asList(argv), t);
                return 127;
            }
            LinkedList<String> arguments = new LinkedList<String>();
            for (Object o : commands.getArgList()) {
                arguments.add(o.toString()); // dodge untyped param warning
            }
            if (commands.hasOption("?")) {
                printAllUsage();
                return 0;
            }
            if (arguments.size() < 1) {
                printAllUsage();
                return 127; // "command not found"
            }
            if (!commands.hasOption("strict")) {
                // most trsst nodes run with self-signed certificates,
                // so by default we accept them
                Common.enableAnonymousSSL();
            } else {
                System.err.println("Requiring signed SSL");
            }
            // System.out.println("Commands: " + arguments );
            String mode = arguments.removeFirst().toString();

            // for port requests
            if ("serve".equals(mode)) {
                // start a server and exit
                result = doServe(commands, arguments);
                return 0;
            }

            // attempt to parse next argument as a server url
            Client client = null;
            if (commands.hasOption("h")) {
                String host = commands.getOptionValue("h");
                try {
                    URL url = new URL(host);
                    // this argument is a server url
                    client = new Client(url);
                    System.err.println("Using service: " + host);
                } catch (MalformedURLException e) {
                    // otherwise: ignore and continue
                    System.err.println("Bad hostname: " + host);
                }
            }

            // if a server url wasn't specified
            if (client == null) {
                // start a client with a local server
                server = new Server();
                client = new Client(server.getServiceURL());
                System.err.println("Starting temporary service at: " + server.getServiceURL());
            }

            if ("pull".equals(mode)) {
                // pull feeds from server
                result = doPull(client, commands, arguments, out);
            } else if ("push".equals(mode)) {
                // push feeds to server
                result = doPush(client, commands, arguments, out);
            } else if ("post".equals(mode)) {
                // post (and push) entries
                result = doPost(client, commands, arguments, out, in);
            } else {
                printAllUsage();
                result = 127; // "command not found"
            }
        } catch (Throwable t) {
            log.error("Unexpected error: " + t, t);
            result = 1; // "catchall for general errors"
        }
        if (server != null) {
            server.stop();
        }
        return result;
    }

    public int doPull(Client client, CommandLine commands, LinkedList<String> arguments, PrintStream out) {

        if (arguments.size() < 1) {
            printPullUsage();
            return 127; // "command not found"
        }

        // decryption option
        String id = commands.getOptionValue("d");
        PrivateKey[] decryptionKeys = null;

        if (id != null) {
            // obtain password
            id = Common.toFeedIdString(id);
            char[] password = null;
            String pass = commands.getOptionValue("p");
            if (pass != null) {
                password = pass.toCharArray();
            } else {
                try {
                    Console console = System.console();
                    if (console != null) {
                        password = console.readPassword("Password: ");
                    } else {
                        log.info("No console detected for password input.");
                    }
                } catch (Throwable t) {
                    log.error("Unexpected error while reading password", t);
                }
            }
            if (password == null) {
                log.error("Password is required to decrypt.");
                return 127; // "command not found"
            }
            if (password.length < 6) {
                System.err.println("Password must be at least six characters in length.");
                return 127; // "command not found"
            }

            // obtain keys
            KeyPair signingKeys = null;
            KeyPair encryptionKeys = null;
            String keyPath = commands.getOptionValue("k");

            File keyFile;
            if (keyPath != null) {
                keyFile = new File(keyPath, id + Common.KEY_EXTENSION);
            } else {
                keyFile = new File(Common.getClientRoot(), id + Common.KEY_EXTENSION);
            }

            if (keyFile.exists()) {
                System.err.println("Using existing account id: " + id);

            } else {
                System.err.println("Cannot locate keys for account id: " + id);
                return 78; // "configuration error"
            }

            signingKeys = readSigningKeyPair(id, keyFile, password);
            if (signingKeys != null) {
                encryptionKeys = readEncryptionKeyPair(id, keyFile, password);
                if (encryptionKeys == null) {
                    decryptionKeys = new PrivateKey[] { signingKeys.getPrivate() };
                } else {
                    decryptionKeys = new PrivateKey[] { encryptionKeys.getPrivate(), signingKeys.getPrivate() };
                }
            }
        }

        List<String> ids = new LinkedList<String>();
        for (String arg : arguments) {
            ids.add(arg);
        }

        for (String feedId : ids) {
            try {
                Object feed;
                if (decryptionKeys != null) {
                    feed = client.pull(feedId, decryptionKeys);
                } else {
                    feed = client.pull(feedId);
                }
                if (feed != null) {
                    if (format) {
                        out.println(Common.formatXML(feed.toString()));
                    } else {
                        out.println(feed.toString());
                    }
                } else {
                    System.err.println("Could not fetch: " + feedId + " : " + client);
                }
            } catch (Throwable t) {
                log.error("Unexpected error on pull: " + feedId + " : " + client, t);
            }
        }
        return 0; // "OK"
    }

    public int doPush(Client client, CommandLine commands, LinkedList<String> arguments, PrintStream out) {

        // if a second argument was specified
        if (arguments.size() < 2) {
            printPushUsage();
            return 127; // "command not found"
        }
        URL url;
        String host = arguments.removeFirst().toString();
        Client destinationClient;
        try {
            url = new URL(host);
            // this argument is a server url
            destinationClient = new Client(url);
            System.err.println("Using service: " + host);
        } catch (MalformedURLException e) {
            printPushUsage();
            return 127; // "command not found"
        }

        for (String id : arguments) {
            System.out.println(destinationClient.push(client.pull(id), url));
            // Feed feed = client.pull(id);
            // if ( feed != null ) {
            // feed = client.push(feed, url);
            // if ( feed != null ) {
            // out.println(feed);
            // } else {
            // System.err.println("Failed to push feed for id: " + id);
            // }
            // } else {
            // System.err.println("Failed to pull feed for id: " + id);
            // }
        }
        return 0; // "OK"
    }

    public int doServe(CommandLine commands, LinkedList<String> arguments) {

        boolean apiOption = commands.hasOption("api");
        boolean guiOption = commands.hasOption("gui");
        boolean clearOption = commands.hasOption("clear");
        int portOption = 0; // default to random port

        if (commands.hasOption("port")) {
            String portString = commands.getOptionValue("port");
            try {
                portOption = Integer.parseInt(portString);
            } catch (NumberFormatException t) {
                log.error("Invalid port: " + portString);
                return 78; // "configuration error"
            }
        }

        Server service;
        try {
            service = new Server(portOption, "feed", !clearOption);
            if (apiOption || guiOption) {
                service.getServletContextHandler().addServlet(new ServletHolder(new AppServlet(!apiOption)), "/*");
                URL url = service.getServiceURL();
                String path = url.getPath();
                int i = url.toString().indexOf(path);
                if (i != -1) {
                    path = url.toString().substring(0, i);
                    System.err.println("Client services now available at: " + path);
                }
            }
            System.err.println("Services now available at: " + service.getServiceURL());
        } catch (Exception e) {
            log.error("Could not start server: " + e);
            return 71; // "system error"
        }

        // attempt GUI mode
        if (guiOption) {
            try {
                AppMain.main(new String[] { service.getServiceURL().toString() });
            } catch (Throwable t) {
                log.error("Could not launch UI client", t);
            }
        }
        return 0; // "OK"
    }

    public int doPost(Client client, CommandLine commands, LinkedList<String> arguments, PrintStream out,
            InputStream in) {

        String id = null;

        if (arguments.size() == 0 && commands.getArgList().size() == 0) {
            printPostUsage();
            return 127; // "command not found"
        }

        if (arguments.size() > 0) {
            id = arguments.removeFirst();
            System.err.println("Obtaining keys for feed id: " + id);
        } else {
            System.err.println("Generating new feed id... ");
        }

        // read input text
        String subject = commands.getOptionValue("s");
        String verb = commands.getOptionValue("v");
        String base = commands.getOptionValue("b");
        String body = commands.getOptionValue("c");
        String name = commands.getOptionValue("n");
        String email = commands.getOptionValue("m");
        String uri = commands.getOptionValue("uri");
        String title = commands.getOptionValue("t");
        String subtitle = commands.getOptionValue("subtitle");
        String icon = commands.getOptionValue("i");
        if (icon == null && commands.hasOption("i")) {
            icon = "-";
        }
        String logo = commands.getOptionValue("l");
        if (logo == null && commands.hasOption("l")) {
            logo = "-";
        }
        String attach = commands.getOptionValue("a");
        if (attach == null && commands.hasOption("a")) {
            attach = "-";
        }
        String[] recipients = commands.getOptionValues("e");
        String[] mentions = commands.getOptionValues("r");
        String[] tags = commands.getOptionValues("g");
        String url = commands.getOptionValue("u");
        String vanity = commands.getOptionValue("vanity");

        // obtain password
        char[] password = null;
        String pass = commands.getOptionValue("p");
        if (pass != null) {
            password = pass.toCharArray();
        } else {
            try {
                Console console = System.console();
                if (console != null) {
                    password = console.readPassword("Password: ");
                } else {
                    log.info("No console detected for password input.");
                }
            } catch (Throwable t) {
                log.error("Unexpected error while reading password", t);
            }
        }
        if (password == null) {
            log.error("Password is required to post.");
            return 127; // "command not found"
        }
        if (password.length < 6) {
            System.err.println("Password must be at least six characters in length.");
            return 127; // "command not found"
        }

        // obtain keys
        KeyPair signingKeys = null;
        KeyPair encryptionKeys = null;
        String keyPath = commands.getOptionValue("k");

        // if id was not specified from the command line
        if (id == null) {

            // if password was not specified from command line
            if (pass == null) {
                try {
                    // verify password
                    char[] verify = null;
                    Console console = System.console();
                    if (console != null) {
                        verify = console.readPassword("Re-type Password: ");
                    } else {
                        log.info("No console detected for password verification.");
                    }
                    if (verify == null || verify.length != password.length) {
                        System.err.println("Passwords do not match.");
                        return 127; // "command not found"
                    }
                    for (int i = 0; i < verify.length; i++) {
                        if (verify[i] != password[i]) {
                            System.err.println("Passwords do not match.");
                            return 127; // "command not found"
                        }
                        verify[i] = 0;
                    }
                } catch (Throwable t) {
                    log.error("Unexpected error while verifying password: " + t.getMessage(), t);
                }
            }

            // create new account
            if (base == null) {
                // default to trsst hub
                base = "https://home.trsst.com/feed";
            }

            // generate vanity id if required
            if (vanity != null) {
                System.err.println("Searching for vanity feed id prefix: " + vanity);
                switch (vanity.length()) {
                case 0:
                case 1:
                    break;
                case 2:
                    System.err.println("This may take several minutes.");
                    break;
                case 3:
                    System.err.println("This may take several hours.");
                    break;
                case 4:
                    System.err.println("This may take several days.");
                    break;
                case 5:
                    System.err.println("This may take several months.");
                    break;
                default:
                    System.err.println("This may take several years.");
                    break;
                }
                System.err.println("Started: " + new Date());
                System.err.println("^C to exit");
            }
            do {
                signingKeys = Common.generateSigningKeyPair();
                id = Common.toFeedId(signingKeys.getPublic());
            } while (vanity != null && !id.startsWith(vanity));
            if (vanity != null) {
                System.err.println("Finished: " + new Date());
            }

            encryptionKeys = Common.generateEncryptionKeyPair();
            System.err.println("New feed id created: " + id);

            File keyFile;
            if (keyPath != null) {
                keyFile = new File(keyPath, id + Common.KEY_EXTENSION);
            } else {
                keyFile = new File(Common.getClientRoot(), id + Common.KEY_EXTENSION);
            }

            // persist to keystore
            writeSigningKeyPair(signingKeys, id, keyFile, password);
            writeEncryptionKeyPair(encryptionKeys, id, keyFile, password);

        } else {

            File keyFile;
            if (keyPath != null) {
                keyFile = new File(Common.getClientRoot(), keyPath);
            } else {
                keyFile = new File(Common.getClientRoot(), id + Common.KEY_EXTENSION);
            }

            if (keyFile.exists()) {
                System.err.println("Using existing account id: " + id);

            } else {
                System.err.println("Cannot locate keys for account id: " + id);
                return 78; // "configuration error"
            }

            signingKeys = readSigningKeyPair(id, keyFile, password);
            if (signingKeys != null) {
                encryptionKeys = readEncryptionKeyPair(id, keyFile, password);
                if (encryptionKeys == null) {
                    encryptionKeys = signingKeys;
                }
            }
        }

        // clear password chars
        for (int i = 0; i < password.length; i++) {
            password[i] = 0;
        }
        if (signingKeys == null) {
            System.err.println("Could not obtain keys for signing.");
            return 73; // "can't create output error"
        }

        String[] recipientIds = null;
        if (recipients != null) {
            LinkedList<String> keys = new LinkedList<String>();
            for (int i = 0; i < recipients.length; i++) {
                if ("-".equals(recipients[i])) {
                    // "-" is shorthand for encrypt for mentioned ids
                    if (mentions != null) {
                        for (String mention : mentions) {
                            if (Common.isFeedId(mention)) {
                                keys.add(mention);
                            }
                        }
                    }
                } else if (Common.isFeedId(recipients[i])) {
                    keys.add(recipients[i]);
                } else {
                    log.warn("Could not parse recipient id: " + recipients[i]);
                }
            }
            recipientIds = keys.toArray(new String[0]);
        }

        // handle binary attachment
        String mimetype = null;
        byte[] attachment = null;
        if (attach != null) {
            InputStream input = null;
            try {
                if ("-".equals(attach)) {
                    input = new BufferedInputStream(in);
                } else {
                    File file = new File(attach);
                    input = new BufferedInputStream(new FileInputStream(file));
                    System.err.println("Attaching: " + file.getCanonicalPath());
                }
                attachment = Common.readFully(input);
                mimetype = new Tika().detect(attachment);
                System.err.println("Detected type: " + mimetype);
            } catch (Throwable t) {
                log.error("Could not read attachment: " + attach, t);
                return 73; // "can't create output error"
            } finally {
                try {
                    input.close();
                } catch (IOException ioe) {
                    // suppress any futher error on closing
                }
            }
        }

        Object result;
        try {
            EntryOptions options = new EntryOptions();
            options.setStatus(subject);
            options.setVerb(verb);
            if (mentions != null) {
                options.setMentions(mentions);
            }
            if (tags != null) {
                options.setTags(tags);
            }
            options.setBody(body);
            if (attachment != null) {
                options.addContentData(attachment, mimetype);
            } else if (url != null) {
                options.setContentUrl(url);
            }
            FeedOptions feedOptions = new FeedOptions();
            feedOptions.setAuthorEmail(email);
            feedOptions.setAuthorName(name);
            feedOptions.setAuthorUri(uri);
            feedOptions.setTitle(title);
            feedOptions.setSubtitle(subtitle);
            feedOptions.setBase(base);
            if (icon != null) {
                if ("-".equals(icon)) {
                    feedOptions.setAsIcon(true);
                } else {
                    feedOptions.setIconURL(icon);
                }
            }
            if (logo != null) {
                if ("-".equals(logo)) {
                    feedOptions.setAsLogo(true);
                } else {
                    feedOptions.setLogoURL(logo);
                }
            }
            if (recipientIds != null) {
                EntryOptions publicEntry = new EntryOptions().setStatus("Encrypted content").setVerb("encrypt");
                // TODO: add duplicate mentions to outside of envelope
                options.encryptFor(recipientIds, publicEntry);
            }
            result = client.post(signingKeys, encryptionKeys, options, feedOptions);
        } catch (IllegalArgumentException e) {
            log.error("Invalid request: " + id + " : " + e.getMessage(), e);
            return 76; // "remote error"
        } catch (IOException e) {
            log.error("Error connecting to service for id: " + id, e);
            return 76; // "remote error"
        } catch (org.apache.abdera.security.SecurityException e) {
            log.error("Error generating signatures for id: " + id, e);
            return 73; // "can't create output error"
        } catch (Exception e) {
            log.error("General security error for id: " + id, e);
            return 74; // "general io error"
        }

        if (result != null) {
            if (format) {
                out.println(Common.formatXML(result.toString()));
            } else {
                out.println(result.toString());
            }
        }

        return 0; // "OK"
    }

    private void printAllUsage() {
        System.err.println(Common.getBuildString());
        printPostUsage();
        printPullUsage();
        printPushUsage();
        printPortUsage();
    }

    private void printPullUsage() {
        HelpFormatter formatter = new HelpFormatter();
        formatter.setSyntaxPrefix("");
        formatter.printHelp("pull <id>... ", pullOptions);
    }

    private void printPushUsage() {
        HelpFormatter formatter = new HelpFormatter();
        formatter.setSyntaxPrefix("");
        formatter.printHelp("push <url> <id>...", pullOptions);
    }

    private void printPortUsage() {
        HelpFormatter formatter = new HelpFormatter();
        formatter.setSyntaxPrefix("");
        formatter.printHelp("serve ", portOptions);
    }

    private void printPostUsage() {
        HelpFormatter formatter = new HelpFormatter();
        formatter.setSyntaxPrefix("");
        formatter.printHelp("post [<id>] [--status <text>] [--encrypt <pubkey>]", postOptions);
    }

    public static final KeyPair readSigningKeyPair(String id, File file, char[] pwd) {
        return readKeyPairFromFile(id + '-' + Common.SIGN, file, pwd);
    }

    public static final KeyPair readEncryptionKeyPair(String id, File file, char[] pwd) {
        return readKeyPairFromFile(id + '-' + Common.ENCRYPT, file, pwd);
    }

    public static final void writeSigningKeyPair(KeyPair keyPair, String id, File file, char[] pwd) {
        writeKeyPairToFile(keyPair, createCertificate(keyPair, "SHA1withECDSA"), id + '-' + Common.SIGN, file, pwd);
    }

    public static final void writeEncryptionKeyPair(KeyPair keyPair, String id, File file, char[] pwd) {
        writeKeyPairToFile(keyPair, createCertificate(keyPair, "SHA1withECDSA"), id + '-' + Common.ENCRYPT, file,
                pwd);
    }

    public static final KeyPair readKeyPairFromFile(String alias, File file, char[] pwd) {
        FileInputStream input = null;
        try {
            KeyStore keyStore = KeyStore.getInstance("PKCS12");
            input = new FileInputStream(file);
            keyStore.load(new FileInputStream(file), pwd);
            input.close();

            KeyStore.PrivateKeyEntry pkEntry = (KeyStore.PrivateKeyEntry) keyStore.getEntry(alias,
                    new KeyStore.PasswordProtection(pwd));
            PrivateKey privateKey = pkEntry.getPrivateKey();
            PublicKey publicKey = pkEntry.getCertificate().getPublicKey();
            return new KeyPair(publicKey, privateKey);
        } catch (/* javax.crypto.BadPaddingException */IOException bpe) {
            log.error("Passphrase could not decrypt key: " + bpe.getMessage());
        } catch (Throwable e) {
            log.error("Unexpected error while reading key: " + e.getMessage(), e);
        } finally {
            if (input != null) {
                try {
                    input.close();
                } catch (IOException e) {
                    // ignore while closing
                    log.trace("Error while closing: " + e.getMessage(), e);
                }
            }
        }
        return null;
    }

    public static final void writeKeyPairToFile(KeyPair keyPair, X509Certificate cert, String alias, File file,
            char[] pwd) {
        FileInputStream input = null;
        FileOutputStream output = null;
        try {
            KeyStore keyStore = KeyStore.getInstance("PKCS12");
            if (file.exists()) {
                input = new FileInputStream(file);
                keyStore.load(new FileInputStream(file), pwd);
                input.close();
            } else {
                keyStore.load(null); // weird but required
            }

            // save my private key
            keyStore.setKeyEntry(alias, keyPair.getPrivate(), pwd, new X509Certificate[] { cert });

            // store away the keystore
            output = new java.io.FileOutputStream(file);
            keyStore.store(output, pwd);
            output.flush();
        } catch (Exception e) {
            log.error("Error while storing key: " + e.getMessage(), e);
        } finally {
            if (input != null) {
                try {
                    input.close();
                } catch (IOException e) {
                    // ignore while closing
                    log.trace("Error while closing: " + e.getMessage(), e);
                }
            }
            if (output != null) {
                try {
                    output.close();
                } catch (IOException e) {
                    // ignore while closing
                    log.trace("Error while closing: " + e.getMessage(), e);
                }
            }
        }
    }

    private static final X509Certificate createCertificate(KeyPair keyPair, String algorithm) {
        org.bouncycastle.x509.X509V3CertificateGenerator certGen = new org.bouncycastle.x509.X509V3CertificateGenerator();

        long now = System.currentTimeMillis();
        certGen.setSerialNumber(java.math.BigInteger.valueOf(now));

        org.bouncycastle.jce.X509Principal subject = new org.bouncycastle.jce.X509Principal(
                "CN=Trsst Keystore,DC=trsst,DC=com");
        certGen.setIssuerDN(subject);
        certGen.setSubjectDN(subject);

        Date fromDate = new java.util.Date(now);
        certGen.setNotBefore(fromDate);
        Calendar cal = new java.util.GregorianCalendar();
        cal.setTime(fromDate);
        cal.add(java.util.Calendar.YEAR, 100);
        Date toDate = cal.getTime();
        certGen.setNotAfter(toDate);

        certGen.setPublicKey(keyPair.getPublic());
        certGen.setSignatureAlgorithm(algorithm);
        certGen.addExtension(org.bouncycastle.asn1.x509.X509Extensions.BasicConstraints, true,
                new org.bouncycastle.asn1.x509.BasicConstraints(false));
        certGen.addExtension(org.bouncycastle.asn1.x509.X509Extensions.KeyUsage, true,
                new org.bouncycastle.asn1.x509.KeyUsage(org.bouncycastle.asn1.x509.KeyUsage.digitalSignature
                        | org.bouncycastle.asn1.x509.KeyUsage.keyEncipherment
                        | org.bouncycastle.asn1.x509.KeyUsage.keyCertSign
                        | org.bouncycastle.asn1.x509.KeyUsage.cRLSign));
        X509Certificate x509 = null;
        try {
            x509 = certGen.generateX509Certificate(keyPair.getPrivate());
        } catch (InvalidKeyException e) {
            log.error("Error generating certificate: invalid key", e);
        } catch (SecurityException e) {
            log.error("Unexpected error generating certificate", e);
        } catch (SignatureException e) {
            log.error("Error generating generating certificate signature", e);
        }
        return x509;
    }

    private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(Command.class);
}