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 10fa81402c..3f5df74936 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 @@ -5,25 +5,16 @@ import java.util.Optional; public class BootstrapEventParser { - public static Optional tryParse(String line) { + public static Optional tryParse(String[] parts) { // 650 STATUS_CLIENT NOTICE BOOTSTRAP PROGRESS=50 TAG=loading_descriptors SUMMARY="Loading relay descriptors" - 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" return parts.length >= 7 && parts[3].equals("BOOTSTRAP"); diff --git a/network/tor/tor/src/main/java/bisq/tor/controller/HsDescEventParser.java b/network/tor/tor/src/main/java/bisq/tor/controller/HsDescEventParser.java new file mode 100644 index 0000000000..4f962bf96b --- /dev/null +++ b/network/tor/tor/src/main/java/bisq/tor/controller/HsDescEventParser.java @@ -0,0 +1,69 @@ +package bisq.tor.controller; + +import bisq.tor.controller.events.events.*; + +import java.util.Optional; + +public class HsDescEventParser { + public static Optional tryParse(String[] parts) { + if (HsDescEvent.Action.CREATED.isAction(parts)) { + // 650 HS_DESC CREATED UNKNOWN UNKNOWN + HsDescCreatedOrReceivedEvent hsDescEvent = HsDescCreatedOrReceivedEvent.builder() + .action(HsDescEvent.Action.CREATED) + .hsAddress(parts[3]) + .authType(parts[4]) + .hsDir(parts[5]) + .descriptorId(parts[6]) + .build(); + return Optional.of(hsDescEvent); + + } else if (HsDescEvent.Action.UPLOAD.isAction(parts)) { + // 650 HS_DESC UPLOAD UNKNOWN HSDIR_INDEX= + HsDescUploadEvent hsDescEvent = HsDescUploadEvent.builder() + .action(HsDescEvent.Action.UPLOAD) + .hsAddress(parts[3]) + .authType(parts[4]) + .hsDir(parts[5]) + .descriptorId(parts[6]) + .hsDirIndex(parts[7]) + .build(); + return Optional.of(hsDescEvent); + + } else if (HsDescEvent.Action.UPLOADED.isAction(parts)) { + // 650 HS_DESC UPLOADED UNKNOWN + HsDescUploadedEventV2 hsDescEvent = HsDescUploadedEventV2.builder() + .action(HsDescEvent.Action.UPLOADED) + .hsAddress(parts[3]) + .authType(parts[4]) + .hsDir(parts[5]) + .build(); + return Optional.of(hsDescEvent); + + } else if (HsDescEvent.Action.RECEIVED.isAction(parts)) { + // 650 HS_DESC RECEIVED + HsDescCreatedOrReceivedEvent hsDescEvent = HsDescCreatedOrReceivedEvent.builder() + .action(HsDescEvent.Action.RECEIVED) + .hsAddress(parts[3]) + .authType(parts[4]) + .hsDir(parts[5]) + .descriptorId(parts[6]) + .build(); + return Optional.of(hsDescEvent); + + } else if (HsDescEvent.Action.FAILED.isAction(parts)) { + // 650 HS_DESC FAILED REASON=NOT_FOUND + HsDescCreatedOrReceivedEvent hsDescEvent = HsDescFailedEvent.builder() + .action(HsDescEvent.Action.FAILED) + .hsAddress(parts[3]) + .authType(parts[4]) + .hsDir(parts[5]) + .descriptorId(parts[6]) + .reason(parts[7]) + .build(); + return Optional.of(hsDescEvent); + + } else { + return Optional.empty(); + } + } +} 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 a177b2ea99..5b33d0011c 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 @@ -3,6 +3,7 @@ import bisq.common.encoding.Hex; import bisq.security.keys.TorKeyPair; import bisq.tor.controller.events.listener.BootstrapEventListener; +import bisq.tor.controller.events.listener.HsDescEventListener; import net.freehaven.tor.control.PasswordDigest; import java.io.IOException; @@ -20,9 +21,8 @@ public class TorControlProtocol implements AutoCloseable { 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+\\+.+"); + private final Pattern multiLineReplyPattern = Pattern.compile("^\\d+[-+].+"); public TorControlProtocol(int port) throws IOException { controlSocket = new Socket("127.0.0.1", port); @@ -72,6 +72,15 @@ public String getInfo(String keyword) throws IOException { return assertTwoLineOkReply(replyStream, "GETINFO"); } + public void hsFetch(String hsAddress) throws IOException { + String command = "HSFETCH " + hsAddress + "\r\n"; + sendCommand(command); + String reply = receiveReply().findFirst().orElseThrow(); + if (!reply.equals("250 OK")) { + throw new ControlCommandFailedException("Couldn't initiate HSFETCH for : " + hsAddress); + } + } + public void resetConf(String configName) throws IOException { String command = "RESETCONF " + configName + "\r\n"; sendCommand(command); @@ -120,6 +129,14 @@ public void removeBootstrapEventListener(BootstrapEventListener listener) { whonixTorControlReader.removeBootstrapEventListener(listener); } + public void addHsDescEventListener(HsDescEventListener listener) { + whonixTorControlReader.addHsDescEventListener(listener); + } + + public void removeHsDescEventListener(HsDescEventListener listener) { + whonixTorControlReader.removeHsDescEventListener(listener); + } + private void sendCommand(String command) throws IOException { byte[] commandBytes = command.getBytes(StandardCharsets.US_ASCII); outputStream.write(commandBytes); @@ -148,7 +165,7 @@ private String tryReadNextReply() { } private boolean isMultilineReply(String reply) { - return midReplyLinePattern.matcher(reply).matches() || dataReplyLinePattern.matcher(reply).matches(); + return multiLineReplyPattern.matcher(reply).matches(); } private String assertTwoLineOkReply(Stream replyStream, String commandName) { 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 dc53bfca0c..9c6612fd10 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 @@ -1,9 +1,14 @@ package bisq.tor.controller; import bisq.common.observable.Observable; +import bisq.security.keys.TorKeyPair; import bisq.tor.TorrcClientConfigFactory; import bisq.tor.controller.events.events.BootstrapEvent; +import bisq.tor.controller.events.events.HsDescEvent; +import bisq.tor.controller.events.events.HsDescFailedEvent; import bisq.tor.controller.events.listener.BootstrapEventListener; +import bisq.tor.controller.events.listener.HsDescEventListener; +import bisq.tor.controller.exceptions.HsDescUploadFailedException; import bisq.tor.controller.exceptions.TorBootstrapFailedException; import bisq.tor.process.NativeTorProcess; import lombok.Getter; @@ -15,21 +20,27 @@ import java.time.temporal.ChronoUnit; import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.Optional; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; +import java.util.concurrent.*; @Slf4j -public class TorController implements BootstrapEventListener { - private final int bootstrapTimeout; +public class TorController implements BootstrapEventListener, HsDescEventListener { + private final int bootstrapTimeout; // in ms + private final int hsUploadTimeout; // in ms private final CountDownLatch isBootstrappedCountdownLatch = new CountDownLatch(1); @Getter private final Observable bootstrapEvent = new Observable<>(); + private final Map pendingOnionServicePublishLatchMap = new ConcurrentHashMap<>(); + private final Map> pendingIsOnionServiceOnlineLookupFutureMap = + new ConcurrentHashMap<>(); + private Optional torControlProtocol = Optional.empty(); - public TorController(int bootstrapTimeout) { + public TorController(int bootstrapTimeout, int hsUploadTimeout) { this.bootstrapTimeout = bootstrapTimeout; + this.hsUploadTimeout = hsUploadTimeout; } public void initialize(int controlPort) throws IOException { @@ -57,6 +68,45 @@ public void bootstrapTor() throws IOException { waitUntilBootstrapped(); } + public CompletableFuture isOnionServiceOnline(String onionAddress) throws IOException, ExecutionException, InterruptedException, TimeoutException { + var onionServiceLookupCompletableFuture = new CompletableFuture(); + pendingIsOnionServiceOnlineLookupFutureMap.put(onionAddress, onionServiceLookupCompletableFuture); + subscribeToHsDescEvents(); + + TorControlProtocol torControlProtocol = getTorControlProtocol(); + String serviceId = onionAddress.replace(".onion", ""); + torControlProtocol.hsFetch(serviceId); + + onionServiceLookupCompletableFuture.thenRun(() -> { + torControlProtocol.removeHsDescEventListener(this); + try { + torControlProtocol.setEvents(Collections.emptyList()); + } catch (IOException e) { + throw new RuntimeException(e); + } + }); + + return onionServiceLookupCompletableFuture; + } + + public void publish(TorKeyPair torKeyPair, int onionServicePort, int localPort) throws IOException, InterruptedException { + String onionAddress = torKeyPair.getOnionAddress(); + var onionServicePublishedLatch = new CountDownLatch(1); + pendingOnionServicePublishLatchMap.put(onionAddress, onionServicePublishedLatch); + + subscribeToHsDescEvents(); + TorControlProtocol torControlProtocol = getTorControlProtocol(); + torControlProtocol.addOnion(torKeyPair, onionServicePort, localPort); + + boolean isSuccess = onionServicePublishedLatch.await(hsUploadTimeout, TimeUnit.MILLISECONDS); + if (!isSuccess) { + throw new HsDescUploadFailedException("HS_DESC upload timer triggered."); + } + + torControlProtocol.removeHsDescEventListener(this); + torControlProtocol.setEvents(Collections.emptyList()); + } + public Optional getSocksPort() { try { TorControlProtocol torControlProtocol = getTorControlProtocol(); @@ -90,6 +140,40 @@ public void onBootstrapStatusEvent(BootstrapEvent bootstrapEvent) { } } + @Override + public void onHsDescEvent(HsDescEvent hsDescEvent) { + log.info("Tor HS_DESC event: {}", hsDescEvent); + + String onionAddress = hsDescEvent.getHsAddress() + ".onion"; + CompletableFuture completableFuture; + switch (hsDescEvent.getAction()) { + case FAILED: + HsDescFailedEvent hsDescFailedEvent = (HsDescFailedEvent) hsDescEvent; + if (hsDescFailedEvent.getReason().equals("REASON=NOT_FOUND")) { + completableFuture = pendingIsOnionServiceOnlineLookupFutureMap.get(onionAddress); + if (completableFuture != null) { + completableFuture.complete(false); + pendingIsOnionServiceOnlineLookupFutureMap.remove(onionAddress); + } + } + break; + case RECEIVED: + completableFuture = pendingIsOnionServiceOnlineLookupFutureMap.get(onionAddress); + if (completableFuture != null) { + completableFuture.complete(true); + pendingIsOnionServiceOnlineLookupFutureMap.remove(onionAddress); + } + break; + case UPLOADED: + CountDownLatch countDownLatch = pendingOnionServicePublishLatchMap.get(onionAddress); + if (countDownLatch != null) { + countDownLatch.countDown(); + pendingOnionServicePublishLatchMap.remove(onionAddress); + } + break; + } + } + private void initialize(int controlPort, Optional hashedControlPassword) throws IOException { var torControlProtocol = new TorControlProtocol(controlPort); this.torControlProtocol = Optional.of(torControlProtocol); @@ -112,6 +196,12 @@ private void subscribeToBootstrapEvents() throws IOException { torControlProtocol.setEvents(List.of("STATUS_CLIENT")); } + private void subscribeToHsDescEvents() throws IOException { + TorControlProtocol torControlProtocol = getTorControlProtocol(); + torControlProtocol.addHsDescEventListener(this); + torControlProtocol.setEvents(List.of("HS_DESC")); + } + private void enableNetworking() throws IOException { TorControlProtocol torControlProtocol = getTorControlProtocol(); torControlProtocol.setConfig(TorrcClientConfigFactory.DISABLE_NETWORK_CONFIG_KEY, "0"); 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 76da07a56a..8899ff6747 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,7 +1,9 @@ package bisq.tor.controller; import bisq.tor.controller.events.events.BootstrapEvent; +import bisq.tor.controller.events.events.HsDescEvent; import bisq.tor.controller.events.listener.BootstrapEventListener; +import bisq.tor.controller.events.listener.HsDescEventListener; import lombok.extern.slf4j.Slf4j; import java.io.BufferedReader; @@ -20,6 +22,7 @@ public class WhonixTorControlReader implements AutoCloseable { private final BufferedReader bufferedReader; private final BlockingQueue replies = new LinkedBlockingQueue<>(); private final List bootstrapEventListeners = new CopyOnWriteArrayList<>(); + private final List hsDescEventListeners = new CopyOnWriteArrayList<>(); private Optional workerThread = Optional.empty(); @@ -34,11 +37,31 @@ public void start() { while ((line = bufferedReader.readLine()) != null) { if (isEvent(line)) { - Optional bootstrapEventOptional = BootstrapEventParser.tryParse(line); - if (bootstrapEventOptional.isPresent()) { - BootstrapEvent bootstrapEvent = bootstrapEventOptional.get(); - bootstrapEventListeners.forEach(listener -> listener.onBootstrapStatusEvent(bootstrapEvent)); - } else { + String[] parts = line.split(" "); + + boolean parsedEvent = false; + if (parts.length > 2) { + String eventType = parts[1]; + + if (isStatusClientEvent(eventType)) { + Optional bootstrapEventOptional = BootstrapEventParser.tryParse(parts); + if (bootstrapEventOptional.isPresent()) { + parsedEvent = true; + BootstrapEvent bootstrapEvent = bootstrapEventOptional.get(); + bootstrapEventListeners.forEach(listener -> listener.onBootstrapStatusEvent(bootstrapEvent)); + } + + } else if (isHsDescEvent(eventType)) { + Optional hsDescEventOptional = HsDescEventParser.tryParse(parts); + if (hsDescEventOptional.isPresent()) { + parsedEvent = true; + HsDescEvent hsDescEvent = hsDescEventOptional.get(); + hsDescEventListeners.forEach(listener -> listener.onHsDescEvent(hsDescEvent)); + } + } + } + + if (!parsedEvent) { log.info("Unknown Tor event: {}", line); } @@ -80,8 +103,26 @@ public void removeBootstrapEventListener(BootstrapEventListener listener) { bootstrapEventListeners.remove(listener); } + public void addHsDescEventListener(HsDescEventListener listener) { + hsDescEventListeners.add(listener); + } + + public void removeHsDescEventListener(HsDescEventListener listener) { + hsDescEventListeners.remove(listener); + } + private boolean isEvent(String line) { // 650 STATUS_CLIENT NOTICE CIRCUIT_ESTABLISHED return line.startsWith("650"); } + + private static boolean isStatusClientEvent(String eventType) { + // 650 STATUS_CLIENT NOTICE CIRCUIT_ESTABLISHED + return eventType.equals("STATUS_CLIENT"); + } + + private static boolean isHsDescEvent(String eventType) { + // 650 HS_DESC CREATED UNKNOWN UNKNOWN + return eventType.equals("HS_DESC"); + } } diff --git a/network/tor/tor/src/main/java/bisq/tor/controller/events/events/HsDescCreatedOrReceivedEvent.java b/network/tor/tor/src/main/java/bisq/tor/controller/events/events/HsDescCreatedOrReceivedEvent.java new file mode 100644 index 0000000000..ceea44f8bc --- /dev/null +++ b/network/tor/tor/src/main/java/bisq/tor/controller/events/events/HsDescCreatedOrReceivedEvent.java @@ -0,0 +1,13 @@ +package bisq.tor.controller.events.events; + +import lombok.Getter; +import lombok.ToString; +import lombok.experimental.SuperBuilder; + + +@SuperBuilder +@Getter +@ToString(callSuper = true) +public class HsDescCreatedOrReceivedEvent extends HsDescEvent { + private final String descriptorId; +} diff --git a/network/tor/tor/src/main/java/bisq/tor/controller/events/events/HsDescEvent.java b/network/tor/tor/src/main/java/bisq/tor/controller/events/events/HsDescEvent.java new file mode 100644 index 0000000000..091f627beb --- /dev/null +++ b/network/tor/tor/src/main/java/bisq/tor/controller/events/events/HsDescEvent.java @@ -0,0 +1,36 @@ +package bisq.tor.controller.events.events; + +import lombok.Getter; +import lombok.ToString; +import lombok.experimental.SuperBuilder; + +@SuperBuilder +@Getter +@ToString +public abstract class HsDescEvent { + + @Getter + public enum Action { + CREATED(7), + FAILED(8), + RECEIVED(7), + UPLOAD(8), + UPLOADED(6); + + private final int numberOfArgs; + + Action(int numberOfArgs) { + this.numberOfArgs = numberOfArgs; + } + + public boolean isAction(String[] parts) { + // Example: 650 HS_DESC CREATED UNKNOWN UNKNOWN + return parts[2].equals(this.toString()) && parts.length == numberOfArgs; + } + } + + protected final Action action; + protected final String hsAddress; + protected final String authType; + protected final String hsDir; +} diff --git a/network/tor/tor/src/main/java/bisq/tor/controller/events/events/HsDescFailedEvent.java b/network/tor/tor/src/main/java/bisq/tor/controller/events/events/HsDescFailedEvent.java new file mode 100644 index 0000000000..83dbd1d0ce --- /dev/null +++ b/network/tor/tor/src/main/java/bisq/tor/controller/events/events/HsDescFailedEvent.java @@ -0,0 +1,12 @@ +package bisq.tor.controller.events.events; + +import lombok.Getter; +import lombok.ToString; +import lombok.experimental.SuperBuilder; + +@SuperBuilder +@Getter +@ToString(callSuper = true) +public class HsDescFailedEvent extends HsDescCreatedOrReceivedEvent { + private final String reason; +} diff --git a/network/tor/tor/src/main/java/bisq/tor/controller/events/events/HsDescUploadEvent.java b/network/tor/tor/src/main/java/bisq/tor/controller/events/events/HsDescUploadEvent.java new file mode 100644 index 0000000000..7affdc67ae --- /dev/null +++ b/network/tor/tor/src/main/java/bisq/tor/controller/events/events/HsDescUploadEvent.java @@ -0,0 +1,12 @@ +package bisq.tor.controller.events.events; + +import lombok.Getter; +import lombok.ToString; +import lombok.experimental.SuperBuilder; + +@SuperBuilder +@Getter +@ToString(callSuper = true) +public class HsDescUploadEvent extends HsDescCreatedOrReceivedEvent { + private final String hsDirIndex; +} diff --git a/network/tor/tor/src/main/java/bisq/tor/controller/events/events/HsDescUploadedEventV2.java b/network/tor/tor/src/main/java/bisq/tor/controller/events/events/HsDescUploadedEventV2.java new file mode 100644 index 0000000000..fcc0ea4d1c --- /dev/null +++ b/network/tor/tor/src/main/java/bisq/tor/controller/events/events/HsDescUploadedEventV2.java @@ -0,0 +1,11 @@ +package bisq.tor.controller.events.events; + +import lombok.Getter; +import lombok.ToString; +import lombok.experimental.SuperBuilder; + +@SuperBuilder +@Getter +@ToString(callSuper = true) +public class HsDescUploadedEventV2 extends HsDescEvent { +} diff --git a/network/tor/tor/src/main/java/bisq/tor/controller/events/listener/HsDescEventListener.java b/network/tor/tor/src/main/java/bisq/tor/controller/events/listener/HsDescEventListener.java new file mode 100644 index 0000000000..5ed9ef83b9 --- /dev/null +++ b/network/tor/tor/src/main/java/bisq/tor/controller/events/listener/HsDescEventListener.java @@ -0,0 +1,7 @@ +package bisq.tor.controller.events.listener; + +import bisq.tor.controller.events.events.HsDescEvent; + +public interface HsDescEventListener { + void onHsDescEvent(HsDescEvent hsDescEvent); +}