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