From 1358fe9d36eadbd3e95103727e8d1d3ea2d01b50 Mon Sep 17 00:00:00 2001 From: Alva Swanson Date: Fri, 28 Jul 2023 20:20:08 +0200 Subject: [PATCH] Tor: Implement bootstrap event handler and listener --- .../bisq/tor/bootstrap/BootstrapEvent.java | 87 +++++++++++++++++++ .../tor/bootstrap/BootstrapEventHandler.java | 85 ++++++++++++++++++ .../tor/bootstrap/BootstrapEventListener.java | 22 +++++ .../tor/bootstrap/TorBootstrapFailed.java | 28 ++++++ .../bisq/tor/process/NativeTorController.java | 53 ++++++++++- 5 files changed, 274 insertions(+), 1 deletion(-) create mode 100644 network/tor/src/main/java/bisq/tor/bootstrap/BootstrapEvent.java create mode 100644 network/tor/src/main/java/bisq/tor/bootstrap/BootstrapEventHandler.java create mode 100644 network/tor/src/main/java/bisq/tor/bootstrap/BootstrapEventListener.java create mode 100644 network/tor/src/main/java/bisq/tor/bootstrap/TorBootstrapFailed.java diff --git a/network/tor/src/main/java/bisq/tor/bootstrap/BootstrapEvent.java b/network/tor/src/main/java/bisq/tor/bootstrap/BootstrapEvent.java new file mode 100644 index 0000000000..70be093bf0 --- /dev/null +++ b/network/tor/src/main/java/bisq/tor/bootstrap/BootstrapEvent.java @@ -0,0 +1,87 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.tor.bootstrap; + +import lombok.Builder; +import lombok.Getter; +import lombok.ToString; + +@Builder +@Getter +@ToString +public class BootstrapEvent { + + private static final String DONE_TAG = "done"; + + private final int progress; + private final String tag; + private final String summary; + + public BootstrapEvent(int progress, String tag, String summary) { + if (progress < 0 || tag.isEmpty() || summary.isEmpty()) { + throw new IllegalArgumentException("Invalid bootstrap event: " + progress + " " + tag + " " + summary); + } + + this.progress = progress; + this.tag = tag; + this.summary = summary; + } + + public boolean isDoneEvent() { + return tag.equals(DONE_TAG); + } + + public static boolean isBootstrapMessage(String type, String message) { + return type.equals("STATUS_CLIENT") && message.contains("BOOTSTRAP"); + } + + public static BootstrapEvent fromEventMessage(String message) { + String[] keyValuePairs = message.split(" "); + + int progress = -1; + String tag = ""; + String summary = ""; + for (String item : keyValuePairs) { + if (!item.contains("=")) { + continue; + } + + String[] parts = item.split("="); + if (parts.length != 2) { + throw new IllegalStateException("Tor event key value pair has more than two '=' signs."); + } + + String key = parts[0]; + String value = parts[1]; + + switch (key) { + case "PROGRESS": + progress = Integer.parseInt(value); + break; + case "TAG": + tag = value; + break; + case "SUMMARY": + summary = value; + break; + } + } + + return new BootstrapEvent(progress, tag, summary); + } +} diff --git a/network/tor/src/main/java/bisq/tor/bootstrap/BootstrapEventHandler.java b/network/tor/src/main/java/bisq/tor/bootstrap/BootstrapEventHandler.java new file mode 100644 index 0000000000..661c4d522b --- /dev/null +++ b/network/tor/src/main/java/bisq/tor/bootstrap/BootstrapEventHandler.java @@ -0,0 +1,85 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.tor.bootstrap; + +import net.freehaven.tor.control.EventHandler; + +import java.util.List; +import java.util.Set; +import java.util.concurrent.CopyOnWriteArraySet; + +public class BootstrapEventHandler implements EventHandler { + + private final Set listeners = new CopyOnWriteArraySet<>(); + + public void addListener(BootstrapEventListener listener) { + listeners.add(listener); + } + + public void removeListener(BootstrapEventListener listener) { + listeners.remove(listener); + } + + @Override + public void circuitStatus(String status, String circID, String path) { + } + + @Override + public void streamStatus(String status, String streamID, String target) { + } + + @Override + public void orConnStatus(String status, String orName) { + } + + @Override + public void bandwidthUsed(long read, long written) { + } + + @Override + public void newDescriptors(List orList) { + } + + @Override + public void message(String severity, String msg) { + } + + @Override + public void hiddenServiceEvent(String action, String msg) { + } + + @Override + public void hiddenServiceFailedEvent(String reason, String msg) { + } + + @Override + public void hiddenServiceDescriptor(String descriptorId, String descriptor, String msg) { + } + + @Override + public void unrecognized(String type, String msg) { + if (BootstrapEvent.isBootstrapMessage(type, msg)) { + BootstrapEvent bootstrapEvent = BootstrapEvent.fromEventMessage(msg); + listeners.forEach(l -> l.onBootstrapStatusEvent(bootstrapEvent)); + } + } + + @Override + public void timeout() { + } +} diff --git a/network/tor/src/main/java/bisq/tor/bootstrap/BootstrapEventListener.java b/network/tor/src/main/java/bisq/tor/bootstrap/BootstrapEventListener.java new file mode 100644 index 0000000000..14c847504f --- /dev/null +++ b/network/tor/src/main/java/bisq/tor/bootstrap/BootstrapEventListener.java @@ -0,0 +1,22 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.tor.bootstrap; + +public interface BootstrapEventListener { + void onBootstrapStatusEvent(BootstrapEvent bootstrapEvent); +} diff --git a/network/tor/src/main/java/bisq/tor/bootstrap/TorBootstrapFailed.java b/network/tor/src/main/java/bisq/tor/bootstrap/TorBootstrapFailed.java new file mode 100644 index 0000000000..901c7b7004 --- /dev/null +++ b/network/tor/src/main/java/bisq/tor/bootstrap/TorBootstrapFailed.java @@ -0,0 +1,28 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.tor.bootstrap; + +public class TorBootstrapFailed extends RuntimeException { + public TorBootstrapFailed(String message) { + super(message); + } + + public TorBootstrapFailed(Throwable cause) { + super(cause); + } +} diff --git a/network/tor/src/main/java/bisq/tor/process/NativeTorController.java b/network/tor/src/main/java/bisq/tor/process/NativeTorController.java index a934eddcd4..28c24013df 100644 --- a/network/tor/src/main/java/bisq/tor/process/NativeTorController.java +++ b/network/tor/src/main/java/bisq/tor/process/NativeTorController.java @@ -18,15 +18,27 @@ package bisq.tor.process; import bisq.tor.ClientTorrcGenerator; +import bisq.tor.bootstrap.BootstrapEvent; +import bisq.tor.bootstrap.BootstrapEventHandler; +import bisq.tor.bootstrap.BootstrapEventListener; +import bisq.tor.bootstrap.TorBootstrapFailed; +import lombok.extern.slf4j.Slf4j; import net.freehaven.tor.control.PasswordDigest; import net.freehaven.tor.control.TorControlConnection; import java.io.IOException; import java.net.Socket; +import java.util.Collections; +import java.util.List; import java.util.Optional; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; -public class NativeTorController { +@Slf4j +public class NativeTorController implements BootstrapEventListener { + private final CountDownLatch isBootstrappedCountdownLatch = new CountDownLatch(1); + private final BootstrapEventHandler bootstrapEventHandler = new BootstrapEventHandler(); private Optional torControlConnection = Optional.empty(); public void connect(int controlPort, PasswordDigest controlConnectionSecret) throws IOException { @@ -45,11 +57,50 @@ public void bindTorToConnection() throws IOException { public void enableTorNetworking() throws IOException { TorControlConnection controlConnection = torControlConnection.orElseThrow(); + addBootstrapEventListener(controlConnection); controlConnection.setConf(ClientTorrcGenerator.DISABLE_NETWORK_CONFIG_KEY, "0"); } + public void waitUntilBootstrapped() { + try { + boolean isSuccess = isBootstrappedCountdownLatch.await(2, TimeUnit.MINUTES); + if (!isSuccess) { + throw new TorBootstrapFailed("Tor bootstrap timout (2 minutes) triggered."); + } + } catch (InterruptedException e) { + throw new TorBootstrapFailed(e); + } + } + public void shutdown() throws IOException { TorControlConnection controlConnection = torControlConnection.orElseThrow(); controlConnection.shutdownTor("SHUTDOWN"); } + + @Override + public void onBootstrapStatusEvent(BootstrapEvent bootstrapEvent) { + log.info("Tor bootstrap event: {}", bootstrapEvent); + if (bootstrapEvent.isDoneEvent()) { + isBootstrappedCountdownLatch.countDown(); + removeBootstrapEventListener(); + } + } + + private void addBootstrapEventListener(TorControlConnection controlConnection) throws IOException { + bootstrapEventHandler.addListener(this); + controlConnection.setEventHandler(bootstrapEventHandler); + controlConnection.setEvents(List.of("STATUS_CLIENT")); + } + + private void removeBootstrapEventListener() { + TorControlConnection controlConnection = torControlConnection.orElseThrow(); + bootstrapEventHandler.removeListener(this); + + controlConnection.setEventHandler(null); + try { + controlConnection.setEvents(Collections.emptyList()); + } catch (IOException e) { + throw new IllegalStateException("Can't set tor events."); + } + } }