001package co.codewizards.cloudstore.client;
002
003import java.io.Console;
004import java.io.File;
005import java.io.IOException;
006import java.security.KeyStore;
007import java.util.ArrayList;
008import java.util.Arrays;
009import java.util.Collections;
010import java.util.HashMap;
011import java.util.List;
012import java.util.Map;
013
014import org.kohsuke.args4j.CmdLineException;
015import org.kohsuke.args4j.CmdLineParser;
016import org.slf4j.Logger;
017import org.slf4j.LoggerFactory;
018
019import ch.qos.logback.classic.LoggerContext;
020import ch.qos.logback.classic.joran.JoranConfigurator;
021import ch.qos.logback.core.joran.spi.JoranException;
022import ch.qos.logback.core.util.StatusPrinter;
023import co.codewizards.cloudstore.core.config.ConfigDir;
024import co.codewizards.cloudstore.core.repo.transport.RepoTransportFactoryRegistry;
025import co.codewizards.cloudstore.core.updater.CloudStoreUpdaterCore;
026import co.codewizards.cloudstore.core.util.DerbyUtil;
027import co.codewizards.cloudstore.core.util.HashUtil;
028import co.codewizards.cloudstore.core.util.IOUtil;
029import co.codewizards.cloudstore.core.util.MainArgsUtil;
030import co.codewizards.cloudstore.rest.client.ssl.CheckServerTrustedCertificateExceptionContext;
031import co.codewizards.cloudstore.rest.client.ssl.CheckServerTrustedCertificateExceptionResult;
032import co.codewizards.cloudstore.rest.client.ssl.DynamicX509TrustManagerCallback;
033import co.codewizards.cloudstore.rest.client.transport.RestRepoTransportFactory;
034
035public class CloudStoreClient {
036        private static final Logger logger = LoggerFactory.getLogger(CloudStoreClient.class);
037
038        public static final List<Class<? extends SubCommand>> subCommandClasses;
039        static {
040                List<Class<? extends SubCommand>> l = Arrays.asList(
041                                AcceptRepoConnectionSubCommand.class,
042                                AfterUpdateHookSubCommand.class,
043                                CreateRepoSubCommand.class,
044                                CreateRepoAliasSubCommand.class,
045                                DropRepoAliasSubCommand.class,
046                                DropRepoConnectionSubCommand.class,
047                                HelpSubCommand.class,
048                                RepairDatabaseSubCommand.class,
049                                RepoInfoSubCommand.class,
050                                RepoListSubCommand.class,
051                                RequestRepoConnectionSubCommand.class,
052                                SyncSubCommand.class,
053                                VersionSubCommand.class
054                                );
055
056                subCommandClasses = Collections.unmodifiableList(l);
057        };
058
059        public final List<SubCommand> subCommands;
060        public final Map<String, SubCommand> subCommandName2subCommand;
061        {
062                try {
063                        ArrayList<SubCommand> l = new ArrayList<SubCommand>();
064                        Map<String, SubCommand> m = new HashMap<String, SubCommand>();
065                        for (Class<? extends SubCommand> c : subCommandClasses) {
066                                SubCommand subCommand = c.newInstance();
067                                l.add(subCommand);
068                                m.put(subCommand.getSubCommandName(), subCommand);
069                        }
070
071                        l.trimToSize();
072                        subCommands = Collections.unmodifiableList(l);
073                        subCommandName2subCommand = Collections.unmodifiableMap(m);
074                } catch (Exception e) {
075                        throw new RuntimeException(e);
076                }
077        }
078
079        private static final String CMD_PREFIX = "cloudstore"; // shell script (or windoof batch file)
080        private boolean throwException = true;
081        /**
082         * The program arguments. Never <code>null</code>, but maybe an empty array (length 0).
083         */
084        private final String[] args;
085
086        public static class ConsoleDynamicX509TrustManagerCallback implements DynamicX509TrustManagerCallback {
087                @Override
088                public CheckServerTrustedCertificateExceptionResult handleCheckServerTrustedCertificateException(CheckServerTrustedCertificateExceptionContext context) {
089                        CheckServerTrustedCertificateExceptionResult result = new CheckServerTrustedCertificateExceptionResult();
090                        String certificateSha1 = null;
091                        try {
092                                certificateSha1 = HashUtil.sha1ForHuman(context.getCertificateChain()[0].getEncoded());
093                        } catch (Exception e) {
094                                // we're in the console client, hence we can and should print the exception here and then exit.
095                                e.printStackTrace();
096                                System.exit(66);
097                        }
098                        while (true) {
099                                System.out.println("You are connecting to this server for the first time or someone is tampering with your");
100                                System.out.println("connection to this server!");
101                                System.out.println();
102                                System.out.println("The server presented a certificate with the following fingerprint (SHA1):");
103                                System.out.println();
104                                System.out.println("    " + certificateSha1);
105                                System.out.println();
106                                System.out.println("Please verify that this is really your server's certificate and not a man in the middle!");
107                                System.out.println("Your server shows its certificate's fingerprint during startup.");
108                                System.out.println();
109                                String trustedString = prompt("Do you want to register this certificate and trust this connection? (y/n) ");
110                                if ("y".equals(trustedString)) {
111                                        result.setTrusted(true);
112                                        break;
113                                }
114                                else if ("n".equals(trustedString)) {
115                                        result.setTrusted(false);
116                                        break;
117                                }
118                                System.err.println("Invalid input! Please enter 'y' for yes and 'n' for no!");
119                        }
120                        return result;
121                }
122
123                protected String prompt(String fmt, Object ... args) {
124                        Console console = System.console();
125                        if (console == null)
126                                throw new IllegalStateException("There is no system console! Cannot prompt \"" + String.format(fmt, args) + "\"!!!");
127
128                        String result = console.readLine(fmt, args);
129                        return result;
130                }
131        }
132
133        private static final String[] stripSubCommand(String[] args)
134        {
135                String[] result = new String[args.length - 1];
136                for (int i = 0; i < result.length; i++)
137                        result[i] = args[i + 1];
138
139                return result;
140        }
141
142        /**
143         * Main method providing a command line interface (CLI) to the {@link KeyStore}.
144         *
145         * @param args the program arguments.
146         */
147        public static void main(String... args) throws Exception
148        {
149                initLogging();
150                try {
151                        final int programExitStatus;
152                        try {
153                                RestRepoTransportFactory restRepoTransportFactory = RepoTransportFactoryRegistry.getInstance().getRepoTransportFactoryOrFail(RestRepoTransportFactory.class);
154                                restRepoTransportFactory.setDynamicX509TrustManagerCallbackClass(ConsoleDynamicX509TrustManagerCallback.class);
155                                programExitStatus = new CloudStoreClient(args).throwException(false).execute();
156                        } finally {
157                                // Doing it after execute(), because the system-properties are otherwise maybe not set.
158                                // Doing it in a finally-block, because the server might already be updated and incompatible - thus causing an error.
159                                // The following method catches all exceptions and logs them, hence this should not interfere with
160                                // the clean program completion.
161                                new CloudStoreUpdaterCore().createUpdaterDirIfUpdateNeeded();
162                        }
163                        System.exit(programExitStatus);
164                } catch (Throwable x) {
165                        logger.error(x.toString(), x);
166                        System.exit(999);
167                }
168        }
169
170        public CloudStoreClient(final String... args) {
171                this.args = args == null ? new String[0] : args;
172        }
173
174        public boolean isThrowException() {
175                return throwException;
176        }
177        public void setThrowException(boolean throwException) {
178                this.throwException = throwException;
179        }
180        public CloudStoreClient throwException(boolean throwException) {
181                setThrowException(throwException);
182                return this;
183        }
184
185        public int execute() throws Exception {
186                logger.debug("execute: CloudStore CLI version {} is executing.", VersionSubCommand.getVersion());
187                final String[] args = MainArgsUtil.extractAndApplySystemPropertiesReturnOthers(this.args);
188                int programExitStatus = 1;
189                boolean displayHelp = true;
190                String subCommandName = null;
191                SubCommand subCommand = null;
192
193                if (args.length > 0) {
194                        subCommandName = args[0];
195
196                        if ("help".equals(subCommandName)) {
197                                if (args.length > 1) {
198                                        subCommandName = args[1];
199                                        subCommand = subCommandName2subCommand.get(subCommandName);
200                                        if (subCommand == null) {
201                                                System.err.println("Unknown sub-command: " + subCommandName);
202                                                subCommandName = null;
203                                        }
204                                }
205                        }
206                        else {
207                                subCommand = subCommandName2subCommand.get(subCommandName);
208                                if (subCommand == null) {
209                                        System.err.println("Unknown sub-command: " + subCommandName);
210                                        subCommandName = null;
211                                }
212                                else {
213                                        displayHelp = false;
214
215                                        CmdLineParser parser = new CmdLineParser(subCommand);
216                                        try {
217                                                String[] argsWithoutSubCommand = stripSubCommand(args);
218                                                parser.parseArgument(argsWithoutSubCommand);
219                                                subCommand.prepare();
220                                                subCommand.run();
221                                                programExitStatus = 0;
222                                        } catch (CmdLineException e) {
223                                                // handling of wrong arguments
224                                                programExitStatus = 2;
225                                                displayHelp = true;
226                                                System.err.println("Error: " + e.getMessage());
227                                                System.err.println();
228                                                if (throwException)
229                                                        throw e;
230                                        } catch (Exception x) {
231                                                programExitStatus = 3;
232                                                logger.error(x.toString(), x);
233                                                if (throwException)
234                                                        throw x;
235                                        }
236                                }
237                        }
238                }
239
240                if (displayHelp) {
241                        if (subCommand == null) {
242                                System.err.println("Syntax: " + CMD_PREFIX + " <sub-command> <options>");
243                                System.err.println();
244                                System.err.println("Get help for a specific sub-command: " + CMD_PREFIX + " help <sub-command>");
245                                System.err.println();
246                                System.err.println("Available sub-commands:");
247                                for (SubCommand sc : subCommands) {
248                                        if (sc.isVisibleInHelp())
249                                                System.err.println("  " + sc.getSubCommandName());
250                                }
251                        }
252                        else {
253                                CmdLineParser parser = new CmdLineParser(subCommand);
254                                System.err.println(subCommand.getSubCommandName() + ": " + subCommand.getSubCommandDescription());
255                                System.err.println();
256                                System.err.print("Syntax: " + CMD_PREFIX + " " + subCommand.getSubCommandName());
257                                parser.printSingleLineUsage(System.err);
258                                System.err.println();
259                                System.err.println();
260                                System.err.println("Options:");
261                                parser.printUsage(System.err);
262                        }
263                }
264
265                return programExitStatus;
266        }
267
268        private static void initLogging() throws IOException, JoranException {
269                File logDir = ConfigDir.getInstance().getLogDir();
270                DerbyUtil.setLogFile(new File(logDir, "derby.log"));
271
272                final String logbackXmlName = "logback.client.xml";
273                File logbackXmlFile = new File(ConfigDir.getInstance().getFile(), logbackXmlName);
274                if (!logbackXmlFile.exists())
275                        IOUtil.copyResource(CloudStoreClient.class, logbackXmlName, logbackXmlFile);
276
277                LoggerContext context = (LoggerContext) LoggerFactory.getILoggerFactory();
278            try {
279              JoranConfigurator configurator = new JoranConfigurator();
280              configurator.setContext(context);
281              // Call context.reset() to clear any previous configuration, e.g. default
282              // configuration. For multi-step configuration, omit calling context.reset().
283              context.reset();
284              configurator.doConfigure(logbackXmlFile);
285            } catch (JoranException je) {
286                // StatusPrinter will handle this
287                doNothing();
288            }
289            StatusPrinter.printInCaseOfErrorsOrWarnings(context);
290        }
291
292        private static void doNothing() { }
293}