package io.bitsquare.p2p.network; import com.google.common.util.concurrent.*; import com.runjva.sourceforge.jsocks.protocol.Socks5Proxy; import io.bitsquare.app.Log; import io.bitsquare.common.UserThread; import io.bitsquare.common.util.Utilities; import io.bitsquare.p2p.Message; import io.bitsquare.p2p.NodeAddress; import javafx.beans.property.ObjectProperty; import javafx.beans.property.ReadOnlyObjectProperty; import javafx.beans.property.SimpleObjectProperty; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.net.ConnectException; import java.net.ServerSocket; import java.net.Socket; import java.util.HashSet; import java.util.Optional; import java.util.Set; import java.util.concurrent.CopyOnWriteArraySet; import java.util.concurrent.TimeoutException; import java.util.stream.Collectors; import static com.google.common.base.Preconditions.checkNotNull; // Run in UserThread public abstract class NetworkNode implements MessageListener { private static final Logger log = LoggerFactory.getLogger(NetworkNode.class); private static final int CREATE_SOCKET_TIMEOUT_MILLIS = 10000; final int servicePort; private final CopyOnWriteArraySet<InboundConnection> inBoundConnections = new CopyOnWriteArraySet<>(); private final CopyOnWriteArraySet<MessageListener> messageListeners = new CopyOnWriteArraySet<>(); private final CopyOnWriteArraySet<ConnectionListener> connectionListeners = new CopyOnWriteArraySet<>(); final CopyOnWriteArraySet<SetupListener> setupListeners = new CopyOnWriteArraySet<>(); ListeningExecutorService executorService; private Server server; private volatile boolean shutDownInProgress; // accessed from different threads private final CopyOnWriteArraySet<OutboundConnection> outBoundConnections = new CopyOnWriteArraySet<>(); protected final ObjectProperty<NodeAddress> nodeAddressProperty = new SimpleObjectProperty<>(); /////////////////////////////////////////////////////////////////////////////////////////// // Constructor /////////////////////////////////////////////////////////////////////////////////////////// NetworkNode(int servicePort) { this.servicePort = servicePort; } /////////////////////////////////////////////////////////////////////////////////////////// // API /////////////////////////////////////////////////////////////////////////////////////////// // Calls this (and other registered) setup listener's ``onTorNodeReady()`` and ``onHiddenServicePublished`` // when the events happen. abstract public void start(@Nullable SetupListener setupListener); public SettableFuture<Connection> sendMessage(@NotNull NodeAddress peersNodeAddress, Message message) { Log.traceCall("peersNodeAddress=" + peersNodeAddress + "\n\tmessage=" + Utilities.toTruncatedString(message)); checkNotNull(peersNodeAddress, "peerAddress must not be null"); Connection connection = getOutboundConnection(peersNodeAddress); if (connection == null) connection = getInboundConnection(peersNodeAddress); if (connection != null) { return sendMessage(connection, message); } else { log.debug("We have not found any connection for peerAddress {}.\n\t" + "We will create a new outbound connection.", peersNodeAddress); final SettableFuture<Connection> resultFuture = SettableFuture.create(); ListenableFuture<Connection> future = executorService.submit(() -> { Thread.currentThread().setName("NetworkNode:SendMessage-to-" + peersNodeAddress); OutboundConnection outboundConnection = null; try { // can take a while when using tor long startTs = System.currentTimeMillis(); log.debug("Start create socket to peersNodeAddress {}", peersNodeAddress.getFullAddress()); Socket socket = createSocket(peersNodeAddress); long duration = System.currentTimeMillis() - startTs; log.debug("Socket creation to peersNodeAddress {} took {} ms", peersNodeAddress.getFullAddress(), duration); if (duration > CREATE_SOCKET_TIMEOUT_MILLIS) throw new TimeoutException("A timeout occurred when creating a socket."); // Tor needs sometimes quite long to create a connection. To avoid that we get too many double // sided connections we check again if we still don't have any connection for that node address. Connection existingConnection = getInboundConnection(peersNodeAddress); if (existingConnection == null) existingConnection = getOutboundConnection(peersNodeAddress); if (existingConnection != null) { log.debug("We found in the meantime a connection for peersNodeAddress {}, " + "so we use that for sending the message.\n" + "That can happen if Tor needs long for creating a new outbound connection.\n" + "We might have got a new inbound or outbound connection.", peersNodeAddress.getFullAddress()); try { socket.close(); } catch (Throwable throwable) { log.error("Error at closing socket " + throwable); } existingConnection.sendMessage(message); return existingConnection; } else { outboundConnection = new OutboundConnection(socket, NetworkNode.this, new ConnectionListener() { @Override public void onConnection(Connection connection) { if (!connection.isStopped()) { outBoundConnections.add((OutboundConnection) connection); printOutBoundConnections(); connectionListeners.stream().forEach(e -> e.onConnection(connection)); } } @Override public void onDisconnect(CloseConnectionReason closeConnectionReason, Connection connection) { log.trace("onDisconnect connectionListener\n\tconnection={}" + connection); outBoundConnections.remove(connection); printOutBoundConnections(); connectionListeners.stream().forEach(e -> e.onDisconnect(closeConnectionReason, connection)); } @Override public void onError(Throwable throwable) { log.error("new OutboundConnection.ConnectionListener.onError " + throwable.getMessage()); connectionListeners.stream().forEach(e -> e.onError(throwable)); } }, peersNodeAddress); log.debug("\n\n%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%\n" + "NetworkNode created new outbound connection:" + "\nmyNodeAddress=" + getNodeAddress() + "\npeersNodeAddress=" + peersNodeAddress + "\nuid=" + outboundConnection.getUid() + "\nmessage=" + message + "\n%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%\n"); // can take a while when using tor outboundConnection.sendMessage(message); return outboundConnection; } } catch (Throwable throwable) { if (!(throwable instanceof ConnectException || throwable instanceof IOException || throwable instanceof TimeoutException)) { log.warn("Executing task failed. " + throwable.getMessage()); } throw throwable; } }); Futures.addCallback(future, new FutureCallback<Connection>() { public void onSuccess(Connection connection) { UserThread.execute(() -> resultFuture.set(connection)); } public void onFailure(@NotNull Throwable throwable) { UserThread.execute(() -> resultFuture.setException(throwable)); } }); return resultFuture; } } @Nullable private InboundConnection getInboundConnection(@NotNull NodeAddress peersNodeAddress) { Optional<InboundConnection> inboundConnectionOptional = lookupInBoundConnection(peersNodeAddress); if (inboundConnectionOptional.isPresent()) { InboundConnection connection = inboundConnectionOptional.get(); log.trace("We have found a connection in inBoundConnections. Connection.uid=" + connection.getUid()); if (connection.isStopped()) { log.warn("We have a connection which is already stopped in inBoundConnections. Connection.uid=" + connection.getUid()); inBoundConnections.remove(connection); return null; } else { return connection; } } else { return null; } } @Nullable private OutboundConnection getOutboundConnection(@NotNull NodeAddress peersNodeAddress) { Optional<OutboundConnection> outboundConnectionOptional = lookupOutBoundConnection(peersNodeAddress); if (outboundConnectionOptional.isPresent()) { OutboundConnection connection = outboundConnectionOptional.get(); log.trace("We have found a connection in outBoundConnections. Connection.uid=" + connection.getUid()); if (connection.isStopped()) { log.warn("We have a connection which is already stopped in outBoundConnections. Connection.uid=" + connection.getUid()); outBoundConnections.remove(connection); return null; } else { return connection; } } else { return null; } } @Nullable public Socks5Proxy getSocksProxy() { return null; } public SettableFuture<Connection> sendMessage(Connection connection, Message message) { Log.traceCall("\n\tmessage=" + Utilities.toTruncatedString(message) + "\n\tconnection=" + connection); // connection.sendMessage might take a bit (compression, write to stream), so we use a thread to not block ListenableFuture<Connection> future = executorService.submit(() -> { Thread.currentThread().setName("NetworkNode:SendMessage-to-" + connection.getUid()); connection.sendMessage(message); return connection; }); final SettableFuture<Connection> resultFuture = SettableFuture.create(); Futures.addCallback(future, new FutureCallback<Connection>() { public void onSuccess(Connection connection) { UserThread.execute(() -> resultFuture.set(connection)); } public void onFailure(@NotNull Throwable throwable) { UserThread.execute(() -> resultFuture.setException(throwable)); } }); return resultFuture; } public ReadOnlyObjectProperty<NodeAddress> nodeAddressProperty() { return nodeAddressProperty; } public Set<Connection> getAllConnections() { // Can contain inbound and outbound connections with the same peer node address, // as connection hashcode is using uid and port info Set<Connection> set = new HashSet<>(inBoundConnections); set.addAll(outBoundConnections); return set; } public Set<Connection> getConfirmedConnections() { // Can contain inbound and outbound connections with the same peer node address, // as connection hashcode is using uid and port info return getAllConnections().stream() .filter(Connection::hasPeersNodeAddress) .collect(Collectors.toSet()); } public Set<NodeAddress> getNodeAddressesOfConfirmedConnections() { // Does not contain inbound and outbound connection with the same peer node address return getConfirmedConnections().stream() .map(e -> e.getPeersNodeAddressOptional().get()) .collect(Collectors.toSet()); } public void shutDown(Runnable shutDownCompleteHandler) { Log.traceCall(); if (!shutDownInProgress) { shutDownInProgress = true; if (server != null) { server.shutDown(); server = null; } getAllConnections().stream().forEach(c -> c.shutDown(CloseConnectionReason.APP_SHUT_DOWN)); log.debug("NetworkNode shutdown complete"); } if (shutDownCompleteHandler != null) shutDownCompleteHandler.run(); } /////////////////////////////////////////////////////////////////////////////////////////// // SetupListener /////////////////////////////////////////////////////////////////////////////////////////// void addSetupListener(SetupListener setupListener) { boolean isNewEntry = setupListeners.add(setupListener); if (!isNewEntry) log.warn("Try to add a setupListener which was already added."); } /////////////////////////////////////////////////////////////////////////////////////////// // MessageListener implementation /////////////////////////////////////////////////////////////////////////////////////////// @Override public void onMessage(Message message, Connection connection) { messageListeners.stream().forEach(e -> e.onMessage(message, connection)); } /////////////////////////////////////////////////////////////////////////////////////////// // Listeners /////////////////////////////////////////////////////////////////////////////////////////// public void addConnectionListener(ConnectionListener connectionListener) { boolean isNewEntry = connectionListeners.add(connectionListener); if (!isNewEntry) log.warn("Try to add a connectionListener which was already added.\n\tconnectionListener={}\n\tconnectionListeners={}" , connectionListener, connectionListeners); } public void removeConnectionListener(ConnectionListener connectionListener) { boolean contained = connectionListeners.remove(connectionListener); if (!contained) log.debug("Try to remove a connectionListener which was never added.\n\t" + "That might happen because of async behaviour of CopyOnWriteArraySet"); } public void addMessageListener(MessageListener messageListener) { boolean isNewEntry = messageListeners.add(messageListener); if (!isNewEntry) log.warn("Try to add a messageListener which was already added."); } public void removeMessageListener(MessageListener messageListener) { boolean contained = messageListeners.remove(messageListener); if (!contained) log.debug("Try to remove a messageListener which was never added.\n\t" + "That might happen because of async behaviour of CopyOnWriteArraySet"); } /////////////////////////////////////////////////////////////////////////////////////////// // Protected /////////////////////////////////////////////////////////////////////////////////////////// void createExecutorService() { executorService = Utilities.getListeningExecutorService("NetworkNode-" + servicePort, 15, 30, 60); } void startServer(ServerSocket serverSocket) { server = new Server(serverSocket, NetworkNode.this, new ConnectionListener() { @Override public void onConnection(Connection connection) { if (!connection.isStopped()) { inBoundConnections.add((InboundConnection) connection); printInboundConnections(); connectionListeners.stream().forEach(e -> e.onConnection(connection)); } } @Override public void onDisconnect(CloseConnectionReason closeConnectionReason, Connection connection) { log.trace("onDisconnect at server socket connectionListener\n\tconnection={}" + connection); inBoundConnections.remove(connection); printInboundConnections(); connectionListeners.stream().forEach(e -> e.onDisconnect(closeConnectionReason, connection)); } @Override public void onError(Throwable throwable) { log.error("server.ConnectionListener.onError " + throwable.getMessage()); connectionListeners.stream().forEach(e -> e.onError(throwable)); } }); executorService.submit(server); } private Optional<OutboundConnection> lookupOutBoundConnection(NodeAddress peersNodeAddress) { log.trace("lookupOutboundConnection for peersNodeAddress={}", peersNodeAddress.getFullAddress()); printOutBoundConnections(); return outBoundConnections.stream() .filter(connection -> connection.hasPeersNodeAddress() && peersNodeAddress.equals(connection.getPeersNodeAddressOptional().get())).findAny(); } private void printOutBoundConnections() { StringBuilder sb = new StringBuilder("outBoundConnections size()=") .append(outBoundConnections.size()).append("\n\toutBoundConnections="); outBoundConnections.stream().forEach(e -> sb.append(e).append("\n\t")); log.debug(sb.toString()); } private Optional<InboundConnection> lookupInBoundConnection(NodeAddress peersNodeAddress) { log.trace("lookupInboundConnection for peersNodeAddress={}", peersNodeAddress.getFullAddress()); printInboundConnections(); return inBoundConnections.stream() .filter(connection -> connection.hasPeersNodeAddress() && peersNodeAddress.equals(connection.getPeersNodeAddressOptional().get())).findAny(); } private void printInboundConnections() { StringBuilder sb = new StringBuilder("inBoundConnections size()=") .append(inBoundConnections.size()).append("\n\tinBoundConnections="); inBoundConnections.stream().forEach(e -> sb.append(e).append("\n\t")); log.debug(sb.toString()); } abstract protected Socket createSocket(NodeAddress peersNodeAddress) throws IOException; @Nullable public NodeAddress getNodeAddress() { return nodeAddressProperty.get(); } }