From 1bd00a519c60c3e9f2c3903d78c1c82a226ac7f6 Mon Sep 17 00:00:00 2001 From: Alva Swanson Date: Sat, 1 Jun 2024 13:05:38 +0000 Subject: [PATCH 01/11] tor: Implement BootstrapEventParser --- .../tor/controller/BootstrapEventParser.java | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 network/tor/tor/src/main/java/bisq/tor/controller/BootstrapEventParser.java diff --git a/network/tor/tor/src/main/java/bisq/tor/controller/BootstrapEventParser.java b/network/tor/tor/src/main/java/bisq/tor/controller/BootstrapEventParser.java new file mode 100644 index 0000000000..d72cd673dd --- /dev/null +++ b/network/tor/tor/src/main/java/bisq/tor/controller/BootstrapEventParser.java @@ -0,0 +1,55 @@ +package bisq.tor.controller; + +import bisq.tor.controller.events.events.BootstrapEvent; + +import java.util.Optional; + +public class BootstrapEventParser { + public static Optional tryParse(String line) { + // 650 STATUS_CLIENT NOTICE BOOTSTRAP PROGRESS=50 TAG=loading_descriptors SUMMARY="Loading relay descriptors" + String[] parts = line.split(" "); + + if (isBootstrapEvent(parts)) { + BootstrapEvent bootstrapEvent = parseBootstrapEvent(parts); + return Optional.of(bootstrapEvent); + } + + return Optional.empty(); + } + + + private static boolean isBootstrapEvent(String[] parts) { + // 650 STATUS_CLIENT NOTICE BOOTSTRAP PROGRESS=50 TAG=loading_descriptors SUMMARY="Loading relay descriptors" + return parts.length >= 7 && parts[3].equals("BOOTSTRAP"); + } + + + private static BootstrapEvent parseBootstrapEvent(String[] parts) { + String progress = parts[4].replace("PROGRESS=", ""); + String tag = parts[5].replace("TAG=", ""); + String summary = parseBootstrapSummary(parts); + + int progressInt = Integer.parseInt(progress); + return new BootstrapEvent(progressInt, tag, summary); + } + + private static String parseBootstrapSummary(String[] parts) { + StringBuilder summary = new StringBuilder(); + + // SUMMARY="Loading relay descriptors" has whitespaces in string + for (int i = 6; i < parts.length; i++) { + String summaryPart = parts[i]; + summary.append(summaryPart) + .append(" "); + } + + String summaryPrefix = "SUMMARY=\""; + summary.delete(0, summaryPrefix.length()); + + // ends with `" ` + int length = summary.length(); + summary.delete(length - 2, length); + + return summary.toString(); + } +} From f86a6cac6512be0bc024a4fad65c7b5def4db0b0 Mon Sep 17 00:00:00 2001 From: Alva Swanson Date: Sat, 1 Jun 2024 13:06:05 +0000 Subject: [PATCH 02/11] Implement WhonixTorControlReader --- .../controller/WhonixTorControlReader.java | 74 +++++++++++++++++++ .../tor/controller/WhonixTorController.java | 15 ++-- 2 files changed, 82 insertions(+), 7 deletions(-) create mode 100644 network/tor/tor/src/main/java/bisq/tor/controller/WhonixTorControlReader.java diff --git a/network/tor/tor/src/main/java/bisq/tor/controller/WhonixTorControlReader.java b/network/tor/tor/src/main/java/bisq/tor/controller/WhonixTorControlReader.java new file mode 100644 index 0000000000..02bb71b830 --- /dev/null +++ b/network/tor/tor/src/main/java/bisq/tor/controller/WhonixTorControlReader.java @@ -0,0 +1,74 @@ +package bisq.tor.controller; + +import bisq.tor.controller.events.events.BootstrapEvent; +import lombok.extern.slf4j.Slf4j; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.util.Optional; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; + +@Slf4j +public class WhonixTorControlReader implements AutoCloseable { + private final BufferedReader bufferedReader; + private final BlockingQueue replies = new LinkedBlockingQueue<>(); + private Optional workerThread = Optional.empty(); + + public WhonixTorControlReader(InputStream inputStream) { + bufferedReader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.US_ASCII)); + } + + public void start() { + Thread thread = new Thread(() -> { + try { + String line; + while ((line = bufferedReader.readLine()) != null) { + + if (isStatusClientEvent(line)) { + Optional bootstrapEvent = BootstrapEventParser.tryParse(line); + + if (bootstrapEvent.isPresent()) { + log.info("{}", bootstrapEvent.get()); + } else { + log.info("Unknown status client event: {}", line); + } + + } else { + replies.add(line); + } + + if (Thread.interrupted()) { + break; + } + } + } catch (IOException e) { + log.error("Tor control port reader couldn't read reply.", e); + } + + }); + workerThread = Optional.of(thread); + thread.start(); + } + + @Override + public void close() throws Exception { + workerThread.ifPresent(Thread::interrupt); + } + + public String readLine() { + try { + return replies.take(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + + private boolean isStatusClientEvent(String line) { + // 650 STATUS_CLIENT NOTICE CIRCUIT_ESTABLISHED + return line.startsWith("650 STATUS_CLIENT"); + } +} diff --git a/network/tor/tor/src/main/java/bisq/tor/controller/WhonixTorController.java b/network/tor/tor/src/main/java/bisq/tor/controller/WhonixTorController.java index 821a915c47..8c96361f87 100644 --- a/network/tor/tor/src/main/java/bisq/tor/controller/WhonixTorController.java +++ b/network/tor/tor/src/main/java/bisq/tor/controller/WhonixTorController.java @@ -11,15 +11,12 @@ public class WhonixTorController implements AutoCloseable { private final Socket controlSocket; - private final BufferedReader bufferedReader; + private final WhonixTorControlReader whonixTorControlReader; private final OutputStream outputStream; public WhonixTorController() throws IOException { controlSocket = new Socket("127.0.0.1", 9051); - - InputStream inputStream = controlSocket.getInputStream(); - bufferedReader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.US_ASCII)); - + whonixTorControlReader = new WhonixTorControlReader(controlSocket.getInputStream()); outputStream = controlSocket.getOutputStream(); } @@ -28,6 +25,10 @@ public void close() throws IOException { controlSocket.close(); } + public void initialize() { + whonixTorControlReader.start(); + } + public void authenticate(PasswordDigest passwordDigest) throws IOException { byte[] secret = passwordDigest.getSecret(); String secretHex = Hex.encode(secret); @@ -99,8 +100,8 @@ private void sendCommand(String command) throws IOException { outputStream.flush(); } - private String receiveReply() throws IOException { - String reply = bufferedReader.readLine(); + private String receiveReply() { + String reply = whonixTorControlReader.readLine(); if (reply.equals("510 Command filtered")) { throw new TorCommandFilteredException(); } From 3d2f632bcaeb7af0f56a923cf4ae59a514aef067 Mon Sep 17 00:00:00 2001 From: Alva Swanson Date: Sat, 1 Jun 2024 13:28:46 +0000 Subject: [PATCH 03/11] WhonixTorController: Add BootstrapEventListener support --- .../controller/WhonixTorControlReader.java | 20 ++++++++++++++++--- .../tor/controller/WhonixTorController.java | 9 +++++++++ 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/network/tor/tor/src/main/java/bisq/tor/controller/WhonixTorControlReader.java b/network/tor/tor/src/main/java/bisq/tor/controller/WhonixTorControlReader.java index 02bb71b830..99bc1aef56 100644 --- a/network/tor/tor/src/main/java/bisq/tor/controller/WhonixTorControlReader.java +++ b/network/tor/tor/src/main/java/bisq/tor/controller/WhonixTorControlReader.java @@ -1,6 +1,7 @@ package bisq.tor.controller; import bisq.tor.controller.events.events.BootstrapEvent; +import bisq.tor.controller.events.listener.BootstrapEventListener; import lombok.extern.slf4j.Slf4j; import java.io.BufferedReader; @@ -8,14 +9,18 @@ import java.io.InputStream; import java.io.InputStreamReader; import java.nio.charset.StandardCharsets; +import java.util.List; import java.util.Optional; import java.util.concurrent.BlockingQueue; +import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.LinkedBlockingQueue; @Slf4j public class WhonixTorControlReader implements AutoCloseable { private final BufferedReader bufferedReader; private final BlockingQueue replies = new LinkedBlockingQueue<>(); + private final List bootstrapEventListeners = new CopyOnWriteArrayList<>(); + private Optional workerThread = Optional.empty(); public WhonixTorControlReader(InputStream inputStream) { @@ -29,10 +34,11 @@ public void start() { while ((line = bufferedReader.readLine()) != null) { if (isStatusClientEvent(line)) { - Optional bootstrapEvent = BootstrapEventParser.tryParse(line); + Optional bootstrapEventOptional = BootstrapEventParser.tryParse(line); - if (bootstrapEvent.isPresent()) { - log.info("{}", bootstrapEvent.get()); + if (bootstrapEventOptional.isPresent()) { + BootstrapEvent bootstrapEvent = bootstrapEventOptional.get(); + bootstrapEventListeners.forEach(listener -> listener.onBootstrapStatusEvent(bootstrapEvent)); } else { log.info("Unknown status client event: {}", line); } @@ -67,6 +73,14 @@ public String readLine() { } } + public void addBootstrapEventListener(BootstrapEventListener listener) { + bootstrapEventListeners.add(listener); + } + + public void removeBootstrapEventListener(BootstrapEventListener listener) { + bootstrapEventListeners.remove(listener); + } + private boolean isStatusClientEvent(String line) { // 650 STATUS_CLIENT NOTICE CIRCUIT_ESTABLISHED return line.startsWith("650 STATUS_CLIENT"); diff --git a/network/tor/tor/src/main/java/bisq/tor/controller/WhonixTorController.java b/network/tor/tor/src/main/java/bisq/tor/controller/WhonixTorController.java index 8c96361f87..f83a845ea5 100644 --- a/network/tor/tor/src/main/java/bisq/tor/controller/WhonixTorController.java +++ b/network/tor/tor/src/main/java/bisq/tor/controller/WhonixTorController.java @@ -2,6 +2,7 @@ import bisq.common.encoding.Hex; import bisq.security.keys.TorKeyPair; +import bisq.tor.controller.events.listener.BootstrapEventListener; import net.freehaven.tor.control.PasswordDigest; import java.io.*; @@ -94,6 +95,14 @@ public void takeOwnership() throws IOException { } } + public void addBootstrapEventListener(BootstrapEventListener listener) { + whonixTorControlReader.addBootstrapEventListener(listener); + } + + public void removeBootstrapEventListener(BootstrapEventListener listener) { + whonixTorControlReader.removeBootstrapEventListener(listener); + } + private void sendCommand(String command) throws IOException { byte[] commandBytes = command.getBytes(StandardCharsets.US_ASCII); outputStream.write(commandBytes); From 74eb82281665c7aeb06eff50a1700b9870f45819 Mon Sep 17 00:00:00 2001 From: Alva Swanson Date: Sat, 1 Jun 2024 13:31:39 +0000 Subject: [PATCH 04/11] Pass control port number to WhonixTorController --- .../main/java/bisq/tor/controller/WhonixTorController.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/network/tor/tor/src/main/java/bisq/tor/controller/WhonixTorController.java b/network/tor/tor/src/main/java/bisq/tor/controller/WhonixTorController.java index f83a845ea5..bd39262bf5 100644 --- a/network/tor/tor/src/main/java/bisq/tor/controller/WhonixTorController.java +++ b/network/tor/tor/src/main/java/bisq/tor/controller/WhonixTorController.java @@ -15,8 +15,8 @@ public class WhonixTorController implements AutoCloseable { private final WhonixTorControlReader whonixTorControlReader; private final OutputStream outputStream; - public WhonixTorController() throws IOException { - controlSocket = new Socket("127.0.0.1", 9051); + public WhonixTorController(int port) throws IOException { + controlSocket = new Socket("127.0.0.1", port); whonixTorControlReader = new WhonixTorControlReader(controlSocket.getInputStream()); outputStream = controlSocket.getOutputStream(); } From 1ad30b30caa900325d754de967220d1425236e98 Mon Sep 17 00:00:00 2001 From: Alva Swanson Date: Sat, 1 Jun 2024 13:32:52 +0000 Subject: [PATCH 05/11] Rename WhonixTorController to TorControlProtocol --- .../{WhonixTorController.java => TorControlProtocol.java} | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename network/tor/tor/src/main/java/bisq/tor/controller/{WhonixTorController.java => TorControlProtocol.java} (97%) diff --git a/network/tor/tor/src/main/java/bisq/tor/controller/WhonixTorController.java b/network/tor/tor/src/main/java/bisq/tor/controller/TorControlProtocol.java similarity index 97% rename from network/tor/tor/src/main/java/bisq/tor/controller/WhonixTorController.java rename to network/tor/tor/src/main/java/bisq/tor/controller/TorControlProtocol.java index bd39262bf5..44c54d044a 100644 --- a/network/tor/tor/src/main/java/bisq/tor/controller/WhonixTorController.java +++ b/network/tor/tor/src/main/java/bisq/tor/controller/TorControlProtocol.java @@ -10,12 +10,12 @@ import java.nio.charset.StandardCharsets; import java.util.List; -public class WhonixTorController implements AutoCloseable { +public class TorControlProtocol implements AutoCloseable { private final Socket controlSocket; private final WhonixTorControlReader whonixTorControlReader; private final OutputStream outputStream; - public WhonixTorController(int port) throws IOException { + public TorControlProtocol(int port) throws IOException { controlSocket = new Socket("127.0.0.1", port); whonixTorControlReader = new WhonixTorControlReader(controlSocket.getInputStream()); outputStream = controlSocket.getOutputStream(); From 61e5ee5e9704411df3bb11443ba977824bd746c5 Mon Sep 17 00:00:00 2001 From: Alva Swanson Date: Sat, 1 Jun 2024 14:16:12 +0000 Subject: [PATCH 06/11] Implement TorController --- .../bisq/tor/controller/TorController.java | 121 ++++++++++++++++++ 1 file changed, 121 insertions(+) create mode 100644 network/tor/tor/src/main/java/bisq/tor/controller/TorController.java diff --git a/network/tor/tor/src/main/java/bisq/tor/controller/TorController.java b/network/tor/tor/src/main/java/bisq/tor/controller/TorController.java new file mode 100644 index 0000000000..04867ec9af --- /dev/null +++ b/network/tor/tor/src/main/java/bisq/tor/controller/TorController.java @@ -0,0 +1,121 @@ +package bisq.tor.controller; + +import bisq.common.observable.Observable; +import bisq.tor.TorrcClientConfigFactory; +import bisq.tor.controller.events.events.BootstrapEvent; +import bisq.tor.controller.events.listener.BootstrapEventListener; +import bisq.tor.controller.exceptions.TorBootstrapFailedException; +import bisq.tor.process.NativeTorProcess; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import net.freehaven.tor.control.PasswordDigest; + +import java.io.IOException; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +@Slf4j +public class TorController implements BootstrapEventListener { + private final int bootstrapTimeout; + private final CountDownLatch isBootstrappedCountdownLatch = new CountDownLatch(1); + @Getter + private final Observable bootstrapEvent = new Observable<>(); + + private Optional torControlProtocol = Optional.empty(); + + public TorController(int bootstrapTimeout) { + this.bootstrapTimeout = bootstrapTimeout; + } + + public void initialize(int controlPort, PasswordDigest hashedControlPassword) throws IOException { + var torControlProtocol = new TorControlProtocol(controlPort); + this.torControlProtocol = Optional.of(torControlProtocol); + + torControlProtocol.initialize(); + torControlProtocol.authenticate(hashedControlPassword); + } + + public void shutdown() { + torControlProtocol.ifPresent(torControlProtocol -> { + try { + torControlProtocol.close(); + } catch (IOException e) { + throw new RuntimeException(e); + } + }); + } + + public void bootstrapTor() throws IOException { + bindToBisq(); + subscribeToBootstrapEvents(); + enableNetworking(); + waitUntilBootstrapped(); + } + + @Override + public void onBootstrapStatusEvent(BootstrapEvent bootstrapEvent) { + log.info("Tor bootstrap event: {}", bootstrapEvent); + this.bootstrapEvent.set(bootstrapEvent); + if (bootstrapEvent.isDoneEvent()) { + isBootstrappedCountdownLatch.countDown(); + } + } + + private void bindToBisq() throws IOException { + TorControlProtocol torControlProtocol = getTorControlProtocol(); + torControlProtocol.takeOwnership(); + torControlProtocol.resetConf(NativeTorProcess.ARG_OWNER_PID); + } + + private void subscribeToBootstrapEvents() throws IOException { + TorControlProtocol torControlProtocol = getTorControlProtocol(); + torControlProtocol.addBootstrapEventListener(this); + torControlProtocol.setEvents(List.of("STATUS_CLIENT")); + } + + private void enableNetworking() throws IOException { + TorControlProtocol torControlProtocol = getTorControlProtocol(); + torControlProtocol.setConfig(TorrcClientConfigFactory.DISABLE_NETWORK_CONFIG_KEY, "0"); + } + + private void waitUntilBootstrapped() { + try { + while (true) { + if (torControlProtocol.isEmpty()) { + throw new TorBootstrapFailedException("Tor is not initializing."); + } + + boolean isSuccess = isBootstrappedCountdownLatch.await(bootstrapTimeout, TimeUnit.MILLISECONDS); + + if (isSuccess) { + TorControlProtocol torControlProtocol = this.torControlProtocol.get(); + torControlProtocol.removeBootstrapEventListener(this); + torControlProtocol.setEvents(Collections.emptyList()); + break; + } else if (isBootstrapTimeoutTriggered()) { + throw new TorBootstrapFailedException("Tor bootstrap timeout triggered."); + } + } + } catch (InterruptedException e) { + throw new TorBootstrapFailedException(e); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private boolean isBootstrapTimeoutTriggered() { + BootstrapEvent bootstrapEvent = this.bootstrapEvent.get(); + Instant timestamp = bootstrapEvent.getTimestamp(); + Instant bootstrapTimeoutAgo = Instant.now().minus(bootstrapTimeout, ChronoUnit.MILLIS); + return bootstrapTimeoutAgo.isAfter(timestamp); + } + + private TorControlProtocol getTorControlProtocol() { + return this.torControlProtocol.orElseThrow(); + } +} From a5c858fdc5846da0fedd86752b3ce9b7dd10f046 Mon Sep 17 00:00:00 2001 From: Alva Swanson Date: Sat, 1 Jun 2024 14:20:51 +0000 Subject: [PATCH 07/11] WhonixTorControlReader: Handle all events --- .../bisq/tor/controller/BootstrapEventParser.java | 14 ++++++++++---- .../tor/controller/WhonixTorControlReader.java | 9 ++++----- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/network/tor/tor/src/main/java/bisq/tor/controller/BootstrapEventParser.java b/network/tor/tor/src/main/java/bisq/tor/controller/BootstrapEventParser.java index d72cd673dd..10fa81402c 100644 --- a/network/tor/tor/src/main/java/bisq/tor/controller/BootstrapEventParser.java +++ b/network/tor/tor/src/main/java/bisq/tor/controller/BootstrapEventParser.java @@ -7,16 +7,22 @@ public class BootstrapEventParser { public static Optional tryParse(String line) { // 650 STATUS_CLIENT NOTICE BOOTSTRAP PROGRESS=50 TAG=loading_descriptors SUMMARY="Loading relay descriptors" - String[] parts = line.split(" "); + if (isStatusClientEvent(line)) { + String[] parts = line.split(" "); - if (isBootstrapEvent(parts)) { - BootstrapEvent bootstrapEvent = parseBootstrapEvent(parts); - return Optional.of(bootstrapEvent); + if (isBootstrapEvent(parts)) { + BootstrapEvent bootstrapEvent = parseBootstrapEvent(parts); + return Optional.of(bootstrapEvent); + } } return Optional.empty(); } + private static boolean isStatusClientEvent(String line) { + // 650 STATUS_CLIENT NOTICE CIRCUIT_ESTABLISHED + return line.startsWith("650 STATUS_CLIENT"); + } private static boolean isBootstrapEvent(String[] parts) { // 650 STATUS_CLIENT NOTICE BOOTSTRAP PROGRESS=50 TAG=loading_descriptors SUMMARY="Loading relay descriptors" diff --git a/network/tor/tor/src/main/java/bisq/tor/controller/WhonixTorControlReader.java b/network/tor/tor/src/main/java/bisq/tor/controller/WhonixTorControlReader.java index 99bc1aef56..76da07a56a 100644 --- a/network/tor/tor/src/main/java/bisq/tor/controller/WhonixTorControlReader.java +++ b/network/tor/tor/src/main/java/bisq/tor/controller/WhonixTorControlReader.java @@ -33,14 +33,13 @@ public void start() { String line; while ((line = bufferedReader.readLine()) != null) { - if (isStatusClientEvent(line)) { + if (isEvent(line)) { Optional bootstrapEventOptional = BootstrapEventParser.tryParse(line); - if (bootstrapEventOptional.isPresent()) { BootstrapEvent bootstrapEvent = bootstrapEventOptional.get(); bootstrapEventListeners.forEach(listener -> listener.onBootstrapStatusEvent(bootstrapEvent)); } else { - log.info("Unknown status client event: {}", line); + log.info("Unknown Tor event: {}", line); } } else { @@ -81,8 +80,8 @@ public void removeBootstrapEventListener(BootstrapEventListener listener) { bootstrapEventListeners.remove(listener); } - private boolean isStatusClientEvent(String line) { + private boolean isEvent(String line) { // 650 STATUS_CLIENT NOTICE CIRCUIT_ESTABLISHED - return line.startsWith("650 STATUS_CLIENT"); + return line.startsWith("650"); } } From 437e8979c3b22f0ed11f3d29329e00a7df062c16 Mon Sep 17 00:00:00 2001 From: Alva Swanson Date: Sun, 2 Jun 2024 14:19:33 +0000 Subject: [PATCH 08/11] TorControlProtocol: Implement GETINFO command --- .../java/bisq/tor/controller/TorControlProtocol.java | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/network/tor/tor/src/main/java/bisq/tor/controller/TorControlProtocol.java b/network/tor/tor/src/main/java/bisq/tor/controller/TorControlProtocol.java index 44c54d044a..dea50a7154 100644 --- a/network/tor/tor/src/main/java/bisq/tor/controller/TorControlProtocol.java +++ b/network/tor/tor/src/main/java/bisq/tor/controller/TorControlProtocol.java @@ -55,6 +55,16 @@ public void addOnion(TorKeyPair torKeyPair, int onionPort, int localPort) throws String reply = receiveReply(); } + public String getInfo(String keyword) throws IOException { + String command = "GETINFO " + keyword + "\r\n"; + sendCommand(command); + String reply = receiveReply(); + if (!reply.startsWith("250-")) { + throw new ControlCommandFailedException("Couldn't get info: " + keyword); + } + return reply; + } + public void resetConf(String configName) throws IOException { String command = "RESETCONF " + configName + "\r\n"; sendCommand(command); From 78919096825b7ffc4e986c75ca5c073366e69b03 Mon Sep 17 00:00:00 2001 From: Alva Swanson Date: Sun, 2 Jun 2024 14:20:56 +0000 Subject: [PATCH 09/11] TorController: Implement getSocksPort --- .../bisq/tor/controller/TorController.java | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/network/tor/tor/src/main/java/bisq/tor/controller/TorController.java b/network/tor/tor/src/main/java/bisq/tor/controller/TorController.java index 04867ec9af..c7e4ff9af3 100644 --- a/network/tor/tor/src/main/java/bisq/tor/controller/TorController.java +++ b/network/tor/tor/src/main/java/bisq/tor/controller/TorController.java @@ -57,6 +57,30 @@ public void bootstrapTor() throws IOException { waitUntilBootstrapped(); } + public Optional getSocksPort() { + try { + TorControlProtocol torControlProtocol = getTorControlProtocol(); + String socksListenersString = torControlProtocol.getInfo("net/listeners/socks"); + + String socksListener; + if (socksListenersString.contains(" ")) { + String[] socksPorts = socksListenersString.split(" "); + socksListener = socksPorts[0]; + } else { + socksListener = socksListenersString; + } + + // "127.0.0.1:12345" + socksListener = socksListener.replace("\"", ""); + String portString = socksListener.split(":")[1]; + + int port = Integer.parseInt(portString); + return Optional.of(port); + } catch (IOException e) { + return Optional.empty(); + } + } + @Override public void onBootstrapStatusEvent(BootstrapEvent bootstrapEvent) { log.info("Tor bootstrap event: {}", bootstrapEvent); From 0a0c02e4c9a62a4d3bc907eb5a11d612c2daa85f Mon Sep 17 00:00:00 2001 From: Alva Swanson Date: Mon, 3 Jun 2024 13:53:29 +0000 Subject: [PATCH 10/11] TorController: Support non-authenticated connections --- .../bisq/tor/controller/TorController.java | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/network/tor/tor/src/main/java/bisq/tor/controller/TorController.java b/network/tor/tor/src/main/java/bisq/tor/controller/TorController.java index c7e4ff9af3..dc53bfca0c 100644 --- a/network/tor/tor/src/main/java/bisq/tor/controller/TorController.java +++ b/network/tor/tor/src/main/java/bisq/tor/controller/TorController.java @@ -32,12 +32,12 @@ public TorController(int bootstrapTimeout) { this.bootstrapTimeout = bootstrapTimeout; } - public void initialize(int controlPort, PasswordDigest hashedControlPassword) throws IOException { - var torControlProtocol = new TorControlProtocol(controlPort); - this.torControlProtocol = Optional.of(torControlProtocol); + public void initialize(int controlPort) throws IOException { + initialize(controlPort, Optional.empty()); + } - torControlProtocol.initialize(); - torControlProtocol.authenticate(hashedControlPassword); + public void initialize(int controlPort, PasswordDigest hashedControlPassword) throws IOException { + initialize(controlPort, Optional.of(hashedControlPassword)); } public void shutdown() { @@ -90,6 +90,16 @@ public void onBootstrapStatusEvent(BootstrapEvent bootstrapEvent) { } } + private void initialize(int controlPort, Optional hashedControlPassword) throws IOException { + var torControlProtocol = new TorControlProtocol(controlPort); + this.torControlProtocol = Optional.of(torControlProtocol); + + torControlProtocol.initialize(); + if (hashedControlPassword.isPresent()) { + torControlProtocol.authenticate(hashedControlPassword.get()); + } + } + private void bindToBisq() throws IOException { TorControlProtocol torControlProtocol = getTorControlProtocol(); torControlProtocol.takeOwnership(); From a25d7eec4377713fa8e72e7fd321b32072b88278 Mon Sep 17 00:00:00 2001 From: Alva Swanson Date: Mon, 3 Jun 2024 14:45:56 +0000 Subject: [PATCH 11/11] TorControlProtocol: Support multiline Tor replies --- .../tor/controller/TorControlProtocol.java | 69 +++++++++++++++---- 1 file changed, 56 insertions(+), 13 deletions(-) diff --git a/network/tor/tor/src/main/java/bisq/tor/controller/TorControlProtocol.java b/network/tor/tor/src/main/java/bisq/tor/controller/TorControlProtocol.java index dea50a7154..a177b2ea99 100644 --- a/network/tor/tor/src/main/java/bisq/tor/controller/TorControlProtocol.java +++ b/network/tor/tor/src/main/java/bisq/tor/controller/TorControlProtocol.java @@ -5,16 +5,25 @@ import bisq.tor.controller.events.listener.BootstrapEventListener; import net.freehaven.tor.control.PasswordDigest; -import java.io.*; +import java.io.IOException; +import java.io.OutputStream; import java.net.Socket; import java.nio.charset.StandardCharsets; import java.util.List; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.Stream; public class TorControlProtocol implements AutoCloseable { private final Socket controlSocket; private final WhonixTorControlReader whonixTorControlReader; private final OutputStream outputStream; + // MidReplyLine = StatusCode "-" ReplyLine + private final Pattern midReplyLinePattern = Pattern.compile("^\\d+-.+"); + // DataReplyLine = StatusCode "+" ReplyLine CmdData + private final Pattern dataReplyLinePattern = Pattern.compile("^\\d+\\+.+"); + public TorControlProtocol(int port) throws IOException { controlSocket = new Socket("127.0.0.1", port); whonixTorControlReader = new WhonixTorControlReader(controlSocket.getInputStream()); @@ -36,7 +45,7 @@ public void authenticate(PasswordDigest passwordDigest) throws IOException { String command = "AUTHENTICATE " + secretHex + "\r\n"; sendCommand(command); - String reply = receiveReply(); + String reply = receiveReply().findFirst().orElseThrow(); if (reply.equals("250 OK")) { return; @@ -52,23 +61,21 @@ public void addOnion(TorKeyPair torKeyPair, int onionPort, int localPort) throws String command = "ADD_ONION " + "ED25519-V3:" + base64SecretScalar + " Port=" + onionPort + "," + localPort + "\r\n"; sendCommand(command); - String reply = receiveReply(); + Stream replyStream = receiveReply(); + assertTwoLineOkReply(replyStream, "ADD_ONION"); } public String getInfo(String keyword) throws IOException { String command = "GETINFO " + keyword + "\r\n"; sendCommand(command); - String reply = receiveReply(); - if (!reply.startsWith("250-")) { - throw new ControlCommandFailedException("Couldn't get info: " + keyword); - } - return reply; + Stream replyStream = receiveReply(); + return assertTwoLineOkReply(replyStream, "GETINFO"); } public void resetConf(String configName) throws IOException { String command = "RESETCONF " + configName + "\r\n"; sendCommand(command); - String reply = receiveReply(); + String reply = receiveReply().findFirst().orElseThrow(); if (!reply.equals("250 OK")) { throw new ControlCommandFailedException("Couldn't reset config: " + configName); } @@ -77,7 +84,7 @@ public void resetConf(String configName) throws IOException { public void setConfig(String configName, String configValue) throws IOException { String command = "SETCONF " + configName + "=" + configValue + "\r\n"; sendCommand(command); - String reply = receiveReply(); + String reply = receiveReply().findFirst().orElseThrow(); if (!reply.equals("250 OK")) { throw new ControlCommandFailedException("Couldn't set config: " + configName + "=" + configValue); } @@ -90,7 +97,7 @@ public void setEvents(List events) throws IOException { String command = stringBuilder.toString(); sendCommand(command); - String reply = receiveReply(); + String reply = receiveReply().findFirst().orElseThrow(); if (!reply.equals("250 OK")) { throw new ControlCommandFailedException("Couldn't set events: " + events); } @@ -99,7 +106,7 @@ public void setEvents(List events) throws IOException { public void takeOwnership() throws IOException { String command = "TAKEOWNERSHIP\r\n"; sendCommand(command); - String reply = receiveReply(); + String reply = receiveReply().findFirst().orElseThrow(); if (!reply.equals("250 OK")) { throw new ControlCommandFailedException("Couldn't take ownership"); } @@ -119,11 +126,47 @@ private void sendCommand(String command) throws IOException { outputStream.flush(); } - private String receiveReply() { + private Stream receiveReply() { + String reply = tryReadNextReply(); + var streamBuilder = Stream.builder(); + streamBuilder.add(reply); + + while (isMultilineReply(reply)) { + reply = tryReadNextReply(); + streamBuilder.add(reply); + } + + return streamBuilder.build(); + } + + private String tryReadNextReply() { String reply = whonixTorControlReader.readLine(); if (reply.equals("510 Command filtered")) { throw new TorCommandFilteredException(); } return reply; } + + private boolean isMultilineReply(String reply) { + return midReplyLinePattern.matcher(reply).matches() || dataReplyLinePattern.matcher(reply).matches(); + } + + private String assertTwoLineOkReply(Stream replyStream, String commandName) { + List replies = replyStream.collect(Collectors.toList()); + if (replies.size() != 2) { + throw new ControlCommandFailedException("Invalid " + commandName + " reply: " + replies); + } + + String firstLine = replies.get(0); + if (!firstLine.startsWith("250-")) { + throw new ControlCommandFailedException("Invalid " + commandName + " reply: " + replies); + } + + String secondLine = replies.get(1); + if (!secondLine.equals("250 OK")) { + throw new ControlCommandFailedException("Invalid " + commandName + " reply: " + replies); + } + + return firstLine; + } }