first compile-clean attempt to integrate the layered I/O subsystem with the existing UI

This commit is contained in:
Sebastian Stenzel
2016-01-26 20:17:33 +01:00
parent e4d626eef5
commit c56d0b7d4a
42 changed files with 539 additions and 430 deletions
@@ -7,18 +7,17 @@ import java.util.concurrent.Executors;
import javax.inject.Named;
import javax.inject.Singleton;
import org.cryptomator.crypto.Cryptor;
import org.cryptomator.crypto.SamplingCryptorDecorator;
import org.cryptomator.crypto.aes256.CryptoModule;
import org.cryptomator.filesystem.crypto.CryptoFileSystemModule;
import org.cryptomator.frontend.FrontendFactory;
import org.cryptomator.frontend.webdav.WebDavServer;
import org.cryptomator.frontend.webdav.mount.WebDavMounter;
import org.cryptomator.frontend.webdav.mount.WebDavMounterProvider;
import org.cryptomator.ui.model.VaultObjectMapperProvider;
import org.cryptomator.ui.settings.Settings;
import org.cryptomator.ui.settings.SettingsProvider;
import org.cryptomator.ui.util.DeferredCloser;
import org.cryptomator.ui.util.DeferredCloser.Closer;
import org.cryptomator.ui.util.SemVerComparator;
import org.cryptomator.ui.util.mount.WebDavMounter;
import org.cryptomator.ui.util.mount.WebDavMounterProvider;
import org.cryptomator.webdav.WebDavServer;
import com.fasterxml.jackson.databind.ObjectMapper;
@@ -26,7 +25,7 @@ import dagger.Module;
import dagger.Provides;
import javafx.application.Application;
@Module(includes = CryptoModule.class)
@Module(includes = CryptoFileSystemModule.class)
class CryptomatorModule {
private final Application application;
@@ -83,18 +82,11 @@ class CryptomatorModule {
@Provides
@Singleton
WebDavServer provideWebDavServer() {
final WebDavServer webDavServer = new WebDavServer();
FrontendFactory provideFrontendFactory(WebDavServer webDavServer) {
webDavServer.start();
return closeLater(webDavServer, WebDavServer::stop);
}
@Provides
@Named("SamplingCryptor")
Cryptor provideCryptor(Cryptor cryptor) {
return SamplingCryptorDecorator.decorate(cryptor);
}
private <T> T closeLater(T object, Closer<T> closer) {
return deferredCloser.closeLater(object, closer).get().get();
}
@@ -1,28 +1,17 @@
package org.cryptomator.ui.controllers;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.nio.file.StandardOpenOption;
import java.util.ResourceBundle;
import javax.inject.Inject;
import javax.inject.Singleton;
import org.cryptomator.crypto.exceptions.UnsupportedKeyLengthException;
import org.cryptomator.crypto.exceptions.UnsupportedVaultException;
import org.cryptomator.crypto.exceptions.WrongPasswordException;
import org.cryptomator.ui.controls.SecPasswordField;
import org.cryptomator.ui.model.Vault;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.beans.value.ObservableValue;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
@@ -107,75 +96,80 @@ public class ChangePasswordController extends AbstractFXMLViewController {
@FXML
private void didClickChangePasswordButton(ActionEvent event) {
downloadsPageLink.setVisible(false);
final Path masterKeyPath = vault.getPath().resolve(Vault.VAULT_MASTERKEY_FILE);
final Path masterKeyBackupPath = vault.getPath().resolve(Vault.VAULT_MASTERKEY_BACKUP_FILE);
// decrypt with old password:
final CharSequence oldPassword = oldPasswordField.getCharacters();
try (final InputStream masterKeyInputStream = Files.newInputStream(masterKeyPath, StandardOpenOption.READ)) {
vault.getCryptor().decryptMasterKey(masterKeyInputStream, oldPassword);
Files.copy(masterKeyPath, masterKeyBackupPath, StandardCopyOption.REPLACE_EXISTING);
} catch (IOException ex) {
messageText.setText(resourceBundle.getString("changePassword.errorMessage.decryptionFailed"));
LOG.error("Decryption failed for technical reasons.", ex);
newPasswordField.swipe();
retypePasswordField.swipe();
return;
} catch (WrongPasswordException e) {
messageText.setText(resourceBundle.getString("changePassword.errorMessage.wrongPassword"));
newPasswordField.swipe();
retypePasswordField.swipe();
Platform.runLater(oldPasswordField::requestFocus);
return;
} catch (UnsupportedKeyLengthException ex) {
messageText.setText(resourceBundle.getString("changePassword.errorMessage.unsupportedKeyLengthInstallJCE"));
LOG.warn("Unsupported Key-Length. Please install Oracle Java Cryptography Extension (JCE).", ex);
newPasswordField.swipe();
retypePasswordField.swipe();
return;
} catch (UnsupportedVaultException e) {
downloadsPageLink.setVisible(true);
if (e.isVaultOlderThanSoftware()) {
messageText.setText(resourceBundle.getString("changePassword.errorMessage.unsupportedVersion.vaultOlderThanSoftware") + " ");
} else if (e.isSoftwareOlderThanVault()) {
messageText.setText(resourceBundle.getString("changePassword.errorMessage.unsupportedVersion.softwareOlderThanVault") + " ");
}
newPasswordField.swipe();
retypePasswordField.swipe();
return;
} finally {
oldPasswordField.swipe();
}
// when we reach this line, decryption was successful.
// encrypt with new password:
final CharSequence newPassword = newPasswordField.getCharacters();
try (final OutputStream masterKeyOutputStream = Files.newOutputStream(masterKeyPath, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.SYNC)) {
vault.getCryptor().encryptMasterKey(masterKeyOutputStream, newPassword);
messageText.setText(resourceBundle.getString("changePassword.infoMessage.success"));
Platform.runLater(this::didChangePassword);
// At this point the backup is still using the old password.
// It will be changed as soon as the user unlocks the vault the next time.
// This way he can still restore the old password, if he doesn't remember the new one.
} catch (IOException ex) {
LOG.error("Re-encryption failed for technical reasons. Restoring Backup.", ex);
this.restoreBackupQuietly();
} finally {
newPasswordField.swipe();
retypePasswordField.swipe();
}
throw new UnsupportedOperationException("TODO");
/*
* downloadsPageLink.setVisible(false);
* final Path masterKeyPath = vault.getPath().resolve(Vault.VAULT_MASTERKEY_FILE);
* final Path masterKeyBackupPath = vault.getPath().resolve(Vault.VAULT_MASTERKEY_BACKUP_FILE);
*
* // decrypt with old password:
* final CharSequence oldPassword = oldPasswordField.getCharacters();
* try (final InputStream masterKeyInputStream = Files.newInputStream(masterKeyPath, StandardOpenOption.READ)) {
* vault.getCryptor().decryptMasterKey(masterKeyInputStream, oldPassword);
* Files.copy(masterKeyPath, masterKeyBackupPath, StandardCopyOption.REPLACE_EXISTING);
* } catch (IOException ex) {
* messageText.setText(resourceBundle.getString("changePassword.errorMessage.decryptionFailed"));
* LOG.error("Decryption failed for technical reasons.", ex);
* newPasswordField.swipe();
* retypePasswordField.swipe();
* return;
* } catch (WrongPasswordException e) {
* messageText.setText(resourceBundle.getString("changePassword.errorMessage.wrongPassword"));
* newPasswordField.swipe();
* retypePasswordField.swipe();
* Platform.runLater(oldPasswordField::requestFocus);
* return;
* } catch (UnsupportedKeyLengthException ex) {
* messageText.setText(resourceBundle.getString("changePassword.errorMessage.unsupportedKeyLengthInstallJCE"));
* LOG.warn("Unsupported Key-Length. Please install Oracle Java Cryptography Extension (JCE).", ex);
* newPasswordField.swipe();
* retypePasswordField.swipe();
* return;
* } catch (UnsupportedVaultException e) {
* downloadsPageLink.setVisible(true);
* if (e.isVaultOlderThanSoftware()) {
* messageText.setText(resourceBundle.getString("changePassword.errorMessage.unsupportedVersion.vaultOlderThanSoftware") + " ");
* } else if (e.isSoftwareOlderThanVault()) {
* messageText.setText(resourceBundle.getString("changePassword.errorMessage.unsupportedVersion.softwareOlderThanVault") + " ");
* }
* newPasswordField.swipe();
* retypePasswordField.swipe();
* return;
* } finally {
* oldPasswordField.swipe();
* }
*
* // when we reach this line, decryption was successful.
*
* // encrypt with new password:
* final CharSequence newPassword = newPasswordField.getCharacters();
* try (final OutputStream masterKeyOutputStream = Files.newOutputStream(masterKeyPath, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.SYNC)) {
* vault.getCryptor().encryptMasterKey(masterKeyOutputStream, newPassword);
* messageText.setText(resourceBundle.getString("changePassword.infoMessage.success"));
* Platform.runLater(this::didChangePassword);
* // At this point the backup is still using the old password.
* // It will be changed as soon as the user unlocks the vault the next time.
* // This way he can still restore the old password, if he doesn't remember the new one.
* } catch (IOException ex) {
* LOG.error("Re-encryption failed for technical reasons. Restoring Backup.", ex);
* this.restoreBackupQuietly();
* } finally {
* newPasswordField.swipe();
* retypePasswordField.swipe();
* }
*/
}
private void restoreBackupQuietly() {
final Path masterKeyPath = vault.getPath().resolve(Vault.VAULT_MASTERKEY_FILE);
final Path masterKeyBackupPath = vault.getPath().resolve(Vault.VAULT_MASTERKEY_BACKUP_FILE);
try {
Files.copy(masterKeyBackupPath, masterKeyPath, StandardCopyOption.REPLACE_EXISTING);
} catch (IOException ex) {
LOG.error("Restoring Backup failed.", ex);
}
/*
* final Path masterKeyPath = vault.getPath().resolve(Vault.VAULT_MASTERKEY_FILE);
* final Path masterKeyBackupPath = vault.getPath().resolve(Vault.VAULT_MASTERKEY_BACKUP_FILE);
* try {
* Files.copy(masterKeyBackupPath, masterKeyPath, StandardCopyOption.REPLACE_EXISTING);
* } catch (IOException ex) {
* LOG.error("Restoring Backup failed.", ex);
* }
*/
}
private void didChangePassword() {
@@ -9,14 +9,8 @@
package org.cryptomator.ui.controllers;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URL;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.InvalidPathException;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.ResourceBundle;
import javax.inject.Inject;
@@ -90,23 +84,11 @@ public class InitializeController extends AbstractFXMLViewController {
@FXML
protected void initializeVault(ActionEvent event) {
setControlsDisabled(true);
final Path masterKeyPath = vault.getPath().resolve(Vault.VAULT_MASTERKEY_FILE);
final CharSequence password = passwordField.getCharacters();
try (OutputStream masterKeyOutputStream = Files.newOutputStream(masterKeyPath, StandardOpenOption.WRITE, StandardOpenOption.CREATE_NEW)) {
vault.getCryptor().randomizeMasterKey();
vault.getCryptor().encryptMasterKey(masterKeyOutputStream, password);
final String dataRootDir = vault.getCryptor().encryptDirectoryPath("", FileSystems.getDefault().getSeparator());
final Path dataRootPath = vault.getPath().resolve("d").resolve(dataRootDir);
final Path metadataPath = vault.getPath().resolve("m");
Files.createDirectories(dataRootPath);
Files.createDirectories(metadataPath);
if (listener != null) {
listener.didInitialize(this);
}
final CharSequence passphrase = passwordField.getCharacters();
try {
vault.create(passphrase);
} catch (FileAlreadyExistsException ex) {
messageLabel.setText(resourceBundle.getString("initialize.messageLabel.alreadyInitialized"));
} catch (InvalidPathException ex) {
messageLabel.setText(resourceBundle.getString("initialize.messageLabel.invalidPath"));
} catch (IOException ex) {
LOG.error("I/O Exception", ex);
} finally {
@@ -8,31 +8,22 @@
******************************************************************************/
package org.cryptomator.ui.controllers;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.nio.file.StandardOpenOption;
import java.util.Comparator;
import java.util.ResourceBundle;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import javax.inject.Inject;
import javax.security.auth.DestroyFailedException;
import org.apache.commons.lang3.CharUtils;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.crypto.exceptions.UnsupportedKeyLengthException;
import org.cryptomator.crypto.exceptions.UnsupportedVaultException;
import org.cryptomator.crypto.exceptions.WrongPasswordException;
import org.cryptomator.crypto.engine.InvalidPassphraseException;
import org.cryptomator.frontend.FrontendCreationFailedException;
import org.cryptomator.frontend.webdav.mount.WindowsDriveLetters;
import org.cryptomator.ui.controls.SecPasswordField;
import org.cryptomator.ui.model.Vault;
import org.cryptomator.ui.util.FXThreads;
import org.cryptomator.ui.util.mount.CommandFailedException;
import org.cryptomator.ui.util.mount.WindowsDriveLetters;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -258,49 +249,30 @@ public class UnlockController extends AbstractFXMLViewController {
setControlsDisabled(true);
progressIndicator.setVisible(true);
downloadsPageLink.setVisible(false);
final Path masterKeyPath = vault.getPath().resolve(Vault.VAULT_MASTERKEY_FILE);
final Path masterKeyBackupPath = vault.getPath().resolve(Vault.VAULT_MASTERKEY_BACKUP_FILE);
final CharSequence password = passwordField.getCharacters();
try (final InputStream masterKeyInputStream = Files.newInputStream(masterKeyPath, StandardOpenOption.READ)) {
vault.getCryptor().decryptMasterKey(masterKeyInputStream, password);
if (!vault.startServer()) {
messageText.setText(resourceBundle.getString("unlock.messageLabel.startServerFailed"));
vault.getCryptor().destroy();
return;
}
// at this point we know for sure, that the masterkey can be decrypted, so lets make a backup:
Files.copy(masterKeyPath, masterKeyBackupPath, StandardCopyOption.REPLACE_EXISTING);
vault.setUnlocked(true);
final Future<Boolean> futureMount = exec.submit(vault::mount);
try {
vault.activateFrontend(password);
Future<Boolean> futureMount = exec.submit(vault::mount);
FXThreads.runOnMainThreadWhenFinished(exec, futureMount, this::unlockAndMountFinished);
} catch (IOException ex) {
setControlsDisabled(false);
progressIndicator.setVisible(false);
messageText.setText(resourceBundle.getString("unlock.errorMessage.decryptionFailed"));
LOG.error("Decryption failed for technical reasons.", ex);
} catch (WrongPasswordException e) {
} catch (InvalidPassphraseException e) {
setControlsDisabled(false);
progressIndicator.setVisible(false);
messageText.setText(resourceBundle.getString("unlock.errorMessage.wrongPassword"));
Platform.runLater(passwordField::requestFocus);
} catch (UnsupportedKeyLengthException ex) {
} catch (FrontendCreationFailedException ex) {
setControlsDisabled(false);
progressIndicator.setVisible(false);
messageText.setText(resourceBundle.getString("unlock.errorMessage.unsupportedKeyLengthInstallJCE"));
LOG.warn("Unsupported Key-Length. Please install Oracle Java Cryptography Extension (JCE).", ex);
} catch (UnsupportedVaultException e) {
setControlsDisabled(false);
progressIndicator.setVisible(false);
downloadsPageLink.setVisible(true);
if (e.isVaultOlderThanSoftware()) {
messageText.setText(resourceBundle.getString("unlock.errorMessage.unsupportedVersion.vaultOlderThanSoftware") + " ");
} else if (e.isSoftwareOlderThanVault()) {
messageText.setText(resourceBundle.getString("unlock.errorMessage.unsupportedVersion.softwareOlderThanVault") + " ");
}
} catch (DestroyFailedException e) {
setControlsDisabled(false);
progressIndicator.setVisible(false);
LOG.error("Destruction of cryptor threw an exception.", e);
messageText.setText(resourceBundle.getString("unlock.errorMessage.decryptionFailed"));
LOG.error("Decryption failed for technical reasons.", ex);
// } catch (UnsupportedVaultException e) {
// setControlsDisabled(false);
// progressIndicator.setVisible(false);
// downloadsPageLink.setVisible(true);
// if (e.isVaultOlderThanSoftware()) {
// messageText.setText(resourceBundle.getString("unlock.errorMessage.unsupportedVersion.vaultOlderThanSoftware") + " ");
// } else if (e.isSoftwareOlderThanVault()) {
// messageText.setText(resourceBundle.getString("unlock.errorMessage.unsupportedVersion.softwareOlderThanVault") + " ");
// }
} finally {
passwordField.swipe();
}
@@ -317,18 +289,9 @@ public class UnlockController extends AbstractFXMLViewController {
progressIndicator.setVisible(false);
setControlsDisabled(false);
if (vault.isUnlocked() && !mountSuccess) {
exec.submit(() -> {
vault.stopServer();
vault.setUnlocked(false);
});
exec.submit(vault::deactivateFrontend);
} else if (vault.isUnlocked() && mountSuccess) {
exec.submit(() -> {
try {
vault.reveal();
} catch (CommandFailedException e) {
LOG.error("Failed to reveal mounted vault", e);
}
});
exec.submit(vault::reveal);
}
if (mountSuccess && listener != null) {
listener.didUnlock(this);
@@ -15,10 +15,8 @@ import java.util.concurrent.ExecutorService;
import javax.inject.Inject;
import javax.inject.Provider;
import org.cryptomator.crypto.CryptorIOSampling;
import org.cryptomator.ui.model.Vault;
import org.cryptomator.ui.util.ActiveWindowStyleSupport;
import org.cryptomator.ui.util.mount.CommandFailedException;
import javafx.animation.Animation;
import javafx.animation.KeyFrame;
@@ -30,7 +28,6 @@ import javafx.event.EventHandler;
import javafx.fxml.FXML;
import javafx.scene.chart.LineChart;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.XYChart.Data;
import javafx.scene.chart.XYChart.Series;
import javafx.scene.control.Label;
import javafx.stage.Stage;
@@ -81,30 +78,21 @@ public class UnlockedController extends AbstractFXMLViewController {
@FXML
private void didClickRevealVault(ActionEvent event) {
exec.submit(() -> {
try {
vault.reveal();
} catch (CommandFailedException e) {
Platform.runLater(() -> {
messageLabel.setText(resourceBundle.getString("unlocked.label.revealFailed"));
});
}
});
exec.submit(vault::reveal);
}
@FXML
private void didClickCloseVault(ActionEvent event) {
exec.submit(() -> {
try {
vault.unmount();
} catch (CommandFailedException e) {
Platform.runLater(() -> {
messageLabel.setText(resourceBundle.getString("unlocked.label.unmountFailed"));
});
return;
}
vault.stopServer();
vault.setUnlocked(false);
// try {
vault.unmount();
// } catch (CommandFailedException e) {
// Platform.runLater(() -> {
// messageLabel.setText(resourceBundle.getString("unlocked.label.unmountFailed"));
// });
// return;
// }
vault.deactivateFrontend();
if (listener != null) {
Platform.runLater(() -> {
listener.didLock(this);
@@ -129,7 +117,7 @@ public class UnlockedController extends AbstractFXMLViewController {
// IO Graph
// ****************************************
private void startIoSampling(final CryptorIOSampling sampler) {
private void startIoSampling(final Object sampler) {
final Series<Number, Number> decryptedBytes = new Series<>();
decryptedBytes.setName("decrypted");
final Series<Number, Number> encryptedBytes = new Series<>();
@@ -156,14 +144,14 @@ public class UnlockedController extends AbstractFXMLViewController {
private static final double BYTES_TO_MEGABYTES_FACTOR = 1.0 / IO_SAMPLING_INTERVAL / 1024.0 / 1024.0;
private static final double SMOOTHING_FACTOR = 0.3;
private static final long EFFECTIVELY_ZERO = 100000; // 100kb
private final CryptorIOSampling sampler;
private final Object sampler;
private final Series<Number, Number> decryptedBytes;
private final Series<Number, Number> encryptedBytes;
private int step = 0;
private long oldDecBytes = 0;
private long oldEncBytes = 0;
private final int step = 0;
private final long oldDecBytes = 0;
private final long oldEncBytes = 0;
public IoSamplingAnimationHandler(CryptorIOSampling sampler, Series<Number, Number> decryptedBytes, Series<Number, Number> encryptedBytes) {
public IoSamplingAnimationHandler(Object sampler, Series<Number, Number> decryptedBytes, Series<Number, Number> encryptedBytes) {
this.sampler = sampler;
this.decryptedBytes = decryptedBytes;
this.encryptedBytes = encryptedBytes;
@@ -171,28 +159,30 @@ public class UnlockedController extends AbstractFXMLViewController {
@Override
public void handle(ActionEvent event) {
step++;
final long decBytes = sampler.pollDecryptedBytes(true);
final double smoothedDecBytes = oldDecBytes + SMOOTHING_FACTOR * (decBytes - oldDecBytes);
final double smoothedDecMb = smoothedDecBytes * BYTES_TO_MEGABYTES_FACTOR;
oldDecBytes = smoothedDecBytes > EFFECTIVELY_ZERO ? (long) smoothedDecBytes : 0l;
decryptedBytes.getData().add(new Data<Number, Number>(step, smoothedDecMb));
if (decryptedBytes.getData().size() > IO_SAMPLING_STEPS) {
decryptedBytes.getData().remove(0);
}
final long encBytes = sampler.pollEncryptedBytes(true);
final double smoothedEncBytes = oldEncBytes + SMOOTHING_FACTOR * (encBytes - oldEncBytes);
final double smoothedEncMb = smoothedEncBytes * BYTES_TO_MEGABYTES_FACTOR;
oldEncBytes = smoothedEncBytes > EFFECTIVELY_ZERO ? (long) smoothedEncBytes : 0l;
encryptedBytes.getData().add(new Data<Number, Number>(step, smoothedEncMb));
if (encryptedBytes.getData().size() > IO_SAMPLING_STEPS) {
encryptedBytes.getData().remove(0);
}
xAxis.setLowerBound(step - IO_SAMPLING_STEPS);
xAxis.setUpperBound(step);
/*
* step++;
*
* final long decBytes = sampler.pollDecryptedBytes(true);
* final double smoothedDecBytes = oldDecBytes + SMOOTHING_FACTOR * (decBytes - oldDecBytes);
* final double smoothedDecMb = smoothedDecBytes * BYTES_TO_MEGABYTES_FACTOR;
* oldDecBytes = smoothedDecBytes > EFFECTIVELY_ZERO ? (long) smoothedDecBytes : 0l;
* decryptedBytes.getData().add(new Data<Number, Number>(step, smoothedDecMb));
* if (decryptedBytes.getData().size() > IO_SAMPLING_STEPS) {
* decryptedBytes.getData().remove(0);
* }
*
* final long encBytes = sampler.pollEncryptedBytes(true);
* final double smoothedEncBytes = oldEncBytes + SMOOTHING_FACTOR * (encBytes - oldEncBytes);
* final double smoothedEncMb = smoothedEncBytes * BYTES_TO_MEGABYTES_FACTOR;
* oldEncBytes = smoothedEncBytes > EFFECTIVELY_ZERO ? (long) smoothedEncBytes : 0l;
* encryptedBytes.getData().add(new Data<Number, Number>(step, smoothedEncMb));
* if (encryptedBytes.getData().size() > IO_SAMPLING_STEPS) {
* encryptedBytes.getData().remove(0);
* }
*
* xAxis.setLowerBound(step - IO_SAMPLING_STEPS);
* xAxis.setUpperBound(step);
*/
}
}
@@ -216,12 +206,14 @@ public class UnlockedController extends AbstractFXMLViewController {
});
// sample crypto-throughput:
stopIoSampling();
if (vault.getCryptor() instanceof CryptorIOSampling) {
startIoSampling((CryptorIOSampling) vault.getCryptor());
} else {
ioGraph.setVisible(false);
}
/*
* stopIoSampling();
* if (vault.getCryptor() instanceof CryptorIOSampling) {
* startIoSampling((CryptorIOSampling) vault.getCryptor());
* } else {
* ioGraph.setVisible(false);
* }
*/
}
public LockListener getListener() {
@@ -2,6 +2,8 @@ package org.cryptomator.ui.model;
import java.io.IOException;
import java.io.Serializable;
import java.io.UncheckedIOException;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.text.Normalizer;
@@ -11,62 +13,58 @@ import java.util.Map;
import java.util.Optional;
import java.util.Set;
import javax.security.auth.DestroyFailedException;
import org.apache.commons.lang3.CharUtils;
import org.apache.commons.lang3.StringUtils;
import org.cryptomator.crypto.Cryptor;
import org.cryptomator.filesystem.FileSystem;
import org.cryptomator.filesystem.crypto.CryptoFileSystemDelegate;
import org.cryptomator.filesystem.crypto.CryptoFileSystemFactory;
import org.cryptomator.filesystem.nio.NioFileSystem;
import org.cryptomator.frontend.Frontend;
import org.cryptomator.frontend.Frontend.MountParam;
import org.cryptomator.frontend.FrontendCreationFailedException;
import org.cryptomator.frontend.FrontendFactory;
import org.cryptomator.ui.util.DeferredClosable;
import org.cryptomator.ui.util.DeferredCloser;
import org.cryptomator.ui.util.FXThreads;
import org.cryptomator.ui.util.mount.CommandFailedException;
import org.cryptomator.ui.util.mount.WebDavMount;
import org.cryptomator.ui.util.mount.WebDavMounter;
import org.cryptomator.ui.util.mount.WebDavMounter.MountParam;
import org.cryptomator.webdav.WebDavServer;
import org.cryptomator.webdav.WebDavServer.ServletLifeCycleAdapter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.collect.ImmutableMap;
import dagger.Lazy;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
public class Vault implements Serializable {
public class Vault implements Serializable, CryptoFileSystemDelegate {
private static final long serialVersionUID = 3754487289683599469L;
private static final Logger LOG = LoggerFactory.getLogger(Vault.class);
@Deprecated
public static final String VAULT_FILE_EXTENSION = ".cryptomator";
@Deprecated
public static final String VAULT_MASTERKEY_FILE = "masterkey.cryptomator";
public static final String VAULT_MASTERKEY_BACKUP_FILE = "masterkey.cryptomator.bkup";
private final Path path;
private final WebDavServer server;
private final Cryptor cryptor;
private final WebDavMounter mounter;
private final Lazy<FrontendFactory> frontendFactory;
private final DeferredCloser closer;
private final CryptoFileSystemFactory cryptoFileSystemFactory;
private final ObjectProperty<Boolean> unlocked = new SimpleObjectProperty<Boolean>(this, "unlocked", Boolean.FALSE);
private final ObservableList<String> namesOfResourcesWithInvalidMac = FXThreads.observableListOnMainThread(FXCollections.observableArrayList());
private final Set<String> whitelistedResourcesWithInvalidMac = new HashSet<>();
private String mountName;
private Character winDriveLetter;
private DeferredClosable<ServletLifeCycleAdapter> webDavServlet = DeferredClosable.empty();
private DeferredClosable<WebDavMount> webDavMount = DeferredClosable.empty();
private DeferredClosable<Frontend> filesystemFrontend = DeferredClosable.empty();
/**
* Package private constructor, use {@link VaultFactory}.
*/
Vault(final Path vaultDirectoryPath, final WebDavServer server, final Cryptor cryptor, final WebDavMounter mounter, final DeferredCloser closer) {
Vault(Path vaultDirectoryPath, Lazy<FrontendFactory> frontendFactory, CryptoFileSystemFactory cryptoFileSystemFactory, DeferredCloser closer) {
this.path = vaultDirectoryPath;
this.server = server;
this.cryptor = cryptor;
this.mounter = mounter;
this.frontendFactory = frontendFactory;
this.closer = closer;
this.cryptoFileSystemFactory = cryptoFileSystemFactory;
try {
setMountName(getName());
@@ -84,35 +82,34 @@ public class Vault implements Serializable {
return Files.isRegularFile(masterKeyPath);
}
public synchronized boolean startServer() {
namesOfResourcesWithInvalidMac.clear();
whitelistedResourcesWithInvalidMac.clear();
Optional<ServletLifeCycleAdapter> o = webDavServlet.get();
if (o.isPresent() && o.get().isRunning()) {
return false;
public void create(CharSequence passphrase) throws IOException {
try {
FileSystem fs = NioFileSystem.rootedAt(path);
if (fs.children().count() > 0) {
throw new FileAlreadyExistsException(null, null, "Vault location not empty.");
}
cryptoFileSystemFactory.get(fs, passphrase, this);
} catch (UncheckedIOException e) {
throw new IOException(e);
}
ServletLifeCycleAdapter servlet = server.createServlet(path, cryptor, namesOfResourcesWithInvalidMac, whitelistedResourcesWithInvalidMac, mountName);
if (servlet.start()) {
webDavServlet = closer.closeLater(servlet);
return true;
}
return false;
}
public void stopServer() {
public synchronized void activateFrontend(CharSequence passphrase) throws FrontendCreationFailedException {
try {
unmount();
} catch (CommandFailedException e) {
LOG.warn("Unmounting failed. Locking anyway...", e);
FileSystem fs = NioFileSystem.rootedAt(path);
FileSystem cryptoFs = cryptoFileSystemFactory.get(fs, passphrase, this);
String contextPath = StringUtils.prependIfMissing(mountName, "/");
Frontend frontend = frontendFactory.get().create(cryptoFs, contextPath);
filesystemFrontend = closer.closeLater(frontend);
setUnlocked(true);
} catch (UncheckedIOException e) {
throw new FrontendCreationFailedException(e);
}
webDavServlet.close();
try {
cryptor.destroy();
} catch (DestroyFailedException e) {
LOG.error("Destruction of cryptor throw an exception.", e);
}
whitelistedResourcesWithInvalidMac.clear();
namesOfResourcesWithInvalidMac.clear();
}
public synchronized void deactivateFrontend() {
filesystemFrontend.close();
setUnlocked(false);
}
private Map<MountParam, Optional<String>> getMountParams() {
@@ -123,32 +120,35 @@ public class Vault implements Serializable {
}
public Boolean mount() {
final ServletLifeCycleAdapter servlet = webDavServlet.get().orElse(null);
if (servlet == null || !servlet.isRunning()) {
return false;
}
try {
webDavMount = closer.closeLater(mounter.mount(servlet.getServletUri(), getMountParams()));
return true;
} catch (CommandFailedException e) {
LOG.warn("mount failed", e);
// TODO exception handling
Frontend frontend = filesystemFrontend.get().orElse(null);
if (frontend == null) {
return false;
} else {
return frontend.mount(getMountParams());
}
}
public void reveal() throws CommandFailedException {
final WebDavMount mnt = webDavMount.get().orElse(null);
if (mnt != null) {
mnt.reveal();
}
public void reveal() {
// TODO exception handling
filesystemFrontend.get().ifPresent(Frontend::reveal);
}
public void unmount() throws CommandFailedException {
final WebDavMount mnt = webDavMount.get().orElse(null);
if (mnt != null) {
mnt.unmount();
}
webDavMount = DeferredClosable.empty();
public void unmount() {
// TODO exception handling
filesystemFrontend.get().ifPresent(Frontend::unmount);
}
/* Delegate Methods */
@Override
public void authenticationFailed(String cleartextPath) {
namesOfResourcesWithInvalidMac.add(cleartextPath);
}
@Override
public boolean shouldSkipAuthentication(String cleartextPath) {
return namesOfResourcesWithInvalidMac.contains(cleartextPath);
}
/* Getter/Setter */
@@ -164,9 +164,9 @@ public class Vault implements Serializable {
return StringUtils.removeEnd(path.getFileName().toString(), VAULT_FILE_EXTENSION);
}
public Cryptor getCryptor() {
return cryptor;
}
// public Cryptor getCryptor() {
// return cryptor;
// }
public ObjectProperty<Boolean> unlockedProperty() {
return unlocked;
@@ -3,33 +3,31 @@ package org.cryptomator.ui.model;
import java.nio.file.Path;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Provider;
import javax.inject.Singleton;
import org.cryptomator.crypto.Cryptor;
import org.cryptomator.filesystem.crypto.CryptoFileSystemFactory;
import org.cryptomator.frontend.FrontendFactory;
import org.cryptomator.frontend.webdav.mount.WebDavMounter;
import org.cryptomator.ui.util.DeferredCloser;
import org.cryptomator.ui.util.mount.WebDavMounter;
import org.cryptomator.webdav.WebDavServer;
import dagger.Lazy;
@Singleton
public class VaultFactory {
private final WebDavServer server;
private final Provider<Cryptor> cryptorProvider;
private final WebDavMounter mounter;
private final Lazy<FrontendFactory> frontendFactory;
private final CryptoFileSystemFactory cryptoFileSystemFactory;
private final DeferredCloser closer;
@Inject
public VaultFactory(WebDavServer server, @Named("SamplingCryptor") Provider<Cryptor> cryptorProvider, WebDavMounter mounter, DeferredCloser closer) {
this.server = server;
this.cryptorProvider = cryptorProvider;
this.mounter = mounter;
public VaultFactory(Lazy<FrontendFactory> frontendFactory, CryptoFileSystemFactory cryptoFileSystemFactory, WebDavMounter mounter, DeferredCloser closer) {
this.frontendFactory = frontendFactory;
this.cryptoFileSystemFactory = cryptoFileSystemFactory;
this.closer = closer;
}
public Vault createVault(Path path) {
return new Vault(path, server, cryptorProvider.get(), mounter, closer);
return new Vault(path, frontendFactory, cryptoFileSystemFactory, closer);
}
}
@@ -1,107 +0,0 @@
/*******************************************************************************
* Copyright (c) 2014 Markus Kreusch
* This file is licensed under the terms of the MIT license.
* See the LICENSE.txt file for more info.
*
* Contributors:
* Markus Kreusch
* Sebastian Stenzel - using Futures, lazy loading for out/err.
******************************************************************************/
package org.cryptomator.ui.util.command;
import static java.lang.String.format;
import java.io.IOException;
import org.apache.commons.io.IOUtils;
import org.apache.logging.log4j.util.Strings;
import org.cryptomator.ui.util.mount.CommandFailedException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public final class CommandResult {
private static final Logger LOG = LoggerFactory.getLogger(CommandResult.class);
private final Process process;
private final String stdout;
private final String stderr;
private final CommandFailedException exception;
/**
* Constructs a CommandResult from a terminated process and closes all its streams.
* @param process An <strong>already finished</strong> process.
*/
CommandResult(Process process) {
String out = null;
String err = null;
CommandFailedException ex = null;
try {
out = IOUtils.toString(process.getInputStream());
err = IOUtils.toString(process.getErrorStream());
} catch (IOException e) {
ex = new CommandFailedException(e);
} finally {
this.process = process;
this.stdout = out;
this.stderr = err;
this.exception = ex;
IOUtils.closeQuietly(process.getInputStream());
IOUtils.closeQuietly(process.getOutputStream());
IOUtils.closeQuietly(process.getErrorStream());
logDebugInfo();
}
}
/**
* @return Data written to STDOUT
*/
public String getStdOut() throws CommandFailedException {
assertNoException();
return stdout;
}
/**
* @return Data written to STDERR
*/
public String getStdErr() throws CommandFailedException {
assertNoException();
return stderr;
}
/**
* @return Exit value of the process
*/
public int getExitValue() {
return process.exitValue();
}
private void logDebugInfo() {
if (LOG.isDebugEnabled()) {
if (Strings.isEmpty(stderr) && Strings.isEmpty(stdout)) {
LOG.debug("Command execution finished. Exit code: {}", process.exitValue());
} else if (Strings.isEmpty(stderr)) {
LOG.debug("Command execution finished. Exit code: {}\nOutput: {}", process.exitValue(), stdout);
} else if (Strings.isEmpty(stdout)) {
LOG.debug("Command execution finished. Exit code: {}\nError: {}", process.exitValue(), stderr);
} else {
LOG.debug("Command execution finished. Exit code: {}\n Output: {}\nError: {}", process.exitValue(), stdout, stderr);
}
}
}
void assertOk() throws CommandFailedException {
assertNoException();
int exitValue = getExitValue();
if (exitValue != 0) {
throw new CommandFailedException(format("Command execution failed. Exit code: %d\n" + "# Output:\n" + "%s\n" + "# Error:\n" + "%s", exitValue, stdout, stderr));
}
}
private void assertNoException() throws CommandFailedException {
if (exception != null) {
throw exception;
}
}
}
@@ -1,100 +0,0 @@
/*******************************************************************************
* Copyright (c) 2014 Markus Kreusch
* This file is licensed under the terms of the MIT license.
* See the LICENSE.txt file for more info.
*
* Contributors:
* Markus Kreusch
* Sebastian Stenzel - Refactoring
******************************************************************************/
package org.cryptomator.ui.util.command;
import static java.lang.String.format;
import static org.apache.commons.lang3.SystemUtils.IS_OS_UNIX;
import static org.apache.commons.lang3.SystemUtils.IS_OS_WINDOWS;
import java.io.IOException;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.stream.Collectors;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.ui.util.mount.CommandFailedException;
/**
* <p>
* Runs commands using a system compatible CLI.
* <p>
* To detect the system type {@link SystemUtils} is used. The following CLIs are used by default:
* <ul>
* <li><i>{@link #WINDOWS_DEFAULT_CLI}</i> if {@link SystemUtils#IS_OS_WINDOWS}
* <li><i>{@link #UNIX_DEFAULT_CLI}</i> if {@link SystemUtils#IS_OS_UNIX}
* </ul>
* <p>
* If the path to the executables differs from the default or the system can not be detected the Java system property
* {@value #CLI_EXECUTABLE_PROPERTY} can be set to define it.
* <p>
* If a CLI executable can not be determined using these methods operation of {@link CommandRunner} will fail with
* {@link IllegalStateException}s.
*
* @author Markus Kreusch
*/
final class CommandRunner {
public static final String CLI_EXECUTABLE_PROPERTY = "cryptomator.cli";
public static final String WINDOWS_DEFAULT_CLI[] = {"cmd", "/C"};
public static final String UNIX_DEFAULT_CLI[] = {"/bin/sh", "-c"};
private static final Executor CMD_EXECUTOR = Executors.newCachedThreadPool();
/**
* Executes all lines in the given script in the specified order. Stops as soon as the first command fails.
*
* @param script Script containing command lines and environment variables.
* @return Result of the last command, if it exited successfully.
* @throws CommandFailedException If one of the command lines in the given script fails.
*/
static CommandResult execute(Script script, long timeout, TimeUnit unit) throws CommandFailedException {
try {
final List<String> env = script.environment().entrySet().stream().map(e -> e.getKey() + "=" + e.getValue()).collect(Collectors.toList());
CommandResult result = null;
for (final String line : script.getLines()) {
final String[] cmds = ArrayUtils.add(determineCli(), line);
final Process proc = Runtime.getRuntime().exec(cmds, env.toArray(new String[0]));
result = run(proc, timeout, unit);
result.assertOk();
}
return result;
} catch (IOException e) {
throw new CommandFailedException(e);
}
}
private static CommandResult run(Process process, long timeout, TimeUnit unit) throws CommandFailedException {
try {
final FutureCommandResult futureCommandResult = new FutureCommandResult(process);
CMD_EXECUTOR.execute(futureCommandResult);
return futureCommandResult.get(timeout, unit);
} catch (InterruptedException | ExecutionException | TimeoutException e) {
throw new CommandFailedException("Waiting time elapsed before command execution finished");
}
}
private static String[] determineCli() {
final String cliFromProperty = System.getProperty(CLI_EXECUTABLE_PROPERTY);
if (cliFromProperty != null) {
return cliFromProperty.split("");
} else if (IS_OS_WINDOWS) {
return WINDOWS_DEFAULT_CLI;
} else if (IS_OS_UNIX) {
return UNIX_DEFAULT_CLI;
} else {
throw new IllegalStateException(format("Failed to determine cli to use. Set Java system property %s to the executable.", CLI_EXECUTABLE_PROPERTY));
}
}
}
@@ -1,111 +0,0 @@
/*******************************************************************************
* Copyright (c) 2014 Markus Kreusch
* This file is licensed under the terms of the MIT license.
* See the LICENSE.txt file for more info.
*
* Contributors:
* Sebastian Stenzel
******************************************************************************/
package org.cryptomator.ui.util.command;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import org.cryptomator.ui.util.mount.CommandFailedException;
final class FutureCommandResult implements Future<CommandResult>, Runnable {
private final Process process;
private final AtomicBoolean canceled = new AtomicBoolean();
private final AtomicBoolean done = new AtomicBoolean();
private final Lock lock = new ReentrantLock();
private final Condition doneCondition = lock.newCondition();
private CommandFailedException exception;
FutureCommandResult(Process process) {
this.process = process;
}
@Override
public boolean cancel(boolean mayInterruptIfRunning) {
if (done.get()) {
return false;
} else if (canceled.compareAndSet(false, true)) {
if (mayInterruptIfRunning) {
process.destroyForcibly();
}
}
return true;
}
@Override
public boolean isCancelled() {
return canceled.get();
}
private void setDone() {
lock.lock();
try {
done.set(true);
doneCondition.signalAll();
} finally {
lock.unlock();
}
}
@Override
public boolean isDone() {
return done.get();
}
@Override
public CommandResult get() throws InterruptedException, ExecutionException {
lock.lock();
try {
while(!done.get()) {
doneCondition.await();
}
} finally {
lock.unlock();
}
if (exception != null) {
throw new ExecutionException(exception);
}
return new CommandResult(process);
}
@Override
public CommandResult get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException {
lock.lock();
try {
while(!done.get()) {
doneCondition.await(timeout, unit);
}
} finally {
lock.unlock();
}
if (exception != null) {
throw new ExecutionException(exception);
}
return new CommandResult(process);
}
@Override
public void run() {
try {
process.waitFor();
} catch (InterruptedException e) {
exception = new CommandFailedException(e);
} finally {
setDone();
}
}
}
@@ -1,65 +0,0 @@
/*******************************************************************************
* Copyright (c) 2014 Markus Kreusch
* This file is licensed under the terms of the MIT license.
* See the LICENSE.txt file for more info.
*
* Contributors:
* Markus Kreusch
******************************************************************************/
package org.cryptomator.ui.util.command;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import org.cryptomator.ui.util.mount.CommandFailedException;
public final class Script {
private static final int DEFAULT_TIMEOUT_MILLISECONDS = 3000;
public static Script fromLines(String... commands) {
return new Script(commands);
}
private final String[] lines;
private final Map<String, String> environment = new HashMap<>();
private Script(String[] lines) {
this.lines = lines;
setEnv(System.getenv());
}
public String[] getLines() {
return lines;
}
public CommandResult execute() throws CommandFailedException {
return CommandRunner.execute(this, DEFAULT_TIMEOUT_MILLISECONDS, TimeUnit.MILLISECONDS);
}
public CommandResult execute(long timeout, TimeUnit unit) throws CommandFailedException {
return CommandRunner.execute(this, timeout, unit);
}
Map<String, String> environment() {
return environment;
}
public Script setEnv(Map<String, String> environment) {
this.environment.clear();
addEnv(environment);
return this;
}
public Script addEnv(Map<String, String> environment) {
this.environment.putAll(environment);
return this;
}
public Script addEnv(String name, String value) {
environment.put(name, value);
return this;
}
}
@@ -1,10 +0,0 @@
package org.cryptomator.ui.util.mount;
abstract class AbstractWebDavMount implements WebDavMount {
@Override
public void close() throws Exception {
this.unmount();
}
}
@@ -1,24 +0,0 @@
/*******************************************************************************
* Copyright (c) 2014 Sebastian Stenzel
* This file is licensed under the terms of the MIT license.
* See the LICENSE.txt file for more info.
*
* Contributors:
* Sebastian Stenzel - initial API and implementation
* Markus Kreusch - Refactored WebDavMounter to use strategy pattern
******************************************************************************/
package org.cryptomator.ui.util.mount;
public class CommandFailedException extends Exception {
private static final long serialVersionUID = 5784853630182321479L;
public CommandFailedException(String message) {
super(message);
}
public CommandFailedException(Throwable cause) {
super(cause);
}
}
@@ -1,61 +0,0 @@
/*******************************************************************************
* Copyright (c) 2014 Markus Kreusch
* This file is licensed under the terms of the MIT license.
* See the LICENSE.txt file for more info.
*
* Contributors:
* Markus Kreusch - Refactored WebDavMounter to use strategy pattern
******************************************************************************/
package org.cryptomator.ui.util.mount;
import java.net.URI;
import java.util.Map;
import java.util.Optional;
/**
* A WebDavMounter acting as fallback if no other mounter works.
*
* @author Markus Kreusch
*/
final class FallbackWebDavMounter implements WebDavMounterStrategy {
@Override
public boolean shouldWork() {
return true;
}
@Override
public void warmUp(int serverPort) {
// no-op
}
@Override
public WebDavMount mount(URI uri, Map<MountParam, Optional<String>> mountParams) {
displayMountInstructions();
return new AbstractWebDavMount() {
@Override
public void unmount() {
displayUnmountInstructions();
}
@Override
public void reveal() throws CommandFailedException {
displayRevealInstructions();
}
};
}
private void displayMountInstructions() {
// TODO display message to user pointing to cryptomator.org/mounting#mount which describes what to do
// Machine-readable mount instructions: http://tools.ietf.org/html/rfc4709#page-5 :-)
}
private void displayUnmountInstructions() {
// TODO display message to user pointing to cryptomator.org/mounting#unmount which describes what to do
}
private void displayRevealInstructions() {
// TODO display message to user pointing to cryptomator.org/mounting#reveal which describes what to do
}
}
@@ -1,100 +0,0 @@
/*******************************************************************************
* Copyright (c) 2014 Sebastian Stenzel, Markus Kreusch
* This file is licensed under the terms of the MIT license.
* See the LICENSE.txt file for more info.
*
* Contributors:
* Sebastian Stenzel - initial API and implementation
* Markus Kreusch - Refactored WebDavMounter to use strategy pattern
* Mohit Raju - Added fallback schema-name "webdav" when opening file managers
******************************************************************************/
package org.cryptomator.ui.util.mount;
import java.net.URI;
import java.util.Map;
import java.util.Optional;
import javax.inject.Inject;
import javax.inject.Singleton;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.ui.util.command.Script;
@Singleton
final class LinuxGvfsWebDavMounter implements WebDavMounterStrategy {
@Inject
LinuxGvfsWebDavMounter() {}
@Override
public boolean shouldWork() {
if (SystemUtils.IS_OS_LINUX) {
final Script checkScripts = Script.fromLines("which gvfs-mount xdg-open");
try {
checkScripts.execute();
return true;
} catch (CommandFailedException e) {
return false;
}
} else {
return false;
}
}
@Override
public void warmUp(int serverPort) {
// no-op
}
@Override
public WebDavMount mount(URI uri, Map<MountParam, Optional<String>> mountParams) throws CommandFailedException {
final Script mountScript = Script.fromLines(
"set -x",
"gvfs-mount \"dav:$DAV_SSP\"")
.addEnv("DAV_SSP", uri.getRawSchemeSpecificPart());
mountScript.execute();
return new LinuxGvfsWebDavMount(uri);
}
private static class LinuxGvfsWebDavMount extends AbstractWebDavMount {
private final URI webDavUri;
private final Script testMountStillExistsScript;
private final Script unmountScript;
private LinuxGvfsWebDavMount(URI webDavUri) {
this.webDavUri = webDavUri;
this.testMountStillExistsScript = Script.fromLines("set -x", "test `gvfs-mount --list | grep \"$DAV_SSP\" | wc -l` -eq 1").addEnv("DAV_SSP", webDavUri.getRawSchemeSpecificPart());
this.unmountScript = Script.fromLines("set -x", "gvfs-mount -u \"dav:$DAV_SSP\"").addEnv("DAV_SSP", webDavUri.getRawSchemeSpecificPart());
}
@Override
public void unmount() throws CommandFailedException {
boolean mountStillExists;
try {
testMountStillExistsScript.execute();
mountStillExists = true;
} catch(CommandFailedException e) {
mountStillExists = false;
}
// only attempt unmount if user didn't unmount manually:
if (mountStillExists) {
unmountScript.execute();
}
}
@Override
public void reveal() throws CommandFailedException {
try {
openMountWithWebdavUri("dav:"+webDavUri.getRawSchemeSpecificPart()).execute();
} catch (CommandFailedException exception) {
openMountWithWebdavUri("webdav:"+webDavUri.getRawSchemeSpecificPart()).execute();
}
}
private Script openMountWithWebdavUri(String webdavUri){
return Script.fromLines("set -x", "xdg-open \"$DAV_URI\"").addEnv("DAV_URI", webdavUri);
}
}
}
@@ -1,86 +0,0 @@
/*******************************************************************************
* Copyright (c) 2014 Sebastian Stenzel, Markus Kreusch
* This file is licensed under the terms of the MIT license.
* See the LICENSE.txt file for more info.
*
* Contributors:
* Sebastian Stenzel - initial API and implementation, strategy fine tuning
* Markus Kreusch - Refactored WebDavMounter to use strategy pattern
******************************************************************************/
package org.cryptomator.ui.util.mount;
import java.net.URI;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import javax.inject.Inject;
import javax.inject.Singleton;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.ui.util.command.Script;
@Singleton
final class MacOsXWebDavMounter implements WebDavMounterStrategy {
@Inject
MacOsXWebDavMounter() {}
@Override
public boolean shouldWork() {
return SystemUtils.IS_OS_MAC_OSX;
}
@Override
public void warmUp(int serverPort) {
// no-op
}
@Override
public WebDavMount mount(URI uri, Map<MountParam, Optional<String>> mountParams) throws CommandFailedException {
final String mountName = mountParams.get(MountParam.MOUNT_NAME).orElseThrow(() -> {
return new IllegalArgumentException("Missing mount parameter MOUNT_NAME.");
});
// we don't use the uri to derive a path, as it *could* be longer than 255 chars.
final String path = "/Volumes/Cryptomator_" + UUID.randomUUID().toString();
final Script mountScript = Script.fromLines(
"mkdir \"$MOUNT_PATH\"",
"mount_webdav -S -v $MOUNT_NAME \"$DAV_AUTHORITY$DAV_PATH\" \"$MOUNT_PATH\"")
.addEnv("DAV_AUTHORITY", uri.getRawAuthority())
.addEnv("DAV_PATH", uri.getRawPath())
.addEnv("MOUNT_PATH", path)
.addEnv("MOUNT_NAME", mountName);
mountScript.execute();
return new MacWebDavMount(path);
}
private static class MacWebDavMount extends AbstractWebDavMount {
private final String mountPath;
private final Script revealScript;
private final Script unmountScript;
private MacWebDavMount(String mountPath) {
this.mountPath = mountPath;
this.revealScript = Script.fromLines("open \"$MOUNT_PATH\"").addEnv("MOUNT_PATH", mountPath);
this.unmountScript = Script.fromLines("diskutil umount $MOUNT_PATH").addEnv("MOUNT_PATH", mountPath);
}
@Override
public void unmount() throws CommandFailedException {
// only attempt unmount if user didn't unmount manually:
if (Files.exists(FileSystems.getDefault().getPath(mountPath))) {
unmountScript.execute();
}
}
@Override
public void reveal() throws CommandFailedException {
revealScript.execute();
}
}
}
@@ -1,84 +0,0 @@
package org.cryptomator.ui.util.mount;
import static java.util.Arrays.asList;
import static java.util.Collections.unmodifiableList;
import java.util.Collection;
import java.util.Iterator;
import javax.inject.Inject;
import javax.inject.Singleton;
@Singleton
class MountStrategies implements Collection<WebDavMounterStrategy> {
private final Collection<WebDavMounterStrategy> delegate;
@Inject
MountStrategies(LinuxGvfsWebDavMounter linuxMounter, MacOsXWebDavMounter osxMounter, WindowsWebDavMounter winMounter) {
delegate = unmodifiableList(asList(linuxMounter, osxMounter, winMounter));
}
public int size() {
return delegate.size();
}
public boolean isEmpty() {
return delegate.isEmpty();
}
public boolean contains(Object o) {
return delegate.contains(o);
}
public Iterator<WebDavMounterStrategy> iterator() {
return delegate.iterator();
}
public Object[] toArray() {
return delegate.toArray();
}
public <T> T[] toArray(T[] a) {
return delegate.toArray(a);
}
public boolean add(WebDavMounterStrategy e) {
return delegate.add(e);
}
public boolean remove(Object o) {
return delegate.remove(o);
}
public boolean containsAll(Collection<?> c) {
return delegate.containsAll(c);
}
public boolean addAll(Collection<? extends WebDavMounterStrategy> c) {
return delegate.addAll(c);
}
public boolean removeAll(Collection<?> c) {
return delegate.removeAll(c);
}
public boolean retainAll(Collection<?> c) {
return delegate.retainAll(c);
}
public void clear() {
delegate.clear();
}
public boolean equals(Object o) {
return delegate.equals(o);
}
public int hashCode() {
return delegate.hashCode();
}
}
@@ -1,32 +0,0 @@
/*******************************************************************************
* Copyright (c) 2014 Markus Kreusch
* This file is licensed under the terms of the MIT license.
* See the LICENSE.txt file for more info.
*
* Contributors:
* Markus Kreusch - Refactored WebDavMounter to use strategy pattern
******************************************************************************/
package org.cryptomator.ui.util.mount;
/**
* A mounted webdav share.
*
* @author Markus Kreusch
*/
public interface WebDavMount extends AutoCloseable {
/**
* Unmounts this {@code WebDavMount}.
*
* @throws CommandFailedException if the unmount operation fails
*/
void unmount() throws CommandFailedException;
/**
* Reveals the mounted drive in the operating systems default file browser.
*
* @throws CommandFailedException if the reveal operation fails
*/
void reveal() throws CommandFailedException;
}
@@ -1,31 +0,0 @@
/*******************************************************************************
* Copyright (c) 2014 Sebastian Stenzel
* This file is licensed under the terms of the MIT license.
* See the LICENSE.txt file for more info.
*
* Contributors:
* Sebastian Stenzel - initial API and implementation
* Markus Kreusch - Refactored to use strategy pattern
******************************************************************************/
package org.cryptomator.ui.util.mount;
import java.net.URI;
import java.util.Map;
import java.util.Optional;
public interface WebDavMounter {
public static enum MountParam {MOUNT_NAME, WIN_DRIVE_LETTER}
/**
* Tries to mount a given webdav share.
*
* @param uri URI of the webdav share
* @param mountParams additional mount parameters, that might not get ignored by some OS-specific mounters.
* @return a {@link WebDavMount} representing the mounted share
* @throws CommandFailedException if the mount operation fails
* @throws IllegalArgumentException if mountParams is missing expected options
*/
WebDavMount mount(URI uri, Map<MountParam, Optional<String>> mountParams) throws CommandFailedException;
}
@@ -1,48 +0,0 @@
/*******************************************************************************
* Copyright (c) 2014 Sebastian Stenzel
* This file is licensed under the terms of the MIT license.
* See the LICENSE.txt file for more info.
*
* Contributors:
* Markus Kreusch - Refactored to use strategy pattern
* Sebastian Stenzel - Refactored to use Guice provider, added warmup-phase for windows mounts.
******************************************************************************/
package org.cryptomator.ui.util.mount;
import java.util.Collection;
import java.util.concurrent.ExecutorService;
import javax.inject.Inject;
import javax.inject.Provider;
import javax.inject.Singleton;
import org.cryptomator.webdav.WebDavServer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@Singleton
public class WebDavMounterProvider implements Provider<WebDavMounter> {
private static final Logger LOG = LoggerFactory.getLogger(WebDavMounterProvider.class);
private final WebDavMounterStrategy choosenStrategy;
@Inject
public WebDavMounterProvider(WebDavServer server, ExecutorService executorService, MountStrategies availableStrategies) {
this.choosenStrategy = getStrategyWhichShouldWork(availableStrategies);
executorService.execute(() -> {
this.choosenStrategy.warmUp(server.getPort());
});
}
@Override
public WebDavMounterStrategy get() {
return this.choosenStrategy;
}
private WebDavMounterStrategy getStrategyWhichShouldWork(Collection<WebDavMounterStrategy> availableStrategies) {
WebDavMounterStrategy strategy = availableStrategies.stream().filter(WebDavMounterStrategy::shouldWork).findFirst().orElse(new FallbackWebDavMounter());
LOG.info("Using {}", strategy.getClass().getSimpleName());
return strategy;
}
}
@@ -1,30 +0,0 @@
/*******************************************************************************
* Copyright (c) 2014 Markus Kreusch
* This file is licensed under the terms of the MIT license.
* See the LICENSE.txt file for more info.
*
* Contributors:
* Markus Kreusch - Refactored WebDavMounter to use strategy pattern
* Sebastian Stenzel - minor strategy fine tuning
******************************************************************************/
package org.cryptomator.ui.util.mount;
/**
* A strategy able to mount a webdav share and display it to the user.
*
* @author Markus Kreusch
*/
interface WebDavMounterStrategy extends WebDavMounter {
/**
* @return {@code false} if this {@code WebDavMounterStrategy} can not work on the local machine, {@code true} if it could work
*/
boolean shouldWork();
/**
* Invoked when mounting strategy gets chosen. On some operating systems (we don't want to tell names here) mounting might be faster,
* when certain things are prepared before the actual mount attempt.
*/
void warmUp(int serverPort);
}
@@ -1,41 +0,0 @@
package org.cryptomator.ui.util.mount;
import static java.util.stream.Collectors.toSet;
import static java.util.stream.IntStream.rangeClosed;
import java.nio.file.FileSystems;
import java.nio.file.Path;
import java.util.Set;
import java.util.stream.StreamSupport;
import javax.inject.Inject;
import javax.inject.Singleton;
import org.apache.commons.lang3.CharUtils;
import org.apache.commons.lang3.SystemUtils;
import com.google.common.collect.Sets;
@Singleton
public final class WindowsDriveLetters {
private static final Set<Character> A_TO_Z = rangeClosed('A', 'Z').mapToObj(i -> (char) i).collect(toSet());
@Inject
public WindowsDriveLetters() {
}
public Set<Character> getOccupiedDriveLetters() {
if (!SystemUtils.IS_OS_WINDOWS) {
throw new UnsupportedOperationException("This method is only defined for Windows file systems");
}
Iterable<Path> rootDirs = FileSystems.getDefault().getRootDirectories();
return StreamSupport.stream(rootDirs.spliterator(), false).map(Path::toString).map(CharUtils::toChar).map(Character::toUpperCase).collect(toSet());
}
public Set<Character> getAvailableDriveLetters() {
return Sets.difference(A_TO_Z, getOccupiedDriveLetters());
}
}
@@ -1,142 +0,0 @@
/*******************************************************************************
* Copyright (c) 2014 Sebastian Stenzel, Markus Kreusch
* This file is licensed under the terms of the MIT license.
* See the LICENSE.txt file for more info.
*
* Contributors:
* Sebastian Stenzel - initial API and implementation, strategy fine tuning
* Markus Kreusch - Refactored WebDavMounter to use strategy pattern
******************************************************************************/
package org.cryptomator.ui.util.mount;
import static org.cryptomator.ui.util.command.Script.fromLines;
import java.net.URI;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.inject.Inject;
import javax.inject.Singleton;
import org.apache.commons.lang3.CharUtils;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.ui.util.command.CommandResult;
import org.cryptomator.ui.util.command.Script;
/**
* A {@link WebDavMounterStrategy} utilizing the "net use" command.
* <p>
* Tested on Windows 7 but should also work on Windows 8.
*/
@Singleton
final class WindowsWebDavMounter implements WebDavMounterStrategy {
private static final Pattern WIN_MOUNT_DRIVELETTER_PATTERN = Pattern.compile("\\s*([A-Z]):\\s*");
private static final int MAX_MOUNT_ATTEMPTS = 8;
private static final char AUTO_ASSIGN_DRIVE_LETTER = '*';
private final WindowsDriveLetters driveLetters;
@Inject
WindowsWebDavMounter(WindowsDriveLetters driveLetters) {
this.driveLetters = driveLetters;
}
@Override
public boolean shouldWork() {
return SystemUtils.IS_OS_WINDOWS;
}
@Override
public void warmUp(int serverPort) {
// no-op
}
@Override
public WebDavMount mount(URI uri, Map<MountParam, Optional<String>> mountParams) throws CommandFailedException {
final Character driveLetter = mountParams.get(MountParam.WIN_DRIVE_LETTER).map(CharUtils::toCharacterObject).orElse(AUTO_ASSIGN_DRIVE_LETTER);
if (driveLetters.getOccupiedDriveLetters().contains(driveLetter)) {
throw new CommandFailedException("Drive letter occupied.");
}
final String driveLetterStr = driveLetter.charValue() == AUTO_ASSIGN_DRIVE_LETTER ? CharUtils.toString(AUTO_ASSIGN_DRIVE_LETTER) : driveLetter + ":";
final Script localhostMountScript = fromLines("net use %DRIVE_LETTER% \\\\localhost@%DAV_PORT%\\DavWWWRoot%DAV_UNC_PATH% /persistent:no");
localhostMountScript.addEnv("DRIVE_LETTER", driveLetterStr);
localhostMountScript.addEnv("DAV_PORT", String.valueOf(uri.getPort()));
localhostMountScript.addEnv("DAV_UNC_PATH", uri.getRawPath().replace('/', '\\'));
CommandResult mountResult;
try {
mountResult = localhostMountScript.execute(5, TimeUnit.SECONDS);
} catch (CommandFailedException ex) {
final Script ipv6literaltMountScript = fromLines("net use %DRIVE_LETTER% \\\\0--1.ipv6-literal.net@%DAV_PORT%\\DavWWWRoot%DAV_UNC_PATH% /persistent:no");
ipv6literaltMountScript.addEnv("DRIVE_LETTER", driveLetterStr);
ipv6literaltMountScript.addEnv("DAV_PORT", String.valueOf(uri.getPort()));
ipv6literaltMountScript.addEnv("DAV_UNC_PATH", uri.getRawPath().replace('/', '\\'));
final Script proxyBypassScript = fromLines("reg add \"HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings\" /v \"ProxyOverride\" /d \"<local>;0--1.ipv6-literal.net;0--1.ipv6-literal.net:%DAV_PORT%\" /f");
proxyBypassScript.addEnv("DAV_PORT", String.valueOf(uri.getPort()));
mountResult = bypassProxyAndRetryMount(localhostMountScript, ipv6literaltMountScript, proxyBypassScript);
}
return new WindowsWebDavMount(driveLetter.charValue() == AUTO_ASSIGN_DRIVE_LETTER ? getDriveLetter(mountResult.getStdOut()) : driveLetter);
}
private CommandResult bypassProxyAndRetryMount(Script localhostMountScript, Script ipv6literalMountScript, Script proxyBypassScript) throws CommandFailedException {
CommandFailedException latestException = null;
for (int i = 0; i < MAX_MOUNT_ATTEMPTS; i++) {
try {
// wait a moment before next attempt
Thread.sleep(5000);
proxyBypassScript.execute();
// alternate localhost and 0--1.ipv6literal.net
final Script mountScript = (i % 2 == 0) ? localhostMountScript : ipv6literalMountScript;
return mountScript.execute(3, TimeUnit.SECONDS);
} catch (CommandFailedException ex) {
latestException = ex;
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
throw new CommandFailedException(ex);
}
}
throw latestException;
}
private Character getDriveLetter(String result) throws CommandFailedException {
final Matcher matcher = WIN_MOUNT_DRIVELETTER_PATTERN.matcher(result);
if (matcher.find()) {
return CharUtils.toCharacterObject(matcher.group(1));
} else {
throw new CommandFailedException("Failed to get a drive letter from net use output.");
}
}
private class WindowsWebDavMount extends AbstractWebDavMount {
private final Character driveLetter;
private final Script openExplorerScript;
private final Script unmountScript;
private WindowsWebDavMount(Character driveLetter) {
this.driveLetter = driveLetter;
this.openExplorerScript = fromLines("start explorer.exe " + driveLetter + ":");
this.unmountScript = fromLines("net use " + driveLetter + ": /delete").addEnv("DRIVE_LETTER", Character.toString(driveLetter));
}
@Override
public void unmount() throws CommandFailedException {
// only attempt unmount if user didn't unmount manually:
if (isVolumeMounted()) {
unmountScript.execute();
}
}
@Override
public void reveal() throws CommandFailedException {
openExplorerScript.execute();
}
private boolean isVolumeMounted() {
return driveLetters.getOccupiedDriveLetters().contains(driveLetter);
}
}
}