diff --git a/main/core/src/main/java/org/cryptomator/webdav/WebDavServer.java b/main/core/src/main/java/org/cryptomator/webdav/WebDavServer.java index 9f67a4a74..f4265f80f 100644 --- a/main/core/src/main/java/org/cryptomator/webdav/WebDavServer.java +++ b/main/core/src/main/java/org/cryptomator/webdav/WebDavServer.java @@ -8,6 +8,10 @@ ******************************************************************************/ package org.cryptomator.webdav; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.file.Path; +import java.util.UUID; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; @@ -18,6 +22,7 @@ import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; import org.eclipse.jetty.servlet.ServletContextHandler; import org.eclipse.jetty.servlet.ServletHolder; +import org.eclipse.jetty.util.component.LifeCycle; import org.eclipse.jetty.util.thread.QueuedThreadPool; import org.eclipse.jetty.util.thread.ThreadPool; import org.slf4j.Logger; @@ -31,40 +36,33 @@ public final class WebDavServer { private static final int MAX_THREADS = 200; private static final int MIN_THREADS = 4; private static final int THREAD_IDLE_SECONDS = 20; + private static final WebDavServer INSTANCE = new WebDavServer(); private final Server server; - private int port; + private final ServerConnector localConnector; + private final ServletContextHandler servletContext; - public WebDavServer() { + public static WebDavServer getInstance() { + return INSTANCE; + } + + private WebDavServer() { final BlockingQueue queue = new LinkedBlockingQueue<>(MAX_PENDING_REQUESTS); final ThreadPool tp = new QueuedThreadPool(MAX_THREADS, MIN_THREADS, THREAD_IDLE_SECONDS, queue); server = new Server(tp); + localConnector = new ServerConnector(server); + localConnector.setHost(LOCALHOST); + servletContext = new ServletContextHandler(ServletContextHandler.SESSIONS); + servletContext.setContextPath("/"); + server.setConnectors(new Connector[] {localConnector}); + server.setHandler(servletContext); } - /** - * @param workDir Path of encrypted folder. - * @param cryptor A fully initialized cryptor instance ready to en- or decrypt streams. - * @return true upon success - */ - public synchronized boolean start(final String workDir, final boolean checkFileIntegrity, final Cryptor cryptor) { - final ServerConnector connector = new ServerConnector(server); - connector.setHost(LOCALHOST); - - final String contextPath = "/"; - final String servletPathSpec = "/*"; - - final ServletContextHandler context = new ServletContextHandler(ServletContextHandler.SESSIONS); - context.addServlet(getWebDavServletHolder(workDir, contextPath, checkFileIntegrity, cryptor), servletPathSpec); - context.setContextPath(contextPath); - server.setHandler(context); - + public synchronized void start() { try { - server.setConnectors(new Connector[] {connector}); server.start(); - port = connector.getLocalPort(); - return true; + LOG.info("Cryptomator is running on port {}", getPort()); } catch (Exception ex) { - LOG.error("Server couldn't be started", ex); - return false; + throw new RuntimeException("Server couldn't be started", ex); } } @@ -72,14 +70,33 @@ public final class WebDavServer { return server.isRunning(); } - public synchronized boolean stop() { + public synchronized void stop() { try { server.stop(); - port = 0; } catch (Exception ex) { LOG.error("Server couldn't be stopped", ex); } - return server.isStopped(); + } + + /** + * @param workDir Path of encrypted folder. + * @param cryptor A fully initialized cryptor instance ready to en- or decrypt streams. + * @return servlet + */ + public ServletLifeCycleAdapter createServlet(final Path workDir, final boolean checkFileIntegrity, final Cryptor cryptor) { + try { + final URI uri = new URI(null, null, localConnector.getHost(), localConnector.getLocalPort(), "/" + UUID.randomUUID().toString(), null, null); + + final String pathPrefix = uri.getRawPath() + "/"; + final String pathSpec = pathPrefix + "*"; + final ServletHolder servlet = getWebDavServletHolder(workDir.toString(), pathPrefix, checkFileIntegrity, cryptor); + servletContext.addServlet(servlet, pathSpec); + + LOG.info("{} available on http://{}", workDir, uri.getRawSchemeSpecificPart()); + return new ServletLifeCycleAdapter(servlet, uri); + } catch (URISyntaxException e) { + throw new IllegalArgumentException("Can't create URI from given workDir", e); + } } private ServletHolder getWebDavServletHolder(final String workDir, final String contextPath, final boolean checkFileIntegrity, final Cryptor cryptor) { @@ -91,7 +108,50 @@ public final class WebDavServer { } public int getPort() { - return port; + return localConnector.getLocalPort(); + } + + /** + * Exposes implementation-specific methods to other modules. + */ + public class ServletLifeCycleAdapter { + + private final LifeCycle lifecycle; + private final URI servletUri; + + private ServletLifeCycleAdapter(LifeCycle lifecycle, URI servletUri) { + this.lifecycle = lifecycle; + this.servletUri = servletUri; + } + + public boolean isRunning() { + return lifecycle.isRunning(); + } + + public boolean start() { + try { + lifecycle.start(); + return true; + } catch (Exception e) { + LOG.error("Failed to start", e); + return false; + } + } + + public boolean stop() { + try { + lifecycle.stop(); + return true; + } catch (Exception e) { + LOG.error("Failed to stop", e); + return false; + } + } + + public URI getServletUri() { + return servletUri; + } + } } diff --git a/main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/Aes256Cryptor.java b/main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/Aes256Cryptor.java index a672fa23b..ee44bd1af 100644 --- a/main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/Aes256Cryptor.java +++ b/main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/Aes256Cryptor.java @@ -13,6 +13,7 @@ import java.io.InputStream; import java.io.OutputStream; import java.nio.ByteBuffer; import java.nio.channels.SeekableByteChannel; +import java.nio.charset.StandardCharsets; import java.nio.file.DirectoryStream.Filter; import java.nio.file.Path; import java.security.InvalidAlgorithmParameterException; @@ -40,7 +41,6 @@ import javax.crypto.spec.SecretKeySpec; import javax.security.auth.DestroyFailedException; import javax.security.auth.Destroyable; -import org.apache.commons.io.Charsets; import org.apache.commons.io.IOUtils; import org.apache.commons.io.output.NullOutputStream; import org.apache.commons.lang3.ArrayUtils; @@ -330,7 +330,7 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo final ByteBuffer iv = ByteBuffer.allocate(AES_BLOCK_LENGTH); iv.put(partialIv); final Cipher cipher = this.aesCtrCipher(key, iv.array(), Cipher.ENCRYPT_MODE); - final byte[] cleartextBytes = cleartext.getBytes(Charsets.UTF_8); + final byte[] cleartextBytes = cleartext.getBytes(StandardCharsets.UTF_8); final byte[] encryptedBytes = cipher.doFinal(cleartextBytes); final String ivAndCiphertext = ENCRYPTED_FILENAME_CODEC.encodeAsString(partialIv) + IV_PREFIX_SEPARATOR + ENCRYPTED_FILENAME_CODEC.encodeAsString(encryptedBytes); @@ -387,7 +387,7 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo final Cipher cipher = this.aesCtrCipher(key, iv.array(), Cipher.DECRYPT_MODE); final byte[] encryptedBytes = ENCRYPTED_FILENAME_CODEC.decode(ciphertext); final byte[] cleartextBytes = cipher.doFinal(encryptedBytes); - return new String(cleartextBytes, Charsets.UTF_8); + return new String(cleartextBytes, StandardCharsets.UTF_8); } private LongFilenameMetadata getMetadata(CryptorIOSupport ioSupport, String metadataFile) throws IOException { diff --git a/main/ui/src/main/java/org/cryptomator/ui/MainApplication.java b/main/ui/src/main/java/org/cryptomator/ui/MainApplication.java index 014f48419..de5f564fb 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/MainApplication.java +++ b/main/ui/src/main/java/org/cryptomator/ui/MainApplication.java @@ -23,6 +23,7 @@ import org.apache.commons.lang3.SystemUtils; import org.cryptomator.ui.settings.Settings; import org.cryptomator.ui.util.ActiveWindowStyleSupport; import org.cryptomator.ui.util.TrayIconUtil; +import org.cryptomator.webdav.WebDavServer; import org.eclipse.jetty.util.ConcurrentHashSet; public class MainApplication extends Application { @@ -37,6 +38,7 @@ public class MainApplication extends Application { @Override public void start(final Stage primaryStage) throws IOException { + WebDavServer.getInstance().start(); chooseNativeStylesheet(); final ResourceBundle rb = ResourceBundle.getBundle("localization"); final FXMLLoader loader = new FXMLLoader(getClass().getResource("/fxml/main.fxml"), rb); @@ -67,6 +69,7 @@ public class MainApplication extends Application { private void quit() { Platform.runLater(() -> { + WebDavServer.getInstance().stop(); CLEAN_SHUTDOWN_PERFORMER.run(); Settings.save(); Platform.exit(); diff --git a/main/ui/src/main/java/org/cryptomator/ui/UnlockedController.java b/main/ui/src/main/java/org/cryptomator/ui/UnlockedController.java index c557ea99c..4166bfbf8 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/UnlockedController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/UnlockedController.java @@ -27,6 +27,7 @@ import javafx.util.Duration; import org.cryptomator.crypto.CryptorIOSampling; import org.cryptomator.ui.model.Directory; +import org.cryptomator.webdav.WebDavServer; public class UnlockedController implements Initializable { @@ -123,7 +124,7 @@ public class UnlockedController implements Initializable { public void setDirectory(Directory directory) { this.directory = directory; - final String msg = String.format(rb.getString("unlocked.messageLabel.runningOnPort"), directory.getServer().getPort()); + final String msg = String.format(rb.getString("unlocked.messageLabel.runningOnPort"), WebDavServer.getInstance().getPort()); messageLabel.setText(msg); if (directory.getCryptor() instanceof CryptorIOSampling) { diff --git a/main/ui/src/main/java/org/cryptomator/ui/model/Directory.java b/main/ui/src/main/java/org/cryptomator/ui/model/Directory.java index 3c3d81d18..93c5f7c56 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/model/Directory.java +++ b/main/ui/src/main/java/org/cryptomator/ui/model/Directory.java @@ -17,6 +17,7 @@ import org.cryptomator.ui.util.mount.CommandFailedException; import org.cryptomator.ui.util.mount.WebDavMount; import org.cryptomator.ui.util.mount.WebDavMounter; import org.cryptomator.webdav.WebDavServer; +import org.cryptomator.webdav.WebDavServer.ServletLifeCycleAdapter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -29,20 +30,20 @@ public class Directory implements Serializable { private static final long serialVersionUID = 3754487289683599469L; private static final Logger LOG = LoggerFactory.getLogger(Directory.class); - - private final WebDavServer server = new WebDavServer(); private final Cryptor cryptor = SamplingDecorator.decorate(new Aes256Cryptor()); private final ObjectProperty unlocked = new SimpleObjectProperty(this, "unlocked", Boolean.FALSE); + private final Runnable shutdownTask = new ShutdownTask(); private final Path path; private boolean verifyFileIntegrity; + private ServletLifeCycleAdapter webDavServlet; private WebDavMount webDavMount; - private final Runnable shutdownTask = new ShutdownTask(); public Directory(final Path path) { if (!Files.isDirectory(path)) { throw new IllegalArgumentException("Not a directory: " + path); } this.path = path; + } public boolean containsMasterKey() throws IOException { @@ -50,7 +51,11 @@ public class Directory implements Serializable { } public synchronized boolean startServer() { - if (server.start(path.toString(), verifyFileIntegrity, cryptor)) { + if (webDavServlet != null && webDavServlet.isRunning()) { + return false; + } + webDavServlet = WebDavServer.getInstance().createServlet(path, verifyFileIntegrity, cryptor); + if (webDavServlet.start()) { MainApplication.addShutdownTask(shutdownTask); return true; } else { @@ -58,18 +63,21 @@ public class Directory implements Serializable { } } - public synchronized void stopServer() { - if (server.isRunning()) { + public void stopServer() { + if (webDavServlet != null && webDavServlet.isRunning()) { MainApplication.removeShutdownTask(shutdownTask); this.unmount(); - server.stop(); + webDavServlet.stop(); cryptor.swipeSensitiveData(); } } public boolean mount() { + if (webDavServlet == null || !webDavServlet.isRunning()) { + return false; + } try { - webDavMount = WebDavMounter.mount(server.getPort()); + webDavMount = WebDavMounter.mount(webDavServlet.getServletUri()); return true; } catch (CommandFailedException e) { LOG.warn("mount failed", e); @@ -127,10 +135,6 @@ public class Directory implements Serializable { this.unlocked.set(unlocked); } - public WebDavServer getServer() { - return server; - } - /* hashcode/equals */ @Override diff --git a/main/ui/src/main/java/org/cryptomator/ui/util/mount/FallbackWebDavMounter.java b/main/ui/src/main/java/org/cryptomator/ui/util/mount/FallbackWebDavMounter.java index 8f114cf6c..b27be907c 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/util/mount/FallbackWebDavMounter.java +++ b/main/ui/src/main/java/org/cryptomator/ui/util/mount/FallbackWebDavMounter.java @@ -8,6 +8,8 @@ ******************************************************************************/ package org.cryptomator.ui.util.mount; +import java.net.URI; + /** * A WebDavMounter acting as fallback if no other mounter works. * @@ -21,7 +23,7 @@ final class FallbackWebDavMounter implements WebDavMounterStrategy { } @Override - public WebDavMount mount(int localPort) { + public WebDavMount mount(URI uri) { displayMountInstructions(); return new WebDavMount() { @Override diff --git a/main/ui/src/main/java/org/cryptomator/ui/util/mount/LinuxGvfsWebDavMounter.java b/main/ui/src/main/java/org/cryptomator/ui/util/mount/LinuxGvfsWebDavMounter.java index dac1582ed..2f2105a51 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/util/mount/LinuxGvfsWebDavMounter.java +++ b/main/ui/src/main/java/org/cryptomator/ui/util/mount/LinuxGvfsWebDavMounter.java @@ -9,6 +9,8 @@ ******************************************************************************/ package org.cryptomator.ui.util.mount; +import java.net.URI; + import org.apache.commons.lang3.SystemUtils; import org.cryptomator.ui.util.command.Script; @@ -30,16 +32,16 @@ final class LinuxGvfsWebDavMounter implements WebDavMounterStrategy { } @Override - public WebDavMount mount(int localPort) throws CommandFailedException { + public WebDavMount mount(URI uri) throws CommandFailedException { final Script mountScript = Script.fromLines( "set -x", - "gvfs-mount \"dav://[::1]:$PORT\"", - "xdg-open \"$URI\"") - .addEnv("PORT", String.valueOf(localPort)); + "gvfs-mount \"dav:$DAV_SSP\"", + "xdg-open \"dav:$DAV_SSP\"") + .addEnv("DAV_SSP", uri.getRawSchemeSpecificPart()); final Script unmountScript = Script.fromLines( "set -x", - "gvfs-mount -u \"dav://[::1]:$PORT\"") - .addEnv("URI", String.valueOf(localPort)); + "gvfs-mount -u \"dav:$DAV_SSP\"") + .addEnv("$DAV_SSP", uri.getRawSchemeSpecificPart()); mountScript.execute(); return new WebDavMount() { @Override diff --git a/main/ui/src/main/java/org/cryptomator/ui/util/mount/MacOsXWebDavMounter.java b/main/ui/src/main/java/org/cryptomator/ui/util/mount/MacOsXWebDavMounter.java index 2641d3c4e..49b6430ec 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/util/mount/MacOsXWebDavMounter.java +++ b/main/ui/src/main/java/org/cryptomator/ui/util/mount/MacOsXWebDavMounter.java @@ -9,6 +9,8 @@ ******************************************************************************/ package org.cryptomator.ui.util.mount; +import java.net.URI; + import org.apache.commons.lang3.SystemUtils; import org.cryptomator.ui.util.command.Script; @@ -20,13 +22,14 @@ final class MacOsXWebDavMounter implements WebDavMounterStrategy { } @Override - public WebDavMount mount(int localPort) throws CommandFailedException { - final String path = "/Volumes/Cryptomator" + localPort; + public WebDavMount mount(URI uri) throws CommandFailedException { + final String path = "/Volumes/Cryptomator" + uri.getRawPath().replace('/', '_'); final Script mountScript = Script.fromLines( "mkdir \"$MOUNT_PATH\"", - "mount_webdav -S -v Cryptomator \"[::1]:$PORT\" \"$MOUNT_PATH\"", + "mount_webdav -S -v Cryptomator \"[::1]:$PORT$DAV_PATH\" \"$MOUNT_PATH\"", "open \"$MOUNT_PATH\"") - .addEnv("PORT", String.valueOf(localPort)) + .addEnv("PORT", String.valueOf(uri.getPort())) + .addEnv("DAV_PATH", uri.getRawPath()) .addEnv("MOUNT_PATH", path); final Script unmountScript = Script.fromLines( "umount $MOUNT_PATH") diff --git a/main/ui/src/main/java/org/cryptomator/ui/util/mount/WebDavMounter.java b/main/ui/src/main/java/org/cryptomator/ui/util/mount/WebDavMounter.java index 01e87fc89..c01bc21dd 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/util/mount/WebDavMounter.java +++ b/main/ui/src/main/java/org/cryptomator/ui/util/mount/WebDavMounter.java @@ -9,6 +9,8 @@ ******************************************************************************/ package org.cryptomator.ui.util.mount; +import java.net.URI; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -23,12 +25,12 @@ public final class WebDavMounter { /** * Tries to mount a given webdav share. * - * @param localPort local TCP port of the webdav share + * @param uri URI of the webdav share * @return a {@link WebDavMount} representing the mounted share * @throws CommandFailedException if the mount operation fails */ - public static WebDavMount mount(int localPort) throws CommandFailedException { - return chooseStrategy().mount(localPort); + public static WebDavMount mount(URI uri) throws CommandFailedException { + return chooseStrategy().mount(uri); } private static WebDavMounterStrategy chooseStrategy() { diff --git a/main/ui/src/main/java/org/cryptomator/ui/util/mount/WebDavMounterStrategy.java b/main/ui/src/main/java/org/cryptomator/ui/util/mount/WebDavMounterStrategy.java index d382875ed..9490d1bd8 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/util/mount/WebDavMounterStrategy.java +++ b/main/ui/src/main/java/org/cryptomator/ui/util/mount/WebDavMounterStrategy.java @@ -9,6 +9,7 @@ ******************************************************************************/ package org.cryptomator.ui.util.mount; +import java.net.URI; /** * A strategy able to mount a webdav share and display it to the user. @@ -25,10 +26,10 @@ interface WebDavMounterStrategy { /** * Tries to mount a given webdav share. * - * @param localPort local TCP port of the webdav share + * @param uri URI of the webdav share * @return a {@link WebDavMount} representing the mounted share * @throws CommandFailedException if the mount operation fails */ - WebDavMount mount(int localPort) throws CommandFailedException; + WebDavMount mount(URI uri) throws CommandFailedException; } diff --git a/main/ui/src/main/java/org/cryptomator/ui/util/mount/WindowsWebDavMounter.java b/main/ui/src/main/java/org/cryptomator/ui/util/mount/WindowsWebDavMounter.java index c539a110c..7fdd8be02 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/util/mount/WindowsWebDavMounter.java +++ b/main/ui/src/main/java/org/cryptomator/ui/util/mount/WindowsWebDavMounter.java @@ -11,6 +11,7 @@ package org.cryptomator.ui.util.mount; import static org.cryptomator.ui.util.command.Script.fromLines; +import java.net.URI; import java.util.concurrent.TimeUnit; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -36,8 +37,10 @@ final class WindowsWebDavMounter implements WebDavMounterStrategy { } @Override - public WebDavMount mount(int localPort) throws CommandFailedException { - final Script mountScript = fromLines("net use * http://0--1.ipv6-literal.net:%PORT% /persistent:no").addEnv("PORT", String.valueOf(localPort)); + public WebDavMount mount(URI uri) throws CommandFailedException { + final Script mountScript = fromLines("net use * http://0--1.ipv6-literal.net:%PORT%%DAV_PATH% /persistent:no") + .addEnv("PORT", String.valueOf(uri.getPort())) + .addEnv("DAV_PATH", uri.getRawPath()); final CommandResult mountResult = mountScript.execute(30, TimeUnit.SECONDS); final String driveLetter = getDriveLetter(mountResult.getStdOut()); final Script unmountScript = fromLines("net use " + driveLetter + " /delete").addEnv("DRIVE_LETTER", driveLetter);