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}