From 19d2332d86acb090d475c52f7587b04d1e2ccfe5 Mon Sep 17 00:00:00 2001 From: ghubstan <36207203+ghubstan@users.noreply.github.com> Date: Tue, 29 Jun 2021 21:41:53 -0300 Subject: [PATCH] Convert TorDemo into blocking and async bootstrap tests And refactor TorIntegrationTest into AsyncBootstrapTest #2. Note: Adding Tor bootstrapping tests add time to build. To skip tests, run `$./gradlew clean build -x test`. --- tor/src/test/java/network/misq/TorDemo.java | 219 ----------------- .../java/network/misq/TorIntegrationTest.java | 70 ------ .../network/misq/tor/AbstractTorTest.java | 228 ++++++++++++++++++ .../network/misq/tor/AsyncBootstrapTest.java | 98 ++++++++ .../misq/tor/BlockingBootstrapTest.java | 70 ++++++ 5 files changed, 396 insertions(+), 289 deletions(-) delete mode 100644 tor/src/test/java/network/misq/TorDemo.java delete mode 100644 tor/src/test/java/network/misq/TorIntegrationTest.java create mode 100644 tor/src/test/java/network/misq/tor/AbstractTorTest.java create mode 100644 tor/src/test/java/network/misq/tor/AsyncBootstrapTest.java create mode 100644 tor/src/test/java/network/misq/tor/BlockingBootstrapTest.java diff --git a/tor/src/test/java/network/misq/TorDemo.java b/tor/src/test/java/network/misq/TorDemo.java deleted file mode 100644 index 7917a96111..0000000000 --- a/tor/src/test/java/network/misq/TorDemo.java +++ /dev/null @@ -1,219 +0,0 @@ -/* - * 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 network.misq; - -import com.runjva.sourceforge.jsocks.protocol.SocksSocket; -import network.misq.common.util.OsUtils; -import network.misq.tor.OnionAddress; -import network.misq.tor.Tor; -import network.misq.tor.TorServerSocket; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import javax.net.SocketFactory; -import java.io.IOException; -import java.io.ObjectInputStream; -import java.io.ObjectOutputStream; -import java.net.InetSocketAddress; -import java.net.Proxy; -import java.net.Socket; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; - -public class TorDemo { - private static final Logger log = LoggerFactory.getLogger(TorDemo.class); - private static Tor tor; - - public static void main(String[] args) throws InterruptedException { - String torDirPath = OsUtils.getUserDataDir() + "/TorDemo"; - // useBlockingAPI(torDirPath); - useNonBlockingAPI(torDirPath); - } - - private static void useBlockingAPI(String torDirPath) { - try { - tor = Tor.getTor(torDirPath); - tor.start(); - TorServerSocket torServerSocket = startServer(); - OnionAddress onionAddress = torServerSocket.getOnionAddress().get(); - sendViaSocketFactory(tor, onionAddress); - sendViaProxy(tor, onionAddress); - sendViaSocket(tor, onionAddress); - sendViaSocksSocket(tor, onionAddress); - } catch (Exception e) { - e.printStackTrace(); - } - } - - private static void useNonBlockingAPI(String torDirPath) throws InterruptedException { - AtomicBoolean stopped = new AtomicBoolean(false); - tor = Tor.getTor(torDirPath); - CountDownLatch latch = new CountDownLatch(1); - tor.startAsync() - .thenCompose(result -> startServerAsync() - .thenAccept(onionAddress -> { - if (onionAddress == null) { - return; - } - - sendViaSocketFactory(tor, onionAddress); - sendViaProxy(tor, onionAddress); - sendViaSocket(tor, onionAddress); - sendViaSocksSocket(tor, onionAddress); - latch.countDown(); - })); - - latch.await(2, TimeUnit.MINUTES); - } - - private static TorServerSocket startServer() throws IOException, InterruptedException { - TorServerSocket torServerSocket = tor.getTorServerSocket(); - torServerSocket.bind(4000, 9999, "hiddenservice_2"); - runServer(torServerSocket); - return torServerSocket; - } - - private static CompletableFuture startServerAsync() { - CompletableFuture future = new CompletableFuture<>(); - try { - TorServerSocket torServerSocket = tor.getTorServerSocket(); - torServerSocket - .bindAsync(3000, "hiddenservice_3") - .whenComplete((onionAddress, throwable) -> { - if (throwable == null) { - runServer(torServerSocket); - future.complete(onionAddress); - } else { - future.completeExceptionally(throwable); - } - }); - } catch (IOException e) { - future.completeExceptionally(e); - } - return future; - } - - private static void runServer(TorServerSocket torServerSocket) { - new Thread(() -> { - Thread.currentThread().setName("Server"); - while (true) { - try { - log.info("Start listening for new connections on {}", torServerSocket.getOnionAddress()); - Socket clientSocket = torServerSocket.accept(); - createInboundConnection(clientSocket); - } catch (IOException e) { - try { - torServerSocket.close(); - } catch (IOException ignore) { - } - } - } - }).start(); - } - - private static void createInboundConnection(Socket clientSocket) { - log.info("New client connection accepted"); - new Thread(() -> { - Thread.currentThread().setName("Read at inbound connection"); - try (ObjectOutputStream objectOutputStream = new ObjectOutputStream(clientSocket.getOutputStream()); - ObjectInputStream objectInputStream = new ObjectInputStream(clientSocket.getInputStream())) { - objectOutputStream.flush(); - listenOnInputStream(clientSocket, objectInputStream, "inbound connection"); - } catch (IOException e) { - try { - clientSocket.close(); - } catch (IOException ignore) { - } - } - }).start(); - } - - private static void listenOnInputStream(Socket socket, ObjectInputStream objectInputStream, String info) { - try { - while (!Thread.currentThread().isInterrupted()) { - Object object = objectInputStream.readObject(); - log.info("Received at {} {}", info, object); - } - } catch (IOException | ClassNotFoundException e) { - try { - socket.close(); - } catch (IOException ignore) { - } - } - } - - // Outbound connection - private static void sendViaSocket(Tor tor, OnionAddress onionAddress) { - try { - Socket socket = tor.getSocket("test_stream_id"); - socket.connect(new InetSocketAddress(onionAddress.getHost(), onionAddress.getPort())); - sendOnOutboundConnection(socket, "test via Socket"); - } catch (IOException e) { - e.printStackTrace(); - } - } - - private static void sendViaSocksSocket(Tor tor, OnionAddress onionAddress) { - try { - SocksSocket socket = tor.getSocksSocket(onionAddress.getHost(), onionAddress.getPort(), "test_stream_id"); - sendOnOutboundConnection(socket, "test via SocksSocket"); - } catch (IOException e) { - e.printStackTrace(); - } - } - - private static void sendViaSocketFactory(Tor tor, OnionAddress onionAddress) { - try { - SocketFactory socketFactory = tor.getSocketFactory("test_stream_id"); - Socket socket = socketFactory.createSocket(onionAddress.getHost(), onionAddress.getPort()); - sendOnOutboundConnection(socket, "test via SocketFactory"); - } catch (IOException e) { - e.printStackTrace(); - } - } - - private static void sendViaProxy(Tor tor, OnionAddress onionAddress) { - try { - Proxy proxy = tor.getProxy("test_stream_id"); - Socket socket = new Socket(proxy); - socket.connect(new InetSocketAddress(onionAddress.getHost(), onionAddress.getPort())); - sendOnOutboundConnection(socket, "test via Proxy"); - } catch (IOException e) { - e.printStackTrace(); - } - } - - private static void sendOnOutboundConnection(Socket socket, String msg) { - log.info("sendViaOutboundConnection {}", msg); - new Thread(() -> { - try (ObjectOutputStream objectOutputStream = new ObjectOutputStream(socket.getOutputStream()); - ObjectInputStream objectInputStream = new ObjectInputStream(socket.getInputStream())) { - objectOutputStream.writeObject(msg); - objectOutputStream.flush(); - listenOnInputStream(socket, objectInputStream, "outbound connection"); - } catch (IOException e) { - try { - socket.close(); - } catch (IOException ignore) { - } - } - }).start(); - } -} diff --git a/tor/src/test/java/network/misq/TorIntegrationTest.java b/tor/src/test/java/network/misq/TorIntegrationTest.java deleted file mode 100644 index b3d5f0dfd4..0000000000 --- a/tor/src/test/java/network/misq/TorIntegrationTest.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * 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 network.misq; - -import network.misq.common.util.FileUtils; -import network.misq.common.util.OsUtils; -import network.misq.tor.Constants; -import network.misq.tor.Tor; -import org.junit.jupiter.api.Test; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.File; - -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.fail; - - -public class TorIntegrationTest { - private static final Logger log = LoggerFactory.getLogger(TorIntegrationTest.class); - - @Test - public void testShutdownDuringStartup() { - String torDirPath = OsUtils.getUserDataDir() + "/TorifyIntegrationTest"; - File versionFile = new File(torDirPath + "/" + Constants.VERSION); - FileUtils.deleteDirectory(new File(torDirPath)); - assertFalse(versionFile.exists()); - Tor tor = Tor.getTor(torDirPath); - new Thread(() -> { - try { - Thread.sleep(200); - } catch (InterruptedException ignore) { - } - tor.shutdown(); - - }).start(); - Thread mainThread = Thread.currentThread(); - tor.startAsync() - .exceptionally(throwable -> { - assertFalse(versionFile.exists()); - mainThread.interrupt(); - return null; - }) - .thenAccept(result -> { - if (result == null) { - return; - } - fail(); - }); - try { - Thread.sleep(2000); - } catch (InterruptedException ignore) { - } - } -} diff --git a/tor/src/test/java/network/misq/tor/AbstractTorTest.java b/tor/src/test/java/network/misq/tor/AbstractTorTest.java new file mode 100644 index 0000000000..7c1782c084 --- /dev/null +++ b/tor/src/test/java/network/misq/tor/AbstractTorTest.java @@ -0,0 +1,228 @@ +package network.misq.tor; + + +import com.runjva.sourceforge.jsocks.protocol.SocksSocket; +import lombok.extern.slf4j.Slf4j; +import network.misq.common.util.FileUtils; +import network.misq.common.util.OsUtils; + +import javax.net.SocketFactory; +import java.io.*; +import java.net.InetSocketAddress; +import java.net.Proxy; +import java.net.Socket; +import java.net.SocketException; +import java.util.concurrent.CompletableFuture; +import java.util.function.Predicate; +import java.util.function.Supplier; + +import static java.lang.String.format; +import static network.misq.tor.Constants.VERSION; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.fail; + +@Slf4j +abstract class AbstractTorTest { + + private static final String TEST_STREAM_ID = "test_stream_id"; + + private enum ConnectionType { + INBOUND, + OUTBOUND + } + + protected static Tor tor; + protected static TorServerSocket torServerSocket; + protected static OnionAddress onionAddress; + protected static boolean isShutdown = false; + protected static String expectedMessage; + + protected final Supplier torTestDirPathSpec = () -> + OsUtils.getUserDataDir() + File.separator + this.getClass().getSimpleName(); + protected final Predicate isClassNotFoundException = (ex) -> ex instanceof ClassNotFoundException; + protected final Predicate isEOFException = (ex) -> ex instanceof EOFException; + protected final Predicate isSocketClosedException = (ex) -> + ex instanceof SocketException && ex.getMessage().equals("Socket closed"); + + public static void cleanTorInstallDir(String torDirPathSpec) { + File torDir = new File(torDirPathSpec); + if (torDir.exists()) { + log.info("Cleaning tor install dir {}", torDirPathSpec); + FileUtils.deleteDirectory(torDir); + } + File versionFile = new File(torDir, VERSION); + assertFalse(versionFile.exists()); + } + + protected TorServerSocket startServer() throws IOException, InterruptedException { + TorServerSocket torServerSocket = tor.getTorServerSocket(); + torServerSocket.bind(4000, 9999, "hiddenservice_2"); + runServer(torServerSocket); + return torServerSocket; + } + + protected CompletableFuture startServerAsync() { + CompletableFuture future = new CompletableFuture<>(); + try { + TorServerSocket torServerSocket = tor.getTorServerSocket(); + torServerSocket + .bindAsync(3000, "hiddenservice_3") + .whenComplete((onionAddress, throwable) -> { + if (throwable == null) { + runServer(torServerSocket); + future.complete(onionAddress); + } else { + future.completeExceptionally(throwable); + } + }); + } catch (IOException e) { + future.completeExceptionally(e); + } + return future; + } + + protected void runServer(TorServerSocket torServerSocket) { + new Thread(() -> { + Thread.currentThread().setName("Server"); + while (true) { + try { + log.info("Start listening for connections on {}.", torServerSocket.getOnionAddress()); + Socket clientSocket = torServerSocket.accept(); + createInboundConnection(clientSocket); + } catch (IOException ex) { + try { + if (isShutdown && isSocketClosedException.test(ex)) { + log.info("Socket already closed prior to Tor shutdown."); + return; + } else { + torServerSocket.close(); + fail(ex); + } + } catch (IOException ignore) { + // empty + } + } + } + }).start(); + } + + protected void createInboundConnection(Socket clientSocket) { + log.info("New client connection accepted"); + new Thread(() -> { + Thread.currentThread().setName("Read at inbound connection"); + try (ObjectOutputStream objectOutputStream = new ObjectOutputStream(clientSocket.getOutputStream()); + ObjectInputStream objectInputStream = new ObjectInputStream(clientSocket.getInputStream())) { + objectOutputStream.flush(); + + // listenOnInputStream(clientSocket, objectInputStream, "inbound connection"); + listenOnInputStream(clientSocket, objectInputStream, ConnectionType.INBOUND); + + } catch (IOException ex) { + try { + clientSocket.close(); + if (!isEOFException.test(ex)) + fail(ex); + } catch (IOException ignore) { + // empty + } + } + }).start(); + } + + protected void listenOnInputStream(Socket socket, + ObjectInputStream objectInputStream, + ConnectionType connectionType) { + try { + while (!Thread.currentThread().isInterrupted()) { + Object message = objectInputStream.readObject(); + log.info("Received '{}' on {} connection.", + message, + connectionType.name().toLowerCase()); + if (connectionType.equals(ConnectionType.INBOUND)) { + if (!message.equals(expectedMessage)) { + fail(format("Did not read expected message from input stream. Was '%s', expected '%s'", + expectedMessage, + message)); + } + } + } + } catch (ClassNotFoundException | IOException ex) { + closeClientSocketOnException(socket, ex); + } + } + + private void closeClientSocketOnException(Socket socket, Exception exception) { + try { + socket.close(); + } catch (IOException ignored) { + // empty + } + if (!isEOFException.test(exception) || isClassNotFoundException.test(exception)) + fail(exception); + } + + // Outbound Connection + + protected void sendViaSocket(Tor tor, OnionAddress onionAddress) { + try { + Socket socket = tor.getSocket(TEST_STREAM_ID); + socket.connect(new InetSocketAddress(onionAddress.getHost(), onionAddress.getPort())); + sendOnOutboundConnection(socket, "Message via Socket"); + } catch (IOException ex) { + fail(ex); + } + } + + protected void sendViaSocksSocket(Tor tor, OnionAddress onionAddress) { + try { + SocksSocket socket = tor.getSocksSocket(onionAddress.getHost(), onionAddress.getPort(), TEST_STREAM_ID); + sendOnOutboundConnection(socket, "Message via SocksSocket"); + } catch (IOException ex) { + fail(ex); + } + } + + protected void sendViaSocketFactory(Tor tor, OnionAddress onionAddress) { + try { + SocketFactory socketFactory = tor.getSocketFactory(TEST_STREAM_ID); + Socket socket = socketFactory.createSocket(onionAddress.getHost(), onionAddress.getPort()); + sendOnOutboundConnection(socket, "Message via SocketFactory"); + } catch (IOException ex) { + fail(ex); + } + } + + protected void sendViaProxy(Tor tor, OnionAddress onionAddress) { + try { + Proxy proxy = tor.getProxy(TEST_STREAM_ID); + Socket socket = new Socket(proxy); + socket.connect(new InetSocketAddress(onionAddress.getHost(), onionAddress.getPort())); + sendOnOutboundConnection(socket, "Message via Proxy"); + } catch (IOException ex) { + fail(ex); + } + } + + protected void sendOnOutboundConnection(Socket socket, String message) { + log.info("sendOnOutboundConnection: '{}'", message); + new Thread(() -> { + try (ObjectOutputStream objectOutputStream = new ObjectOutputStream(socket.getOutputStream()); + ObjectInputStream objectInputStream = new ObjectInputStream(socket.getInputStream())) { + objectOutputStream.writeObject(message); + objectOutputStream.flush(); + + expectedMessage = message; + listenOnInputStream(socket, objectInputStream, ConnectionType.OUTBOUND); + + } catch (IOException ex) { + try { + socket.close(); + if (!isEOFException.test(ex)) + fail(ex); + } catch (IOException ignore) { + // empty + } + } + }).start(); + } +} diff --git a/tor/src/test/java/network/misq/tor/AsyncBootstrapTest.java b/tor/src/test/java/network/misq/tor/AsyncBootstrapTest.java new file mode 100644 index 0000000000..a6dfc58fdc --- /dev/null +++ b/tor/src/test/java/network/misq/tor/AsyncBootstrapTest.java @@ -0,0 +1,98 @@ +package network.misq.tor; + +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; + +import java.io.File; +import java.io.IOException; +import java.util.concurrent.CountDownLatch; + +import static java.util.concurrent.TimeUnit.MINUTES; +import static network.misq.tor.Constants.VERSION; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.fail; + +@Slf4j +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class AsyncBootstrapTest extends AbstractTorTest { + + @Test + @Order(1) + public void testAsyncBootstrap() { + try { + String torDirPathSpec = torTestDirPathSpec.get(); + cleanTorInstallDir(torDirPathSpec); + tor = Tor.getTor(torDirPathSpec); + + CountDownLatch latch = new CountDownLatch(1); + tor.startAsync() + .thenCompose(result -> startServerAsync() + .thenAccept(onionAddress -> { + if (onionAddress == null) { + return; + } + sendViaSocketFactory(tor, onionAddress); + sendViaProxy(tor, onionAddress); + sendViaSocket(tor, onionAddress); + sendViaSocksSocket(tor, onionAddress); + latch.countDown(); + })); + //noinspection ResultOfMethodCallIgnored + latch.await(2, MINUTES); + + shutdownTor(); + } catch (InterruptedException ignored) { + // empty + } + } + + @Test + @Order(2) + public void testShutdownDuringStartup() { + String torDirPathSpec = torTestDirPathSpec.get(); + cleanTorInstallDir(torDirPathSpec); + tor = Tor.getTor(torDirPathSpec); + + new Thread(() -> { + try { + Thread.sleep(200); + } catch (InterruptedException ignored) { + // empty + } + tor.shutdown(); + }).start(); + + Thread mainThread = Thread.currentThread(); + tor.startAsync() + .exceptionally(throwable -> { + File versionFile = new File(torDirPathSpec + File.separator + VERSION); + assertFalse(versionFile.exists()); + mainThread.interrupt(); + return null; + }) + .thenAccept(result -> { + if (result == null) { + return; + } + fail(); + }); + try { + Thread.sleep(2000); + } catch (InterruptedException ignored) { + // empty + } + } + + private void shutdownTor() { + try { + isShutdown = true; + tor.getTorServerSocket().close(); + tor.shutdown(); + } catch (IOException ex) { + fail("Error during Tor shutdown.", ex); + } + } +} diff --git a/tor/src/test/java/network/misq/tor/BlockingBootstrapTest.java b/tor/src/test/java/network/misq/tor/BlockingBootstrapTest.java new file mode 100644 index 0000000000..8077c9cc52 --- /dev/null +++ b/tor/src/test/java/network/misq/tor/BlockingBootstrapTest.java @@ -0,0 +1,70 @@ +package network.misq.tor; + +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.*; + +import java.io.IOException; + +import static org.junit.jupiter.api.Assertions.fail; + +@Slf4j +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class BlockingBootstrapTest extends AbstractTorTest { + + @Test + @Order(1) + public void testBlockingBootstrap() { + try { + String torDirPathSpec = torTestDirPathSpec.get(); + cleanTorInstallDir(torDirPathSpec); + + tor = Tor.getTor(torDirPathSpec); + tor.start(); + torServerSocket = startServer(); + onionAddress = torServerSocket.getOnionAddress() + .orElseThrow(() -> new IllegalStateException("Could not get onion address from tor server socket.")); + } catch (IOException ex) { + fail(ex); + } catch (InterruptedException ignored) { + // empty + } + } + + @Test + @Order(2) + public void testSendMessageViaSocketFactory() { + sendViaSocketFactory(tor, onionAddress); + } + + @Test + @Order(3) + public void testSendMessageViaProxy() { + sendViaProxy(tor, onionAddress); + } + + @Test + @Order(4) + public void testSendMessageViaSocket() { + sendViaSocket(tor, onionAddress); + } + + @Test + @Order(5) + public void testSendMessageViaSocksSocket() { + sendViaSocksSocket(tor, onionAddress); + } + + @AfterAll + public static void shutdownTor() { + try { + Thread.sleep(5000); + isShutdown = true; + torServerSocket.close(); + tor.shutdown(); + } catch (IOException ex) { + fail("Error during Tor shutdown.", ex); + } catch (InterruptedException ignored) { + // empty + } + } +}