diff --git a/main/frontend-api/src/main/java/org/cryptomator/frontend/Frontend.java b/main/frontend-api/src/main/java/org/cryptomator/frontend/Frontend.java index ed0a44e21..2be80c778 100644 --- a/main/frontend-api/src/main/java/org/cryptomator/frontend/Frontend.java +++ b/main/frontend-api/src/main/java/org/cryptomator/frontend/Frontend.java @@ -14,7 +14,7 @@ import java.util.Optional; public interface Frontend extends AutoCloseable { public enum MountParam { - MOUNT_NAME, WIN_DRIVE_LETTER + MOUNT_NAME, HOSTNAME, WIN_DRIVE_LETTER } void mount(Map> map) throws CommandFailedException; diff --git a/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/mount/WindowsWebDavMounter.java b/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/mount/WindowsWebDavMounter.java index 9279ab741..a2a257a61 100644 --- a/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/mount/WindowsWebDavMounter.java +++ b/main/frontend-webdav/src/main/java/org/cryptomator/frontend/webdav/mount/WindowsWebDavMounter.java @@ -12,6 +12,7 @@ package org.cryptomator.frontend.webdav.mount; import static org.cryptomator.frontend.webdav.mount.command.Script.fromLines; import java.net.URI; +import java.net.URISyntaxException; import java.util.Map; import java.util.Optional; import java.util.concurrent.TimeUnit; @@ -31,14 +32,15 @@ import org.cryptomator.frontend.webdav.mount.command.Script; /** * A {@link WebDavMounterStrategy} utilizing the "net use" command. *

- * Tested on Windows 7 but should also work on Windows 8. + * Tested on Windows 7, 8.1 and 10. */ @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 static final String AUTO_ASSIGN_DRIVE_LETTER = "*"; + private static final String LOCALHOST = "localhost"; + private static final int MOUNT_TIMEOUT_SECONDS = 60; private final WindowsDriveLetters driveLetters; @Inject @@ -58,56 +60,41 @@ final class WindowsWebDavMounter implements WebDavMounterStrategy { @Override public WebDavMount mount(URI uri, Map> 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)) { + final String driveLetter = mountParams.getOrDefault(MountParam.WIN_DRIVE_LETTER, Optional.of(AUTO_ASSIGN_DRIVE_LETTER)).orElse(AUTO_ASSIGN_DRIVE_LETTER); + if (driveLetters.getOccupiedDriveLetters().contains(CharUtils.toChar(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; + + final String hostname = mountParams.getOrDefault(MountParam.HOSTNAME, Optional.of(LOCALHOST)).orElse(LOCALHOST); 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 \";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); + final URI adjustedUri = new URI(uri.getScheme(), uri.getUserInfo(), hostname, uri.getPort(), uri.getPath(), uri.getQuery(), uri.getFragment()); + CommandResult mountResult = mount(adjustedUri, driveLetter); + return new WindowsWebDavMount(AUTO_ASSIGN_DRIVE_LETTER.equals(driveLetter) ? getDriveLetter(mountResult.getStdOut()) : driveLetter); + } catch (URISyntaxException e) { + throw new IllegalArgumentException("Invalid host: " + hostname); } - return new WindowsWebDavMount(driveLetter.charValue() == AUTO_ASSIGN_DRIVE_LETTER ? getDriveLetter(mountResult.getStdOut()) : driveLetter); + } + + private CommandResult mount(URI uri, String driveLetter) throws CommandFailedException { + final Script proxyBypassScript = fromLines( + "reg add \"HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings\" /v \"ProxyOverride\" /d \";%DAV_HOST%;%DAV_HOST%:%DAV_PORT%\" /f"); + proxyBypassScript.addEnv("DAV_HOST", uri.getHost()); + proxyBypassScript.addEnv("DAV_PORT", String.valueOf(uri.getPort())); + proxyBypassScript.execute(); + + final String driveLetterStr = AUTO_ASSIGN_DRIVE_LETTER.equals(driveLetter) ? AUTO_ASSIGN_DRIVE_LETTER : driveLetter + ":"; + final Script mountScript = fromLines("net use %DRIVE_LETTER% \\\\%DAV_HOST%@%DAV_PORT%\\DavWWWRoot%DAV_UNC_PATH% /persistent:no"); + mountScript.addEnv("DRIVE_LETTER", driveLetterStr); + mountScript.addEnv("DAV_HOST", uri.getHost()); + mountScript.addEnv("DAV_PORT", String.valueOf(uri.getPort())); + mountScript.addEnv("DAV_UNC_PATH", uri.getRawPath().replace('/', '\\')); + return mountScript.execute(MOUNT_TIMEOUT_SECONDS, TimeUnit.SECONDS); } - 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 { + private String getDriveLetter(String result) throws CommandFailedException { final Matcher matcher = WIN_MOUNT_DRIVELETTER_PATTERN.matcher(result); if (matcher.find()) { - return CharUtils.toCharacterObject(matcher.group(1)); + return matcher.group(1); } else { throw new CommandFailedException("Failed to get a drive letter from net use output."); } @@ -118,10 +105,10 @@ final class WindowsWebDavMounter implements WebDavMounterStrategy { private final Script openExplorerScript; private final Script unmountScript; - private WindowsWebDavMount(Character driveLetter) { - this.driveLetter = driveLetter; + private WindowsWebDavMount(String driveLetter) { + this.driveLetter = CharUtils.toCharacterObject(driveLetter); this.openExplorerScript = fromLines("start explorer.exe " + driveLetter + ":"); - this.unmountScript = fromLines("net use " + driveLetter + ": /delete").addEnv("DRIVE_LETTER", Character.toString(driveLetter)); + this.unmountScript = fromLines("net use " + driveLetter + ": /delete"); } @Override diff --git a/main/ui/src/main/java/org/cryptomator/ui/controllers/SettingsController.java b/main/ui/src/main/java/org/cryptomator/ui/controllers/SettingsController.java index eae4dab26..67094dd63 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/controllers/SettingsController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/controllers/SettingsController.java @@ -16,6 +16,7 @@ import javax.inject.Inject; import javax.inject.Singleton; import org.apache.commons.lang3.CharUtils; +import org.apache.commons.lang3.SystemUtils; import org.cryptomator.ui.settings.Localization; import org.cryptomator.ui.settings.Settings; import org.fxmisc.easybind.EasyBind; @@ -43,6 +44,9 @@ public class SettingsController extends AbstractFXMLViewController { @FXML private TextField portField; + + @FXML + private CheckBox useIpv6Checkbox; @FXML private Label versionLabel; @@ -53,10 +57,13 @@ public class SettingsController extends AbstractFXMLViewController { checkForUpdatesCheckbox.setSelected(settings.isCheckForUpdatesEnabled() && !areUpdatesManagedExternally()); portField.setText(String.valueOf(settings.getPort())); portField.addEventFilter(KeyEvent.KEY_TYPED, this::filterNumericKeyEvents); + useIpv6Checkbox.setDisable(!SystemUtils.IS_OS_WINDOWS); + useIpv6Checkbox.setSelected(SystemUtils.IS_OS_WINDOWS && settings.shouldUseIpv6()); versionLabel.setText(String.format(localization.getString("settings.version.label"), applicationVersion().orElse("SNAPSHOT"))); - EasyBind.subscribe(portField.textProperty(), this::portDidChange); EasyBind.subscribe(checkForUpdatesCheckbox.selectedProperty(), settings::setCheckForUpdatesEnabled); + EasyBind.subscribe(portField.textProperty(), this::portDidChange); + EasyBind.subscribe(useIpv6Checkbox.selectedProperty(), settings::setUseIpv6); } @Override diff --git a/main/ui/src/main/java/org/cryptomator/ui/controllers/UnlockController.java b/main/ui/src/main/java/org/cryptomator/ui/controllers/UnlockController.java index 697b9948b..bead49f46 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/controllers/UnlockController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/controllers/UnlockController.java @@ -26,6 +26,7 @@ import org.cryptomator.frontend.webdav.mount.WindowsDriveLetters; import org.cryptomator.ui.controls.SecPasswordField; import org.cryptomator.ui.model.Vault; import org.cryptomator.ui.settings.Localization; +import org.cryptomator.ui.settings.Settings; import org.fxmisc.easybind.EasyBind; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -58,16 +59,18 @@ public class UnlockController extends AbstractFXMLViewController { private final Localization localization; private final ExecutorService exec; private final Lazy frontendFactory; + private final Settings settings; private final WindowsDriveLetters driveLetters; private final ChangeListener driveLetterChangeListener = this::winDriveLetterDidChange; final ObjectProperty vault = new SimpleObjectProperty<>(); @Inject - public UnlockController(Application app, Localization localization, ExecutorService exec, Lazy frontendFactory, WindowsDriveLetters driveLetters) { + public UnlockController(Application app, Localization localization, ExecutorService exec, Lazy frontendFactory, Settings settings, WindowsDriveLetters driveLetters) { this.app = app; this.localization = localization; this.exec = exec; this.frontendFactory = frontendFactory; + this.settings = settings; this.driveLetters = driveLetters; } @@ -279,7 +282,7 @@ public class UnlockController extends AbstractFXMLViewController { private void unlock(CharSequence password) { try { - vault.get().activateFrontend(frontendFactory.get(), password); + vault.get().activateFrontend(frontendFactory.get(), settings, password); vault.get().reveal(); } catch (InvalidPassphraseException e) { Platform.runLater(() -> { diff --git a/main/ui/src/main/java/org/cryptomator/ui/model/Vault.java b/main/ui/src/main/java/org/cryptomator/ui/model/Vault.java index c7e619d74..56483cfd0 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/model/Vault.java +++ b/main/ui/src/main/java/org/cryptomator/ui/model/Vault.java @@ -38,6 +38,7 @@ 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.settings.Settings; import org.cryptomator.ui.util.DeferredClosable; import org.cryptomator.ui.util.DeferredCloser; import org.cryptomator.ui.util.FXThreads; @@ -112,7 +113,7 @@ public class Vault implements CryptoFileSystemDelegate { } } - public synchronized void activateFrontend(FrontendFactory frontendFactory, CharSequence passphrase) throws FrontendCreationFailedException { + public synchronized void activateFrontend(FrontendFactory frontendFactory, Settings settings, CharSequence passphrase) throws FrontendCreationFailedException { boolean success = false; try { FileSystem fs = getNioFileSystem(); @@ -123,7 +124,7 @@ public class Vault implements CryptoFileSystemDelegate { String contextPath = StringUtils.prependIfMissing(mountName, "/"); Frontend frontend = frontendFactory.create(statsFs, contextPath); filesystemFrontend = closer.closeLater(frontend); - frontend.mount(getMountParams()); + frontend.mount(getMountParams(settings)); success = true; } catch (UncheckedIOException | CommandFailedException e) { throw new FrontendCreationFailedException(e); @@ -140,10 +141,12 @@ public class Vault implements CryptoFileSystemDelegate { Platform.runLater(() -> unlocked.set(false)); } - private Map> getMountParams() { + private Map> getMountParams(Settings settings) { + String hostname = SystemUtils.IS_OS_WINDOWS && settings.shouldUseIpv6() ? "0--1.ipv6-literal.net" : "localhost"; return ImmutableMap.of( // MountParam.MOUNT_NAME, Optional.ofNullable(mountName), // - MountParam.WIN_DRIVE_LETTER, Optional.ofNullable(CharUtils.toString(winDriveLetter)) // + MountParam.WIN_DRIVE_LETTER, Optional.ofNullable(CharUtils.toString(winDriveLetter)), // + MountParam.HOSTNAME, Optional.of(hostname) // ); } diff --git a/main/ui/src/main/java/org/cryptomator/ui/model/VaultFactory.java b/main/ui/src/main/java/org/cryptomator/ui/model/VaultFactory.java index a2c3f8f25..ec0b0d420 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/model/VaultFactory.java +++ b/main/ui/src/main/java/org/cryptomator/ui/model/VaultFactory.java @@ -15,7 +15,6 @@ import javax.inject.Singleton; import org.cryptomator.filesystem.crypto.CryptoFileSystemFactory; import org.cryptomator.filesystem.shortening.ShorteningFileSystemFactory; -import org.cryptomator.frontend.webdav.mount.WebDavMounter; import org.cryptomator.ui.util.DeferredCloser; @Singleton @@ -26,7 +25,7 @@ public class VaultFactory { private final DeferredCloser closer; @Inject - public VaultFactory(ShorteningFileSystemFactory shorteningFileSystemFactory, CryptoFileSystemFactory cryptoFileSystemFactory, WebDavMounter mounter, DeferredCloser closer) { + public VaultFactory(ShorteningFileSystemFactory shorteningFileSystemFactory, CryptoFileSystemFactory cryptoFileSystemFactory, DeferredCloser closer) { this.shorteningFileSystemFactory = shorteningFileSystemFactory; this.cryptoFileSystemFactory = cryptoFileSystemFactory; this.closer = closer; diff --git a/main/ui/src/main/java/org/cryptomator/ui/settings/Settings.java b/main/ui/src/main/java/org/cryptomator/ui/settings/Settings.java index ec91fc036..85c8fefdc 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/settings/Settings.java +++ b/main/ui/src/main/java/org/cryptomator/ui/settings/Settings.java @@ -17,13 +17,14 @@ import org.cryptomator.ui.model.Vault; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonPropertyOrder; -@JsonPropertyOrder(value = {"directories", "checkForUpdatesEnabled", "port", "numTrayNotifications"}) +@JsonPropertyOrder(value = {"directories", "checkForUpdatesEnabled", "port", "useIpv6", "numTrayNotifications"}) public class Settings implements Serializable { private static final long serialVersionUID = 7609959894417878744L; public static final int MIN_PORT = 1024; public static final int MAX_PORT = 65535; public static final int DEFAULT_PORT = 0; + public static final boolean DEFAULT_USE_IPV6 = false; public static final Integer DEFAULT_NUM_TRAY_NOTIFICATIONS = 3; @JsonProperty("directories") @@ -34,6 +35,9 @@ public class Settings implements Serializable { @JsonProperty("port") private Integer port; + + @JsonProperty("useIpv6") + private Boolean useIpv6; @JsonProperty("numTrayNotifications") private Integer numTrayNotifications; @@ -86,6 +90,14 @@ public class Settings implements Serializable { return port == DEFAULT_PORT || port >= MIN_PORT && port <= MAX_PORT; } + public boolean shouldUseIpv6() { + return useIpv6 == null ? DEFAULT_USE_IPV6 : useIpv6; + } + + public void setUseIpv6(boolean useIpv6) { + this.useIpv6 = useIpv6; + } + public Integer getNumTrayNotifications() { return numTrayNotifications == null ? DEFAULT_NUM_TRAY_NOTIFICATIONS : numTrayNotifications; } diff --git a/main/ui/src/main/resources/fxml/settings.fxml b/main/ui/src/main/resources/fxml/settings.fxml index 98499ed4a..eebe54528 100644 --- a/main/ui/src/main/resources/fxml/settings.fxml +++ b/main/ui/src/main/resources/fxml/settings.fxml @@ -36,6 +36,10 @@