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 static java.util.Arrays.asList; 021 022import java.util.ArrayList; 023import java.util.List; 024import java.util.Map; 025 026import org.syncany.config.to.ConfigTO; 027import org.syncany.config.to.DefaultRepoTOFactory; 028import org.syncany.config.to.RepoTO; 029import org.syncany.config.to.RepoTOFactory; 030import org.syncany.crypto.CipherSpec; 031import org.syncany.crypto.CipherSpecs; 032import org.syncany.crypto.CipherUtil; 033import org.syncany.operations.OperationResult; 034import org.syncany.operations.init.GenlinkOperationOptions; 035import org.syncany.operations.init.InitOperation; 036import org.syncany.operations.init.InitOperationOptions; 037import org.syncany.operations.init.InitOperationResult; 038import org.syncany.operations.init.InitOperationResult.InitResultCode; 039import org.syncany.plugins.transfer.StorageTestResult; 040import org.syncany.plugins.transfer.TransferSettings; 041import joptsimple.OptionParser; 042import joptsimple.OptionSet; 043import joptsimple.OptionSpec; 044 045public class InitCommand extends AbstractInitCommand { 046 public static final int REPO_ID_LENGTH = 32; 047 048 private InitOperationOptions operationOptions; 049 050 @Override 051 public CommandScope getRequiredCommandScope() { 052 return CommandScope.UNINITIALIZED_LOCALDIR; 053 } 054 055 @Override 056 public boolean canExecuteInDaemonScope() { 057 return false; 058 } 059 060 @Override 061 public int execute(String[] operationArgs) throws Exception { 062 boolean retryNeeded = true; 063 boolean performOperation = true; 064 065 operationOptions = parseOptions(operationArgs); 066 067 while (retryNeeded && performOperation) { 068 InitOperationResult operationResult = new InitOperation(operationOptions, this).execute(); 069 printResults(operationResult); 070 071 retryNeeded = operationResult.getResultCode() != InitResultCode.OK; 072 073 if (retryNeeded) { 074 performOperation = isInteractive && askRetryConnection(); 075 076 if (performOperation) { 077 updateTransferSettings(operationOptions.getConfigTO().getTransferSettings()); 078 } 079 } 080 } 081 082 return 0; 083 } 084 085 @Override 086 public InitOperationOptions parseOptions(String[] operationArguments) throws Exception { 087 InitOperationOptions operationOptions = new InitOperationOptions(); 088 089 OptionParser parser = new OptionParser(); 090 OptionSpec<Void> optionNoCreateTarget = parser.acceptsAll(asList("T", "no-create-target")); 091 OptionSpec<Void> optionAdvanced = parser.acceptsAll(asList("a", "advanced")); 092 OptionSpec<Void> optionNoCompression = parser.acceptsAll(asList("G", "no-compression")); 093 OptionSpec<Void> optionNoEncryption = parser.acceptsAll(asList("E", "no-encryption")); 094 OptionSpec<String> optionPlugin = parser.acceptsAll(asList("P", "plugin")).withRequiredArg(); 095 OptionSpec<String> optionPluginOpts = parser.acceptsAll(asList("o", "plugin-option")).withRequiredArg(); 096 OptionSpec<Void> optionAddDaemon = parser.acceptsAll(asList("n", "add-daemon")); 097 OptionSpec<Void> optionShortUrl = parser.acceptsAll(asList("s", "short")); 098 OptionSpec<Void> optionHeadlessMode = parser.acceptsAll(asList("l", "headless")); 099 OptionSpec<String> optionPassword = parser.acceptsAll(asList("password")).withRequiredArg(); 100 101 OptionSet options = parser.parse(operationArguments); 102 103 // Set interactivity mode 104 isInteractive = !options.has(optionPlugin); 105 106 // Set headless mode 107 isHeadless = options.has(optionHeadlessMode); 108 109 // Ask or set transfer settings 110 TransferSettings transferSettings = createTransferSettingsFromOptions(options, optionPlugin, optionPluginOpts); 111 112 // Some misc settings 113 boolean createTargetPath = !options.has(optionNoCreateTarget); 114 boolean advancedModeEnabled = options.has(optionAdvanced); 115 boolean encryptionEnabled = !options.has(optionNoEncryption); 116 boolean compressionEnabled = !options.has(optionNoCompression); 117 118 // Cipher specs: --no-encryption, --advanced 119 List<CipherSpec> cipherSpecs = getCipherSpecs(encryptionEnabled, advancedModeEnabled); 120 121 // Compression: --no-compression 122 // DefaultRepoTOFactory also creates default chunkers 123 RepoTOFactory repoTOFactory = new DefaultRepoTOFactory(compressionEnabled, cipherSpecs); 124 125 // Genlink options: --short 126 GenlinkOperationOptions genlinkOptions = new GenlinkOperationOptions(); 127 genlinkOptions.setShortUrl(options.has(optionShortUrl)); 128 129 // Set repo password 130 String password = validateAndGetPassword(options, optionNoEncryption, optionPassword); 131 operationOptions.setPassword(password); 132 133 // Create configTO and repoTO 134 ConfigTO configTO = createConfigTO(transferSettings); 135 RepoTO repoTO = repoTOFactory.createRepoTO(); 136 137 operationOptions.setLocalDir(localDir); 138 operationOptions.setConfigTO(configTO); 139 operationOptions.setRepoTO(repoTO); 140 141 operationOptions.setCreateTarget(createTargetPath); 142 operationOptions.setEncryptionEnabled(encryptionEnabled); 143 operationOptions.setCipherSpecs(cipherSpecs); 144 operationOptions.setDaemon(options.has(optionAddDaemon)); 145 operationOptions.setGenlinkOptions(genlinkOptions); 146 147 return operationOptions; 148 } 149 150 private String validateAndGetPassword(OptionSet options, OptionSpec<Void> optionNoEncryption, OptionSpec<String> optionPassword) { 151 if (!isInteractive) { 152 if (options.has(optionPassword) && options.has(optionNoEncryption)) { 153 throw new IllegalArgumentException("Cannot provide --password and --no-encryption. Conflicting options."); 154 } 155 else if (!options.has(optionPassword) && !options.has(optionNoEncryption)) { 156 throw new IllegalArgumentException("Non-interactive must either provide --no-encryption or --password."); 157 } 158 else if (options.has(optionPassword) && !options.has(optionNoEncryption)) { 159 String password = options.valueOf(optionPassword); 160 161 if (password.length() < PASSWORD_MIN_LENGTH) { 162 throw new IllegalArgumentException("This password is not allowed (too short, min. " + PASSWORD_MIN_LENGTH + " chars)"); 163 } 164 165 return options.valueOf(optionPassword); 166 } 167 else { 168 return null; // No encryption, no password. 169 } 170 } 171 else { 172 return null; // Will be set in callback! 173 } 174 } 175 176 @Override 177 public void printResults(OperationResult operationResult) { 178 InitOperationResult concreteOperationResult = (InitOperationResult) operationResult; 179 180 if (concreteOperationResult.getResultCode() == InitResultCode.OK) { 181 out.println(); 182 out.println("Repository created, and local folder initialized. To share the same repository"); 183 out.println("with others, you can share this link:"); 184 185 printLink(concreteOperationResult.getGenLinkResult(), false); 186 187 if (concreteOperationResult.isAddedToDaemon()) { 188 out.println("To automatically sync this folder, simply restart the daemon with 'sy daemon restart'."); 189 out.println(); 190 } 191 } 192 else if (concreteOperationResult.getResultCode() == InitResultCode.NOK_TEST_FAILED) { 193 StorageTestResult testResult = concreteOperationResult.getTestResult(); 194 out.println(); 195 196 if (testResult.isRepoFileExists()) { 197 out.println("ERROR: Repository cannot be initialized, because it already exists ('syncany' file"); 198 out.println(" exists). Are you sure that you want to create a new repo? Use 'sy connect'"); 199 out.println(" to connect to an existing repository."); 200 } 201 else if (!testResult.isTargetCanConnect()) { 202 out.println("ERROR: Repository cannot be initialized, because the connection to the storage backend failed."); 203 out.println(" Possible reasons for this could be connectivity issues (are you connect to the Internet?),"); 204 out.println(" or invalid user credentials (are username/password valid?)."); 205 } 206 else if (!testResult.isTargetExists()) { 207 if (!operationOptions.isCreateTarget()) { 208 out.println("ERROR: Repository cannot be initialized, because the target does not exist and"); 209 out.println(" the --create-target/-t option has not been enabled. Either create the target"); 210 out.println(" manually or retry with the --create-target/-t option."); 211 } 212 else { 213 out.println("ERROR: Repository cannot be initialized, because the target does not exist and"); 214 out.println(" it cannot be created. Please check your permissions or create the target manually."); 215 } 216 } 217 else if (!testResult.isTargetCanWrite()) { 218 out.println("ERROR: Repository cannot be initialized, because the target is not writable. This is probably"); 219 out.println(" a permission issue (does the user have write permissions to the target?)."); 220 } 221 else { 222 out.println("ERROR: Repository cannot be initialized."); 223 } 224 225 out.println(); 226 printTestResult(testResult); 227 } 228 else { 229 out.println(); 230 out.println("ERROR: Cannot connect to repository. Unknown error code: " + concreteOperationResult.getResultCode()); 231 out.println(); 232 } 233 } 234 235 private List<CipherSpec> getCipherSpecs(boolean encryptionEnabled, boolean advancedModeEnabled) throws Exception { 236 List<CipherSpec> cipherSpecs = new ArrayList<CipherSpec>(); 237 238 if (encryptionEnabled) { 239 if (advancedModeEnabled) { 240 cipherSpecs = askCipherSpecs(); 241 } 242 else { // Default 243 cipherSpecs = CipherSpecs.getDefaultCipherSpecs(); 244 } 245 } 246 247 return cipherSpecs; 248 } 249 250 private List<CipherSpec> askCipherSpecs() throws Exception { 251 List<CipherSpec> cipherSpecs = new ArrayList<CipherSpec>(); 252 Map<Integer, CipherSpec> availableCipherSpecs = CipherSpecs.getAvailableCipherSpecs(); 253 254 out.println(); 255 out.println("Please choose your encryption settings. If you're paranoid,"); 256 out.println("you can choose multiple cipher suites by separating with a comma."); 257 out.println(); 258 out.println("Options:"); 259 260 for (CipherSpec cipherSuite : availableCipherSpecs.values()) { 261 out.println(" [" + cipherSuite.getId() + "] " + cipherSuite); 262 } 263 264 out.println(); 265 266 boolean continueLoop = true; 267 boolean unlimitedStrengthNeeded = false; 268 269 while (continueLoop) { 270 String commaSeparatedCipherIdStr = console.readLine("Cipher(s): "); 271 String[] cipherSpecIdStrs = commaSeparatedCipherIdStr.split(","); 272 273 // Choose cipher 274 try { 275 // Add cipher suites 276 for (String cipherSpecIdStr : cipherSpecIdStrs) { 277 Integer cipherSpecId = Integer.parseInt(cipherSpecIdStr); 278 CipherSpec cipherSpec = availableCipherSpecs.get(cipherSpecId); 279 280 if (cipherSpec == null) { 281 throw new Exception(); 282 } 283 284 if (cipherSpec.needsUnlimitedStrength()) { 285 unlimitedStrengthNeeded = true; 286 } 287 288 cipherSpecs.add(cipherSpec); 289 } 290 291 // Unlimited strength 292 if (unlimitedStrengthNeeded) { 293 out.println(); 294 out.println("At least one of the chosen ciphers or key sizes might"); 295 out.println("not be allowed in your country."); 296 out.println(); 297 298 String yesno = console.readLine("Are you sure you want to use it (y/n)? "); 299 300 if (yesno.toLowerCase().startsWith("y")) { 301 try { 302 CipherUtil.enableUnlimitedStrength(); 303 } 304 catch (Exception e) { 305 throw new Exception( 306 "Unable to enable unlimited crypto. Check out: http://www.oracle.com/technetwork/java/javase/downloads/jce-6-download-429243.html"); 307 } 308 } 309 else { 310 continue; 311 } 312 } 313 314 continueLoop = false; 315 break; 316 } 317 catch (Exception e) { 318 out.println("ERROR: Please choose at least one valid option."); 319 out.println(); 320 321 continue; 322 } 323 } 324 325 return cipherSpecs; 326 } 327}