mirror of
https://github.com/cryptomator/cryptomator.git
synced 2026-05-14 08:41:28 +00:00
spawn server listening on localhost, used for oauth redirect_uri
This commit is contained in:
7
pom.xml
7
pom.xml
@@ -48,6 +48,7 @@
|
||||
<zxcvbn.version>1.5.2</zxcvbn.version>
|
||||
<slf4j.version>1.7.31</slf4j.version>
|
||||
<logback.version>1.2.3</logback.version>
|
||||
<jetty.version>10.0.6</jetty.version>
|
||||
|
||||
<!-- test dependencies -->
|
||||
<junit.jupiter.version>5.7.2</junit.jupiter.version>
|
||||
@@ -136,6 +137,12 @@
|
||||
<version>${bouncycastle.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty</groupId>
|
||||
<artifactId>jetty-server</artifactId>
|
||||
<version>${jetty.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- JWT -->
|
||||
<dependency>
|
||||
<groupId>com.auth0</groupId>
|
||||
|
||||
@@ -21,6 +21,7 @@ module org.cryptomator.desktop {
|
||||
requires com.nulabinc.zxcvbn;
|
||||
requires org.slf4j;
|
||||
requires org.apache.commons.lang3;
|
||||
requires org.eclipse.jetty.server;
|
||||
requires dagger;
|
||||
requires com.auth0.jwt;
|
||||
requires org.bouncycastle.provider;
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
package org.cryptomator.ui.keyloading.hub;
|
||||
|
||||
import com.google.common.io.BaseEncoding;
|
||||
import org.eclipse.jetty.server.Connector;
|
||||
import org.eclipse.jetty.server.Request;
|
||||
import org.eclipse.jetty.server.Server;
|
||||
import org.eclipse.jetty.server.ServerConnector;
|
||||
import org.eclipse.jetty.server.handler.AbstractHandler;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Queue;
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
import java.util.concurrent.LinkedTransferQueue;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import java.util.concurrent.TransferQueue;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* A basic implementation for RFC 8252, Section 7.3:
|
||||
* <p>
|
||||
* We're spawning a local http server on a system-assigned high port and
|
||||
* use <code>http://127.0.0.1:{PORT}/success</code> as a redirect URI.
|
||||
* <p>
|
||||
* Furthermore, we can deliver a html response to inform the user that the
|
||||
* auth workflow finished and she can close the browser tab.
|
||||
*/
|
||||
class AuthReceiver implements AutoCloseable {
|
||||
|
||||
private static final String REDIRECT_SCHEME = "http";
|
||||
private static final String LOOPBACK_ADDR = "127.0.0.1";
|
||||
private static final String JSON_200 = """
|
||||
{"status": "success"}
|
||||
""";
|
||||
private static final String JSON_400 = """
|
||||
{"status": "missing param key"}
|
||||
""";
|
||||
|
||||
private final Server server;
|
||||
private final ServerConnector connector;
|
||||
private final Handler handler;
|
||||
|
||||
private AuthReceiver(Server server, ServerConnector connector, Handler handler) {
|
||||
assert server.isRunning();
|
||||
this.server = server;
|
||||
this.connector = connector;
|
||||
this.handler = handler;
|
||||
}
|
||||
|
||||
public URI getRedirectURL() {
|
||||
try {
|
||||
return new URI(REDIRECT_SCHEME, null, LOOPBACK_ADDR, connector.getLocalPort(), null, null, null);
|
||||
} catch (URISyntaxException e) {
|
||||
throw new IllegalStateException("URI constructed from well-formed components.", e);
|
||||
}
|
||||
}
|
||||
|
||||
public static AuthReceiver start() throws Exception {
|
||||
Server server = new Server();
|
||||
var handler = new Handler();
|
||||
var connector = new ServerConnector(server);
|
||||
connector.setPort(0);
|
||||
connector.setHost(LOOPBACK_ADDR);
|
||||
server.setConnectors(new Connector[]{connector});
|
||||
server.setHandler(handler);
|
||||
server.start();
|
||||
return new AuthReceiver(server, connector, handler);
|
||||
}
|
||||
|
||||
public String receive() throws InterruptedException {
|
||||
return handler.receivedKeys.take();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws Exception {
|
||||
server.stop();
|
||||
}
|
||||
|
||||
private static class Handler extends AbstractHandler {
|
||||
|
||||
private final BlockingQueue<String> receivedKeys = new LinkedBlockingQueue<>();
|
||||
|
||||
@Override
|
||||
public void handle(String target, Request baseRequest, HttpServletRequest req, HttpServletResponse res) throws IOException {
|
||||
baseRequest.setHandled(true);
|
||||
var key = req.getParameter("key");
|
||||
byte[] response;
|
||||
if (key != null) {
|
||||
res.setStatus(HttpServletResponse.SC_OK);
|
||||
response = JSON_200.getBytes(StandardCharsets.UTF_8);
|
||||
} else {
|
||||
res.setStatus(HttpServletResponse.SC_BAD_REQUEST);
|
||||
response = JSON_400.getBytes(StandardCharsets.UTF_8);
|
||||
}
|
||||
res.setContentType("application/json;charset=utf-8");
|
||||
res.setContentLength(response.length);
|
||||
res.getOutputStream().write(response);
|
||||
res.getOutputStream().flush();
|
||||
|
||||
// the following line might trigger a server shutdown,
|
||||
// so let's make sure the response is flushed first
|
||||
if (key != null) {
|
||||
receivedKeys.add(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
package org.cryptomator.ui.keyloading.hub;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.google.common.base.Splitter;
|
||||
import dagger.Lazy;
|
||||
import org.cryptomator.common.vaults.Vault;
|
||||
import org.cryptomator.cryptolib.api.Masterkey;
|
||||
@@ -12,23 +14,28 @@ import org.cryptomator.ui.keyloading.KeyLoadingStrategy;
|
||||
import org.cryptomator.ui.unlock.UnlockCancelledException;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javafx.application.Application;
|
||||
import javafx.application.Platform;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.stage.Stage;
|
||||
import javafx.stage.Window;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.KeyPair;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
@KeyLoading
|
||||
public class HubKeyLoadingStrategy implements KeyLoadingStrategy {
|
||||
|
||||
static final String SCHEME_HUB_HTTP = "hub+http";
|
||||
static final String SCHEME_HUB_HTTPS = "hub+https";
|
||||
private static final String SCHEME_HTTP = "http";
|
||||
private static final String SCHEME_HTTPS = "https";
|
||||
private static final String SCHEME_PREFIX = "hub+";
|
||||
static final String SCHEME_HUB_HTTP = SCHEME_PREFIX + "http";
|
||||
static final String SCHEME_HUB_HTTPS = SCHEME_PREFIX + "https";
|
||||
|
||||
private final Application application;
|
||||
private final ExecutorService executor;
|
||||
private final Vault vault;
|
||||
private final Stage window;
|
||||
private final Lazy<Scene> p12LoadingScene;
|
||||
@@ -36,7 +43,9 @@ public class HubKeyLoadingStrategy implements KeyLoadingStrategy {
|
||||
private final AtomicReference<KeyPair> keyPairRef;
|
||||
|
||||
@Inject
|
||||
public HubKeyLoadingStrategy(@KeyLoading Vault vault, @KeyLoading Stage window, @FxmlScene(FxmlFile.HUB_P12) Lazy<Scene> p12LoadingScene, UserInteractionLock<HubKeyLoadingModule.P12KeyLoading> p12LoadingLock, AtomicReference<KeyPair> keyPairRef) {
|
||||
public HubKeyLoadingStrategy(Application application, ExecutorService executor, @KeyLoading Vault vault, @KeyLoading Stage window, @FxmlScene(FxmlFile.HUB_P12) Lazy<Scene> p12LoadingScene, UserInteractionLock<HubKeyLoadingModule.P12KeyLoading> p12LoadingLock, AtomicReference<KeyPair> keyPairRef) {
|
||||
this.application = application;
|
||||
this.executor = executor;
|
||||
this.vault = vault;
|
||||
this.window = window;
|
||||
this.p12LoadingScene = p12LoadingScene;
|
||||
@@ -46,23 +55,14 @@ public class HubKeyLoadingStrategy implements KeyLoadingStrategy {
|
||||
|
||||
@Override
|
||||
public Masterkey loadKey(URI keyId) throws MasterkeyLoadingFailedException {
|
||||
return switch (keyId.getScheme().toLowerCase()) {
|
||||
case SCHEME_HUB_HTTP -> loadKey(keyId, SCHEME_HTTP);
|
||||
case SCHEME_HUB_HTTPS -> loadKey(keyId, SCHEME_HTTPS);
|
||||
default -> throw new IllegalArgumentException("Only supports keys with schemes " + SCHEME_HUB_HTTP + " or " + SCHEME_HUB_HTTPS);
|
||||
};
|
||||
}
|
||||
|
||||
private Masterkey loadKey(URI keyId, String adjustedScheme) {
|
||||
try {
|
||||
var foo = new URI(adjustedScheme, keyId.getSchemeSpecificPart(), keyId.getFragment());
|
||||
} catch (URISyntaxException e) {
|
||||
throw new IllegalStateException("URI known to be valid, if old URI was valid", e);
|
||||
}
|
||||
|
||||
Preconditions.checkArgument(keyId.getScheme().startsWith(SCHEME_PREFIX));
|
||||
try {
|
||||
loadP12();
|
||||
LOG.info("keypair loaded {}", keyPairRef.get().getPublic());
|
||||
var task = new ReceiveEncryptedMasterkeyTask(redirectUri -> {
|
||||
openBrowser(keyId, redirectUri);
|
||||
});
|
||||
executor.submit(task);
|
||||
throw new UnlockCancelledException("not yet implemented"); // TODO
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
@@ -70,6 +70,18 @@ public class HubKeyLoadingStrategy implements KeyLoadingStrategy {
|
||||
}
|
||||
}
|
||||
|
||||
private void openBrowser(URI keyId, URI redirectUri) {
|
||||
Preconditions.checkArgument(keyId.getScheme().startsWith(SCHEME_PREFIX));
|
||||
var httpScheme = keyId.getScheme().substring(SCHEME_PREFIX.length());
|
||||
var redirectParam = "redirect_uri="+ URLEncoder.encode(redirectUri.toString(), StandardCharsets.US_ASCII);
|
||||
try {
|
||||
var uri = new URI(httpScheme, keyId.getAuthority(), keyId.getPath(), redirectParam, null);
|
||||
application.getHostServices().showDocument(uri.toString());
|
||||
} catch (URISyntaxException e) {
|
||||
throw new IllegalStateException("URI constructed from params known to be valid", e);
|
||||
}
|
||||
}
|
||||
|
||||
private HubKeyLoadingModule.P12KeyLoading loadP12() throws InterruptedException {
|
||||
Platform.runLater(() -> {
|
||||
window.setScene(p12LoadingScene.get());
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
package org.cryptomator.ui.keyloading.hub;
|
||||
|
||||
import com.google.common.io.BaseEncoding;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javafx.concurrent.Task;
|
||||
import java.net.URI;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
class ReceiveEncryptedMasterkeyTask extends Task<byte[]> {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ReceiveEncryptedMasterkeyTask.class);
|
||||
|
||||
private final Consumer<URI> redirectUriConsumer;
|
||||
|
||||
public ReceiveEncryptedMasterkeyTask(Consumer<URI> redirectUriConsumer) {
|
||||
this.redirectUriConsumer = redirectUriConsumer;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected byte[] call() throws Exception {
|
||||
try (var receiver = AuthReceiver.start()) {
|
||||
var redirectUri = receiver.getRedirectURL();
|
||||
LOG.debug("Waiting for key on {}", redirectUri);
|
||||
redirectUriConsumer.accept(redirectUri);
|
||||
var token = receiver.receive();
|
||||
return BaseEncoding.base64Url().decode(token);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package org.cryptomator.ui.keyloading.hub;
|
||||
|
||||
public class AuthReceiverTest {
|
||||
|
||||
static {
|
||||
System.setProperty("LOGLEVEL", "INFO");
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
try (var receiver = AuthReceiver.start()) {
|
||||
System.out.println("Waiting on " + receiver.getRedirectURL());
|
||||
var token = receiver.receive();
|
||||
System.out.println("SUCCESS: " + token);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
System.out.println("CANCELLED");
|
||||
} catch (Exception e) {
|
||||
System.out.println("ERROR");
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
11
src/test/resources/logback-test.xml
Normal file
11
src/test/resources/logback-test.xml
Normal file
@@ -0,0 +1,11 @@
|
||||
<configuration>
|
||||
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<encoder>
|
||||
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<root level="${LOGLEVEL:-debug}">
|
||||
<appender-ref ref="STDOUT" />
|
||||
</root>
|
||||
</configuration>
|
||||
Reference in New Issue
Block a user