/*
* 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);
}