Browse Source

avoid duplicate network drives on windows

Sebastian Stenzel 9 years ago
parent
commit
768f291ff7

+ 1 - 1
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<MountParam, Optional<String>> map) throws CommandFailedException;

+ 34 - 47
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.
  * <p>
- * 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<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)) {
+		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 \"<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);
+			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 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 CommandResult mount(URI uri, String driveLetter) throws CommandFailedException {
+		final Script proxyBypassScript = fromLines(
+				"reg add \"HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings\" /v \"ProxyOverride\" /d \"<local>;%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 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

+ 8 - 1
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

+ 5 - 2
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> frontendFactory;
+	private final Settings settings;
 	private final WindowsDriveLetters driveLetters;
 	private final ChangeListener<Character> driveLetterChangeListener = this::winDriveLetterDidChange;
 	final ObjectProperty<Vault> vault = new SimpleObjectProperty<>();
 
 	@Inject
-	public UnlockController(Application app, Localization localization, ExecutorService exec, Lazy<FrontendFactory> frontendFactory, WindowsDriveLetters driveLetters) {
+	public UnlockController(Application app, Localization localization, ExecutorService exec, Lazy<FrontendFactory> 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(() -> {

+ 7 - 4
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<MountParam, Optional<String>> getMountParams() {
+	private Map<MountParam, Optional<String>> 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) //
 		);
 	}
 

+ 1 - 2
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;

+ 13 - 1
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;
 	}

+ 4 - 0
main/ui/src/main/resources/fxml/settings.fxml

@@ -36,6 +36,10 @@
 			<!-- Row 1 -->
 			<Label GridPane.rowIndex="1" GridPane.columnIndex="0" text="%settings.port.label" cacheShape="true" cache="true" />
 			<TextField GridPane.rowIndex="1" GridPane.columnIndex="1" fx:id="portField" cacheShape="true" cache="true" promptText="%settings.port.prompt" />
+			
+			<!-- Row 2 -->
+			<Label GridPane.rowIndex="2" GridPane.columnIndex="0" text="%settings.useipv6.label" cacheShape="true" cache="true" />
+			<CheckBox GridPane.rowIndex="2" GridPane.columnIndex="1" fx:id="useIpv6Checkbox" cacheShape="true" cache="true" />
 		</children>
 	</GridPane>
 	<Label VBox.vgrow="NEVER" text="%settings.requiresRestartLabel" alignment="CENTER" cacheShape="true" cache="true" />

+ 1 - 0
main/ui/src/main/resources/localization.properties

@@ -78,6 +78,7 @@ settings.version.label=Version %s
 settings.checkForUpdates.label=Check for updates
 settings.port.label=WebDAV Port *
 settings.port.prompt=0 = Choose automatically
+settings.useipv6.label=Use IPv6 literal
 settings.requiresRestartLabel=* Cryptomator needs to restart
 
 # tray icon

+ 1 - 0
main/ui/src/main/resources/localization_de.properties

@@ -78,6 +78,7 @@ settings.version.label=Version %s
 settings.checkForUpdates.label=Auf Updates prüfen
 settings.port.label=WebDAV Port *
 settings.port.prompt=0 = Automatisch wählen
+settings.useipv6.label=IPv6-Literal nutzen
 settings.requiresRestartLabel=* benötigt Neustart von Cryptomator
 
 # tray icon