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}