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; + } }