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}