spawn server listening on localhost, used for oauth redirect_uri

This commit is contained in:
Sebastian Stenzel
2021-07-29 16:57:28 +02:00
parent b21ea61342
commit 7fabc6f52d
7 changed files with 222 additions and 19 deletions

View File

@@ -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>

View File

@@ -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;

View File

@@ -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);
}
}
}
}

View File

@@ -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());

View File

@@ -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);
}
}
}

View File

@@ -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();
}
}
}

View 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>