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