001/* 002 * Syncany, www.syncany.org 003 * Copyright (C) 2011-2016 Philipp C. Heckel <philipp.heckel@gmail.com> 004 * 005 * This program is free software: you can redistribute it and/or modify 006 * it under the terms of the GNU General Public License as published by 007 * the Free Software Foundation, either version 3 of the License, or 008 * (at your option) any later version. 009 * 010 * This program is distributed in the hope that it will be useful, 011 * but WITHOUT ANY WARRANTY; without even the implied warranty of 012 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 013 * GNU General Public License for more details. 014 * 015 * You should have received a copy of the GNU General Public License 016 * along with this program. If not, see <http://www.gnu.org/licenses/>. 017 */ 018package org.syncany.cli; 019 020import java.io.IOException; 021import java.lang.reflect.Constructor; 022import java.lang.reflect.InvocationTargetException; 023import java.net.URI; 024import java.net.UnknownHostException; 025import java.util.Arrays; 026import java.util.HashMap; 027import java.util.List; 028import java.util.Map; 029import java.util.concurrent.ExecutionException; 030import java.util.concurrent.Future; 031import java.util.concurrent.TimeUnit; 032import java.util.concurrent.TimeoutException; 033import java.util.logging.Level; 034import java.util.logging.Logger; 035 036import joptsimple.OptionSet; 037import joptsimple.OptionSpec; 038 039import org.syncany.cli.util.InitConsole; 040import org.syncany.config.to.ConfigTO; 041import org.syncany.crypto.CipherUtil; 042import org.syncany.operations.daemon.messages.ShowMessageExternalEvent; 043import org.syncany.operations.init.GenlinkOperationResult; 044import org.syncany.plugins.Plugins; 045import org.syncany.plugins.UserInteractionListener; 046import org.syncany.plugins.transfer.NestedTransferPluginOption; 047import org.syncany.plugins.transfer.StorageException; 048import org.syncany.plugins.transfer.StorageTestResult; 049import org.syncany.plugins.transfer.TransferPlugin; 050import org.syncany.plugins.transfer.TransferPluginOption; 051import org.syncany.plugins.transfer.TransferPluginOption.ValidationResult; 052import org.syncany.plugins.transfer.TransferPluginOptionCallback; 053import org.syncany.plugins.transfer.TransferPluginOptionConverter; 054import org.syncany.plugins.transfer.TransferPluginOptions; 055import org.syncany.plugins.transfer.TransferPluginUtil; 056import org.syncany.plugins.transfer.TransferSettings; 057import org.syncany.plugins.transfer.oauth.OAuth; 058import org.syncany.plugins.transfer.oauth.OAuthGenerator; 059import org.syncany.plugins.transfer.oauth.OAuthTokenFinish; 060import org.syncany.plugins.transfer.oauth.OAuthTokenWebListener; 061import org.syncany.util.ReflectionUtil; 062import org.syncany.util.StringUtil; 063import org.syncany.util.StringUtil.StringJoinListener; 064 065import com.google.common.base.Predicate; 066import com.google.common.base.Strings; 067import com.google.common.collect.Iterables; 068import com.google.common.eventbus.Subscribe; 069 070/** 071 * The abstract init command provides multiple shared methods for the 'init' 072 * and 'connect' command. Both commands must provide the ability to 073 * query a user for transfer settings or parse settings from the command line 074 * 075 * @author Philipp C. Heckel (philipp.heckel@gmail.com) 076 * @author Christian Roth (christian.roth@port17.de) 077 */ 078public abstract class AbstractInitCommand extends Command implements UserInteractionListener { 079 private static final Logger logger = Logger.getLogger(AbstractInitCommand.class.getName()); 080 081 protected static final char NESTED_OPTIONS_SEPARATOR = '.'; 082 protected static final String GENERIC_PLUGIN_TYPE_IDENTIFIER = ":type"; 083 protected static final int PASSWORD_MIN_LENGTH = 10; 084 protected static final int PASSWORD_WARN_LENGTH = 12; 085 protected static final int OAUTH_TOKEN_WAIT_TIMEOUT = 60; 086 087 protected InitConsole console; 088 protected boolean isInteractive; 089 protected boolean isHeadless; 090 091 public AbstractInitCommand() { 092 console = InitConsole.getInstance(); 093 } 094 095 protected ConfigTO createConfigTO(TransferSettings transferSettings) throws Exception { 096 ConfigTO configTO = new ConfigTO(); 097 098 configTO.setDisplayName(getDefaultDisplayName()); 099 configTO.setMachineName(getRandomMachineName()); 100 configTO.setMasterKey(null); 101 configTO.setTransferSettings(transferSettings); // can be null 102 103 return configTO; 104 } 105 106 protected TransferSettings createTransferSettingsFromOptions(OptionSet options, OptionSpec<String> optionPlugin, 107 OptionSpec<String> optionPluginOpts) throws Exception { 108 109 TransferPlugin plugin; 110 TransferSettings transferSettings; 111 112 // Parse --plugin and --plugin-option values 113 List<String> pluginOptionStrings = options.valuesOf(optionPluginOpts); 114 Map<String, String> knownPluginSettings = parsePluginSettingsFromOptions(pluginOptionStrings); 115 116 // Validation of some constraints 117 if (!options.has(optionPlugin) && knownPluginSettings.size() > 0) { 118 throw new IllegalArgumentException("Provided plugin settings without a plugin name."); 119 } 120 121 plugin = options.has(optionPlugin) ? initPlugin(options.valueOf(optionPlugin)) : askPlugin(); 122 transferSettings = askPluginSettings(plugin.createEmptySettings(), knownPluginSettings); 123 124 return transferSettings; 125 } 126 127 private Map<String, String> parsePluginSettingsFromOptions(List<String> pluginSettingsOptList) throws Exception { 128 Map<String, String> pluginOptionValues = new HashMap<>(); 129 130 // Fill settings map 131 for (String pluginSettingKeyValue : pluginSettingsOptList) { 132 String[] keyValue = pluginSettingKeyValue.split("=", 2); 133 134 if (keyValue.length != 2) { 135 throw new Exception("Invalid setting: " + pluginSettingKeyValue); 136 } 137 138 pluginOptionValues.put(keyValue[0], keyValue[1]); 139 } 140 141 return pluginOptionValues; 142 } 143 144 private TransferPlugin initPlugin(String pluginStr) throws Exception { 145 TransferPlugin plugin = Plugins.get(pluginStr, TransferPlugin.class); 146 147 if (plugin == null) { 148 throw new Exception("ERROR: Plugin '" + pluginStr + "' does not exist."); 149 } 150 151 return plugin; 152 } 153 154 private TransferSettings askPluginSettings(TransferSettings settings, Map<String, String> knownPluginSettings) throws StorageException { 155 if (isInteractive) { 156 out.println(); 157 out.println("Connection details for " + settings.getType() + " connection:"); 158 } 159 else { 160 logger.log(Level.INFO, "Non interactive mode"); 161 } 162 163 try { 164 // Show OAuth output 165 printOAuthInformation(settings); 166 167 // Ask for plugin settings 168 List<TransferPluginOption> pluginOptions = TransferPluginOptions.getOrderedOptions(settings.getClass()); 169 170 for (TransferPluginOption option : pluginOptions) { 171 askPluginSettings(settings, option, knownPluginSettings, ""); 172 } 173 } 174 catch (NoSuchFieldException e) { 175 logger.log(Level.SEVERE, "No token could be found, maybe user denied access", e); 176 throw new StorageException("No token found. Did you accept the authorization?", e); 177 } 178 catch (TimeoutException e) { 179 logger.log(Level.SEVERE, "No token was received in the given time interval", e); 180 throw new StorageException("No token was received in the given time interval", e); 181 } 182 catch (InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException | NoSuchMethodException | SecurityException | IOException | InterruptedException | ExecutionException e) { 183 logger.log(Level.SEVERE, "Unable to execute option generator", e); 184 throw new RuntimeException("Unable to execute option generator: " + e.getMessage()); 185 } 186 187 if (!settings.isValid()) { 188 if (askRetryInvalidSettings(settings.getReasonForLastValidationFail())) { 189 return askPluginSettings(settings, knownPluginSettings); 190 } 191 192 throw new StorageException("Validation failed: " + settings.getReasonForLastValidationFail()); 193 } 194 195 logger.log(Level.INFO, "Settings are " + settings.toString()); 196 197 return settings; 198 } 199 200 private void printOAuthInformation(TransferSettings settings) throws StorageException, NoSuchMethodException, SecurityException, 201 InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException, IOException, ExecutionException, InterruptedException, TimeoutException, NoSuchFieldException { 202 OAuth oAuthSettings = settings.getClass().getAnnotation(OAuth.class); 203 204 if (oAuthSettings != null) { 205 Constructor<? extends OAuthGenerator> optionCallbackClassConstructor = oAuthSettings.value().getDeclaredConstructor(settings.getClass()); 206 OAuthGenerator oAuthGenerator = optionCallbackClassConstructor.newInstance(settings); 207 208 if (isHeadless) { 209 logger.log(Level.FINE, "User is in headless mode and the plugin is OAuth based"); 210 211 if (oAuthGenerator instanceof OAuthGenerator.WithNoRedirectMode) { 212 doOAuthInCopyTokenMode(oAuthGenerator); 213 } 214 else { 215 throw new RuntimeException("OAuth based plugin does not support headless mode"); 216 } 217 } 218 else { 219 doOAuthInRedirectMode(oAuthGenerator, oAuthSettings); 220 } 221 222 } 223 } 224 225 private void doOAuthInCopyTokenMode(OAuthGenerator generator) throws StorageException { 226 URI oAuthURL = ((OAuthGenerator.WithNoRedirectMode) generator).generateAuthUrl(); 227 228 out.println(); 229 out.println("This plugin needs you to authenticate your account so that Syncany can access it."); 230 out.printf("Please navigate to the URL below and enter the token:\n\n %s\n\n", oAuthURL.toString()); 231 out.print("- Token (paste from URL): "); 232 233 String token = console.readLine(); 234 generator.checkToken(token, null); 235 } 236 237 private void doOAuthInRedirectMode(OAuthGenerator generator, OAuth settings) throws IOException, InterruptedException, ExecutionException, TimeoutException, StorageException { 238 OAuthTokenWebListener.Builder tokenListerBuilder = OAuthTokenWebListener.forMode(settings.mode()); 239 240 if (settings.callbackPort() != OAuth.RANDOM_PORT) { 241 tokenListerBuilder.setPort(settings.callbackPort()); 242 } 243 244 if (!settings.callbackId().equals(OAuth.PLUGIN_ID)) { 245 tokenListerBuilder.setId(settings.callbackId()); 246 } 247 248 // non standard plugin? 249 if (generator instanceof OAuthGenerator.WithInterceptor) { 250 tokenListerBuilder.setTokenInterceptor(((OAuthGenerator.WithInterceptor) generator).getInterceptor()); 251 } 252 253 if (generator instanceof OAuthGenerator.WithExtractor) { 254 tokenListerBuilder.setTokenExtractor(((OAuthGenerator.WithExtractor) generator).getExtractor()); 255 } 256 257 OAuthTokenWebListener tokenListener = tokenListerBuilder.build(); 258 259 URI oAuthURL = generator.generateAuthUrl(tokenListener.start()); 260 Future<OAuthTokenFinish> futureTokenResponse = tokenListener.getToken(); 261 262 out.println(); 263 out.println("This plugin needs you to authenticate your account so that Syncany can access it."); 264 out.printf("Please navigate to the URL below and accept the given permissions:\n\n %s\n\n", oAuthURL.toString()); 265 out.print("Waiting for authorization..."); 266 267 OAuthTokenFinish tokenResponse = futureTokenResponse.get(OAUTH_TOKEN_WAIT_TIMEOUT, TimeUnit.SECONDS); 268 269 if (tokenResponse != null) { 270 out.printf(" received token '%s'\n\n", tokenResponse.getToken()); 271 generator.checkToken(tokenResponse.getToken(), tokenResponse.getCsrfState()); 272 } 273 else { 274 out.println(" canceled"); 275 throw new StorageException("Error while acquiring token, perhaps user denied authorization"); 276 } 277 } 278 279 private void askPluginSettings(TransferSettings settings, TransferPluginOption option, Map<String, String> knownPluginSettings, String nestPrefix) 280 throws IllegalAccessException, InstantiationException, StorageException, IllegalArgumentException, InvocationTargetException, 281 NoSuchMethodException, SecurityException { 282 283 if (option instanceof NestedTransferPluginOption) { 284 Class<?> childPluginTransferSettingsClass = ReflectionUtil.getClassFromType(option.getType()); 285 boolean isGenericChildPlugin = TransferSettings.class.equals(childPluginTransferSettingsClass); 286 287 if (isGenericChildPlugin) { 288 askGenericChildPluginSettings(settings, option, knownPluginSettings, nestPrefix); 289 } 290 else { 291 askConreteChildPluginSettings(settings, (NestedTransferPluginOption) option, knownPluginSettings, nestPrefix); 292 } 293 } 294 else { 295 askNormalPluginSettings(settings, option, knownPluginSettings, nestPrefix); 296 } 297 } 298 299 private void askNormalPluginSettings(TransferSettings settings, TransferPluginOption option, Map<String, String> knownPluginSettings, 300 String nestPrefix) 301 throws StorageException, InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException, 302 NoSuchMethodException, SecurityException { 303 304 TransferPluginOptionCallback optionCallback = createOptionCallback(settings, option.getCallback()); 305 TransferPluginOptionConverter optionConverter = createOptionConverter(settings, option.getConverter()); 306 307 if (!isInteractive && !knownPluginSettings.containsKey(nestPrefix + option.getName())) { 308 throw new IllegalArgumentException("Missing plugin option (" + nestPrefix + option.getName() + ") in non-interactive mode."); 309 } 310 else if (knownPluginSettings.containsKey(nestPrefix + option.getName())) { 311 settings.setField(option.getField().getName(), knownPluginSettings.get(nestPrefix + option.getName())); 312 } 313 else if (!option.isVisible()) { 314 // Do nothing. Invisible option! 315 } 316 else { 317 callAndPrintPreQueryCallback(optionCallback); 318 319 String optionValue = askPluginOption(settings, option); 320 321 if (optionConverter != null) { 322 optionValue = optionConverter.convert(optionValue); 323 } 324 325 settings.setField(option.getField().getName(), optionValue); 326 327 callAndPrintPostQueryCallback(optionCallback, optionValue); 328 } 329 } 330 331 /** 332 * Queries the user for a plugin (which plugin to use?) and then 333 * asks for all of the plugin's settings. 334 * 335 * <p>This case is triggered by a field looking like this: 336 * <code>private TransferSettings childPluginSettings;</code> 337 */ 338 private void askGenericChildPluginSettings(TransferSettings settings, TransferPluginOption option, Map<String, String> knownPluginSettings, 339 String nestPrefix) 340 throws StorageException, IllegalAccessException, InstantiationException, IllegalArgumentException, InvocationTargetException, 341 NoSuchMethodException, SecurityException { 342 343 TransferPluginOptionCallback optionCallback = createOptionCallback(settings, option.getCallback()); 344 345 if (isInteractive) { 346 callAndPrintPreQueryCallback(optionCallback); 347 348 out.println(); 349 out.println(option.getDescription() + ":"); 350 } 351 352 TransferPlugin childPlugin = null; 353 Class<? extends TransferPlugin> pluginClass = TransferPluginUtil.getTransferPluginClass(settings.getClass()); 354 355 // Non-interactive: Plugin settings might be given via command line 356 try { 357 childPlugin = initPlugin(knownPluginSettings.get(nestPrefix + option.getName() + GENERIC_PLUGIN_TYPE_IDENTIFIER)); 358 } 359 catch (Exception e) { 360 if (!isInteractive) { 361 throw new IllegalArgumentException("Missing nested plugin type (" + nestPrefix + option.getName() + GENERIC_PLUGIN_TYPE_IDENTIFIER 362 + ") in non-interactive mode."); 363 } 364 } 365 366 // Interactive mode: Ask for sub-plugin 367 while (childPlugin == null) { 368 childPlugin = askPlugin(pluginClass); 369 } 370 371 if (isInteractive) { 372 out.println(); 373 } 374 375 // Create nested/child settings 376 TransferSettings childSettings = childPlugin.createEmptySettings(); 377 378 settings.setField(option.getField().getName(), childSettings); 379 nestPrefix = nestPrefix + option.getName() + NESTED_OPTIONS_SEPARATOR; 380 381 for (TransferPluginOption nestedOption : TransferPluginOptions.getOrderedOptions(childSettings.getClass())) { 382 askPluginSettings(childSettings, nestedOption, knownPluginSettings, nestPrefix); 383 } 384 385 if (isInteractive) { 386 callAndPrintPostQueryCallback(optionCallback, null); 387 } 388 } 389 390 /** 391 * Asks the user for all of the child plugin's settings. 392 * 393 * <p>This case is triggered by a field looking like this: 394 * <code>private LocalTransferSettings localChildPluginSettings;</code> 395 */ 396 private void askConreteChildPluginSettings(TransferSettings settings, NestedTransferPluginOption option, Map<String, String> knownPluginSettings, 397 String nestPrefix) throws StorageException, IllegalAccessException, InstantiationException, IllegalArgumentException, 398 InvocationTargetException, NoSuchMethodException, SecurityException { 399 400 TransferPluginOptionCallback optionCallback = createOptionCallback(settings, option.getCallback()); 401 402 if (isInteractive) { 403 callAndPrintPreQueryCallback(optionCallback); 404 405 out.println(); 406 out.println(option.getDescription() + ":"); 407 } 408 409 for (TransferPluginOption nestedPluginOption : option.getOptions()) { 410 Class<?> nestedTransferSettingsClass = ReflectionUtil.getClassFromType(option.getType()); 411 412 if (nestedTransferSettingsClass == null) { 413 throw new RuntimeException("No class found for type: " + option.getType()); 414 } 415 416 TransferSettings nestedSettings = (TransferSettings) nestedTransferSettingsClass.newInstance(); 417 418 settings.setField(option.getField().getName(), nestedSettings); 419 nestPrefix = nestPrefix + option.getName() + NESTED_OPTIONS_SEPARATOR; 420 421 askPluginSettings(nestedSettings, nestedPluginOption, knownPluginSettings, nestPrefix); 422 } 423 424 if (isInteractive) { 425 callAndPrintPostQueryCallback(optionCallback, null); 426 } 427 } 428 429 private void callAndPrintPreQueryCallback(TransferPluginOptionCallback optionCallback) { 430 if (optionCallback != null) { 431 String preQueryMessage = optionCallback.preQueryCallback(); 432 433 if (preQueryMessage != null) { 434 out.println(preQueryMessage); 435 } 436 } 437 } 438 439 private void callAndPrintPostQueryCallback(TransferPluginOptionCallback optionCallback, String optionValue) { 440 if (optionCallback != null) { 441 String postQueryMessage = optionCallback.postQueryCallback(optionValue); 442 443 if (postQueryMessage != null) { 444 out.println(postQueryMessage); 445 } 446 } 447 } 448 449 private String askPluginOption(TransferSettings settings, TransferPluginOption option) throws StorageException { 450 while (true) { 451 String value; 452 453 // Retrieve value 454 if (option.isSensitive()) { 455 // The option is sensitive. Could be either mandatory or optional 456 value = askPluginOptionSensitive(settings, option); 457 } 458 else if (!option.isRequired()) { 459 // The option is optional 460 value = askPluginOptionOptional(settings, option); 461 } 462 else { 463 // The option is mandatory, but not sensitive 464 value = askPluginOptionNormal(settings, option); 465 } 466 467 if ("".equals(value)) { 468 value = null; 469 } 470 471 // Validate result 472 ValidationResult validationResult = option.isValid(value); 473 474 switch (validationResult) { 475 case INVALID_NOT_SET: 476 out.println("ERROR: This option is mandatory."); 477 out.println(); 478 break; 479 480 case INVALID_TYPE: 481 out.println("ERROR: Not a valid input."); 482 out.println(); 483 break; 484 485 case VALID: 486 return value; 487 488 default: 489 throw new RuntimeException("Invalid return type: " + validationResult); 490 } 491 } 492 } 493 494 private String askPluginOptionNormal(TransferSettings settings, TransferPluginOption option) throws StorageException { 495 String knownOptionValue = settings.getField(option.getField().getName()); 496 String value = knownOptionValue; 497 498 if (option.isSingular() || knownOptionValue == null || "".equals(knownOptionValue)) { 499 out.printf("- %s: ", getDescription(settings, option)); 500 value = console.readLine(); 501 } 502 else { 503 out.printf("- %s (%s): ", getDescription(settings, option), knownOptionValue); 504 value = console.readLine(); 505 506 if ("".equals(value)) { 507 value = knownOptionValue; 508 } 509 } 510 511 return value; 512 } 513 514 private String askPluginOptionOptional(TransferSettings settings, TransferPluginOption option) throws StorageException { 515 String knownOptionValue = settings.getField(option.getField().getName()); 516 String value = knownOptionValue; 517 518 if (knownOptionValue == null || "".equals(knownOptionValue)) { 519 String defaultValueDescription = settings.getField(option.getField().getName()); 520 521 if (defaultValueDescription == null) { 522 defaultValueDescription = "none"; 523 } 524 525 out.printf("- %s (optional, default is %s): ", getDescription(settings, option), defaultValueDescription); 526 value = console.readLine(); 527 } 528 else { 529 out.printf("- %s (%s): ", getDescription(settings, option), knownOptionValue); 530 value = console.readLine(); 531 532 if ("".equals(value)) { 533 value = knownOptionValue; 534 } 535 } 536 537 return value; 538 } 539 540 private String askPluginOptionSensitive(TransferSettings settings, TransferPluginOption option) throws StorageException { 541 String knownOptionValue = settings.getField(option.getField().getName()); 542 String value = knownOptionValue; 543 String optionalIndicator = option.isRequired() ? "" : ", optional"; 544 545 if (option.isSingular() || knownOptionValue == null || "".equals(knownOptionValue)) { 546 out.printf("- %s (not displayed%s): ", getDescription(settings, option), optionalIndicator); 547 value = String.copyValueOf(console.readPassword()); 548 } 549 else { 550 out.printf("- %s (***, not displayed%s): ", getDescription(settings, option), optionalIndicator); 551 value = String.copyValueOf(console.readPassword()); 552 553 if ("".equals(value)) { 554 value = knownOptionValue; 555 } 556 } 557 558 return value; 559 } 560 561 private String getDescription(TransferSettings settings, TransferPluginOption option) { 562 Class<?> clazzForType = ReflectionUtil.getClassFromType(option.getType()); 563 564 if (clazzForType != null && Enum.class.isAssignableFrom(clazzForType)) { 565 Object[] enumValues = clazzForType.getEnumConstants(); 566 567 if (enumValues == null) { 568 throw new RuntimeException("Invalid TransferSettings class found: Enum at " + settings + " has no values"); 569 } 570 571 logger.log(Level.FINE, "Found enum option, values are: " + StringUtil.join(enumValues, ", ")); 572 573 return String.format("%s, choose from %s", option.getDescription(), StringUtil.join(enumValues, ", ")); 574 } 575 else { 576 return option.getDescription(); 577 } 578 } 579 580 protected TransferPlugin askPlugin() { 581 return askPlugin(null); 582 } 583 584 protected TransferPlugin askPlugin(final Class<? extends TransferPlugin> ignoreTransferPluginClass) { 585 TransferPlugin plugin = null; 586 final List<TransferPlugin> plugins = Plugins.list(TransferPlugin.class); 587 588 Iterables.removeIf(plugins, new Predicate<TransferPlugin>() { 589 @Override 590 public boolean apply(TransferPlugin transferPlugin) { 591 return ignoreTransferPluginClass == transferPlugin.getClass(); 592 } 593 }); 594 595 String pluginsList = StringUtil.join(plugins, ", ", new StringJoinListener<TransferPlugin>() { 596 @Override 597 public String getString(TransferPlugin plugin) { 598 return plugin.getId(); 599 } 600 }); 601 602 while (plugin == null) { 603 out.println("Choose a storage plugin. Available plugins are: " + pluginsList); 604 out.print("Plugin: "); 605 String pluginStr = console.readLine(); 606 607 plugin = Plugins.get(pluginStr, TransferPlugin.class); 608 609 if (plugin == null || ignoreTransferPluginClass == plugin.getClass()) { 610 out.println("ERROR: Plugin does not exist or cannot be used."); 611 out.println(); 612 613 plugin = null; 614 } 615 } 616 617 return plugin; 618 } 619 620 private TransferPluginOptionConverter createOptionConverter(TransferSettings settings, 621 Class<? extends TransferPluginOptionConverter> optionConverterClass) throws InstantiationException, IllegalAccessException, 622 IllegalArgumentException, InvocationTargetException, NoSuchMethodException, SecurityException { 623 624 TransferPluginOptionConverter optionConverter = null; 625 626 if (optionConverterClass != null) { 627 Constructor<? extends TransferPluginOptionConverter> optionConverterClassConstructor = optionConverterClass.getDeclaredConstructor(settings.getClass()); 628 optionConverter = optionConverterClassConstructor.newInstance(settings); 629 } 630 631 return optionConverter; 632 } 633 634 private TransferPluginOptionCallback createOptionCallback(TransferSettings settings, 635 Class<? extends TransferPluginOptionCallback> optionCallbackClass) throws InstantiationException, IllegalAccessException, 636 IllegalArgumentException, InvocationTargetException, NoSuchMethodException, SecurityException { 637 638 TransferPluginOptionCallback optionCallback = null; 639 640 if (optionCallbackClass != null) { 641 Constructor<? extends TransferPluginOptionCallback> optionCallbackClassConstructor = optionCallbackClass.getDeclaredConstructor(settings.getClass()); 642 optionCallback = optionCallbackClassConstructor.newInstance(settings); 643 } 644 645 return optionCallback; 646 } 647 648 protected String getRandomMachineName() { 649 return CipherUtil.createRandomAlphabeticString(20); 650 } 651 652 protected String getDefaultDisplayName() throws UnknownHostException { 653 return System.getProperty("user.name"); 654 } 655 656 protected boolean askRetryInvalidSettings(String failReason) { 657 return onUserConfirm("Validation failure", failReason, "Would you change the settings"); 658 } 659 660 protected boolean askRetryConnection() { 661 return onUserConfirm(null, "Connection failure", "Would you change the settings and retry the connection"); 662 } 663 664 protected TransferSettings updateTransferSettings(TransferSettings transferSettings) throws StorageException { 665 try { 666 return askPluginSettings(transferSettings, new HashMap<String, String>()); 667 } 668 catch (Exception e) { 669 logger.log(Level.SEVERE, "Unable to reload old plugin settings", e); 670 throw new StorageException("Unable to reload old plugin settings: " + e.getMessage()); 671 } 672 } 673 674 protected void printLink(GenlinkOperationResult operationResult, boolean shortOutput) { 675 if (shortOutput) { 676 out.println(operationResult.getShareLink()); 677 } 678 else { 679 out.println(); 680 out.println(" " + operationResult.getShareLink()); 681 out.println(); 682 683 if (operationResult.isShareLinkEncrypted()) { 684 out.println("This link is encrypted with the given password, so you can safely share it."); 685 out.println("using unsecure communication (chat, e-mail, etc.)"); 686 out.println(); 687 out.println("Note: The link contains the details of your repo connection which typically"); 688 out.println(" consist of usernames/password of the connection (e.g. FTP user/pass)."); 689 } 690 else { 691 out.println("WARNING: This link is NOT ENCRYPTED and might contain connection credentials"); 692 out.println(" Do NOT share this link unless you know what you are doing!"); 693 out.println(); 694 out.println(" The link contains the details of your repo connection which typically"); 695 out.println(" consist of usernames/password of the connection (e.g. FTP user/pass)."); 696 } 697 698 out.println(); 699 } 700 } 701 702 protected void printTestResult(StorageTestResult testResult) { 703 out.println("Details:"); 704 out.println("- Target connect success: " + testResult.isTargetCanConnect()); 705 out.println("- Target exists: " + testResult.isTargetExists()); 706 out.println("- Target creatable: " + testResult.isTargetCanCreate()); 707 out.println("- Target writable: " + testResult.isTargetCanWrite()); 708 out.println("- Repo file exists: " + testResult.isRepoFileExists()); 709 out.println(); 710 711 if (testResult.getErrorMessage() != null) { 712 out.println("Error message (see log file for details):"); 713 out.println(" " + testResult.getErrorMessage()); 714 } 715 } 716 717 @Override 718 public boolean onUserConfirm(String header, String message, String question) { 719 if (header != null) { 720 out.println(); 721 out.println(header); 722 out.println(Strings.repeat("-", header.length())); 723 } 724 725 out.println(message); 726 out.println(); 727 728 String yesno = console.readLine(question + " (y/n)? "); 729 730 if (!yesno.toLowerCase().startsWith("y") && !"".equals(yesno)) { 731 return false; 732 } 733 else { 734 return true; 735 } 736 } 737 738 @Subscribe 739 public void onShowMessage(ShowMessageExternalEvent messageEvent) { 740 out.println(); 741 out.println(messageEvent.getMessage()); 742 } 743 744 @Override 745 public String onUserPassword(String header, String message) { 746 if (!isInteractive) { 747 throw new RuntimeException("Repository is encrypted, but no password was given in non-interactive mode."); 748 } 749 750 out.println(); 751 752 if (header != null) { 753 out.println(header); 754 out.println(Strings.repeat("-", header.length())); 755 } 756 757 if (!message.trim().endsWith(":")) { 758 message += ": "; 759 } 760 761 char[] passwordChars = console.readPassword(message); 762 return String.copyValueOf(passwordChars); 763 } 764 765 @Override 766 public String onUserNewPassword() { 767 out.println(); 768 out.println("The password is used to encrypt data on the remote storage."); 769 out.println("Choose wisely!"); 770 out.println(); 771 772 String password = null; 773 774 while (password == null) { 775 char[] passwordChars = console.readPassword("Password (min. " + PASSWORD_MIN_LENGTH + " chars): "); 776 777 if (passwordChars.length < PASSWORD_MIN_LENGTH) { 778 out.println("ERROR: This password is not allowed (too short, min. " + PASSWORD_MIN_LENGTH + " chars)"); 779 out.println(); 780 781 continue; 782 } 783 784 char[] confirmPasswordChars = console.readPassword("Confirm: "); 785 786 if (!Arrays.equals(passwordChars, confirmPasswordChars)) { 787 out.println("ERROR: Passwords do not match."); 788 out.println(); 789 790 continue; 791 } 792 793 if (passwordChars.length < PASSWORD_WARN_LENGTH) { 794 out.println(); 795 out.println("WARNING: The password is a bit short. Less than " + PASSWORD_WARN_LENGTH + " chars are not future-proof!"); 796 String yesno = console.readLine("Are you sure you want to use it (y/n)? "); 797 798 if (!yesno.toLowerCase().startsWith("y") && !"".equals(yesno)) { 799 out.println(); 800 continue; 801 } 802 } 803 804 password = new String(passwordChars); 805 } 806 807 return password; 808 } 809}