package co.codewizards.cloudstore.client;
import static co.codewizards.cloudstore.core.oio.OioFileFactory.*;
import static co.codewizards.cloudstore.core.util.Util.*;
import java.io.IOException;
import java.security.KeyStore;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.kohsuke.args4j.CmdLineException;
import org.kohsuke.args4j.CmdLineParser;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ch.qos.logback.classic.LoggerContext;
import ch.qos.logback.classic.joran.JoranConfigurator;
import ch.qos.logback.core.joran.spi.JoranException;
import ch.qos.logback.core.util.StatusPrinter;
import co.codewizards.cloudstore.core.appid.AppIdRegistry;
import co.codewizards.cloudstore.core.config.ConfigDir;
import co.codewizards.cloudstore.core.oio.File;
import co.codewizards.cloudstore.core.repo.transport.RepoTransportFactoryRegistry;
import co.codewizards.cloudstore.core.updater.CloudStoreUpdaterCore;
import co.codewizards.cloudstore.core.util.DerbyUtil;
import co.codewizards.cloudstore.core.util.HashUtil;
import co.codewizards.cloudstore.core.util.MainArgsUtil;
import co.codewizards.cloudstore.rest.client.ssl.CheckServerTrustedCertificateExceptionContext;
import co.codewizards.cloudstore.rest.client.ssl.CheckServerTrustedCertificateExceptionResult;
import co.codewizards.cloudstore.rest.client.ssl.DynamicX509TrustManagerCallback;
import co.codewizards.cloudstore.rest.client.transport.RestRepoTransportFactory;
public class CloudStoreClient {
private static final Logger logger = LoggerFactory.getLogger(CloudStoreClient.class);
public static final List<Class<? extends SubCommand>> subCommandClasses;
static {
final List<Class<? extends SubCommand>> l = Arrays.asList(
AcceptRepoConnectionSubCommand.class,
AfterUpdateHookSubCommand.class,
CreateRepoSubCommand.class,
CreateRepoAliasSubCommand.class,
DropRepoAliasSubCommand.class,
DropRepoConnectionSubCommand.class,
HelpSubCommand.class,
RepairDatabaseSubCommand.class,
RepoInfoSubCommand.class,
RepoListSubCommand.class,
RequestRepoConnectionSubCommand.class,
ChangeLdapPasswordSubCommand.class,
SyncSubCommand.class,
VersionSubCommand.class
);
subCommandClasses = Collections.unmodifiableList(l);
};
public final List<SubCommand> subCommands;
public final Map<String, SubCommand> subCommandName2subCommand;
{
try {
final ArrayList<SubCommand> l = new ArrayList<SubCommand>();
final Map<String, SubCommand> m = new HashMap<String, SubCommand>();
for (final Class<? extends SubCommand> c : subCommandClasses) {
final SubCommand subCommand = c.newInstance();
l.add(subCommand);
m.put(subCommand.getSubCommandName(), subCommand);
}
l.trimToSize();
subCommands = Collections.unmodifiableList(l);
subCommandName2subCommand = Collections.unmodifiableMap(m);
} catch (final Exception e) {
throw new RuntimeException(e);
}
}
private static final String CMD_PREFIX = "cloudstore"; // shell script (or windoof batch file)
private boolean throwException = true;
/**
* The program arguments. Never <code>null</code>, but maybe an empty array (length 0).
*/
private final String[] args;
public static class ConsoleDynamicX509TrustManagerCallback implements DynamicX509TrustManagerCallback {
@Override
public CheckServerTrustedCertificateExceptionResult handleCheckServerTrustedCertificateException(final CheckServerTrustedCertificateExceptionContext context) {
final CheckServerTrustedCertificateExceptionResult result = new CheckServerTrustedCertificateExceptionResult();
String certificateSha1 = null;
try {
certificateSha1 = HashUtil.sha1ForHuman(context.getCertificateChain()[0].getEncoded());
} catch (final Exception e) {
// we're in the console client, hence we can and should print the exception here and then exit.
e.printStackTrace();
System.exit(66);
}
System.out.println("You are connecting to this server for the first time or someone is tampering with your");
System.out.println("connection to this server!");
System.out.println();
System.out.println("The server presented a certificate with the following fingerprint (SHA1):");
System.out.println();
System.out.println(" " + certificateSha1);
System.out.println();
System.out.println("Please verify that this is really your server's certificate and not a man in the middle!");
System.out.println("Your server shows its certificate's fingerprint during startup.");
System.out.println();
final String trustedString = prompt(">>> Do you want to register this certificate and trust this connection? (y/n) ");
if ("y".equals(trustedString)) {
result.setTrusted(true);
}
else if ("n".equals(trustedString)) {
result.setTrusted(false);
}
return result;
}
protected String prompt(final String question, final Object ... args) {
final TimeoutConsoleReader consoleInput = new TimeoutConsoleReader(question, 300*1000, "n");
String result;
try {
result = consoleInput.readLine();
} catch (final InterruptedException e) {
throw new IllegalStateException("A problem occured, while reading from console!");
}
return result;
}
}
private static final String[] stripSubCommand(final String[] args)
{
final String[] result = new String[args.length - 1];
for (int i = 0; i < result.length; i++) {
result[i] = args[i + 1];
}
return result;
}
/**
* Main method providing a command line interface (CLI) to the {@link KeyStore}.
*
* @param args the program arguments.
*/
public static void main(String... args) throws Exception
{
args = MainArgsUtil.extractAndApplySystemPropertiesReturnOthers(args); // must do this before initLogging(), because it already accesses the ConfigDir!
initLogging();
try {
final int programExitStatus;
try {
final RestRepoTransportFactory restRepoTransportFactory = RepoTransportFactoryRegistry.getInstance().getRepoTransportFactoryOrFail(RestRepoTransportFactory.class);
restRepoTransportFactory.setDynamicX509TrustManagerCallbackClass(ConsoleDynamicX509TrustManagerCallback.class);
programExitStatus = new CloudStoreClient(args).throwException(false).execute();
} finally {
// Doing it after execute(), because the system-properties are otherwise maybe not set.
// Doing it in a finally-block, because the server might already be updated and incompatible - thus causing an error.
// The following method catches all exceptions and logs them, hence this should not interfere with
// the clean program completion.
new CloudStoreUpdaterCore().createUpdaterDirIfUpdateNeeded();
}
System.exit(programExitStatus);
} catch (final Throwable x) {
logger.error(x.toString(), x);
System.exit(999);
}
}
public CloudStoreClient(final String... args) {
this.args = args == null ? new String[0] : args;
}
public boolean isThrowException() {
return throwException;
}
public void setThrowException(final boolean throwException) {
this.throwException = throwException;
}
public CloudStoreClient throwException(final boolean throwException) {
setThrowException(throwException);
return this;
}
public int execute() throws Exception {
logger.debug("execute: CloudStore CLI version {} is executing.", VersionSubCommand.getVersion());
final String[] args = MainArgsUtil.extractAndApplySystemPropertiesReturnOthers(this.args);
int programExitStatus = 1;
boolean displayHelp = true;
String subCommandName = null;
SubCommand subCommand = null;
if (args.length > 0) {
subCommandName = args[0];
if ("help".equals(subCommandName)) {
if (args.length > 1) {
subCommandName = args[1];
subCommand = subCommandName2subCommand.get(subCommandName);
if (subCommand == null) {
System.err.println("Unknown sub-command: " + subCommandName);
subCommandName = null;
}
}
}
else {
subCommand = subCommandName2subCommand.get(subCommandName);
if (subCommand == null) {
System.err.println("Unknown sub-command: " + subCommandName);
subCommandName = null;
}
else {
displayHelp = false;
final CmdLineParser parser = new CmdLineParser(subCommand);
try {
final String[] argsWithoutSubCommand = stripSubCommand(args);
parser.parseArgument(argsWithoutSubCommand);
boolean failed = true;
subCommand.prepare();
try {
subCommand.run();
failed = false;
} finally {
try {
subCommand.cleanUp();
} catch (final Exception x) {
if (failed)
logger.error("cleanUp() failed (but suppressing this exception to prevent primary exception from being lost): " + x, x);
else
throw x;
}
}
programExitStatus = 0;
} catch (final CmdLineException e) {
// handling of wrong arguments
programExitStatus = 2;
displayHelp = true;
System.err.println("Error: " + e.getMessage());
System.err.println();
if (throwException)
throw e;
} catch (final Exception x) {
programExitStatus = 3;
logger.error(x.toString(), x);
if (throwException)
throw x;
}
}
}
}
if (displayHelp) {
if (subCommand == null) {
System.err.println("Syntax: " + CMD_PREFIX + " <sub-command> <options>");
System.err.println();
System.err.println("Get help for a specific sub-command: " + CMD_PREFIX + " help <sub-command>");
System.err.println();
System.err.println("Available sub-commands:");
for (final SubCommand sc : subCommands) {
if (sc.isVisibleInHelp()) {
System.err.println(" " + sc.getSubCommandName());
}
}
}
else {
final CmdLineParser parser = new CmdLineParser(subCommand);
System.err.println(subCommand.getSubCommandName() + ": " + subCommand.getSubCommandDescription());
System.err.println();
System.err.print("Syntax: " + CMD_PREFIX + " " + subCommand.getSubCommandName());
parser.printSingleLineUsage(System.err);
System.err.println();
System.err.println();
System.err.println("Options:");
parser.printUsage(System.err);
}
}
return programExitStatus;
}
private static void initLogging() throws IOException, JoranException {
final File logDir = ConfigDir.getInstance().getLogDir();
DerbyUtil.setLogFile(createFile(logDir, "derby.log"));
final String logbackXmlName = "logback.client.xml";
final File logbackXmlFile = createFile(ConfigDir.getInstance().getFile(), logbackXmlName);
if (!logbackXmlFile.exists()) {
AppIdRegistry.getInstance().copyResourceResolvingAppId(
CloudStoreClient.class, logbackXmlName, logbackXmlFile);
}
final LoggerContext context = (LoggerContext) LoggerFactory.getILoggerFactory();
try {
final JoranConfigurator configurator = new JoranConfigurator();
configurator.setContext(context);
// Call context.reset() to clear any previous configuration, e.g. default
// configuration. For multi-step configuration, omit calling context.reset().
context.reset();
configurator.doConfigure(logbackXmlFile.getIoFile());
} catch (final JoranException je) {
// StatusPrinter will handle this
doNothing();
}
StatusPrinter.printInCaseOfErrorsOrWarnings(context);
}
}