package io.bitsquare.p2p; import com.google.common.annotations.VisibleForTesting; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.SettableFuture; import com.google.inject.Inject; import com.google.inject.name.Named; import io.bitsquare.app.Log; import io.bitsquare.common.Clock; import io.bitsquare.common.UserThread; import io.bitsquare.common.crypto.CryptoException; import io.bitsquare.common.crypto.KeyRing; import io.bitsquare.common.crypto.PubKeyRing; import io.bitsquare.common.util.Utilities; import io.bitsquare.crypto.DecryptedMsgWithPubKey; import io.bitsquare.crypto.EncryptionService; import io.bitsquare.network.NetworkOptionKeys; import io.bitsquare.network.Socks5ProxyProvider; import io.bitsquare.p2p.messaging.*; import io.bitsquare.p2p.network.*; import io.bitsquare.p2p.peers.BanList; import io.bitsquare.p2p.peers.BroadcastHandler; import io.bitsquare.p2p.peers.Broadcaster; import io.bitsquare.p2p.peers.PeerManager; import io.bitsquare.p2p.peers.getdata.RequestDataManager; import io.bitsquare.p2p.peers.keepalive.KeepAliveManager; import io.bitsquare.p2p.peers.peerexchange.PeerExchangeManager; import io.bitsquare.p2p.seed.SeedNodesRepository; import io.bitsquare.p2p.storage.HashMapChangedListener; import io.bitsquare.p2p.storage.P2PDataStorage; import io.bitsquare.p2p.storage.messages.AddDataMessage; import io.bitsquare.p2p.storage.messages.BroadcastMessage; import io.bitsquare.p2p.storage.messages.RefreshTTLMessage; import io.bitsquare.p2p.storage.payload.MailboxStoragePayload; import io.bitsquare.p2p.storage.payload.StoragePayload; import io.bitsquare.p2p.storage.storageentry.ProtectedMailboxStorageEntry; import io.bitsquare.p2p.storage.storageentry.ProtectedStorageEntry; import io.bitsquare.storage.FileUtil; import io.bitsquare.storage.Storage; import javafx.beans.property.*; import org.fxmisc.easybind.EasyBind; import org.fxmisc.easybind.Subscription; import org.fxmisc.easybind.monadic.MonadicBinding; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.File; import java.nio.file.Paths; import java.security.PublicKey; import java.util.*; import java.util.concurrent.CopyOnWriteArraySet; import java.util.stream.Collectors; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; public class P2PService implements SetupListener, MessageListener, ConnectionListener, RequestDataManager.Listener, HashMapChangedListener { private static final Logger log = LoggerFactory.getLogger(P2PService.class); public static final int MAX_CONNECTIONS_DEFAULT = 12; private final SeedNodesRepository seedNodesRepository; private final int port; private final int maxConnections; private final File torDir; private Clock clock; //TODO optional can be removed as seednode are created with those objects now private final Optional<EncryptionService> optionalEncryptionService; private final Optional<KeyRing> optionalKeyRing; // set in init private NetworkNode networkNode; private Broadcaster broadcaster; private P2PDataStorage p2PDataStorage; private PeerManager peerManager; private RequestDataManager requestDataManager; private PeerExchangeManager peerExchangeManager; @SuppressWarnings("FieldCanBeLocal") private MonadicBinding<Boolean> networkReadyBinding; private final Set<DecryptedDirectMessageListener> decryptedDirectMessageListeners = new CopyOnWriteArraySet<>(); private final Set<DecryptedMailboxListener> decryptedMailboxListeners = new CopyOnWriteArraySet<>(); private final Set<P2PServiceListener> p2pServiceListeners = new CopyOnWriteArraySet<>(); private final Map<String, ProtectedMailboxStorageEntry> mailboxMap = new HashMap<>(); private final Set<Runnable> shutDownResultHandlers = new CopyOnWriteArraySet<>(); private final BooleanProperty hiddenServicePublished = new SimpleBooleanProperty(); private final BooleanProperty preliminaryDataReceived = new SimpleBooleanProperty(); private final IntegerProperty numConnectedPeers = new SimpleIntegerProperty(0); private volatile boolean shutDownInProgress; private boolean shutDownComplete; private Subscription networkReadySubscription; private boolean isBootstrapped; private KeepAliveManager keepAliveManager; private Socks5ProxyProvider socks5ProxyProvider; /////////////////////////////////////////////////////////////////////////////////////////// // Constructor /////////////////////////////////////////////////////////////////////////////////////////// // Called also from SeedNodeP2PService @Inject public P2PService(SeedNodesRepository seedNodesRepository, @Named(NetworkOptionKeys.PORT_KEY) int port, @Named(NetworkOptionKeys.TOR_DIR) File torDir, @Named(NetworkOptionKeys.USE_LOCALHOST) boolean useLocalhost, @Named(NetworkOptionKeys.NETWORK_ID) int networkId, @Named(NetworkOptionKeys.MAX_CONNECTIONS) int maxConnections, @Named(Storage.DIR_KEY) File storageDir, @Named(NetworkOptionKeys.SEED_NODES_KEY) String seedNodes, @Named(NetworkOptionKeys.MY_ADDRESS) String myAddress, @Named(NetworkOptionKeys.BAN_LIST) String banList, Clock clock, Socks5ProxyProvider socks5ProxyProvider, @Nullable EncryptionService encryptionService, @Nullable KeyRing keyRing) { this( seedNodesRepository, port, maxConnections, torDir, useLocalhost, networkId, storageDir, seedNodes, myAddress, banList, clock, socks5ProxyProvider, encryptionService, keyRing ); } @VisibleForTesting public P2PService(SeedNodesRepository seedNodesRepository, int port, int maxConnections, File torDir, boolean useLocalhost, int networkId, File storageDir, String seedNodes, String myAddress, String banList, Clock clock, Socks5ProxyProvider socks5ProxyProvider, @Nullable EncryptionService encryptionService, @Nullable KeyRing keyRing) { this.seedNodesRepository = seedNodesRepository; this.port = port; this.maxConnections = maxConnections; this.torDir = torDir; this.clock = clock; this.socks5ProxyProvider = socks5ProxyProvider; optionalEncryptionService = Optional.ofNullable(encryptionService); optionalKeyRing = Optional.ofNullable(keyRing); init(useLocalhost, networkId, storageDir, seedNodes, myAddress, banList); } private void init(boolean useLocalhost, int networkId, File storageDir, String seedNodes, String myAddress, String banList) { if (!useLocalhost) FileUtil.rollingBackup(new File(Paths.get(torDir.getAbsolutePath(), "hiddenservice").toString()), "private_key", 20); if (banList != null && !banList.isEmpty()) BanList.setList(Arrays.asList(banList.replace(" ", "").split(",")).stream().map(NodeAddress::new).collect(Collectors.toList())); if (myAddress != null && !myAddress.isEmpty()) seedNodesRepository.setNodeAddressToExclude(new NodeAddress(myAddress)); networkNode = useLocalhost ? new LocalhostNetworkNode(port) : new TorNetworkNode(port, torDir); networkNode.addConnectionListener(this); networkNode.addMessageListener(this); Set<NodeAddress> seedNodeAddresses; if (seedNodes != null && !seedNodes.isEmpty()) seedNodeAddresses = Arrays.asList(seedNodes.replace(" ", "").split(",")).stream().map(NodeAddress::new).collect(Collectors.toSet()); else seedNodeAddresses = seedNodesRepository.getSeedNodeAddresses(useLocalhost, networkId); peerManager = new PeerManager(networkNode, maxConnections, seedNodeAddresses, storageDir, clock); broadcaster = new Broadcaster(networkNode, peerManager); p2PDataStorage = new P2PDataStorage(broadcaster, networkNode, storageDir); p2PDataStorage.addHashMapChangedListener(this); requestDataManager = new RequestDataManager(networkNode, p2PDataStorage, peerManager, seedNodeAddresses, this); peerExchangeManager = new PeerExchangeManager(networkNode, peerManager, seedNodeAddresses); keepAliveManager = new KeepAliveManager(networkNode, peerManager); // We need to have both the initial data delivered and the hidden service published networkReadyBinding = EasyBind.combine(hiddenServicePublished, preliminaryDataReceived, (hiddenServicePublished, preliminaryDataReceived) -> hiddenServicePublished && preliminaryDataReceived); networkReadySubscription = networkReadyBinding.subscribe((observable, oldValue, newValue) -> { if (newValue) onNetworkReady(); }); } /////////////////////////////////////////////////////////////////////////////////////////// // API /////////////////////////////////////////////////////////////////////////////////////////// public void start(@Nullable P2PServiceListener listener) { Log.traceCall(); if (listener != null) addP2PServiceListener(listener); networkNode.start(this); } public void onAllServicesInitialized() { Log.traceCall(); if (networkNode.getNodeAddress() != null) { p2PDataStorage.getMap().values().stream().forEach(protectedStorageEntry -> { if (protectedStorageEntry instanceof ProtectedMailboxStorageEntry) processProtectedMailboxStorageEntry((ProtectedMailboxStorageEntry) protectedStorageEntry); }); } else { networkNode.nodeAddressProperty().addListener((observable, oldValue, newValue) -> { if (newValue != null) { p2PDataStorage.getMap().values().stream().forEach(protectedStorageEntry -> { if (protectedStorageEntry instanceof ProtectedMailboxStorageEntry) processProtectedMailboxStorageEntry((ProtectedMailboxStorageEntry) protectedStorageEntry); }); } }); } } public void shutDown(Runnable shutDownCompleteHandler) { Log.traceCall(); if (!shutDownInProgress) { shutDownInProgress = true; shutDownResultHandlers.add(shutDownCompleteHandler); if (p2PDataStorage != null) p2PDataStorage.shutDown(); if (peerManager != null) peerManager.shutDown(); if (broadcaster != null) broadcaster.shutDown(); if (requestDataManager != null) requestDataManager.shutDown(); if (peerExchangeManager != null) peerExchangeManager.shutDown(); if (keepAliveManager != null) keepAliveManager.shutDown(); if (networkNode != null) networkNode.shutDown(() -> { shutDownResultHandlers.stream().forEach(Runnable::run); shutDownComplete = true; }); if (networkReadySubscription != null) networkReadySubscription.unsubscribe(); } else { log.debug("shutDown already in progress"); if (shutDownComplete) { shutDownCompleteHandler.run(); } else { shutDownResultHandlers.add(shutDownCompleteHandler); } } } /** * Startup sequence: * <p> * Variant 1 (normal expected mode): * onTorNodeReady -> requestDataManager.firstDataRequestFromAnySeedNode() * RequestDataManager.Listener.onDataReceived && onHiddenServicePublished -> onNetworkReady() * <p> * Variant 2 (no seed node available): * onTorNodeReady -> requestDataManager.firstDataRequestFromAnySeedNode * retry after 20-30 sec until we get at least one seed node connected * RequestDataManager.Listener.onDataReceived && onHiddenServicePublished -> onNetworkReady() */ /////////////////////////////////////////////////////////////////////////////////////////// // SetupListener implementation /////////////////////////////////////////////////////////////////////////////////////////// @Override public void onTorNodeReady() { Log.traceCall(); socks5ProxyProvider.setSocks5ProxyInternal(networkNode.getSocksProxy()); requestDataManager.requestPreliminaryData(); keepAliveManager.start(); p2pServiceListeners.stream().forEach(SetupListener::onTorNodeReady); } @Override public void onHiddenServicePublished() { Log.traceCall(); checkArgument(networkNode.getNodeAddress() != null, "Address must be set when we have the hidden service ready"); hiddenServicePublished.set(true); p2pServiceListeners.stream().forEach(SetupListener::onHiddenServicePublished); } @Override public void onSetupFailed(Throwable throwable) { Log.traceCall(); p2pServiceListeners.stream().forEach(e -> e.onSetupFailed(throwable)); } // Called from networkReadyBinding private void onNetworkReady() { Log.traceCall(); networkReadySubscription.unsubscribe(); Optional<NodeAddress> seedNodeOfPreliminaryDataRequest = requestDataManager.getNodeAddressOfPreliminaryDataRequest(); checkArgument(seedNodeOfPreliminaryDataRequest.isPresent(), "seedNodeOfPreliminaryDataRequest must be present"); requestDataManager.requestUpdateData(); } /////////////////////////////////////////////////////////////////////////////////////////// // RequestDataManager.Listener implementation /////////////////////////////////////////////////////////////////////////////////////////// @Override public void onPreliminaryDataReceived() { checkArgument(!preliminaryDataReceived.get(), "preliminaryDataReceived was already set before."); preliminaryDataReceived.set(true); } @Override public void onUpdatedDataReceived() { Optional<NodeAddress> seedNodeOfPreliminaryDataRequest = requestDataManager.getNodeAddressOfPreliminaryDataRequest(); checkArgument(seedNodeOfPreliminaryDataRequest.isPresent(), "seedNodeOfPreliminaryDataRequest must be present"); peerExchangeManager.requestReportedPeersFromSeedNodes(seedNodeOfPreliminaryDataRequest.get()); if (!isBootstrapped) { isBootstrapped = true; p2pServiceListeners.stream().forEach(P2PServiceListener::onBootstrapComplete); p2PDataStorage.onBootstrapComplete(); } } @Override public void onNoSeedNodeAvailable() { p2pServiceListeners.stream().forEach(P2PServiceListener::onNoSeedNodeAvailable); } @Override public void onNoPeersAvailable() { p2pServiceListeners.stream().forEach(P2PServiceListener::onNoPeersAvailable); } @Override public void onDataReceived() { p2pServiceListeners.stream().forEach(P2PServiceListener::onRequestingDataCompleted); } /////////////////////////////////////////////////////////////////////////////////////////// // ConnectionListener implementation /////////////////////////////////////////////////////////////////////////////////////////// @Override public void onConnection(Connection connection) { numConnectedPeers.set(networkNode.getAllConnections().size()); //TODO check if still needed and why UserThread.runAfter(() -> numConnectedPeers.set(networkNode.getAllConnections().size()), 3); } @Override public void onDisconnect(CloseConnectionReason closeConnectionReason, Connection connection) { Log.traceCall(); numConnectedPeers.set(networkNode.getAllConnections().size()); //TODO check if still needed and why UserThread.runAfter(() -> numConnectedPeers.set(networkNode.getAllConnections().size()), 3); } @Override public void onError(Throwable throwable) { } /////////////////////////////////////////////////////////////////////////////////////////// // MessageListener implementation /////////////////////////////////////////////////////////////////////////////////////////// @Override public void onMessage(Message message, Connection connection) { if (message instanceof PrefixedSealedAndSignedMessage) { Log.traceCall("\n\t" + message.toString() + "\n\tconnection=" + connection); // Seed nodes don't have set the encryptionService if (optionalEncryptionService.isPresent()) { try { PrefixedSealedAndSignedMessage prefixedSealedAndSignedMessage = (PrefixedSealedAndSignedMessage) message; if (verifyAddressPrefixHash(prefixedSealedAndSignedMessage)) { // We set connectionType to that connection to avoid that is get closed when // we get too many connection attempts. connection.setPeerType(Connection.PeerType.DIRECT_MSG_PEER); log.debug("Try to decrypt..."); DecryptedMsgWithPubKey decryptedMsgWithPubKey = optionalEncryptionService.get().decryptAndVerify( prefixedSealedAndSignedMessage.sealedAndSigned); log.debug("\n\nDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD\n" + "Decrypted SealedAndSignedMessage:\ndecryptedMsgWithPubKey={}" + "\nDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD\n", decryptedMsgWithPubKey); if (connection.getPeersNodeAddressOptional().isPresent()) decryptedDirectMessageListeners.stream().forEach( e -> e.onDirectMessage(decryptedMsgWithPubKey, connection.getPeersNodeAddressOptional().get())); else log.error("peersNodeAddress is not available at onMessage."); } else { log.debug("Wrong receiverAddressMaskHash. The message is not intended for us."); } } catch (CryptoException e) { log.debug(message.toString()); log.debug(e.toString()); log.debug("Decryption of prefixedSealedAndSignedMessage.sealedAndSigned failed. " + "That is expected if the message is not intended for us."); } } } } /////////////////////////////////////////////////////////////////////////////////////////// // HashMapChangedListener implementation /////////////////////////////////////////////////////////////////////////////////////////// @Override public void onAdded(ProtectedStorageEntry protectedStorageEntry) { if (protectedStorageEntry instanceof ProtectedMailboxStorageEntry) processProtectedMailboxStorageEntry((ProtectedMailboxStorageEntry) protectedStorageEntry); } @Override public void onRemoved(ProtectedStorageEntry data) { } /////////////////////////////////////////////////////////////////////////////////////////// // DirectMessages /////////////////////////////////////////////////////////////////////////////////////////// public void sendEncryptedDirectMessage(NodeAddress peerNodeAddress, PubKeyRing pubKeyRing, DirectMessage message, SendDirectMessageListener sendDirectMessageListener) { Log.traceCall(); checkNotNull(peerNodeAddress, "PeerAddress must not be null (sendEncryptedDirectMessage)"); if (isBootstrapped()) { doSendEncryptedDirectMessage(peerNodeAddress, pubKeyRing, message, sendDirectMessageListener); } else { throw new NetworkNotReadyException(); } } private void doSendEncryptedDirectMessage(@NotNull NodeAddress peersNodeAddress, PubKeyRing pubKeyRing, DirectMessage message, SendDirectMessageListener sendDirectMessageListener) { Log.traceCall(); checkNotNull(peersNodeAddress, "Peer node address must not be null at doSendEncryptedDirectMessage"); checkArgument(optionalEncryptionService.isPresent(), "EncryptionService not set. Seems that is called on a seed node which must not happen."); checkNotNull(networkNode.getNodeAddress(), "My node address must not be null at doSendEncryptedDirectMessage"); try { log.debug("\n\nEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE\n" + "Encrypt message:\nmessage={}" + "\nEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE\n", message); PrefixedSealedAndSignedMessage prefixedSealedAndSignedMessage = new PrefixedSealedAndSignedMessage(networkNode.getNodeAddress(), optionalEncryptionService.get().encryptAndSign(pubKeyRing, message), peersNodeAddress.getAddressPrefixHash()); SettableFuture<Connection> future = networkNode.sendMessage(peersNodeAddress, prefixedSealedAndSignedMessage); Futures.addCallback(future, new FutureCallback<Connection>() { @Override public void onSuccess(@Nullable Connection connection) { sendDirectMessageListener.onArrived(); } @Override public void onFailure(@NotNull Throwable throwable) { throwable.printStackTrace(); sendDirectMessageListener.onFault(); } }); } catch (CryptoException e) { e.printStackTrace(); log.error(message.toString()); log.error(e.toString()); sendDirectMessageListener.onFault(); } } /////////////////////////////////////////////////////////////////////////////////////////// // MailboxMessages /////////////////////////////////////////////////////////////////////////////////////////// private void processProtectedMailboxStorageEntry(ProtectedMailboxStorageEntry protectedMailboxStorageEntry) { Log.traceCall(); final NodeAddress nodeAddress = networkNode.getNodeAddress(); // Seed nodes don't receive mailbox messages if (optionalEncryptionService.isPresent() && nodeAddress != null && !seedNodesRepository.isSeedNode(nodeAddress)) { Log.traceCall(); MailboxStoragePayload mailboxStoragePayload = protectedMailboxStorageEntry.getMailboxStoragePayload(); PrefixedSealedAndSignedMessage prefixedSealedAndSignedMessage = mailboxStoragePayload.prefixedSealedAndSignedMessage; if (verifyAddressPrefixHash(prefixedSealedAndSignedMessage)) { try { DecryptedMsgWithPubKey decryptedMsgWithPubKey = optionalEncryptionService.get().decryptAndVerify( prefixedSealedAndSignedMessage.sealedAndSigned); if (decryptedMsgWithPubKey.message instanceof MailboxMessage) { MailboxMessage mailboxMessage = (MailboxMessage) decryptedMsgWithPubKey.message; NodeAddress senderNodeAddress = mailboxMessage.getSenderNodeAddress(); checkNotNull(senderNodeAddress, "senderAddress must not be null for mailbox messages"); mailboxMap.put(mailboxMessage.getUID(), protectedMailboxStorageEntry); log.trace("Decryption of SealedAndSignedMessage succeeded. senderAddress=" + senderNodeAddress + " / my address=" + getAddress()); decryptedMailboxListeners.stream().forEach( e -> e.onMailboxMessageAdded(decryptedMsgWithPubKey, senderNodeAddress)); } else { log.warn("tryDecryptMailboxData: Expected MailboxMessage but got other type. " + "decryptedMsgWithPubKey.message=", decryptedMsgWithPubKey.message); } } catch (CryptoException e) { log.debug(e.toString()); log.debug("Decryption of prefixedSealedAndSignedMessage.sealedAndSigned failed. " + "That is expected if the message is not intended for us."); } } else { log.debug("Wrong blurredAddressHash. The message is not intended for us."); } } } public void sendEncryptedMailboxMessage(NodeAddress peersNodeAddress, PubKeyRing peersPubKeyRing, MailboxMessage message, SendMailboxMessageListener sendMailboxMessageListener) { Log.traceCall("message " + message); checkNotNull(peersNodeAddress, "PeerAddress must not be null (sendEncryptedMailboxMessage)"); checkNotNull(networkNode.getNodeAddress(), "My node address must not be null at sendEncryptedMailboxMessage"); checkArgument(optionalKeyRing.isPresent(), "keyRing not set. Seems that is called on a seed node which must not happen."); checkArgument(!optionalKeyRing.get().getPubKeyRing().equals(peersPubKeyRing), "We got own keyring instead of that from peer"); checkArgument(optionalEncryptionService.isPresent(), "EncryptionService not set. Seems that is called on a seed node which must not happen."); if (isBootstrapped()) { if (!networkNode.getAllConnections().isEmpty()) { try { log.debug("\n\nEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE\n" + "Encrypt message:\nmessage={}" + "\nEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE\n", message); PrefixedSealedAndSignedMessage prefixedSealedAndSignedMessage = new PrefixedSealedAndSignedMessage( networkNode.getNodeAddress(), optionalEncryptionService.get().encryptAndSign(peersPubKeyRing, message), peersNodeAddress.getAddressPrefixHash()); SettableFuture<Connection> future = networkNode.sendMessage(peersNodeAddress, prefixedSealedAndSignedMessage); Futures.addCallback(future, new FutureCallback<Connection>() { @Override public void onSuccess(@Nullable Connection connection) { log.trace("SendEncryptedMailboxMessage onSuccess"); sendMailboxMessageListener.onArrived(); } @Override public void onFailure(@NotNull Throwable throwable) { log.trace("SendEncryptedMailboxMessage onFailure"); log.debug(throwable.toString()); log.debug("We cannot send message to peer. Peer might be offline. We will store message in mailbox."); log.trace("create MailboxEntry with peerAddress " + peersNodeAddress); PublicKey receiverStoragePublicKey = peersPubKeyRing.getSignaturePubKey(); addMailboxData(new MailboxStoragePayload(prefixedSealedAndSignedMessage, optionalKeyRing.get().getSignatureKeyPair().getPublic(), receiverStoragePublicKey), receiverStoragePublicKey, sendMailboxMessageListener); } }); } catch (CryptoException e) { log.error("sendEncryptedMessage failed"); e.printStackTrace(); sendMailboxMessageListener.onFault("sendEncryptedMailboxMessage failed " + e); } } else { sendMailboxMessageListener.onFault("There are no P2P network nodes connected. " + "Please check your internet connection."); } } else { throw new NetworkNotReadyException(); } } private void addMailboxData(MailboxStoragePayload expirableMailboxStoragePayload, PublicKey receiversPublicKey, SendMailboxMessageListener sendMailboxMessageListener) { Log.traceCall(); checkArgument(optionalKeyRing.isPresent(), "keyRing not set. Seems that is called on a seed node which must not happen."); if (isBootstrapped()) { if (!networkNode.getAllConnections().isEmpty()) { try { ProtectedMailboxStorageEntry protectedMailboxStorageEntry = p2PDataStorage.getMailboxDataWithSignedSeqNr( expirableMailboxStoragePayload, optionalKeyRing.get().getSignatureKeyPair(), receiversPublicKey); BroadcastHandler.Listener listener = new BroadcastHandler.Listener() { @Override public void onBroadcasted(BroadcastMessage message, int numOfCompletedBroadcasts) { } @Override public void onBroadcastedToFirstPeer(BroadcastMessage message) { // The reason for that check was to separate different callback for different send calls. // We only want to notify our sendMailboxMessageListener for the calls he is interested in. if (message instanceof AddDataMessage && ((AddDataMessage) message).protectedStorageEntry.equals(protectedMailboxStorageEntry)) { // We delay a bit to give more time for sufficient propagation in the P2P network. // This should help to avoid situations where a user closes the app too early and the msg // does not arrive. // We could use onBroadcastCompleted instead but it might take too long if one peer // is very badly connected. // TODO We could check for a certain threshold of no. of incoming messages of the same msg // to see how well it is propagated. BitcoinJ uses such an approach for tx propagation. UserThread.runAfter(() -> { log.info("Broadcasted to first peer (with 3 sec. delayed): Message = {}", Utilities.toTruncatedString(message)); sendMailboxMessageListener.onStoredInMailbox(); }, 3); } } @Override public void onBroadcastCompleted(BroadcastMessage message, int numOfCompletedBroadcasts, int numOfFailedBroadcasts) { log.info("Broadcast completed: Sent to {} peers (failed: {}). Message = {}", numOfCompletedBroadcasts, numOfFailedBroadcasts, Utilities.toTruncatedString(message)); if (numOfCompletedBroadcasts == 0) sendMailboxMessageListener.onFault("Broadcast completed without any successful broadcast"); } @Override public void onBroadcastFailed(String errorMessage) { // TODO investigate why not sending sendMailboxMessageListener.onFault. Related probably // to the logic from BroadcastHandler.sendToPeer } }; boolean result = p2PDataStorage.add(protectedMailboxStorageEntry, networkNode.getNodeAddress(), listener, true); if (!result) { //TODO remove and add again with a delay to ensure the data will be broadcasted // The p2PDataStorage.remove makes probably sense but need to be analysed more. // Don't change that if it is not 100% clear. sendMailboxMessageListener.onFault("Data already exists in our local database"); boolean removeResult = p2PDataStorage.remove(protectedMailboxStorageEntry, networkNode.getNodeAddress(), true); log.debug("remove result=" + removeResult); } } catch (CryptoException e) { log.error("Signing at getDataWithSignedSeqNr failed. That should never happen."); } } else { sendMailboxMessageListener.onFault("There are no P2P network nodes connected. " + "Please check your internet connection."); } } else { throw new NetworkNotReadyException(); } } public void removeEntryFromMailbox(DecryptedMsgWithPubKey decryptedMsgWithPubKey) { // We need to delay a bit to avoid that we remove our msg then get it from other peers again and reapply it again. // If we delay the removal we have better chances that repeated messages we got from other peers are already filtered // at the P2PService layer. // Though we have to check in the client classes to not apply the same message again as there is no guarantee // when we would get a message again from the network. UserThread.runAfter(() -> { delayedRemoveEntryFromMailbox(decryptedMsgWithPubKey); }, 2); } private void delayedRemoveEntryFromMailbox(DecryptedMsgWithPubKey decryptedMsgWithPubKey) { Log.traceCall(); checkArgument(optionalKeyRing.isPresent(), "keyRing not set. Seems that is called on a seed node which must not happen."); if (isBootstrapped()) { MailboxMessage mailboxMessage = (MailboxMessage) decryptedMsgWithPubKey.message; String uid = mailboxMessage.getUID(); if (mailboxMap.containsKey(uid)) { ProtectedMailboxStorageEntry mailboxData = mailboxMap.get(uid); if (mailboxData != null && mailboxData.getStoragePayload() instanceof MailboxStoragePayload) { MailboxStoragePayload expirableMailboxStoragePayload = (MailboxStoragePayload) mailboxData.getStoragePayload(); PublicKey receiversPubKey = mailboxData.receiversPubKey; checkArgument(receiversPubKey.equals(optionalKeyRing.get().getSignatureKeyPair().getPublic()), "receiversPubKey is not matching with our key. That must not happen."); try { ProtectedMailboxStorageEntry protectedMailboxStorageEntry = p2PDataStorage.getMailboxDataWithSignedSeqNr( expirableMailboxStoragePayload, optionalKeyRing.get().getSignatureKeyPair(), receiversPubKey); p2PDataStorage.removeMailboxData(protectedMailboxStorageEntry, networkNode.getNodeAddress(), true); } catch (CryptoException e) { log.error("Signing at getDataWithSignedSeqNr failed. That should never happen."); } mailboxMap.remove(uid); log.trace("Removed successfully decryptedMsgWithPubKey."); } } else { log.warn("uid for mailbox entry not found in mailboxMap. That should never happen." + "\n\tuid={}\n\tmailboxMap={}\n\tmailboxMessage={}", uid, mailboxMap, mailboxMessage); } } else { throw new NetworkNotReadyException(); } } /////////////////////////////////////////////////////////////////////////////////////////// // Data storage /////////////////////////////////////////////////////////////////////////////////////////// public boolean addData(StoragePayload storagePayload, boolean isDataOwner) { Log.traceCall(); checkArgument(optionalKeyRing.isPresent(), "keyRing not set. Seems that is called on a seed node which must not happen."); if (isBootstrapped()) { try { ProtectedStorageEntry protectedStorageEntry = p2PDataStorage.getProtectedData(storagePayload, optionalKeyRing.get().getSignatureKeyPair()); return p2PDataStorage.add(protectedStorageEntry, networkNode.getNodeAddress(), null, isDataOwner); } catch (CryptoException e) { log.error("Signing at getDataWithSignedSeqNr failed. That should never happen."); return false; } } else { throw new NetworkNotReadyException(); } } public boolean refreshTTL(StoragePayload storagePayload, boolean isDataOwner) { Log.traceCall(); checkArgument(optionalKeyRing.isPresent(), "keyRing not set. Seems that is called on a seed node which must not happen."); if (isBootstrapped()) { try { RefreshTTLMessage refreshTTLMessage = p2PDataStorage.getRefreshTTLMessage(storagePayload, optionalKeyRing.get().getSignatureKeyPair()); return p2PDataStorage.refreshTTL(refreshTTLMessage, networkNode.getNodeAddress(), isDataOwner); } catch (CryptoException e) { log.error("Signing at getDataWithSignedSeqNr failed. That should never happen."); return false; } } else { throw new NetworkNotReadyException(); } } public boolean removeData(StoragePayload storagePayload, boolean isDataOwner) { Log.traceCall(); checkArgument(optionalKeyRing.isPresent(), "keyRing not set. Seems that is called on a seed node which must not happen."); if (isBootstrapped()) { try { ProtectedStorageEntry protectedStorageEntry = p2PDataStorage.getProtectedData(storagePayload, optionalKeyRing.get().getSignatureKeyPair()); return p2PDataStorage.remove(protectedStorageEntry, networkNode.getNodeAddress(), isDataOwner); } catch (CryptoException e) { log.error("Signing at getDataWithSignedSeqNr failed. That should never happen."); return false; } } else { throw new NetworkNotReadyException(); } } /////////////////////////////////////////////////////////////////////////////////////////// // Listeners /////////////////////////////////////////////////////////////////////////////////////////// public void addDecryptedDirectMessageListener(DecryptedDirectMessageListener listener) { decryptedDirectMessageListeners.add(listener); } public void removeDecryptedDirectMessageListener(DecryptedDirectMessageListener listener) { decryptedDirectMessageListeners.remove(listener); } public void addDecryptedMailboxListener(DecryptedMailboxListener listener) { decryptedMailboxListeners.add(listener); } public void addP2PServiceListener(P2PServiceListener listener) { p2pServiceListeners.add(listener); } public void removeP2PServiceListener(P2PServiceListener listener) { if (p2pServiceListeners.contains(listener)) p2pServiceListeners.remove(listener); } public void addHashSetChangedListener(HashMapChangedListener hashMapChangedListener) { p2PDataStorage.addHashMapChangedListener(hashMapChangedListener); } public void removeHashMapChangedListener(HashMapChangedListener hashMapChangedListener) { p2PDataStorage.removeHashMapChangedListener(hashMapChangedListener); } /////////////////////////////////////////////////////////////////////////////////////////// // Getters /////////////////////////////////////////////////////////////////////////////////////////// public boolean isBootstrapped() { return isBootstrapped; } public NetworkNode getNetworkNode() { return networkNode; } public NodeAddress getAddress() { return networkNode.getNodeAddress(); } public ReadOnlyIntegerProperty getNumConnectedPeers() { return numConnectedPeers; } public Map<P2PDataStorage.ByteArray, ProtectedStorageEntry> getDataMap() { return p2PDataStorage.getMap(); } @VisibleForTesting public P2PDataStorage getP2PDataStorage() { return p2PDataStorage; } @VisibleForTesting public PeerManager getPeerManager() { return peerManager; } @VisibleForTesting @Nullable public KeyRing getKeyRing() { return optionalKeyRing.isPresent() ? optionalKeyRing.get() : null; } /////////////////////////////////////////////////////////////////////////////////////////// // Private /////////////////////////////////////////////////////////////////////////////////////////// private boolean verifyAddressPrefixHash(PrefixedSealedAndSignedMessage prefixedSealedAndSignedMessage) { if (networkNode.getNodeAddress() != null) { byte[] blurredAddressHash = networkNode.getNodeAddress().getAddressPrefixHash(); return blurredAddressHash != null && Arrays.equals(blurredAddressHash, prefixedSealedAndSignedMessage.addressPrefixHash); } else { log.debug("myOnionAddress is null at verifyAddressPrefixHash. That is expected at startup."); return false; } } }