mirror of
https://github.com/haveno-dex/haveno.git
synced 2025-07-25 07:55:21 -04:00
add missing files
This commit is contained in:
parent
c6ece486ed
commit
9ef8b42509
239 changed files with 20558 additions and 51 deletions
48
network/src/main/java/io/bitsquare/p2p/Address.java
Normal file
48
network/src/main/java/io/bitsquare/p2p/Address.java
Normal file
|
@ -0,0 +1,48 @@
|
|||
package io.bitsquare.p2p;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class Address implements Serializable {
|
||||
public final String hostName;
|
||||
public final int port;
|
||||
|
||||
public Address(String hostName, int port) {
|
||||
this.hostName = hostName;
|
||||
this.port = port;
|
||||
}
|
||||
|
||||
public Address(String fullAddress) {
|
||||
final String[] split = fullAddress.split(Pattern.quote(":"));
|
||||
this.hostName = split[0];
|
||||
this.port = Integer.parseInt(split[1]);
|
||||
}
|
||||
|
||||
public String getFullAddress() {
|
||||
return hostName + ":" + port;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (!(o instanceof Address)) return false;
|
||||
|
||||
Address address = (Address) o;
|
||||
|
||||
if (port != address.port) return false;
|
||||
return !(hostName != null ? !hostName.equals(address.hostName) : address.hostName != null);
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = hostName != null ? hostName.hashCode() : 0;
|
||||
result = 31 * result + port;
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return getFullAddress();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
package io.bitsquare.p2p;
|
||||
|
||||
public class AuthenticationException extends RuntimeException {
|
||||
public AuthenticationException() {
|
||||
}
|
||||
|
||||
public AuthenticationException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public AuthenticationException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
public AuthenticationException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
|
||||
public AuthenticationException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
|
||||
super(message, cause, enableSuppression, writableStackTrace);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
package io.bitsquare.p2p;
|
||||
|
||||
import com.google.inject.Inject;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class NetworkStatistics {
|
||||
private static final Logger log = LoggerFactory.getLogger(NetworkStatistics.class);
|
||||
|
||||
@Inject
|
||||
public NetworkStatistics() {
|
||||
|
||||
}
|
||||
}
|
|
@ -49,6 +49,7 @@ public class P2PService {
|
|||
private static final Logger log = LoggerFactory.getLogger(P2PService.class);
|
||||
|
||||
private final EncryptionService encryptionService;
|
||||
private final SetupListener setupListener;
|
||||
private KeyRing keyRing;
|
||||
private final NetworkStatistics networkStatistics;
|
||||
|
||||
|
@ -108,6 +109,58 @@ public class P2PService {
|
|||
|
||||
|
||||
// Listeners
|
||||
setupListener = new SetupListener() {
|
||||
@Override
|
||||
public void onTorNodeReady() {
|
||||
UserThread.execute(() -> p2pServiceListeners.stream().forEach(e -> e.onTorNodeReady()));
|
||||
|
||||
// we don't know yet our own address so we can not filter that from the
|
||||
// seedNodeAddresses in case we are a seed node
|
||||
sendGetAllDataMessage(seedNodeAddresses);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onHiddenServiceReady() {
|
||||
hiddenServiceReady = true;
|
||||
tryStartAuthentication();
|
||||
|
||||
UserThread.execute(() -> p2pServiceListeners.stream().forEach(e -> e.onHiddenServiceReady()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetupFailed(Throwable throwable) {
|
||||
UserThread.execute(() -> p2pServiceListeners.stream().forEach(e -> e.onSetupFailed(throwable)));
|
||||
}
|
||||
};
|
||||
|
||||
networkNode.addConnectionListener(new ConnectionListener() {
|
||||
@Override
|
||||
public void onConnection(Connection connection) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPeerAddressAuthenticated(Address peerAddress, Connection connection) {
|
||||
authenticatedPeerAddresses.add(peerAddress);
|
||||
authenticatedToFirstPeer = true;
|
||||
|
||||
P2PService.this.authenticated = true;
|
||||
dataStorage.setAuthenticated(true);
|
||||
UserThread.execute(() -> p2pServiceListeners.stream().forEach(e -> e.onAuthenticated()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisconnect(Reason reason, Connection connection) {
|
||||
Address peerAddress = connection.getPeerAddress();
|
||||
if (peerAddress != null)
|
||||
authenticatedPeerAddresses.remove(peerAddress);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Throwable throwable) {
|
||||
log.error("onError self/ConnectionException " + networkNode.getAddress() + "/" + throwable);
|
||||
}
|
||||
});
|
||||
|
||||
networkNode.addMessageListener((message, connection) -> {
|
||||
if (message instanceof GetDataSetMessage) {
|
||||
log.trace("Received GetAllDataMessage: " + message);
|
||||
|
@ -201,57 +254,7 @@ public class P2PService {
|
|||
if (listener != null)
|
||||
addP2PServiceListener(listener);
|
||||
|
||||
networkNode.start(new SetupListener() {
|
||||
@Override
|
||||
public void onTorNodeReady() {
|
||||
UserThread.execute(() -> p2pServiceListeners.stream().forEach(e -> e.onTorNodeReady()));
|
||||
|
||||
// we don't know yet our own address so we can not filter that from the
|
||||
// seedNodeAddresses in case we are a seed node
|
||||
sendGetAllDataMessage(seedNodeAddresses);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onHiddenServiceReady() {
|
||||
hiddenServiceReady = true;
|
||||
tryStartAuthentication();
|
||||
|
||||
UserThread.execute(() -> p2pServiceListeners.stream().forEach(e -> e.onHiddenServiceReady()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetupFailed(Throwable throwable) {
|
||||
UserThread.execute(() -> p2pServiceListeners.stream().forEach(e -> e.onSetupFailed(throwable)));
|
||||
}
|
||||
});
|
||||
|
||||
networkNode.addConnectionListener(new ConnectionListener() {
|
||||
@Override
|
||||
public void onConnection(Connection connection) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPeerAddressAuthenticated(Address peerAddress, Connection connection) {
|
||||
authenticatedPeerAddresses.add(peerAddress);
|
||||
authenticatedToFirstPeer = true;
|
||||
|
||||
P2PService.this.authenticated = true;
|
||||
dataStorage.setAuthenticated(true);
|
||||
UserThread.execute(() -> p2pServiceListeners.stream().forEach(e -> e.onAuthenticated()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisconnect(Reason reason, Connection connection) {
|
||||
Address peerAddress = connection.getPeerAddress();
|
||||
if (peerAddress != null)
|
||||
authenticatedPeerAddresses.remove(peerAddress);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Throwable throwable) {
|
||||
log.error("onError self/ConnectionException " + networkNode.getAddress() + "/" + throwable);
|
||||
}
|
||||
});
|
||||
networkNode.start(setupListener);
|
||||
}
|
||||
|
||||
public void shutDown(Runnable shutDownCompleteHandler) {
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
package io.bitsquare.p2p;
|
||||
|
||||
|
||||
import io.bitsquare.p2p.network.SetupListener;
|
||||
|
||||
public interface P2PServiceListener extends SetupListener {
|
||||
|
||||
void onAllDataReceived();
|
||||
|
||||
void onAuthenticated();
|
||||
}
|
96
network/src/main/java/io/bitsquare/p2p/Utils.java
Normal file
96
network/src/main/java/io/bitsquare/p2p/Utils.java
Normal file
|
@ -0,0 +1,96 @@
|
|||
package io.bitsquare.p2p;
|
||||
|
||||
import io.bitsquare.common.ByteArrayUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.Serializable;
|
||||
import java.net.ServerSocket;
|
||||
import java.util.Random;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.zip.DataFormatException;
|
||||
import java.util.zip.Deflater;
|
||||
import java.util.zip.Inflater;
|
||||
|
||||
public class Utils {
|
||||
private static final Logger log = LoggerFactory.getLogger(Utils.class);
|
||||
|
||||
public static int findFreeSystemPort() {
|
||||
try {
|
||||
ServerSocket server = new ServerSocket(0);
|
||||
int port = server.getLocalPort();
|
||||
server.close();
|
||||
return port;
|
||||
} catch (IOException ignored) {
|
||||
} finally {
|
||||
return new Random().nextInt(10000) + 50000;
|
||||
}
|
||||
}
|
||||
|
||||
public static void shutDownExecutorService(ExecutorService executorService) {
|
||||
shutDownExecutorService(executorService, 200);
|
||||
}
|
||||
|
||||
public static void shutDownExecutorService(ExecutorService executorService, long waitBeforeShutDown) {
|
||||
executorService.shutdown();
|
||||
try {
|
||||
executorService.awaitTermination(waitBeforeShutDown, TimeUnit.MILLISECONDS);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
executorService.shutdownNow();
|
||||
}
|
||||
|
||||
public static byte[] compress(Serializable input) {
|
||||
return compress(ByteArrayUtils.objectToByteArray(input));
|
||||
}
|
||||
|
||||
public static byte[] compress(byte[] input) {
|
||||
Deflater compressor = new Deflater();
|
||||
compressor.setLevel(Deflater.BEST_SPEED);
|
||||
compressor.setInput(input);
|
||||
compressor.finish();
|
||||
ByteArrayOutputStream bos = new ByteArrayOutputStream(input.length);
|
||||
byte[] buf = new byte[8192];
|
||||
while (!compressor.finished()) {
|
||||
int count = compressor.deflate(buf);
|
||||
bos.write(buf, 0, count);
|
||||
}
|
||||
try {
|
||||
bos.close();
|
||||
} catch (IOException e) {
|
||||
}
|
||||
return bos.toByteArray();
|
||||
}
|
||||
|
||||
public static byte[] decompress(byte[] compressedData, int offset, int length) {
|
||||
Inflater decompressor = new Inflater();
|
||||
decompressor.setInput(compressedData, offset, length);
|
||||
ByteArrayOutputStream bos = new ByteArrayOutputStream(length);
|
||||
byte[] buf = new byte[8192];
|
||||
while (!decompressor.finished()) {
|
||||
try {
|
||||
int count = decompressor.inflate(buf);
|
||||
bos.write(buf, 0, count);
|
||||
} catch (DataFormatException e) {
|
||||
e.printStackTrace();
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
try {
|
||||
bos.close();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
return bos.toByteArray();
|
||||
}
|
||||
|
||||
public static Serializable decompress(byte[] compressedData) {
|
||||
return (Serializable) ByteArrayUtils.byteArrayToObject(decompress(compressedData, 0, compressedData.length));
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
package io.bitsquare.p2p.messaging;
|
||||
|
||||
import io.bitsquare.p2p.Address;
|
||||
|
||||
public interface DecryptedMailListener {
|
||||
|
||||
void onMailMessage(DecryptedMessageWithPubKey decryptedMessageWithPubKey, Address peerAddress);
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
package io.bitsquare.p2p.messaging;
|
||||
|
||||
import io.bitsquare.p2p.Address;
|
||||
|
||||
public interface DecryptedMailboxListener {
|
||||
|
||||
void onMailboxMessageAdded(DecryptedMessageWithPubKey decryptedMessageWithPubKey, Address senderAddress);
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
/*
|
||||
* This file is part of Bitsquare.
|
||||
*
|
||||
* Bitsquare 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.
|
||||
*
|
||||
* Bitsquare 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 Bitsquare. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package io.bitsquare.p2p.messaging;
|
||||
|
||||
import io.bitsquare.app.Version;
|
||||
import io.bitsquare.p2p.Message;
|
||||
|
||||
import java.security.PublicKey;
|
||||
|
||||
public final class DecryptedMessageWithPubKey implements MailMessage {
|
||||
// That object is sent over the wire, so we need to take care of version compatibility.
|
||||
private static final long serialVersionUID = Version.NETWORK_PROTOCOL_VERSION;
|
||||
|
||||
public final Message message;
|
||||
public final PublicKey signaturePubKey;
|
||||
|
||||
public DecryptedMessageWithPubKey(Message message, PublicKey signaturePubKey) {
|
||||
this.message = message;
|
||||
this.signaturePubKey = signaturePubKey;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (!(o instanceof DecryptedMessageWithPubKey)) return false;
|
||||
|
||||
DecryptedMessageWithPubKey that = (DecryptedMessageWithPubKey) o;
|
||||
|
||||
if (message != null ? !message.equals(that.message) : that.message != null) return false;
|
||||
return !(signaturePubKey != null ? !signaturePubKey.equals(that.signaturePubKey) : that.signaturePubKey != null);
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = message != null ? message.hashCode() : 0;
|
||||
result = 31 * result + (signaturePubKey != null ? signaturePubKey.hashCode() : 0);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "DecryptedMessageWithPubKey{" +
|
||||
"hashCode=" + hashCode() +
|
||||
", message=" + message +
|
||||
", signaturePubKey.hashCode()=" + signaturePubKey.hashCode() +
|
||||
'}';
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* This file is part of Bitsquare.
|
||||
*
|
||||
* Bitsquare 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.
|
||||
*
|
||||
* Bitsquare 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 Bitsquare. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package io.bitsquare.p2p.messaging;
|
||||
|
||||
import io.bitsquare.p2p.Message;
|
||||
|
||||
public interface MailMessage extends Message {
|
||||
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* This file is part of Bitsquare.
|
||||
*
|
||||
* Bitsquare 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.
|
||||
*
|
||||
* Bitsquare 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 Bitsquare. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package io.bitsquare.p2p.messaging;
|
||||
|
||||
|
||||
import io.bitsquare.p2p.Address;
|
||||
|
||||
public interface MailboxMessage extends MailMessage {
|
||||
Address getSenderAddress();
|
||||
}
|
|
@ -0,0 +1,68 @@
|
|||
/*
|
||||
* This file is part of Bitsquare.
|
||||
*
|
||||
* Bitsquare 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.
|
||||
*
|
||||
* Bitsquare 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 Bitsquare. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package io.bitsquare.p2p.messaging;
|
||||
|
||||
import io.bitsquare.app.Version;
|
||||
import io.bitsquare.common.util.Utilities;
|
||||
|
||||
import javax.crypto.SealedObject;
|
||||
import java.security.PublicKey;
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* Packs the encrypted symmetric secretKey and the encrypted and signed message into one object.
|
||||
* SecretKey is encrypted with asymmetric pubKey of peer. Signed message is encrypted with secretKey.
|
||||
* Using that hybrid encryption model we are not restricted by data size and performance as symmetric encryption is very fast.
|
||||
*/
|
||||
public final class SealedAndSignedMessage implements MailMessage {
|
||||
// That object is sent over the wire, so we need to take care of version compatibility.
|
||||
private static final long serialVersionUID = Version.NETWORK_PROTOCOL_VERSION;
|
||||
|
||||
public final SealedObject sealedSecretKey;
|
||||
public final SealedObject sealedMessage;
|
||||
public final PublicKey signaturePubKey;
|
||||
|
||||
public SealedAndSignedMessage(SealedObject sealedSecretKey, SealedObject sealedMessage, PublicKey signaturePubKey) {
|
||||
this.sealedSecretKey = sealedSecretKey;
|
||||
this.sealedMessage = sealedMessage;
|
||||
this.signaturePubKey = signaturePubKey;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (!(o instanceof SealedAndSignedMessage)) return false;
|
||||
|
||||
SealedAndSignedMessage that = (SealedAndSignedMessage) o;
|
||||
|
||||
return Arrays.equals(Utilities.objectToByteArray(this), Utilities.objectToByteArray(that));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
byte[] bytes = Utilities.objectToByteArray(this);
|
||||
return bytes != null ? Arrays.hashCode(bytes) : 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "SealedAndSignedMessage{" +
|
||||
"hashCode=" + hashCode() +
|
||||
'}';
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
package io.bitsquare.p2p.messaging;
|
||||
|
||||
public interface SendMailMessageListener {
|
||||
void onArrived();
|
||||
|
||||
void onFault();
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
package io.bitsquare.p2p.messaging;
|
||||
|
||||
public interface SendMailboxMessageListener {
|
||||
void onArrived();
|
||||
|
||||
void onStoredInMailbox();
|
||||
|
||||
void onFault();
|
||||
}
|
365
network/src/main/java/io/bitsquare/p2p/network/Connection.java
Normal file
365
network/src/main/java/io/bitsquare/p2p/network/Connection.java
Normal file
|
@ -0,0 +1,365 @@
|
|||
package io.bitsquare.p2p.network;
|
||||
|
||||
import io.bitsquare.common.ByteArrayUtils;
|
||||
import io.bitsquare.p2p.Address;
|
||||
import io.bitsquare.p2p.Message;
|
||||
import io.bitsquare.p2p.Utils;
|
||||
import io.bitsquare.p2p.network.messages.CloseConnectionMessage;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.*;
|
||||
import java.net.Socket;
|
||||
import java.net.SocketException;
|
||||
import java.net.SocketTimeoutException;
|
||||
import java.util.Date;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
public class Connection {
|
||||
private static final Logger log = LoggerFactory.getLogger(Connection.class);
|
||||
private static final int MAX_MSG_SIZE = 5 * 1024 * 1024; // 5 MB of compressed data
|
||||
private static final int MAX_ILLEGAL_REQUESTS = 5;
|
||||
private static final int SOCKET_TIMEOUT = 30 * 60 * 1000; // 30 min.
|
||||
|
||||
public static int getMaxMsgSize() {
|
||||
return MAX_MSG_SIZE;
|
||||
}
|
||||
|
||||
private final Socket socket;
|
||||
private final int port;
|
||||
private final MessageListener messageListener;
|
||||
private final ConnectionListener connectionListener;
|
||||
private final String uid;
|
||||
|
||||
private final Map<IllegalRequest, Integer> illegalRequests = new ConcurrentHashMap<>();
|
||||
private final ExecutorService executorService = Executors.newCachedThreadPool();
|
||||
private ObjectOutputStream out;
|
||||
private ObjectInputStream in;
|
||||
|
||||
private volatile boolean stopped;
|
||||
private volatile boolean shutDownInProgress;
|
||||
private volatile boolean inputHandlerStopped;
|
||||
|
||||
private volatile Date lastActivityDate;
|
||||
@Nullable
|
||||
private Address peerAddress;
|
||||
private boolean isAuthenticated;
|
||||
|
||||
|
||||
//TODO got java.util.zip.DataFormatException: invalid distance too far back
|
||||
// java.util.zip.DataFormatException: invalid literal/lengths set
|
||||
// use GZIPInputStream but problems with blocking
|
||||
private boolean useCompression = false;
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Constructor
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
public Connection(Socket socket, MessageListener messageListener, ConnectionListener connectionListener) {
|
||||
this.socket = socket;
|
||||
port = socket.getLocalPort();
|
||||
this.messageListener = messageListener;
|
||||
this.connectionListener = connectionListener;
|
||||
|
||||
uid = UUID.randomUUID().toString();
|
||||
|
||||
try {
|
||||
socket.setSoTimeout(SOCKET_TIMEOUT);
|
||||
// Need to access first the ObjectOutputStream otherwise the ObjectInputStream would block
|
||||
// See: https://stackoverflow.com/questions/5658089/java-creating-a-new-objectinputstream-blocks/5658109#5658109
|
||||
// When you construct an ObjectInputStream, in the constructor the class attempts to read a header that
|
||||
// the associated ObjectOutputStream on the other end of the connection has written.
|
||||
// It will not return until that header has been read.
|
||||
if (useCompression) {
|
||||
out = new ObjectOutputStream(socket.getOutputStream());
|
||||
in = new ObjectInputStream(socket.getInputStream());
|
||||
} else {
|
||||
out = new ObjectOutputStream(socket.getOutputStream());
|
||||
in = new ObjectInputStream(socket.getInputStream());
|
||||
}
|
||||
executorService.submit(new InputHandler());
|
||||
} catch (IOException e) {
|
||||
handleConnectionException(e);
|
||||
}
|
||||
|
||||
lastActivityDate = new Date();
|
||||
|
||||
connectionListener.onConnection(this);
|
||||
}
|
||||
|
||||
public void onAuthenticationComplete(Address peerAddress, Connection connection) {
|
||||
isAuthenticated = true;
|
||||
this.peerAddress = peerAddress;
|
||||
connectionListener.onPeerAddressAuthenticated(peerAddress, connection);
|
||||
}
|
||||
|
||||
public boolean isStopped() {
|
||||
return stopped;
|
||||
}
|
||||
|
||||
public void sendMessage(Message message) {
|
||||
if (!stopped) {
|
||||
try {
|
||||
log.trace("writeObject " + message + " on connection with port " + port);
|
||||
if (!stopped) {
|
||||
Object objectToWrite;
|
||||
if (useCompression) {
|
||||
byte[] messageAsBytes = ByteArrayUtils.objectToByteArray(message);
|
||||
// log.trace("Write object uncompressed data size: " + messageAsBytes.length);
|
||||
byte[] compressed = Utils.compress(message);
|
||||
//log.trace("Write object compressed data size: " + compressed.length);
|
||||
objectToWrite = compressed;
|
||||
} else {
|
||||
// log.trace("Write object data size: " + ByteArrayUtils.objectToByteArray(message).length);
|
||||
objectToWrite = message;
|
||||
}
|
||||
out.writeObject(objectToWrite);
|
||||
out.flush();
|
||||
|
||||
lastActivityDate = new Date();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
handleConnectionException(e);
|
||||
}
|
||||
} else {
|
||||
connectionListener.onDisconnect(ConnectionListener.Reason.ALREADY_CLOSED, Connection.this);
|
||||
}
|
||||
}
|
||||
|
||||
public void reportIllegalRequest(IllegalRequest illegalRequest) {
|
||||
log.warn("We got reported an illegal request " + illegalRequest);
|
||||
int prevCounter = illegalRequests.get(illegalRequest);
|
||||
if (prevCounter > illegalRequest.limit) {
|
||||
log.warn("We close connection as we received too many illegal requests.\n" + illegalRequests.toString());
|
||||
shutDown();
|
||||
} else {
|
||||
illegalRequests.put(illegalRequest, ++prevCounter);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Getters
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
public Socket getSocket() {
|
||||
return socket;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Address getPeerAddress() {
|
||||
return peerAddress;
|
||||
}
|
||||
|
||||
public Date getLastActivityDate() {
|
||||
return lastActivityDate;
|
||||
}
|
||||
|
||||
public boolean isAuthenticated() {
|
||||
return isAuthenticated;
|
||||
}
|
||||
|
||||
public String getUid() {
|
||||
return uid;
|
||||
}
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Setters
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
public void setPeerAddress(Address peerAddress) {
|
||||
this.peerAddress = peerAddress;
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// ShutDown
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
public void shutDown(Runnable completeHandler) {
|
||||
shutDown(true, completeHandler);
|
||||
}
|
||||
|
||||
public void shutDown() {
|
||||
shutDown(true, null);
|
||||
}
|
||||
|
||||
private void shutDown(boolean sendCloseConnectionMessage) {
|
||||
shutDown(sendCloseConnectionMessage, null);
|
||||
}
|
||||
|
||||
private void shutDown(boolean sendCloseConnectionMessage, @Nullable Runnable shutDownCompleteHandler) {
|
||||
if (!shutDownInProgress) {
|
||||
log.info("\n\nShutDown connection:"
|
||||
+ "\npeerAddress=" + peerAddress
|
||||
+ "\nuid=" + getUid()
|
||||
+ "\nisAuthenticated=" + isAuthenticated
|
||||
+ "\nsocket.getPort()=" + socket.getPort()
|
||||
+ "\n\n");
|
||||
log.debug("ShutDown " + this.getObjectId());
|
||||
log.debug("ShutDown connection requested. Connection=" + this.toString());
|
||||
|
||||
shutDownInProgress = true;
|
||||
inputHandlerStopped = true;
|
||||
if (!stopped) {
|
||||
if (sendCloseConnectionMessage) {
|
||||
sendMessage(new CloseConnectionMessage());
|
||||
try {
|
||||
// give a bit of time for closing gracefully
|
||||
Thread.sleep(100);
|
||||
} catch (InterruptedException ignored) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
stopped = true;
|
||||
connectionListener.onDisconnect(ConnectionListener.Reason.SHUT_DOWN, Connection.this);
|
||||
|
||||
try {
|
||||
socket.close();
|
||||
} catch (SocketException e) {
|
||||
log.trace("SocketException at shutdown might be expected " + e.getMessage());
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
} finally {
|
||||
Utils.shutDownExecutorService(executorService);
|
||||
|
||||
log.debug("Connection shutdown complete " + this.toString());
|
||||
// dont use executorService as its shut down but call handler on own thread
|
||||
// to not get interrupted by caller
|
||||
if (shutDownCompleteHandler != null)
|
||||
new Thread(shutDownCompleteHandler).start();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void handleConnectionException(Exception e) {
|
||||
if (e instanceof SocketException) {
|
||||
if (socket.isClosed())
|
||||
connectionListener.onDisconnect(ConnectionListener.Reason.SOCKET_CLOSED, Connection.this);
|
||||
else
|
||||
connectionListener.onDisconnect(ConnectionListener.Reason.RESET, Connection.this);
|
||||
} else if (e instanceof SocketTimeoutException) {
|
||||
connectionListener.onDisconnect(ConnectionListener.Reason.TIMEOUT, Connection.this);
|
||||
} else if (e instanceof EOFException) {
|
||||
connectionListener.onDisconnect(ConnectionListener.Reason.PEER_DISCONNECTED, Connection.this);
|
||||
} else {
|
||||
log.info("Exception at connection with port " + socket.getLocalPort());
|
||||
e.printStackTrace();
|
||||
connectionListener.onDisconnect(ConnectionListener.Reason.UNKNOWN, Connection.this);
|
||||
}
|
||||
|
||||
shutDown(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (!(o instanceof Connection)) return false;
|
||||
|
||||
Connection that = (Connection) o;
|
||||
|
||||
if (port != that.port) return false;
|
||||
if (uid != null ? !uid.equals(that.uid) : that.uid != null) return false;
|
||||
return !(peerAddress != null ? !peerAddress.equals(that.peerAddress) : that.peerAddress != null);
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = port;
|
||||
result = 31 * result + (uid != null ? uid.hashCode() : 0);
|
||||
result = 31 * result + (peerAddress != null ? peerAddress.hashCode() : 0);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Connection{" +
|
||||
"OBJECT ID=" + super.toString().split("@")[1] +
|
||||
", uid=" + uid +
|
||||
", port=" + port +
|
||||
", isAuthenticated=" + isAuthenticated +
|
||||
", peerAddress=" + peerAddress +
|
||||
", lastActivityDate=" + lastActivityDate +
|
||||
", stopped=" + stopped +
|
||||
", inputHandlerStopped=" + inputHandlerStopped +
|
||||
'}';
|
||||
}
|
||||
|
||||
public String getObjectId() {
|
||||
return super.toString().split("@")[1].toString();
|
||||
}
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// InputHandler
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
private class InputHandler implements Runnable {
|
||||
@Override
|
||||
public void run() {
|
||||
Thread.currentThread().setName("InputHandler-" + socket.getLocalPort());
|
||||
while (!inputHandlerStopped) {
|
||||
try {
|
||||
log.trace("InputHandler waiting for incoming messages connection=" + Connection.this.getObjectId());
|
||||
Object rawInputObject = in.readObject();
|
||||
log.trace("New data arrived at inputHandler of connection=" + Connection.this.getObjectId()
|
||||
+ " rawInputObject " + rawInputObject);
|
||||
|
||||
int size = ByteArrayUtils.objectToByteArray(rawInputObject).length;
|
||||
if (size <= MAX_MSG_SIZE) {
|
||||
Serializable serializable = null;
|
||||
if (useCompression) {
|
||||
if (rawInputObject instanceof byte[]) {
|
||||
byte[] compressedObjectAsBytes = (byte[]) rawInputObject;
|
||||
size = compressedObjectAsBytes.length;
|
||||
//log.trace("Read object compressed data size: " + size);
|
||||
serializable = Utils.decompress(compressedObjectAsBytes);
|
||||
} else {
|
||||
reportIllegalRequest(IllegalRequest.InvalidDataType);
|
||||
}
|
||||
} else {
|
||||
if (rawInputObject instanceof Serializable) {
|
||||
serializable = (Serializable) rawInputObject;
|
||||
} else {
|
||||
reportIllegalRequest(IllegalRequest.InvalidDataType);
|
||||
}
|
||||
}
|
||||
//log.trace("Read object decompressed data size: " + ByteArrayUtils.objectToByteArray(serializable).length);
|
||||
|
||||
// compressed size might be bigger theoretically so we check again after decompression
|
||||
if (size <= MAX_MSG_SIZE) {
|
||||
if (serializable instanceof Message) {
|
||||
lastActivityDate = new Date();
|
||||
Message message = (Message) serializable;
|
||||
if (message instanceof CloseConnectionMessage)
|
||||
shutDown(false);
|
||||
else
|
||||
executorService.submit(() -> messageListener.onMessage(message, Connection.this));
|
||||
} else {
|
||||
reportIllegalRequest(IllegalRequest.InvalidDataType);
|
||||
}
|
||||
} else {
|
||||
log.error("Received decompressed data exceeds max. msg size.");
|
||||
reportIllegalRequest(IllegalRequest.MaxSizeExceeded);
|
||||
}
|
||||
} else {
|
||||
log.error("Received compressed data exceeds max. msg size.");
|
||||
reportIllegalRequest(IllegalRequest.MaxSizeExceeded);
|
||||
}
|
||||
} catch (IOException | ClassNotFoundException e) {
|
||||
log.error("Exception" + e);
|
||||
inputHandlerStopped = true;
|
||||
handleConnectionException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
package io.bitsquare.p2p.network;
|
||||
|
||||
|
||||
import io.bitsquare.p2p.Address;
|
||||
|
||||
public interface ConnectionListener {
|
||||
|
||||
enum Reason {
|
||||
SOCKET_CLOSED,
|
||||
RESET,
|
||||
TIMEOUT,
|
||||
SHUT_DOWN,
|
||||
PEER_DISCONNECTED,
|
||||
ALREADY_CLOSED,
|
||||
UNKNOWN
|
||||
}
|
||||
|
||||
void onConnection(Connection connection);
|
||||
|
||||
void onPeerAddressAuthenticated(Address peerAddress, Connection connection);
|
||||
|
||||
void onDisconnect(Reason reason, Connection connection);
|
||||
|
||||
void onError(Throwable throwable);
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
package io.bitsquare.p2p.network;
|
||||
|
||||
public enum IllegalRequest {
|
||||
MaxSizeExceeded(1),
|
||||
NotAuthenticated(2),
|
||||
InvalidDataType(2);
|
||||
|
||||
public final int limit;
|
||||
|
||||
IllegalRequest(int limit) {
|
||||
this.limit = limit;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,132 @@
|
|||
package io.bitsquare.p2p.network;
|
||||
|
||||
import com.google.common.util.concurrent.FutureCallback;
|
||||
import com.google.common.util.concurrent.Futures;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
import com.google.common.util.concurrent.MoreExecutors;
|
||||
import com.msopentech.thali.java.toronionproxy.JavaOnionProxyContext;
|
||||
import com.msopentech.thali.java.toronionproxy.JavaOnionProxyManager;
|
||||
import io.bitsquare.p2p.Address;
|
||||
import io.nucleo.net.HiddenServiceDescriptor;
|
||||
import io.nucleo.net.TorNode;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.BindException;
|
||||
import java.net.ServerSocket;
|
||||
import java.net.Socket;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
public class LocalhostNetworkNode extends NetworkNode {
|
||||
private static final Logger log = LoggerFactory.getLogger(LocalhostNetworkNode.class);
|
||||
|
||||
private static int simulateTorDelayTorNode = 0;
|
||||
private static int simulateTorDelayHiddenService = 0;
|
||||
private Address address;
|
||||
|
||||
public static void setSimulateTorDelayTorNode(int simulateTorDelayTorNode) {
|
||||
LocalhostNetworkNode.simulateTorDelayTorNode = simulateTorDelayTorNode;
|
||||
}
|
||||
|
||||
public static void setSimulateTorDelayHiddenService(int simulateTorDelayHiddenService) {
|
||||
LocalhostNetworkNode.simulateTorDelayHiddenService = simulateTorDelayHiddenService;
|
||||
}
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Constructor
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
public LocalhostNetworkNode(int port) {
|
||||
super(port);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start(@Nullable SetupListener setupListener) {
|
||||
if (setupListener != null) addSetupListener(setupListener);
|
||||
|
||||
executorService = MoreExecutors.listeningDecorator(Executors.newCachedThreadPool());
|
||||
|
||||
//Tor delay simulation
|
||||
createTorNode(torNode -> {
|
||||
setupListeners.stream().forEach(e -> e.onTorNodeReady());
|
||||
|
||||
// Create Hidden Service (takes about 40 sec.)
|
||||
createHiddenService(hiddenServiceDescriptor -> {
|
||||
try {
|
||||
startServer(new ServerSocket(port));
|
||||
} catch (BindException e) {
|
||||
e.printStackTrace();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
address = new Address("localhost", port);
|
||||
|
||||
|
||||
setupListeners.stream().forEach(e -> e.onHiddenServiceReady());
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public Address getAddress() {
|
||||
return address;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Socket getSocket(Address peerAddress) throws IOException {
|
||||
return new Socket(peerAddress.hostName, peerAddress.port);
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Tor delay simulation
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
private void createTorNode(final Consumer<TorNode> resultHandler) {
|
||||
Callable<TorNode<JavaOnionProxyManager, JavaOnionProxyContext>> task = () -> {
|
||||
long ts = System.currentTimeMillis();
|
||||
log.trace("[simulation] Create TorNode");
|
||||
if (simulateTorDelayTorNode > 0) Thread.sleep(simulateTorDelayTorNode);
|
||||
log.trace("\n\n##### TorNode created [simulation]. Took " + (System.currentTimeMillis() - ts) + " ms\n\n");
|
||||
return null;
|
||||
};
|
||||
ListenableFuture<TorNode<JavaOnionProxyManager, JavaOnionProxyContext>> future = executorService.submit(task);
|
||||
Futures.addCallback(future, new FutureCallback<TorNode<JavaOnionProxyManager, JavaOnionProxyContext>>() {
|
||||
public void onSuccess(TorNode<JavaOnionProxyManager, JavaOnionProxyContext> torNode) {
|
||||
resultHandler.accept(torNode);
|
||||
}
|
||||
|
||||
public void onFailure(Throwable throwable) {
|
||||
log.error("[simulation] TorNode creation failed");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void createHiddenService(final Consumer<HiddenServiceDescriptor> resultHandler) {
|
||||
Callable<HiddenServiceDescriptor> task = () -> {
|
||||
long ts = System.currentTimeMillis();
|
||||
log.debug("[simulation] Create hidden service");
|
||||
if (simulateTorDelayHiddenService > 0) Thread.sleep(simulateTorDelayHiddenService);
|
||||
log.debug("\n\n##### Hidden service created [simulation]. Took " + (System.currentTimeMillis() - ts) + " ms\n\n");
|
||||
return null;
|
||||
};
|
||||
ListenableFuture<HiddenServiceDescriptor> future = executorService.submit(task);
|
||||
Futures.addCallback(future, new FutureCallback<HiddenServiceDescriptor>() {
|
||||
public void onSuccess(HiddenServiceDescriptor hiddenServiceDescriptor) {
|
||||
resultHandler.accept(hiddenServiceDescriptor);
|
||||
}
|
||||
|
||||
public void onFailure(Throwable throwable) {
|
||||
log.error("[simulation] Hidden service creation failed");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
package io.bitsquare.p2p.network;
|
||||
|
||||
import io.bitsquare.p2p.Message;
|
||||
|
||||
public interface MessageListener {
|
||||
|
||||
void onMessage(Message message, Connection connection);
|
||||
}
|
320
network/src/main/java/io/bitsquare/p2p/network/NetworkNode.java
Normal file
320
network/src/main/java/io/bitsquare/p2p/network/NetworkNode.java
Normal file
|
@ -0,0 +1,320 @@
|
|||
package io.bitsquare.p2p.network;
|
||||
|
||||
import com.google.common.util.concurrent.*;
|
||||
import io.bitsquare.p2p.Address;
|
||||
import io.bitsquare.p2p.Message;
|
||||
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.ServerSocket;
|
||||
import java.net.Socket;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
|
||||
public abstract class NetworkNode implements MessageListener, ConnectionListener {
|
||||
private static final Logger log = LoggerFactory.getLogger(NetworkNode.class);
|
||||
|
||||
protected final int port;
|
||||
private final Map<Address, Connection> outBoundConnections = new ConcurrentHashMap<>();
|
||||
private final Map<Address, Connection> inBoundAuthenticatedConnections = new ConcurrentHashMap<>();
|
||||
private final List<Connection> inBoundTempConnections = new CopyOnWriteArrayList<>();
|
||||
private final List<MessageListener> messageListeners = new CopyOnWriteArrayList<>();
|
||||
private final List<ConnectionListener> connectionListeners = new CopyOnWriteArrayList<>();
|
||||
protected final List<SetupListener> setupListeners = new CopyOnWriteArrayList<>();
|
||||
protected ListeningExecutorService executorService;
|
||||
private Server server;
|
||||
private volatile boolean shutDownInProgress;
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Constructor
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
public NetworkNode(int port) {
|
||||
this.port = port;
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// API
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
public void start() {
|
||||
start(null);
|
||||
}
|
||||
|
||||
abstract public void start(@Nullable SetupListener setupListener);
|
||||
|
||||
public SettableFuture<Connection> sendMessage(@NotNull Address peerAddress, Message message) {
|
||||
log.trace("sendMessage message=" + message);
|
||||
checkNotNull(peerAddress, "peerAddress must not be null");
|
||||
final SettableFuture<Connection> resultFuture = SettableFuture.create();
|
||||
|
||||
Callable<Connection> task = () -> {
|
||||
Thread.currentThread().setName("Outgoing-connection-to-" + peerAddress);
|
||||
Connection connection = outBoundConnections.get(peerAddress);
|
||||
|
||||
if (connection != null && connection.isStopped()) {
|
||||
// can happen because of threading...
|
||||
log.trace("We have a connection which is already stopped in outBoundConnections. Connection.uid=" + connection.getUid());
|
||||
outBoundConnections.remove(peerAddress);
|
||||
connection = null;
|
||||
}
|
||||
|
||||
if (connection == null) {
|
||||
Optional<Connection> connectionOptional = inBoundAuthenticatedConnections.values().stream()
|
||||
.filter(e -> peerAddress.equals(e.getPeerAddress()))
|
||||
.findAny();
|
||||
if (connectionOptional.isPresent())
|
||||
connection = connectionOptional.get();
|
||||
if (connection != null)
|
||||
log.trace("We have found a connection in inBoundAuthenticatedConnections. Connection.uid=" + connection.getUid());
|
||||
}
|
||||
if (connection == null) {
|
||||
Optional<Connection> connectionOptional = inBoundTempConnections.stream()
|
||||
.filter(e -> peerAddress.equals(e.getPeerAddress()))
|
||||
.findAny();
|
||||
if (connectionOptional.isPresent())
|
||||
connection = connectionOptional.get();
|
||||
if (connection != null)
|
||||
log.trace("We have found a connection in inBoundTempConnections. Connection.uid=" + connection.getUid());
|
||||
}
|
||||
|
||||
if (connection == null) {
|
||||
try {
|
||||
Socket socket = getSocket(peerAddress); // can take a while when using tor
|
||||
connection = new Connection(socket,
|
||||
(message1, connection1) -> NetworkNode.this.onMessage(message1, connection1),
|
||||
new ConnectionListener() {
|
||||
@Override
|
||||
public void onConnection(Connection connection) {
|
||||
NetworkNode.this.onConnection(connection);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPeerAddressAuthenticated(Address peerAddress, Connection connection) {
|
||||
NetworkNode.this.onPeerAddressAuthenticated(peerAddress, connection);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisconnect(Reason reason, Connection connection) {
|
||||
log.trace("onDisconnect at outgoing connection to peerAddress " + peerAddress);
|
||||
NetworkNode.this.onDisconnect(reason, connection);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Throwable throwable) {
|
||||
NetworkNode.this.onError(throwable);
|
||||
}
|
||||
});
|
||||
outBoundConnections.put(peerAddress, connection);
|
||||
|
||||
log.info("\n\nNetworkNode created new outbound connection:"
|
||||
+ "\npeerAddress=" + peerAddress.port
|
||||
+ "\nconnection.uid=" + connection.getUid()
|
||||
+ "\nmessage=" + message
|
||||
+ "\n\n");
|
||||
} catch (Throwable t) {
|
||||
resultFuture.setException(t);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
connection.sendMessage(message);
|
||||
|
||||
return connection;
|
||||
};
|
||||
|
||||
ListenableFuture<Connection> future = executorService.submit(task);
|
||||
Futures.addCallback(future, new FutureCallback<Connection>() {
|
||||
public void onSuccess(Connection connection) {
|
||||
resultFuture.set(connection);
|
||||
}
|
||||
|
||||
public void onFailure(@NotNull Throwable throwable) {
|
||||
resultFuture.setException(throwable);
|
||||
}
|
||||
});
|
||||
return resultFuture;
|
||||
}
|
||||
|
||||
public SettableFuture<Connection> sendMessage(Connection connection, Message message) {
|
||||
final SettableFuture<Connection> resultFuture = SettableFuture.create();
|
||||
|
||||
ListenableFuture<Connection> future = executorService.submit(() -> {
|
||||
connection.sendMessage(message);
|
||||
return connection;
|
||||
});
|
||||
Futures.addCallback(future, new FutureCallback<Connection>() {
|
||||
public void onSuccess(Connection connection) {
|
||||
resultFuture.set(connection);
|
||||
}
|
||||
|
||||
public void onFailure(@NotNull Throwable throwable) {
|
||||
resultFuture.setException(throwable);
|
||||
}
|
||||
});
|
||||
return resultFuture;
|
||||
}
|
||||
|
||||
public Set<Connection> getAllConnections() {
|
||||
Set<Connection> set = new HashSet<>(inBoundAuthenticatedConnections.values());
|
||||
set.addAll(outBoundConnections.values());
|
||||
set.addAll(inBoundTempConnections);
|
||||
return set;
|
||||
}
|
||||
|
||||
public void shutDown(Runnable shutDownCompleteHandler) {
|
||||
log.info("Shutdown NetworkNode");
|
||||
if (!shutDownInProgress) {
|
||||
shutDownInProgress = true;
|
||||
if (server != null) {
|
||||
server.shutDown();
|
||||
server = null;
|
||||
}
|
||||
|
||||
getAllConnections().stream().forEach(e -> e.shutDown());
|
||||
|
||||
log.info("NetworkNode shutdown complete");
|
||||
if (shutDownCompleteHandler != null) new Thread(shutDownCompleteHandler).start();
|
||||
;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// SetupListener
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
public void addSetupListener(SetupListener setupListener) {
|
||||
setupListeners.add(setupListener);
|
||||
}
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// ConnectionListener
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
public void addConnectionListener(ConnectionListener connectionListener) {
|
||||
connectionListeners.add(connectionListener);
|
||||
}
|
||||
|
||||
public void removeConnectionListener(ConnectionListener connectionListener) {
|
||||
connectionListeners.remove(connectionListener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConnection(Connection connection) {
|
||||
connectionListeners.stream().forEach(e -> e.onConnection(connection));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPeerAddressAuthenticated(Address peerAddress, Connection connection) {
|
||||
log.trace("onAuthenticationComplete peerAddress=" + peerAddress);
|
||||
log.trace("onAuthenticationComplete connection=" + connection);
|
||||
|
||||
connectionListeners.stream().forEach(e -> e.onPeerAddressAuthenticated(peerAddress, connection));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisconnect(Reason reason, Connection connection) {
|
||||
Address peerAddress = connection.getPeerAddress();
|
||||
log.trace("onDisconnect connection " + connection + ", peerAddress= " + peerAddress);
|
||||
if (peerAddress != null) {
|
||||
inBoundAuthenticatedConnections.remove(peerAddress);
|
||||
outBoundConnections.remove(peerAddress);
|
||||
} else {
|
||||
// try to find if we have connection
|
||||
outBoundConnections.values().stream()
|
||||
.filter(e -> e.equals(connection))
|
||||
.findAny()
|
||||
.ifPresent(e -> outBoundConnections.remove(e.getPeerAddress()));
|
||||
inBoundAuthenticatedConnections.values().stream()
|
||||
.filter(e -> e.equals(connection))
|
||||
.findAny()
|
||||
.ifPresent(e -> inBoundAuthenticatedConnections.remove(e.getPeerAddress()));
|
||||
}
|
||||
inBoundTempConnections.remove(connection);
|
||||
|
||||
connectionListeners.stream().forEach(e -> e.onDisconnect(reason, connection));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Throwable throwable) {
|
||||
connectionListeners.stream().forEach(e -> e.onError(throwable));
|
||||
}
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// MessageListener
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
public void addMessageListener(MessageListener messageListener) {
|
||||
messageListeners.add(messageListener);
|
||||
}
|
||||
|
||||
public void removeMessageListener(MessageListener messageListener) {
|
||||
messageListeners.remove(messageListener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMessage(Message message, Connection connection) {
|
||||
messageListeners.stream().forEach(e -> e.onMessage(message, connection));
|
||||
}
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Protected
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
protected void startServer(ServerSocket serverSocket) {
|
||||
server = new Server(serverSocket, (message, connection) -> {
|
||||
NetworkNode.this.onMessage(message, connection);
|
||||
}, new ConnectionListener() {
|
||||
@Override
|
||||
public void onConnection(Connection connection) {
|
||||
// we still have not authenticated so put it to the temp list
|
||||
inBoundTempConnections.add(connection);
|
||||
NetworkNode.this.onConnection(connection);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPeerAddressAuthenticated(Address peerAddress, Connection connection) {
|
||||
NetworkNode.this.onPeerAddressAuthenticated(peerAddress, connection);
|
||||
// now we know the the peers address is correct and we add it to inBoundConnections and
|
||||
// remove it from tempConnections
|
||||
inBoundAuthenticatedConnections.put(peerAddress, connection);
|
||||
inBoundTempConnections.remove(connection);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisconnect(Reason reason, Connection connection) {
|
||||
Address peerAddress = connection.getPeerAddress();
|
||||
log.trace("onDisconnect at incoming connection to peerAddress " + peerAddress);
|
||||
if (peerAddress != null)
|
||||
inBoundAuthenticatedConnections.remove(peerAddress);
|
||||
|
||||
inBoundTempConnections.remove(connection);
|
||||
|
||||
NetworkNode.this.onDisconnect(reason, connection);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Throwable throwable) {
|
||||
NetworkNode.this.onError(throwable);
|
||||
}
|
||||
});
|
||||
executorService.submit(server);
|
||||
}
|
||||
|
||||
abstract protected Socket getSocket(Address peerAddress) throws IOException;
|
||||
|
||||
@Nullable
|
||||
abstract public Address getAddress();
|
||||
}
|
75
network/src/main/java/io/bitsquare/p2p/network/Server.java
Normal file
75
network/src/main/java/io/bitsquare/p2p/network/Server.java
Normal file
|
@ -0,0 +1,75 @@
|
|||
package io.bitsquare.p2p.network;
|
||||
|
||||
import io.bitsquare.p2p.Utils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.ServerSocket;
|
||||
import java.net.Socket;
|
||||
import java.net.SocketException;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
public class Server implements Runnable {
|
||||
private static final Logger log = LoggerFactory.getLogger(Server.class);
|
||||
|
||||
private final ServerSocket serverSocket;
|
||||
private final MessageListener messageListener;
|
||||
private final ConnectionListener connectionListener;
|
||||
private final ExecutorService executorService = Executors.newCachedThreadPool();
|
||||
private final List<Connection> connections = new CopyOnWriteArrayList<>();
|
||||
private volatile boolean stopped;
|
||||
|
||||
|
||||
public Server(ServerSocket serverSocket, MessageListener messageListener, ConnectionListener connectionListener) {
|
||||
this.serverSocket = serverSocket;
|
||||
this.messageListener = messageListener;
|
||||
this.connectionListener = connectionListener;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
Thread.currentThread().setName("Server-" + serverSocket.getLocalPort());
|
||||
while (!stopped) {
|
||||
try {
|
||||
log.info("Ready to accept new clients on port " + serverSocket.getLocalPort());
|
||||
final Socket socket = serverSocket.accept();
|
||||
log.info("Accepted new client on port " + socket.getLocalPort());
|
||||
Connection connection = new Connection(socket, messageListener, connectionListener);
|
||||
log.info("\n\nServer created new inbound connection:"
|
||||
+ "\nserverSocket.getLocalPort()=" + serverSocket.getLocalPort()
|
||||
+ "\nsocket.getPort()=" + socket.getPort()
|
||||
+ "\nconnection.uid=" + connection.getUid()
|
||||
+ "\n\n");
|
||||
|
||||
log.info("Server created new socket with port " + socket.getPort());
|
||||
connections.add(connection);
|
||||
} catch (IOException e) {
|
||||
if (!stopped)
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void shutDown() {
|
||||
if (!stopped) {
|
||||
stopped = true;
|
||||
|
||||
connections.stream().forEach(e -> e.shutDown());
|
||||
|
||||
try {
|
||||
serverSocket.close();
|
||||
} catch (SocketException e) {
|
||||
log.warn("SocketException at shutdown might be expected " + e.getMessage());
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
} finally {
|
||||
Utils.shutDownExecutorService(executorService);
|
||||
log.debug("Server shutdown complete");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
package io.bitsquare.p2p.network;
|
||||
|
||||
public interface ServerListener {
|
||||
void onSocketHandler(Connection connection);
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
package io.bitsquare.p2p.network;
|
||||
|
||||
|
||||
public interface SetupListener {
|
||||
|
||||
void onTorNodeReady();
|
||||
|
||||
void onHiddenServiceReady();
|
||||
|
||||
void onSetupFailed(Throwable throwable);
|
||||
|
||||
}
|
|
@ -0,0 +1,369 @@
|
|||
package io.bitsquare.p2p.network;
|
||||
|
||||
import com.google.common.util.concurrent.*;
|
||||
import com.msopentech.thali.java.toronionproxy.JavaOnionProxyContext;
|
||||
import com.msopentech.thali.java.toronionproxy.JavaOnionProxyManager;
|
||||
import io.bitsquare.p2p.Address;
|
||||
import io.bitsquare.p2p.Message;
|
||||
import io.bitsquare.p2p.Utils;
|
||||
import io.bitsquare.p2p.network.messages.SelfTestMessage;
|
||||
import io.nucleo.net.HiddenServiceDescriptor;
|
||||
import io.nucleo.net.TorNode;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.net.Socket;
|
||||
import java.util.Random;
|
||||
import java.util.Timer;
|
||||
import java.util.TimerTask;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkArgument;
|
||||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
|
||||
public class TorNetworkNode extends NetworkNode {
|
||||
private static final Logger log = LoggerFactory.getLogger(TorNetworkNode.class);
|
||||
private static final Random random = new Random();
|
||||
|
||||
private static final long TIMEOUT = 5000;
|
||||
private static final long SELF_TEST_INTERVAL = 10 * 60 * 1000;
|
||||
private static final int MAX_ERRORS_BEFORE_RESTART = 3;
|
||||
private static final int MAX_RESTART_ATTEMPTS = 3;
|
||||
private static final int WAIT_BEFORE_RESTART = 2000;
|
||||
private static final long SHUT_DOWN_TIMEOUT = 5000;
|
||||
|
||||
private final File torDir;
|
||||
private TorNode torNode;
|
||||
private HiddenServiceDescriptor hiddenServiceDescriptor;
|
||||
private Timer shutDownTimeoutTimer, selfTestTimer, selfTestTimeoutTimer;
|
||||
private TimerTask selfTestTimeoutTask, selfTestTask;
|
||||
private AtomicBoolean selfTestRunning = new AtomicBoolean(false);
|
||||
private int nonce;
|
||||
private int errorCounter;
|
||||
private int restartCounter;
|
||||
private Runnable shutDownCompleteHandler;
|
||||
private boolean torShutDownComplete, networkNodeShutDownDoneComplete;
|
||||
|
||||
|
||||
// /////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Constructor
|
||||
// /////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
public TorNetworkNode(int port, File torDir) {
|
||||
super(port);
|
||||
|
||||
this.torDir = torDir;
|
||||
|
||||
selfTestTimeoutTask = new TimerTask() {
|
||||
@Override
|
||||
public void run() {
|
||||
log.error("A timeout occurred at self test");
|
||||
stopSelfTestTimer();
|
||||
selfTestFailed();
|
||||
}
|
||||
};
|
||||
|
||||
selfTestTask = new TimerTask() {
|
||||
@Override
|
||||
public void run() {
|
||||
stopTimeoutTimer();
|
||||
if (selfTestRunning.get()) {
|
||||
log.debug("running self test");
|
||||
selfTestTimeoutTimer = new Timer();
|
||||
selfTestTimeoutTimer.schedule(selfTestTimeoutTask, TIMEOUT);
|
||||
// might be interrupted by timeout task
|
||||
if (selfTestRunning.get()) {
|
||||
nonce = random.nextInt();
|
||||
log.trace("send msg with nonce " + nonce);
|
||||
|
||||
try {
|
||||
SettableFuture<Connection> future = sendMessage(new Address(hiddenServiceDescriptor.getFullAddress()), new SelfTestMessage(nonce));
|
||||
Futures.addCallback(future, new FutureCallback<Connection>() {
|
||||
@Override
|
||||
public void onSuccess(Connection connection) {
|
||||
log.trace("Sending self test message succeeded");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Throwable throwable) {
|
||||
log.error("Error at sending self test message. Exception = " + throwable);
|
||||
stopTimeoutTimer();
|
||||
throwable.printStackTrace();
|
||||
selfTestFailed();
|
||||
}
|
||||
});
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
addMessageListener(new MessageListener() {
|
||||
@Override
|
||||
public void onMessage(Message message, Connection connection) {
|
||||
if (message instanceof SelfTestMessage) {
|
||||
if (((SelfTestMessage) message).nonce == nonce) {
|
||||
runSelfTest();
|
||||
} else {
|
||||
log.error("Nonce not matching our challenge. That should never happen.");
|
||||
selfTestFailed();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// API
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@Override
|
||||
public void start(@Nullable SetupListener setupListener) {
|
||||
if (setupListener != null) addSetupListener(setupListener);
|
||||
|
||||
// executorService might have been shutdown before a restart, so we create a new one
|
||||
executorService = MoreExecutors.listeningDecorator(Executors.newCachedThreadPool());
|
||||
|
||||
// Create the tor node (takes about 6 sec.)
|
||||
createTorNode(torDir, torNode -> {
|
||||
TorNetworkNode.this.torNode = torNode;
|
||||
|
||||
setupListeners.stream().forEach(e -> e.onTorNodeReady());
|
||||
|
||||
// Create Hidden Service (takes about 40 sec.)
|
||||
createHiddenService(torNode, port, hiddenServiceDescriptor -> {
|
||||
TorNetworkNode.this.hiddenServiceDescriptor = hiddenServiceDescriptor;
|
||||
|
||||
startServer(hiddenServiceDescriptor.getServerSocket());
|
||||
try {
|
||||
Thread.sleep(500);
|
||||
} catch (InterruptedException ignored) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
|
||||
setupListeners.stream().forEach(e -> e.onHiddenServiceReady());
|
||||
|
||||
// we are ready. so we start our periodic self test if our HS is available
|
||||
// startSelfTest();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public Address getAddress() {
|
||||
if (hiddenServiceDescriptor != null)
|
||||
return new Address(hiddenServiceDescriptor.getFullAddress());
|
||||
else
|
||||
return null;
|
||||
}
|
||||
|
||||
public void shutDown(Runnable shutDownCompleteHandler) {
|
||||
log.info("Shutdown TorNetworkNode");
|
||||
this.shutDownCompleteHandler = shutDownCompleteHandler;
|
||||
checkNotNull(executorService, "executorService must not be null");
|
||||
|
||||
selfTestRunning.set(false);
|
||||
stopSelfTestTimer();
|
||||
|
||||
shutDownTimeoutTimer = new Timer();
|
||||
shutDownTimeoutTimer.schedule(new TimerTask() {
|
||||
@Override
|
||||
public void run() {
|
||||
log.error("A timeout occurred at shutDown");
|
||||
shutDownExecutorService();
|
||||
}
|
||||
}, SHUT_DOWN_TIMEOUT);
|
||||
|
||||
executorService.submit(() -> super.shutDown(() -> {
|
||||
networkNodeShutDownDoneComplete = true;
|
||||
if (torShutDownComplete)
|
||||
shutDownExecutorService();
|
||||
}));
|
||||
|
||||
ListenableFuture<?> future2 = executorService.submit(() -> {
|
||||
long ts = System.currentTimeMillis();
|
||||
log.info("Shutdown torNode");
|
||||
try {
|
||||
if (torNode != null)
|
||||
torNode.shutdown();
|
||||
log.info("Shutdown torNode done after " + (System.currentTimeMillis() - ts) + " ms.");
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
log.error("Shutdown torNode failed with exception: " + e.getMessage());
|
||||
shutDownExecutorService();
|
||||
}
|
||||
});
|
||||
Futures.addCallback(future2, new FutureCallback<Object>() {
|
||||
@Override
|
||||
public void onSuccess(Object o) {
|
||||
torShutDownComplete = true;
|
||||
if (networkNodeShutDownDoneComplete)
|
||||
shutDownExecutorService();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Throwable throwable) {
|
||||
throwable.printStackTrace();
|
||||
log.error("Shutdown torNode failed with exception: " + throwable.getMessage());
|
||||
shutDownExecutorService();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// /////////////////////////////////////////////////////////////////////////////////////////
|
||||
// shutdown, restart
|
||||
// /////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
private void shutDownExecutorService() {
|
||||
shutDownTimeoutTimer.cancel();
|
||||
ListenableFuture<?> future = executorService.submit(() -> {
|
||||
long ts = System.currentTimeMillis();
|
||||
log.info("Shutdown executorService");
|
||||
Utils.shutDownExecutorService(executorService);
|
||||
log.info("Shutdown executorService done after " + (System.currentTimeMillis() - ts) + " ms.");
|
||||
});
|
||||
Futures.addCallback(future, new FutureCallback<Object>() {
|
||||
@Override
|
||||
public void onSuccess(Object o) {
|
||||
log.info("Shutdown completed");
|
||||
new Thread(shutDownCompleteHandler).start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Throwable throwable) {
|
||||
throwable.printStackTrace();
|
||||
log.error("Shutdown executorService failed with exception: " + throwable.getMessage());
|
||||
new Thread(shutDownCompleteHandler).start();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void restartTor() {
|
||||
restartCounter++;
|
||||
if (restartCounter <= MAX_RESTART_ATTEMPTS) {
|
||||
shutDown(() -> {
|
||||
try {
|
||||
Thread.sleep(WAIT_BEFORE_RESTART);
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
log.warn("We restart tor as too many self tests failed.");
|
||||
start(null);
|
||||
});
|
||||
} else {
|
||||
log.error("We tried to restart tor " + restartCounter
|
||||
+ " times, but we failed to get tor running. We give up now.");
|
||||
}
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// create tor
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
private void createTorNode(final File torDir, final Consumer<TorNode> resultHandler) {
|
||||
Callable<TorNode<JavaOnionProxyManager, JavaOnionProxyContext>> task = () -> {
|
||||
long ts = System.currentTimeMillis();
|
||||
if (torDir.mkdirs())
|
||||
log.trace("Created directory for tor");
|
||||
|
||||
log.trace("Create TorNode");
|
||||
TorNode<JavaOnionProxyManager, JavaOnionProxyContext> torNode1 = new TorNode<JavaOnionProxyManager, JavaOnionProxyContext>(
|
||||
torDir) {
|
||||
};
|
||||
log.trace("\n\n##### TorNode created. Took " + (System.currentTimeMillis() - ts) + " ms\n\n");
|
||||
return torNode1;
|
||||
};
|
||||
ListenableFuture<TorNode<JavaOnionProxyManager, JavaOnionProxyContext>> future = executorService.submit(task);
|
||||
Futures.addCallback(future, new FutureCallback<TorNode<JavaOnionProxyManager, JavaOnionProxyContext>>() {
|
||||
public void onSuccess(TorNode<JavaOnionProxyManager, JavaOnionProxyContext> torNode) {
|
||||
resultHandler.accept(torNode);
|
||||
}
|
||||
|
||||
public void onFailure(Throwable throwable) {
|
||||
log.error("TorNode creation failed");
|
||||
restartTor();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void createHiddenService(final TorNode torNode, final int port,
|
||||
final Consumer<HiddenServiceDescriptor> resultHandler) {
|
||||
Callable<HiddenServiceDescriptor> task = () -> {
|
||||
long ts = System.currentTimeMillis();
|
||||
log.debug("Create hidden service");
|
||||
HiddenServiceDescriptor hiddenServiceDescriptor = torNode.createHiddenService(port);
|
||||
log.debug("\n\n##### Hidden service created. Address = " + hiddenServiceDescriptor.getFullAddress() + ". Took " + (System.currentTimeMillis() - ts) + " ms\n\n");
|
||||
|
||||
return hiddenServiceDescriptor;
|
||||
};
|
||||
ListenableFuture<HiddenServiceDescriptor> future = executorService.submit(task);
|
||||
Futures.addCallback(future, new FutureCallback<HiddenServiceDescriptor>() {
|
||||
public void onSuccess(HiddenServiceDescriptor hiddenServiceDescriptor) {
|
||||
resultHandler.accept(hiddenServiceDescriptor);
|
||||
}
|
||||
|
||||
public void onFailure(Throwable throwable) {
|
||||
log.error("Hidden service creation failed");
|
||||
restartTor();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// /////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Self test
|
||||
// /////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
private void startSelfTest() {
|
||||
selfTestRunning.set(true);
|
||||
//addListener(messageListener);
|
||||
runSelfTest();
|
||||
}
|
||||
|
||||
private void runSelfTest() {
|
||||
stopSelfTestTimer();
|
||||
selfTestTimer = new Timer();
|
||||
selfTestTimer.schedule(selfTestTask, SELF_TEST_INTERVAL);
|
||||
}
|
||||
|
||||
private void stopSelfTestTimer() {
|
||||
stopTimeoutTimer();
|
||||
if (selfTestTimer != null)
|
||||
selfTestTimer.cancel();
|
||||
}
|
||||
|
||||
private void stopTimeoutTimer() {
|
||||
if (selfTestTimeoutTimer != null)
|
||||
selfTestTimeoutTimer.cancel();
|
||||
}
|
||||
|
||||
private void selfTestFailed() {
|
||||
errorCounter++;
|
||||
log.warn("Self test failed. Already " + errorCounter + " failure(s). Max. errors before restart: "
|
||||
+ MAX_ERRORS_BEFORE_RESTART);
|
||||
if (errorCounter >= MAX_ERRORS_BEFORE_RESTART)
|
||||
restartTor();
|
||||
else
|
||||
runSelfTest();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Socket getSocket(Address peerAddress) throws IOException {
|
||||
checkArgument(peerAddress.hostName.endsWith(".onion"), "PeerAddress is not an onion address");
|
||||
|
||||
return torNode.connectToHiddenService(peerAddress.hostName, peerAddress.port);
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
package io.bitsquare.p2p.network.messages;
|
||||
|
||||
import io.bitsquare.app.Version;
|
||||
import io.bitsquare.p2p.Message;
|
||||
|
||||
public final class CloseConnectionMessage implements Message {
|
||||
// That object is sent over the wire, so we need to take care of version compatibility.
|
||||
private static final long serialVersionUID = Version.NETWORK_PROTOCOL_VERSION;
|
||||
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
package io.bitsquare.p2p.network.messages;
|
||||
|
||||
import io.bitsquare.app.Version;
|
||||
import io.bitsquare.p2p.Message;
|
||||
|
||||
public final class SelfTestMessage implements Message {
|
||||
// That object is sent over the wire, so we need to take care of version compatibility.
|
||||
private static final long serialVersionUID = Version.NETWORK_PROTOCOL_VERSION;
|
||||
|
||||
public final Integer nonce;
|
||||
|
||||
public SelfTestMessage(Integer nonce) {
|
||||
this.nonce = nonce;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
package io.bitsquare.p2p.routing;
|
||||
|
||||
import io.bitsquare.p2p.Address;
|
||||
import io.bitsquare.p2p.network.Connection;
|
||||
|
||||
public abstract class AuthenticationListener implements RoutingListener {
|
||||
public void onFirstNeighborAdded(Neighbor neighbor) {
|
||||
}
|
||||
|
||||
public void onNeighborAdded(Neighbor neighbor) {
|
||||
}
|
||||
|
||||
public void onNeighborRemoved(Address address) {
|
||||
}
|
||||
|
||||
abstract public void onConnectionAuthenticated(Connection connection);
|
||||
}
|
49
network/src/main/java/io/bitsquare/p2p/routing/Neighbor.java
Normal file
49
network/src/main/java/io/bitsquare/p2p/routing/Neighbor.java
Normal file
|
@ -0,0 +1,49 @@
|
|||
package io.bitsquare.p2p.routing;
|
||||
|
||||
import io.bitsquare.p2p.Address;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
public class Neighbor implements Serializable {
|
||||
private static final Logger log = LoggerFactory.getLogger(Neighbor.class);
|
||||
|
||||
public final Address address;
|
||||
private int pingNonce;
|
||||
|
||||
public Neighbor(Address address) {
|
||||
this.address = address;
|
||||
}
|
||||
|
||||
public void setPingNonce(int pingNonce) {
|
||||
this.pingNonce = pingNonce;
|
||||
}
|
||||
|
||||
public int getPingNonce() {
|
||||
return pingNonce;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return address != null ? address.hashCode() : 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (!(o instanceof Neighbor)) return false;
|
||||
|
||||
Neighbor neighbor = (Neighbor) o;
|
||||
|
||||
return !(address != null ? !address.equals(neighbor.address) : neighbor.address != null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Neighbor{" +
|
||||
"address=" + address +
|
||||
", pingNonce=" + pingNonce +
|
||||
'}';
|
||||
}
|
||||
}
|
592
network/src/main/java/io/bitsquare/p2p/routing/Routing.java
Normal file
592
network/src/main/java/io/bitsquare/p2p/routing/Routing.java
Normal file
|
@ -0,0 +1,592 @@
|
|||
package io.bitsquare.p2p.routing;
|
||||
|
||||
import com.google.common.util.concurrent.FutureCallback;
|
||||
import com.google.common.util.concurrent.Futures;
|
||||
import com.google.common.util.concurrent.SettableFuture;
|
||||
import io.bitsquare.common.UserThread;
|
||||
import io.bitsquare.p2p.Address;
|
||||
import io.bitsquare.p2p.Utils;
|
||||
import io.bitsquare.p2p.network.*;
|
||||
import io.bitsquare.p2p.routing.messages.*;
|
||||
import io.bitsquare.p2p.storage.messages.BroadcastMessage;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class Routing {
|
||||
private static final Logger log = LoggerFactory.getLogger(Routing.class);
|
||||
|
||||
private static int MAX_CONNECTIONS = 8;
|
||||
private long startAuthTs;
|
||||
|
||||
public static void setMaxConnections(int maxConnections) {
|
||||
MAX_CONNECTIONS = maxConnections;
|
||||
}
|
||||
|
||||
private final NetworkNode networkNode;
|
||||
private final List<Address> seedNodes;
|
||||
private final Map<Address, Integer> nonceMap = new ConcurrentHashMap<>();
|
||||
private final List<RoutingListener> routingListeners = new CopyOnWriteArrayList<>();
|
||||
private final Map<Address, Neighbor> connectedNeighbors = new ConcurrentHashMap<>();
|
||||
private final Map<Address, Neighbor> reportedNeighbors = new ConcurrentHashMap<>();
|
||||
private final Map<Address, Runnable> authenticationCompleteHandlers = new ConcurrentHashMap<>();
|
||||
private final Timer maintenanceTimer = new Timer();
|
||||
private final ExecutorService executorService = Executors.newCachedThreadPool();
|
||||
private volatile boolean shutDownInProgress;
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Constructor
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
public Routing(final NetworkNode networkNode, List<Address> seeds) {
|
||||
this.networkNode = networkNode;
|
||||
|
||||
// We copy it as we remove ourselves later from the list if we are a seed node
|
||||
this.seedNodes = new CopyOnWriteArrayList<>(seeds);
|
||||
|
||||
networkNode.addMessageListener((message, connection) -> {
|
||||
if (message instanceof AuthenticationMessage)
|
||||
processAuthenticationMessage((AuthenticationMessage) message, connection);
|
||||
else if (message instanceof MaintenanceMessage)
|
||||
processMaintenanceMessage((MaintenanceMessage) message, connection);
|
||||
});
|
||||
|
||||
networkNode.addConnectionListener(new ConnectionListener() {
|
||||
@Override
|
||||
public void onConnection(Connection connection) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPeerAddressAuthenticated(Address peerAddress, Connection connection) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisconnect(Reason reason, Connection connection) {
|
||||
// only removes authenticated nodes
|
||||
if (connection.getPeerAddress() != null)
|
||||
removeNeighbor(connection.getPeerAddress());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Throwable throwable) {
|
||||
}
|
||||
});
|
||||
|
||||
networkNode.addSetupListener(new SetupListener() {
|
||||
@Override
|
||||
public void onTorNodeReady() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onHiddenServiceReady() {
|
||||
// remove ourselves in case we are a seed node
|
||||
Address myAddress = getAddress();
|
||||
if (myAddress != null)
|
||||
seedNodes.remove(myAddress);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetupFailed(Throwable throwable) {
|
||||
}
|
||||
});
|
||||
|
||||
int maintenanceInterval = new Random().nextInt(15 * 60 * 1000) + 15 * 60 * 1000; // 15-30 min.
|
||||
maintenanceTimer.scheduleAtFixedRate(new TimerTask() {
|
||||
@Override
|
||||
public void run() {
|
||||
disconnectOldConnections();
|
||||
pingNeighbors();
|
||||
}
|
||||
}, maintenanceInterval, maintenanceInterval);
|
||||
}
|
||||
|
||||
private void disconnectOldConnections() {
|
||||
List<Connection> authenticatedConnections = networkNode.getAllConnections().stream()
|
||||
.filter(e -> e.isAuthenticated())
|
||||
.collect(Collectors.toList());
|
||||
if (authenticatedConnections.size() > MAX_CONNECTIONS) {
|
||||
authenticatedConnections.sort((o1, o2) -> o1.getLastActivityDate().compareTo(o2.getLastActivityDate()));
|
||||
log.info("Number of connections exceeds MAX_CONNECTIONS. Current size=" + authenticatedConnections.size());
|
||||
Connection connection = authenticatedConnections.remove(0);
|
||||
log.info("Shutdown oldest connection with last activity date=" + connection.getLastActivityDate() + " / connection=" + connection);
|
||||
connection.shutDown(() -> disconnectOldConnections());
|
||||
try {
|
||||
Thread.sleep(200);
|
||||
} catch (InterruptedException ignored) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void pingNeighbors() {
|
||||
|
||||
}
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// API
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
public void shutDown() {
|
||||
if (!shutDownInProgress) {
|
||||
shutDownInProgress = true;
|
||||
if (maintenanceTimer != null)
|
||||
maintenanceTimer.cancel();
|
||||
|
||||
Utils.shutDownExecutorService(executorService);
|
||||
}
|
||||
}
|
||||
|
||||
public void broadcast(BroadcastMessage message, Address sender) {
|
||||
log.trace("Broadcast message to " + connectedNeighbors.values().size() + " neighbors.");
|
||||
connectedNeighbors.values().parallelStream()
|
||||
.filter(e -> !e.address.equals(sender))
|
||||
.forEach(neighbor -> {
|
||||
log.trace("Broadcast message " + message + " from " + getAddress() + " to " + neighbor.address + ".");
|
||||
SettableFuture<Connection> future = networkNode.sendMessage(neighbor.address, message);
|
||||
Futures.addCallback(future, new FutureCallback<Connection>() {
|
||||
@Override
|
||||
public void onSuccess(Connection connection) {
|
||||
log.trace("Broadcast from " + getAddress() + " to " + neighbor.address + " succeeded.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(@NotNull Throwable throwable) {
|
||||
log.info("Broadcast failed. " + throwable.getMessage());
|
||||
removeNeighbor(neighbor.address);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public void addMessageListener(MessageListener messageListener) {
|
||||
networkNode.addMessageListener(messageListener);
|
||||
}
|
||||
|
||||
public void removeMessageListener(MessageListener messageListener) {
|
||||
networkNode.removeMessageListener(messageListener);
|
||||
}
|
||||
|
||||
public void addRoutingListener(RoutingListener routingListener) {
|
||||
routingListeners.add(routingListener);
|
||||
}
|
||||
|
||||
public void removeRoutingListener(RoutingListener routingListener) {
|
||||
routingListeners.remove(routingListener);
|
||||
}
|
||||
|
||||
public Map<Address, Neighbor> getReportedNeighbors() {
|
||||
return reportedNeighbors;
|
||||
}
|
||||
|
||||
public Map<Address, Neighbor> getConnectedNeighbors() {
|
||||
return connectedNeighbors;
|
||||
}
|
||||
|
||||
public Map<Address, Neighbor> getAllNeighbors() {
|
||||
Map<Address, Neighbor> hashMap = new ConcurrentHashMap<>(reportedNeighbors);
|
||||
hashMap.putAll(connectedNeighbors);
|
||||
// remove own address and seed nodes
|
||||
hashMap.remove(getAddress());
|
||||
return hashMap;
|
||||
}
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Authentication
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// authentication example:
|
||||
// node2 -> node1 RequestAuthenticationMessage
|
||||
// node1: close connection
|
||||
// node1 -> node2 ChallengeMessage on new connection
|
||||
// node2: authentication to node1 done if nonce ok
|
||||
// node2 -> node1 GetNeighborsMessage
|
||||
// node1: authentication to node2 done if nonce ok
|
||||
// node1 -> node2 NeighborsMessage
|
||||
|
||||
public void startAuthentication(List<Address> connectedSeedNodes) {
|
||||
connectedSeedNodes.forEach(connectedSeedNode -> {
|
||||
executorService.submit(() -> {
|
||||
sendRequestAuthenticationMessage(seedNodes, connectedSeedNode);
|
||||
try {
|
||||
// give a random pause of 3-5 sec. before using the next
|
||||
Thread.sleep(new Random().nextInt(2000) + 3000);
|
||||
} catch (InterruptedException ignored) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
private void sendRequestAuthenticationMessage(final List<Address> remainingSeedNodes, final Address address) {
|
||||
log.info("We try to authenticate to a random seed node. " + address);
|
||||
startAuthTs = System.currentTimeMillis();
|
||||
final boolean[] alreadyConnected = {false};
|
||||
connectedNeighbors.values().stream().forEach(e -> {
|
||||
remainingSeedNodes.remove(e.address);
|
||||
if (address.equals(e.address))
|
||||
alreadyConnected[0] = true;
|
||||
});
|
||||
if (!alreadyConnected[0]) {
|
||||
int nonce = addToMapAndGetNonce(address);
|
||||
SettableFuture<Connection> future = networkNode.sendMessage(address, new RequestAuthenticationMessage(getAddress(), nonce));
|
||||
Futures.addCallback(future, new FutureCallback<Connection>() {
|
||||
@Override
|
||||
public void onSuccess(@Nullable Connection connection) {
|
||||
log.info("send RequestAuthenticationMessage to " + address + " succeeded.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(@NotNull Throwable throwable) {
|
||||
log.info("Send RequestAuthenticationMessage to " + address + " failed. Exception:" + throwable.getMessage());
|
||||
log.trace("We try to authenticate to another random seed nodes of that list: " + remainingSeedNodes);
|
||||
getNextSeedNode(remainingSeedNodes);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
getNextSeedNode(remainingSeedNodes);
|
||||
}
|
||||
}
|
||||
|
||||
private void getNextSeedNode(List<Address> remainingSeedNodes) {
|
||||
List<Address> remainingSeedNodeAddresses = new CopyOnWriteArrayList<>(remainingSeedNodes);
|
||||
|
||||
Address myAddress = getAddress();
|
||||
if (myAddress != null)
|
||||
remainingSeedNodeAddresses.remove(myAddress);
|
||||
|
||||
if (!remainingSeedNodeAddresses.isEmpty()) {
|
||||
Collections.shuffle(remainingSeedNodeAddresses);
|
||||
Address address = remainingSeedNodeAddresses.remove(0);
|
||||
sendRequestAuthenticationMessage(remainingSeedNodeAddresses, address);
|
||||
} else {
|
||||
log.info("No other seed node found. That is expected for the first seed node.");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void processAuthenticationMessage(AuthenticationMessage message, Connection connection) {
|
||||
log.trace("processAuthenticationMessage " + message + " from " + connection.getPeerAddress() + " at " + getAddress());
|
||||
if (message instanceof RequestAuthenticationMessage) {
|
||||
RequestAuthenticationMessage requestAuthenticationMessage = (RequestAuthenticationMessage) message;
|
||||
Address peerAddress = requestAuthenticationMessage.address;
|
||||
log.trace("RequestAuthenticationMessage from " + peerAddress + " at " + getAddress());
|
||||
connection.shutDown(() -> {
|
||||
// we delay a bit as listeners for connection.onDisconnect are on other threads and might lead to
|
||||
// inconsistent state (removal of connection from NetworkNode.authenticatedConnections)
|
||||
try {
|
||||
Thread.sleep(100);
|
||||
} catch (InterruptedException ignored) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
|
||||
log.trace("processAuthenticationMessage: connection.shutDown complete. RequestAuthenticationMessage from " + peerAddress + " at " + getAddress());
|
||||
int nonce = addToMapAndGetNonce(peerAddress);
|
||||
SettableFuture<Connection> future = networkNode.sendMessage(peerAddress, new ChallengeMessage(getAddress(), requestAuthenticationMessage.nonce, nonce));
|
||||
Futures.addCallback(future, new FutureCallback<Connection>() {
|
||||
@Override
|
||||
public void onSuccess(Connection connection) {
|
||||
log.debug("onSuccess ");
|
||||
|
||||
// TODO check nr. of connections, remove older connections (?)
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Throwable throwable) {
|
||||
log.debug("onFailure ");
|
||||
// TODO skip to next node or retry?
|
||||
SettableFuture<Connection> future = networkNode.sendMessage(peerAddress, new ChallengeMessage(getAddress(), requestAuthenticationMessage.nonce, nonce));
|
||||
Futures.addCallback(future, new FutureCallback<Connection>() {
|
||||
@Override
|
||||
public void onSuccess(Connection connection) {
|
||||
log.debug("onSuccess ");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Throwable throwable) {
|
||||
log.debug("onFailure ");
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
} else if (message instanceof ChallengeMessage) {
|
||||
ChallengeMessage challengeMessage = (ChallengeMessage) message;
|
||||
Address peerAddress = challengeMessage.address;
|
||||
connection.setPeerAddress(peerAddress);
|
||||
log.trace("ChallengeMessage from " + peerAddress + " at " + getAddress());
|
||||
boolean verified = verifyNonceAndAuthenticatePeerAddress(challengeMessage.requesterNonce, peerAddress);
|
||||
if (verified) {
|
||||
HashMap<Address, Neighbor> allNeighbors = new HashMap<>(getAllNeighbors());
|
||||
SettableFuture<Connection> future = networkNode.sendMessage(peerAddress, new GetNeighborsMessage(getAddress(), challengeMessage.challengerNonce, allNeighbors));
|
||||
Futures.addCallback(future, new FutureCallback<Connection>() {
|
||||
@Override
|
||||
public void onSuccess(Connection connection) {
|
||||
log.trace("GetNeighborsMessage sent successfully from " + getAddress() + " to " + peerAddress);
|
||||
|
||||
// we wait to get the success to reduce the time span of the moment of
|
||||
// authentication at both sides of the connection
|
||||
setAuthenticated(connection, peerAddress);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(@NotNull Throwable throwable) {
|
||||
log.info("GetNeighborsMessage sending failed " + throwable.getMessage());
|
||||
removeNeighbor(peerAddress);
|
||||
}
|
||||
});
|
||||
}
|
||||
} else if (message instanceof GetNeighborsMessage) {
|
||||
GetNeighborsMessage getNeighborsMessage = (GetNeighborsMessage) message;
|
||||
Address peerAddress = getNeighborsMessage.address;
|
||||
log.trace("GetNeighborsMessage from " + peerAddress + " at " + getAddress());
|
||||
boolean verified = verifyNonceAndAuthenticatePeerAddress(getNeighborsMessage.challengerNonce, peerAddress);
|
||||
if (verified) {
|
||||
setAuthenticated(connection, peerAddress);
|
||||
purgeReportedNeighbors();
|
||||
HashMap<Address, Neighbor> allNeighbors = new HashMap<>(getAllNeighbors());
|
||||
SettableFuture<Connection> future = networkNode.sendMessage(peerAddress, new NeighborsMessage(allNeighbors));
|
||||
log.trace("sent NeighborsMessage to " + peerAddress + " from " + getAddress() + " with allNeighbors=" + allNeighbors.values());
|
||||
Futures.addCallback(future, new FutureCallback<Connection>() {
|
||||
@Override
|
||||
public void onSuccess(Connection connection) {
|
||||
log.trace("NeighborsMessage sent successfully from " + getAddress() + " to " + peerAddress);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(@NotNull Throwable throwable) {
|
||||
log.info("NeighborsMessage sending failed " + throwable.getMessage());
|
||||
removeNeighbor(peerAddress);
|
||||
}
|
||||
});
|
||||
|
||||
// now we add the reported neighbors to our own set
|
||||
final HashMap<Address, Neighbor> neighbors = ((GetNeighborsMessage) message).neighbors;
|
||||
log.trace("Received neighbors: " + neighbors);
|
||||
// remove ourselves
|
||||
neighbors.remove(getAddress());
|
||||
addToReportedNeighbors(neighbors, connection);
|
||||
}
|
||||
} else if (message instanceof NeighborsMessage) {
|
||||
log.trace("NeighborsMessage from " + connection.getPeerAddress() + " at " + getAddress());
|
||||
final HashMap<Address, Neighbor> neighbors = ((NeighborsMessage) message).neighbors;
|
||||
log.trace("Received neighbors: " + neighbors);
|
||||
// remove ourselves
|
||||
neighbors.remove(getAddress());
|
||||
addToReportedNeighbors(neighbors, connection);
|
||||
|
||||
log.info("\n\nAuthenticationComplete\nPeer with address " + connection.getPeerAddress().toString()
|
||||
+ " authenticated (" + connection.getObjectId() + "). Took "
|
||||
+ (System.currentTimeMillis() - startAuthTs) + " ms. \n\n");
|
||||
|
||||
Runnable authenticationCompleteHandler = authenticationCompleteHandlers.remove(connection.getPeerAddress());
|
||||
if (authenticationCompleteHandler != null)
|
||||
authenticationCompleteHandler.run();
|
||||
|
||||
authenticateToNextRandomNeighbor();
|
||||
}
|
||||
}
|
||||
|
||||
private void addToReportedNeighbors(HashMap<Address, Neighbor> neighbors, Connection connection) {
|
||||
// we disconnect misbehaving nodes trying to send too many neighbors
|
||||
// reported neighbors include the peers connected neighbors which is normally max. 8 but we give some headroom
|
||||
// for safety
|
||||
if (neighbors.size() > 1100) {
|
||||
connection.shutDown();
|
||||
} else {
|
||||
reportedNeighbors.putAll(neighbors);
|
||||
purgeReportedNeighbors();
|
||||
}
|
||||
}
|
||||
|
||||
private void purgeReportedNeighbors() {
|
||||
int all = getAllNeighbors().size();
|
||||
if (all > 1000) {
|
||||
int diff = all - 100;
|
||||
ArrayList<Neighbor> reportedNeighborsList = new ArrayList<>(reportedNeighbors.values());
|
||||
for (int i = 0; i < diff; i++) {
|
||||
Neighbor neighborToRemove = reportedNeighborsList.remove(new Random().nextInt(reportedNeighborsList.size()));
|
||||
reportedNeighbors.remove(neighborToRemove.address);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void authenticateToNextRandomNeighbor() {
|
||||
if (getConnectedNeighbors().size() <= MAX_CONNECTIONS) {
|
||||
Neighbor randomNotConnectedNeighbor = getRandomNotConnectedNeighbor();
|
||||
if (randomNotConnectedNeighbor != null) {
|
||||
log.info("We try to build an authenticated connection to a random neighbor. " + randomNotConnectedNeighbor);
|
||||
authenticateToPeer(randomNotConnectedNeighbor.address, null, () -> authenticateToNextRandomNeighbor());
|
||||
} else {
|
||||
log.info("No more neighbors available for connecting.");
|
||||
}
|
||||
} else {
|
||||
log.info("We have already enough connections.");
|
||||
}
|
||||
}
|
||||
|
||||
public void authenticateToPeer(Address address, @Nullable Runnable authenticationCompleteHandler, @Nullable Runnable faultHandler) {
|
||||
startAuthTs = System.currentTimeMillis();
|
||||
|
||||
if (authenticationCompleteHandler != null)
|
||||
authenticationCompleteHandlers.put(address, authenticationCompleteHandler);
|
||||
|
||||
int nonce = addToMapAndGetNonce(address);
|
||||
SettableFuture<Connection> future = networkNode.sendMessage(address, new RequestAuthenticationMessage(getAddress(), nonce));
|
||||
Futures.addCallback(future, new FutureCallback<Connection>() {
|
||||
@Override
|
||||
public void onSuccess(@Nullable Connection connection) {
|
||||
log.debug("send RequestAuthenticationMessage succeeded");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(@NotNull Throwable throwable) {
|
||||
log.info("send IdMessage failed. " + throwable.getMessage());
|
||||
removeNeighbor(address);
|
||||
if (faultHandler != null) faultHandler.run();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private int addToMapAndGetNonce(Address address) {
|
||||
int nonce = new Random().nextInt();
|
||||
while (nonce == 0) {
|
||||
nonce = new Random().nextInt();
|
||||
}
|
||||
nonceMap.put(address, nonce);
|
||||
return nonce;
|
||||
}
|
||||
|
||||
private boolean verifyNonceAndAuthenticatePeerAddress(int peersNonce, Address peerAddress) {
|
||||
int nonce = nonceMap.remove(peerAddress);
|
||||
boolean result = nonce == peersNonce;
|
||||
return result;
|
||||
}
|
||||
|
||||
private void setAuthenticated(Connection connection, Address address) {
|
||||
log.info("We got the connection from " + getAddress() + " to " + address + " authenticated.");
|
||||
Neighbor neighbor = new Neighbor(address);
|
||||
addConnectedNeighbor(address, neighbor);
|
||||
|
||||
connection.onAuthenticationComplete(address, connection);
|
||||
routingListeners.stream().forEach(e -> e.onConnectionAuthenticated(connection));
|
||||
}
|
||||
|
||||
private Neighbor getRandomNotConnectedNeighbor() {
|
||||
List<Neighbor> list = reportedNeighbors.values().stream()
|
||||
.filter(e -> !connectedNeighbors.values().contains(e))
|
||||
.collect(Collectors.toList());
|
||||
if (list.size() > 0) {
|
||||
Collections.shuffle(list);
|
||||
return list.get(0);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Maintenance
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
private void processMaintenanceMessage(MaintenanceMessage message, Connection connection) {
|
||||
log.debug("Received routing message " + message + " at " + getAddress() + " from " + connection.getPeerAddress());
|
||||
if (message instanceof PingMessage) {
|
||||
SettableFuture<Connection> future = networkNode.sendMessage(connection, new PongMessage(((PingMessage) message).nonce));
|
||||
Futures.addCallback(future, new FutureCallback<Connection>() {
|
||||
@Override
|
||||
public void onSuccess(Connection connection) {
|
||||
log.trace("PongMessage sent successfully");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(@NotNull Throwable throwable) {
|
||||
log.info("PongMessage sending failed " + throwable.getMessage());
|
||||
removeNeighbor(connection.getPeerAddress());
|
||||
}
|
||||
});
|
||||
} else if (message instanceof PongMessage) {
|
||||
Neighbor neighbor = connectedNeighbors.get(connection.getPeerAddress());
|
||||
if (neighbor != null) {
|
||||
if (((PongMessage) message).nonce != neighbor.getPingNonce()) {
|
||||
removeNeighbor(neighbor.address);
|
||||
log.warn("PongMessage invalid: self/peer " + getAddress() + "/" + connection.getPeerAddress());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Neighbors
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
private void removeNeighbor(Address address) {
|
||||
reportedNeighbors.remove(address);
|
||||
|
||||
Neighbor disconnectedNeighbor;
|
||||
disconnectedNeighbor = connectedNeighbors.remove(address);
|
||||
|
||||
if (disconnectedNeighbor != null)
|
||||
UserThread.execute(() -> routingListeners.stream().forEach(e -> e.onNeighborRemoved(address)));
|
||||
|
||||
nonceMap.remove(address);
|
||||
}
|
||||
|
||||
private void addConnectedNeighbor(Address address, Neighbor neighbor) {
|
||||
boolean firstNeighborAdded;
|
||||
connectedNeighbors.put(address, neighbor);
|
||||
firstNeighborAdded = connectedNeighbors.size() == 1;
|
||||
|
||||
UserThread.execute(() -> routingListeners.stream().forEach(e -> e.onNeighborAdded(neighbor)));
|
||||
|
||||
if (firstNeighborAdded)
|
||||
UserThread.execute(() -> routingListeners.stream().forEach(e -> e.onFirstNeighborAdded(neighbor)));
|
||||
|
||||
if (connectedNeighbors.size() > MAX_CONNECTIONS)
|
||||
disconnectOldConnections();
|
||||
}
|
||||
|
||||
private Address getAddress() {
|
||||
return networkNode.getAddress();
|
||||
}
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Utils
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
public void printConnectedNeighborsMap() {
|
||||
StringBuilder result = new StringBuilder("\nConnected neighbors for node " + getAddress() + ":");
|
||||
connectedNeighbors.values().stream().forEach(e -> {
|
||||
result.append("\n\t" + e.address);
|
||||
});
|
||||
result.append("\n");
|
||||
log.info(result.toString());
|
||||
}
|
||||
|
||||
public void printReportedNeighborsMap() {
|
||||
StringBuilder result = new StringBuilder("\nReported neighbors for node " + getAddress() + ":");
|
||||
reportedNeighbors.values().stream().forEach(e -> {
|
||||
result.append("\n\t" + e.address);
|
||||
});
|
||||
result.append("\n");
|
||||
log.info(result.toString());
|
||||
}
|
||||
|
||||
private String getObjectId() {
|
||||
return super.toString().split("@")[1].toString();
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
package io.bitsquare.p2p.routing;
|
||||
|
||||
import io.bitsquare.p2p.Address;
|
||||
import io.bitsquare.p2p.network.Connection;
|
||||
|
||||
public interface RoutingListener {
|
||||
void onFirstNeighborAdded(Neighbor neighbor);
|
||||
|
||||
void onNeighborAdded(Neighbor neighbor);
|
||||
|
||||
void onNeighborRemoved(Address address);
|
||||
|
||||
// TODO remove
|
||||
void onConnectionAuthenticated(Connection connection);
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
package io.bitsquare.p2p.routing.messages;
|
||||
|
||||
import io.bitsquare.p2p.Message;
|
||||
|
||||
public interface AuthenticationMessage extends Message {
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
package io.bitsquare.p2p.routing.messages;
|
||||
|
||||
import io.bitsquare.app.Version;
|
||||
import io.bitsquare.p2p.Address;
|
||||
|
||||
public final class ChallengeMessage implements AuthenticationMessage {
|
||||
// That object is sent over the wire, so we need to take care of version compatibility.
|
||||
private static final long serialVersionUID = Version.NETWORK_PROTOCOL_VERSION;
|
||||
|
||||
public final Address address;
|
||||
public final int requesterNonce;
|
||||
public final int challengerNonce;
|
||||
|
||||
public ChallengeMessage(Address address, int requesterNonce, int challengerNonce) {
|
||||
this.address = address;
|
||||
this.requesterNonce = requesterNonce;
|
||||
this.challengerNonce = challengerNonce;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "ChallengeMessage{" +
|
||||
"address=" + address +
|
||||
", requesterNonce=" + requesterNonce +
|
||||
", challengerNonce=" + challengerNonce +
|
||||
'}';
|
||||
}
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
package io.bitsquare.p2p.routing.messages;
|
||||
|
||||
import io.bitsquare.app.Version;
|
||||
import io.bitsquare.p2p.Address;
|
||||
import io.bitsquare.p2p.routing.Neighbor;
|
||||
|
||||
import java.util.HashMap;
|
||||
|
||||
public final class GetNeighborsMessage implements AuthenticationMessage {
|
||||
// That object is sent over the wire, so we need to take care of version compatibility.
|
||||
private static final long serialVersionUID = Version.NETWORK_PROTOCOL_VERSION;
|
||||
|
||||
public final Address address;
|
||||
public final int challengerNonce;
|
||||
public final HashMap<Address, Neighbor> neighbors;
|
||||
|
||||
public GetNeighborsMessage(Address address, int challengerNonce, HashMap<Address, Neighbor> neighbors) {
|
||||
this.address = address;
|
||||
this.challengerNonce = challengerNonce;
|
||||
this.neighbors = neighbors;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "GetNeighborsMessage{" +
|
||||
"address=" + address +
|
||||
", challengerNonce=" + challengerNonce +
|
||||
", neighbors=" + neighbors +
|
||||
'}';
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
package io.bitsquare.p2p.routing.messages;
|
||||
|
||||
import io.bitsquare.p2p.Message;
|
||||
|
||||
public interface MaintenanceMessage extends Message {
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
package io.bitsquare.p2p.routing.messages;
|
||||
|
||||
import io.bitsquare.app.Version;
|
||||
import io.bitsquare.p2p.Address;
|
||||
import io.bitsquare.p2p.routing.Neighbor;
|
||||
|
||||
import java.util.HashMap;
|
||||
|
||||
public final class NeighborsMessage implements AuthenticationMessage {
|
||||
// That object is sent over the wire, so we need to take care of version compatibility.
|
||||
private static final long serialVersionUID = Version.NETWORK_PROTOCOL_VERSION;
|
||||
|
||||
public final HashMap<Address, Neighbor> neighbors;
|
||||
|
||||
public NeighborsMessage(HashMap<Address, Neighbor> neighbors) {
|
||||
this.neighbors = neighbors;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "NeighborsMessage{" + "neighbors=" + neighbors + '}';
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
package io.bitsquare.p2p.routing.messages;
|
||||
|
||||
import io.bitsquare.app.Version;
|
||||
|
||||
public final class PingMessage implements MaintenanceMessage {
|
||||
// That object is sent over the wire, so we need to take care of version compatibility.
|
||||
private static final long serialVersionUID = Version.NETWORK_PROTOCOL_VERSION;
|
||||
|
||||
public final int nonce;
|
||||
|
||||
public PingMessage(int nonce) {
|
||||
this.nonce = nonce;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "PingMessage{" +
|
||||
"nonce=" + nonce +
|
||||
'}';
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
package io.bitsquare.p2p.routing.messages;
|
||||
|
||||
import io.bitsquare.app.Version;
|
||||
|
||||
public final class PongMessage implements MaintenanceMessage {
|
||||
// That object is sent over the wire, so we need to take care of version compatibility.
|
||||
private static final long serialVersionUID = Version.NETWORK_PROTOCOL_VERSION;
|
||||
|
||||
public final int nonce;
|
||||
|
||||
public PongMessage(int nonce) {
|
||||
this.nonce = nonce;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "PongMessage{" +
|
||||
"nonce=" + nonce +
|
||||
'}';
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
package io.bitsquare.p2p.routing.messages;
|
||||
|
||||
import io.bitsquare.app.Version;
|
||||
import io.bitsquare.p2p.Address;
|
||||
|
||||
public final class RequestAuthenticationMessage implements AuthenticationMessage {
|
||||
// That object is sent over the wire, so we need to take care of version compatibility.
|
||||
private static final long serialVersionUID = Version.NETWORK_PROTOCOL_VERSION;
|
||||
|
||||
public final Address address;
|
||||
public final int nonce;
|
||||
|
||||
public RequestAuthenticationMessage(Address address, int nonce) {
|
||||
this.address = address;
|
||||
this.nonce = nonce;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "RequestAuthenticationMessage{" +
|
||||
"address=" + address +
|
||||
", nonce=" + nonce +
|
||||
'}';
|
||||
}
|
||||
}
|
110
network/src/main/java/io/bitsquare/p2p/seed/SeedNode.java
Normal file
110
network/src/main/java/io/bitsquare/p2p/seed/SeedNode.java
Normal file
|
@ -0,0 +1,110 @@
|
|||
package io.bitsquare.p2p.seed;
|
||||
|
||||
import io.bitsquare.common.crypto.KeyRing;
|
||||
import io.bitsquare.crypto.EncryptionService;
|
||||
import io.bitsquare.p2p.Address;
|
||||
import io.bitsquare.p2p.P2PService;
|
||||
import io.bitsquare.p2p.P2PServiceListener;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.*;
|
||||
|
||||
public class SeedNode {
|
||||
private static final Logger log = LoggerFactory.getLogger(SeedNode.class);
|
||||
|
||||
private int port = 8001;
|
||||
private boolean useLocalhost = true;
|
||||
private List<Address> seedNodes;
|
||||
private P2PService p2PService;
|
||||
protected boolean stopped;
|
||||
|
||||
public SeedNode() {
|
||||
}
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// API
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// args: port useLocalhost seedNodes
|
||||
// eg. 4444 true localhost:7777 localhost:8888
|
||||
// To stop enter: q
|
||||
public void processArgs(String[] args) {
|
||||
if (args.length > 0) {
|
||||
port = Integer.parseInt(args[0]);
|
||||
|
||||
if (args.length > 1) {
|
||||
useLocalhost = ("true").equals(args[1]);
|
||||
|
||||
if (args.length > 2) {
|
||||
seedNodes = new ArrayList<>();
|
||||
for (int i = 2; i < args.length; i++) {
|
||||
seedNodes.add(new Address(args[i]));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void listenForExitCommand() {
|
||||
Scanner scan = new Scanner(System.in);
|
||||
String line;
|
||||
while (!stopped && ((line = scan.nextLine()) != null)) {
|
||||
if (line.equals("q")) {
|
||||
Timer timeout = new Timer();
|
||||
timeout.schedule(new TimerTask() {
|
||||
@Override
|
||||
public void run() {
|
||||
log.error("Timeout occurred at shutDown request");
|
||||
System.exit(1);
|
||||
}
|
||||
}, 10 * 1000);
|
||||
|
||||
shutDown(() -> {
|
||||
timeout.cancel();
|
||||
log.debug("Shutdown seed node complete.");
|
||||
System.exit(0);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void createAndStartP2PService() {
|
||||
createAndStartP2PService(null, null, port, useLocalhost, seedNodes, null);
|
||||
}
|
||||
|
||||
public void createAndStartP2PService(EncryptionService encryptionService, KeyRing keyRing, int port, boolean useLocalhost, @Nullable List<Address> seedNodes, @Nullable P2PServiceListener listener) {
|
||||
SeedNodesRepository seedNodesRepository = new SeedNodesRepository();
|
||||
if (seedNodes != null && !seedNodes.isEmpty()) {
|
||||
if (useLocalhost)
|
||||
seedNodesRepository.setLocalhostSeedNodeAddresses(seedNodes);
|
||||
else
|
||||
seedNodesRepository.setTorSeedNodeAddresses(seedNodes);
|
||||
}
|
||||
|
||||
p2PService = new P2PService(seedNodesRepository, port, new File("bitsquare_seed_node_" + port), useLocalhost, encryptionService, keyRing);
|
||||
p2PService.start(listener);
|
||||
}
|
||||
|
||||
public P2PService getP2PService() {
|
||||
return p2PService;
|
||||
}
|
||||
|
||||
public void shutDown() {
|
||||
shutDown(null);
|
||||
}
|
||||
|
||||
public void shutDown(@Nullable Runnable shutDownCompleteHandler) {
|
||||
log.debug("Request shutdown seed node");
|
||||
if (!stopped) {
|
||||
stopped = true;
|
||||
|
||||
p2PService.shutDown(() -> {
|
||||
if (shutDownCompleteHandler != null) new Thread(shutDownCompleteHandler).start();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
package io.bitsquare.p2p.seed;
|
||||
|
||||
import io.bitsquare.p2p.Address;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
public class SeedNodesRepository {
|
||||
|
||||
|
||||
protected List<Address> torSeedNodeAddresses = Arrays.asList(
|
||||
new Address("3anjm5mw2sr6abx6.onion:8001")
|
||||
);
|
||||
|
||||
|
||||
protected List<Address> localhostSeedNodeAddresses = Arrays.asList(
|
||||
new Address("localhost:8001"),
|
||||
new Address("localhost:8002"),
|
||||
new Address("localhost:8003")
|
||||
);
|
||||
|
||||
public List<Address> getTorSeedNodeAddresses() {
|
||||
return torSeedNodeAddresses;
|
||||
}
|
||||
|
||||
public List<Address> geSeedNodeAddresses(boolean useLocalhost) {
|
||||
return useLocalhost ? localhostSeedNodeAddresses : torSeedNodeAddresses;
|
||||
}
|
||||
|
||||
public List<Address> getLocalhostSeedNodeAddresses() {
|
||||
return localhostSeedNodeAddresses;
|
||||
}
|
||||
|
||||
public void setTorSeedNodeAddresses(List<Address> torSeedNodeAddresses) {
|
||||
this.torSeedNodeAddresses = torSeedNodeAddresses;
|
||||
}
|
||||
|
||||
public void setLocalhostSeedNodeAddresses(List<Address> localhostSeedNodeAddresses) {
|
||||
this.localhostSeedNodeAddresses = localhostSeedNodeAddresses;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
package io.bitsquare.p2p.storage;
|
||||
|
||||
import io.bitsquare.p2p.storage.data.ProtectedData;
|
||||
|
||||
public interface HashSetChangedListener {
|
||||
void onAdded(ProtectedData entry);
|
||||
|
||||
void onRemoved(ProtectedData entry);
|
||||
}
|
|
@ -0,0 +1,308 @@
|
|||
package io.bitsquare.p2p.storage;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import io.bitsquare.common.UserThread;
|
||||
import io.bitsquare.common.crypto.CryptoUtil;
|
||||
import io.bitsquare.crypto.EncryptionService;
|
||||
import io.bitsquare.p2p.Address;
|
||||
import io.bitsquare.p2p.network.IllegalRequest;
|
||||
import io.bitsquare.p2p.network.MessageListener;
|
||||
import io.bitsquare.p2p.routing.Routing;
|
||||
import io.bitsquare.p2p.storage.data.*;
|
||||
import io.bitsquare.p2p.storage.messages.*;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.security.*;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Timer;
|
||||
import java.util.TimerTask;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
|
||||
public class ProtectedExpirableDataStorage {
|
||||
private static final Logger log = LoggerFactory.getLogger(ProtectedExpirableDataStorage.class);
|
||||
|
||||
@VisibleForTesting
|
||||
public static int CHECK_TTL_INTERVAL = 10 * 60 * 1000;
|
||||
|
||||
private final Routing routing;
|
||||
private final EncryptionService encryptionService;
|
||||
private final Map<BigInteger, ProtectedData> map = new ConcurrentHashMap<>();
|
||||
private final List<HashSetChangedListener> hashSetChangedListeners = new CopyOnWriteArrayList<>();
|
||||
private final Map<BigInteger, Integer> sequenceNumberMap = new ConcurrentHashMap<>();
|
||||
private boolean authenticated;
|
||||
private final Timer timer = new Timer();
|
||||
private volatile boolean shutDownInProgress;
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Constructor
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
public ProtectedExpirableDataStorage(Routing routing, EncryptionService encryptionService) {
|
||||
this.routing = routing;
|
||||
this.encryptionService = encryptionService;
|
||||
|
||||
addMessageListener((message, connection) -> {
|
||||
if (message instanceof DataMessage) {
|
||||
if (connection.isAuthenticated()) {
|
||||
log.trace("ProtectedExpirableDataMessage received " + message + " on connection " + connection);
|
||||
if (message instanceof AddDataMessage) {
|
||||
add(((AddDataMessage) message).data, connection.getPeerAddress());
|
||||
} else if (message instanceof RemoveDataMessage) {
|
||||
remove(((RemoveDataMessage) message).data, connection.getPeerAddress());
|
||||
} else if (message instanceof RemoveMailboxDataMessage) {
|
||||
removeMailboxData(((RemoveMailboxDataMessage) message).data, connection.getPeerAddress());
|
||||
}
|
||||
} else {
|
||||
log.warn("Connection is not authenticated yet. We don't accept storage operations form non-authenticated nodes.");
|
||||
connection.reportIllegalRequest(IllegalRequest.NotAuthenticated);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
timer.scheduleAtFixedRate(new TimerTask() {
|
||||
@Override
|
||||
public void run() {
|
||||
log.info("removeExpiredEntries called ");
|
||||
map.entrySet().stream().filter(entry -> entry.getValue().isExpired())
|
||||
.forEach(entry -> map.remove(entry.getKey()));
|
||||
}
|
||||
},
|
||||
CHECK_TTL_INTERVAL,
|
||||
CHECK_TTL_INTERVAL);
|
||||
}
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// API
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
public void shutDown() {
|
||||
if (!shutDownInProgress) {
|
||||
shutDownInProgress = true;
|
||||
timer.cancel();
|
||||
routing.shutDown();
|
||||
}
|
||||
}
|
||||
|
||||
public void setAuthenticated(boolean authenticated) {
|
||||
this.authenticated = authenticated;
|
||||
}
|
||||
|
||||
public boolean add(ProtectedData protectedData, Address sender) {
|
||||
BigInteger hashOfPayload = getHashAsBigInteger(protectedData.expirablePayload);
|
||||
boolean containsKey = map.containsKey(hashOfPayload);
|
||||
boolean result = checkPublicKeys(protectedData, true)
|
||||
&& isSequenceNrValid(protectedData, hashOfPayload)
|
||||
&& checkSignature(protectedData)
|
||||
&& (!containsKey || checkIfStoredDataMatchesNewData(protectedData, hashOfPayload))
|
||||
&& doAddProtectedExpirableData(protectedData, hashOfPayload, sender);
|
||||
|
||||
if (result)
|
||||
sequenceNumberMap.put(hashOfPayload, protectedData.sequenceNumber);
|
||||
else
|
||||
log.debug("add failed");
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public boolean remove(ProtectedData protectedData, Address sender) {
|
||||
BigInteger hashOfPayload = getHashAsBigInteger(protectedData.expirablePayload);
|
||||
boolean containsKey = map.containsKey(hashOfPayload);
|
||||
if (!containsKey) log.debug("Remove data ignored as we don't have an entry for that data.");
|
||||
boolean result = containsKey
|
||||
&& checkPublicKeys(protectedData, false)
|
||||
&& isSequenceNrValid(protectedData, hashOfPayload)
|
||||
&& checkSignature(protectedData)
|
||||
&& checkIfStoredDataMatchesNewData(protectedData, hashOfPayload)
|
||||
&& doRemoveProtectedExpirableData(protectedData, hashOfPayload, sender);
|
||||
|
||||
if (result)
|
||||
sequenceNumberMap.put(hashOfPayload, protectedData.sequenceNumber);
|
||||
else
|
||||
log.debug("remove failed");
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public boolean removeMailboxData(ProtectedMailboxData protectedMailboxData, Address sender) {
|
||||
BigInteger hashOfData = getHashAsBigInteger(protectedMailboxData.expirablePayload);
|
||||
boolean containsKey = map.containsKey(hashOfData);
|
||||
if (!containsKey) log.debug("Remove data ignored as we don't have an entry for that data.");
|
||||
boolean result = containsKey
|
||||
&& checkPublicKeys(protectedMailboxData, false)
|
||||
&& isSequenceNrValid(protectedMailboxData, hashOfData)
|
||||
&& protectedMailboxData.receiversPubKey.equals(protectedMailboxData.ownerStoragePubKey) // at remove both keys are the same (only receiver is able to remove data)
|
||||
&& checkSignature(protectedMailboxData)
|
||||
&& checkIfStoredMailboxDataMatchesNewMailboxData(protectedMailboxData, hashOfData)
|
||||
&& doRemoveProtectedExpirableData(protectedMailboxData, hashOfData, sender);
|
||||
|
||||
if (result)
|
||||
sequenceNumberMap.put(hashOfData, protectedMailboxData.sequenceNumber);
|
||||
else
|
||||
log.debug("removeMailboxData failed");
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public Map<BigInteger, ProtectedData> getMap() {
|
||||
return map;
|
||||
}
|
||||
|
||||
public ProtectedData getDataWithSignedSeqNr(ExpirablePayload payload, KeyPair ownerStoragePubKey) throws NoSuchAlgorithmException, SignatureException, InvalidKeyException {
|
||||
BigInteger hashOfData = getHashAsBigInteger(payload);
|
||||
int sequenceNumber;
|
||||
if (sequenceNumberMap.containsKey(hashOfData))
|
||||
sequenceNumber = sequenceNumberMap.get(hashOfData) + 1;
|
||||
else
|
||||
sequenceNumber = 0;
|
||||
|
||||
byte[] hashOfDataAndSeqNr = CryptoUtil.getHash(new DataAndSeqNr(payload, sequenceNumber));
|
||||
byte[] signature = CryptoUtil.signStorageData(ownerStoragePubKey.getPrivate(), hashOfDataAndSeqNr);
|
||||
return new ProtectedData(payload, payload.getTTL(), ownerStoragePubKey.getPublic(), sequenceNumber, signature);
|
||||
}
|
||||
|
||||
public ProtectedMailboxData getMailboxDataWithSignedSeqNr(ExpirableMailboxPayload expirableMailboxPayload, KeyPair storageSignaturePubKey, PublicKey receiversPublicKey)
|
||||
throws NoSuchAlgorithmException, SignatureException, InvalidKeyException {
|
||||
BigInteger hashOfData = getHashAsBigInteger(expirableMailboxPayload);
|
||||
int sequenceNumber;
|
||||
if (sequenceNumberMap.containsKey(hashOfData))
|
||||
sequenceNumber = sequenceNumberMap.get(hashOfData) + 1;
|
||||
else
|
||||
sequenceNumber = 0;
|
||||
|
||||
byte[] hashOfDataAndSeqNr = CryptoUtil.getHash(new DataAndSeqNr(expirableMailboxPayload, sequenceNumber));
|
||||
byte[] signature = CryptoUtil.signStorageData(storageSignaturePubKey.getPrivate(), hashOfDataAndSeqNr);
|
||||
return new ProtectedMailboxData(expirableMailboxPayload, expirableMailboxPayload.getTTL(), storageSignaturePubKey.getPublic(), sequenceNumber, signature, receiversPublicKey);
|
||||
}
|
||||
|
||||
public void addHashSetChangedListener(HashSetChangedListener hashSetChangedListener) {
|
||||
hashSetChangedListeners.add(hashSetChangedListener);
|
||||
}
|
||||
|
||||
public void addMessageListener(MessageListener messageListener) {
|
||||
routing.addMessageListener(messageListener);
|
||||
}
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Private
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
private boolean isSequenceNrValid(ProtectedData data, BigInteger hashOfData) {
|
||||
int newSequenceNumber = data.sequenceNumber;
|
||||
Integer storedSequenceNumber = sequenceNumberMap.get(hashOfData);
|
||||
if (sequenceNumberMap.containsKey(hashOfData) && newSequenceNumber <= storedSequenceNumber) {
|
||||
log.warn("Sequence number is invalid. That might happen in rare cases. newSequenceNumber="
|
||||
+ newSequenceNumber + " / storedSequenceNumber=" + storedSequenceNumber);
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean checkSignature(ProtectedData data) {
|
||||
byte[] hashOfDataAndSeqNr = CryptoUtil.getHash(new DataAndSeqNr(data.expirablePayload, data.sequenceNumber));
|
||||
try {
|
||||
boolean result = CryptoUtil.verifyStorageData(data.ownerStoragePubKey, hashOfDataAndSeqNr, data.signature);
|
||||
if (!result)
|
||||
log.error("Signature verification failed at checkSignature. " +
|
||||
"That should not happen. Consider it might be an attempt of fraud.");
|
||||
|
||||
return result;
|
||||
} catch (NoSuchAlgorithmException | InvalidKeyException | SignatureException e) {
|
||||
log.error("Signature verification failed at checkSignature");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean checkPublicKeys(ProtectedData data, boolean isAddOperation) {
|
||||
boolean result = false;
|
||||
if (data.expirablePayload instanceof ExpirableMailboxPayload) {
|
||||
ExpirableMailboxPayload expirableMailboxPayload = (ExpirableMailboxPayload) data.expirablePayload;
|
||||
if (isAddOperation)
|
||||
result = expirableMailboxPayload.senderStoragePublicKey.equals(data.ownerStoragePubKey);
|
||||
else
|
||||
result = expirableMailboxPayload.receiverStoragePublicKey.equals(data.ownerStoragePubKey);
|
||||
} else if (data.expirablePayload instanceof PubKeyProtectedExpirablePayload) {
|
||||
result = ((PubKeyProtectedExpirablePayload) data.expirablePayload).getPubKey().equals(data.ownerStoragePubKey);
|
||||
}
|
||||
|
||||
if (!result)
|
||||
log.error("PublicKey of payload data and ProtectedData are not matching. Consider it might be an attempt of fraud");
|
||||
return result;
|
||||
}
|
||||
|
||||
private boolean checkIfStoredDataMatchesNewData(ProtectedData data, BigInteger hashOfData) {
|
||||
ProtectedData storedData = map.get(hashOfData);
|
||||
boolean result = getHashAsBigInteger(storedData.expirablePayload).equals(hashOfData)
|
||||
&& storedData.ownerStoragePubKey.equals(data.ownerStoragePubKey);
|
||||
if (!result)
|
||||
log.error("New data entry does not match our stored data. Consider it might be an attempt of fraud");
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private boolean checkIfStoredMailboxDataMatchesNewMailboxData(ProtectedMailboxData data, BigInteger hashOfData) {
|
||||
ProtectedData storedData = map.get(hashOfData);
|
||||
if (storedData instanceof ProtectedMailboxData) {
|
||||
ProtectedMailboxData storedMailboxData = (ProtectedMailboxData) storedData;
|
||||
// publicKey is not the same (stored: sender, new: receiver)
|
||||
boolean result = storedMailboxData.receiversPubKey.equals(data.receiversPubKey)
|
||||
&& getHashAsBigInteger(storedMailboxData.expirablePayload).equals(hashOfData);
|
||||
if (!result)
|
||||
log.error("New data entry does not match our stored data. Consider it might be an attempt of fraud");
|
||||
|
||||
return result;
|
||||
} else {
|
||||
log.error("We expected a MailboxData but got other type. That must never happen. storedData=" + storedData);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean doAddProtectedExpirableData(ProtectedData data, BigInteger hashOfData, Address sender) {
|
||||
map.put(hashOfData, data);
|
||||
log.trace("Data added to our map and it will be broadcasted to our neighbors.");
|
||||
UserThread.execute(() -> hashSetChangedListeners.stream().forEach(e -> e.onAdded(data)));
|
||||
broadcast(new AddDataMessage(data), sender);
|
||||
|
||||
StringBuilder sb = new StringBuilder("\n\nSet after addProtectedExpirableData:\n");
|
||||
map.values().stream().forEach(e -> sb.append(e.toString() + "\n\n"));
|
||||
sb.append("\n\n");
|
||||
log.trace(sb.toString());
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean doRemoveProtectedExpirableData(ProtectedData data, BigInteger hashOfData, Address sender) {
|
||||
map.remove(hashOfData);
|
||||
log.trace("Data removed from our map. We broadcast the message to our neighbors.");
|
||||
UserThread.execute(() -> hashSetChangedListeners.stream().forEach(e -> e.onRemoved(data)));
|
||||
if (data instanceof ProtectedMailboxData)
|
||||
broadcast(new RemoveMailboxDataMessage((ProtectedMailboxData) data), sender);
|
||||
else
|
||||
broadcast(new RemoveDataMessage(data), sender);
|
||||
|
||||
StringBuilder sb = new StringBuilder("\n\nSet after removeProtectedExpirableData:\n");
|
||||
map.values().stream().forEach(e -> sb.append(e.toString() + "\n\n"));
|
||||
sb.append("\n\n");
|
||||
log.trace(sb.toString());
|
||||
return true;
|
||||
}
|
||||
|
||||
private void broadcast(BroadcastMessage message, Address sender) {
|
||||
if (authenticated) {
|
||||
routing.broadcast(message, sender);
|
||||
log.trace("Broadcast message " + message);
|
||||
} else {
|
||||
log.trace("Broadcast not allowed because we are not authenticated yet. That is normal after received AllDataMessage at startup.");
|
||||
}
|
||||
}
|
||||
|
||||
private BigInteger getHashAsBigInteger(ExpirablePayload payload) {
|
||||
return new BigInteger(CryptoUtil.getHash(payload));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
package io.bitsquare.p2p.storage.data;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
public class DataAndSeqNr implements Serializable {
|
||||
public final Serializable data;
|
||||
public final int sequenceNumber;
|
||||
|
||||
public DataAndSeqNr(Serializable data, int sequenceNumber) {
|
||||
this.data = data;
|
||||
this.sequenceNumber = sequenceNumber;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (!(o instanceof DataAndSeqNr)) return false;
|
||||
|
||||
DataAndSeqNr that = (DataAndSeqNr) o;
|
||||
|
||||
if (sequenceNumber != that.sequenceNumber) return false;
|
||||
return !(data != null ? !data.equals(that.data) : that.data != null);
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = data != null ? data.hashCode() : 0;
|
||||
result = 31 * result + sequenceNumber;
|
||||
return result;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
package io.bitsquare.p2p.storage.data;
|
||||
|
||||
import io.bitsquare.app.Version;
|
||||
import io.bitsquare.p2p.messaging.SealedAndSignedMessage;
|
||||
|
||||
import java.security.PublicKey;
|
||||
|
||||
public final class ExpirableMailboxPayload implements ExpirablePayload {
|
||||
// That object is sent over the wire, so we need to take care of version compatibility.
|
||||
private static final long serialVersionUID = Version.NETWORK_PROTOCOL_VERSION;
|
||||
|
||||
public static final long TTL = 10 * 24 * 60 * 60 * 1000; // 10 days
|
||||
|
||||
public final SealedAndSignedMessage sealedAndSignedMessage;
|
||||
public final PublicKey senderStoragePublicKey;
|
||||
public final PublicKey receiverStoragePublicKey;
|
||||
|
||||
public ExpirableMailboxPayload(SealedAndSignedMessage sealedAndSignedMessage, PublicKey senderStoragePublicKey, PublicKey receiverStoragePublicKey) {
|
||||
this.sealedAndSignedMessage = sealedAndSignedMessage;
|
||||
this.senderStoragePublicKey = senderStoragePublicKey;
|
||||
this.receiverStoragePublicKey = receiverStoragePublicKey;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getTTL() {
|
||||
return TTL;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (!(o instanceof ExpirableMailboxPayload)) return false;
|
||||
|
||||
ExpirableMailboxPayload that = (ExpirableMailboxPayload) o;
|
||||
|
||||
return !(sealedAndSignedMessage != null ? !sealedAndSignedMessage.equals(that.sealedAndSignedMessage) : that.sealedAndSignedMessage != null);
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return sealedAndSignedMessage != null ? sealedAndSignedMessage.hashCode() : 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "MailboxEntry{" +
|
||||
"hashCode=" + hashCode() +
|
||||
", sealedAndSignedMessage=" + sealedAndSignedMessage +
|
||||
'}';
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
package io.bitsquare.p2p.storage.data;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
public interface ExpirablePayload extends Serializable {
|
||||
long getTTL();
|
||||
}
|
|
@ -0,0 +1,66 @@
|
|||
package io.bitsquare.p2p.storage.data;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import io.bitsquare.p2p.storage.ProtectedExpirableDataStorage;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.Serializable;
|
||||
import java.security.PublicKey;
|
||||
import java.util.Date;
|
||||
|
||||
public class ProtectedData implements Serializable {
|
||||
private static final Logger log = LoggerFactory.getLogger(ProtectedExpirableDataStorage.class);
|
||||
|
||||
public final ExpirablePayload expirablePayload;
|
||||
transient public long ttl;
|
||||
public final PublicKey ownerStoragePubKey;
|
||||
public final int sequenceNumber;
|
||||
public final byte[] signature;
|
||||
@VisibleForTesting
|
||||
public Date date;
|
||||
|
||||
public ProtectedData(ExpirablePayload expirablePayload, long ttl, PublicKey ownerStoragePubKey, int sequenceNumber, byte[] signature) {
|
||||
this.expirablePayload = expirablePayload;
|
||||
this.ttl = ttl;
|
||||
this.ownerStoragePubKey = ownerStoragePubKey;
|
||||
this.sequenceNumber = sequenceNumber;
|
||||
this.signature = signature;
|
||||
this.date = new Date();
|
||||
}
|
||||
|
||||
private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException {
|
||||
try {
|
||||
in.defaultReadObject();
|
||||
ttl = expirablePayload.getTTL();
|
||||
|
||||
// in case the reported creation date is in the future
|
||||
// we reset the date to the current time
|
||||
if (date.getTime() > new Date().getTime()) {
|
||||
log.warn("Date of object is in future. " +
|
||||
"That might be ok as clocks are not synced but could be also a spam attack. " +
|
||||
"date=" + date + " / now=" + new Date());
|
||||
date = new Date();
|
||||
}
|
||||
date = new Date();
|
||||
|
||||
} catch (Throwable t) {
|
||||
t.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isExpired() {
|
||||
return (new Date().getTime() - date.getTime()) > ttl;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "ProtectedData{" +
|
||||
"data=\n" + expirablePayload +
|
||||
", \nttl=" + ttl +
|
||||
", sequenceNumber=" + sequenceNumber +
|
||||
", date=" + date +
|
||||
"\n}";
|
||||
}
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
package io.bitsquare.p2p.storage.data;
|
||||
|
||||
import io.bitsquare.p2p.storage.ProtectedExpirableDataStorage;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.PublicKey;
|
||||
import java.util.Date;
|
||||
|
||||
public class ProtectedMailboxData extends ProtectedData {
|
||||
private static final Logger log = LoggerFactory.getLogger(ProtectedExpirableDataStorage.class);
|
||||
|
||||
public final PublicKey receiversPubKey;
|
||||
|
||||
public ProtectedMailboxData(ExpirableMailboxPayload data, long ttl, PublicKey ownerStoragePubKey, int sequenceNumber, byte[] signature, PublicKey receiversPubKey) {
|
||||
super(data, ttl, ownerStoragePubKey, sequenceNumber, signature);
|
||||
|
||||
this.receiversPubKey = receiversPubKey;
|
||||
}
|
||||
|
||||
private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException {
|
||||
try {
|
||||
in.defaultReadObject();
|
||||
ttl = expirablePayload.getTTL();
|
||||
|
||||
// in case the reported creation date is in the future
|
||||
// we reset the date to the current time
|
||||
if (date.getTime() > new Date().getTime()) {
|
||||
log.warn("Date of object is in future. " +
|
||||
"That might be ok as clocks are not synced but could be also a spam attack. " +
|
||||
"date=" + date + " / now=" + new Date());
|
||||
date = new Date();
|
||||
}
|
||||
date = new Date();
|
||||
|
||||
} catch (Throwable t) {
|
||||
t.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isExpired() {
|
||||
return (new Date().getTime() - date.getTime()) > ttl;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "MailboxData{" +
|
||||
"data=\n" + expirablePayload +
|
||||
", \nttl=" + ttl +
|
||||
", sequenceNumber=" + sequenceNumber +
|
||||
", date=" + date +
|
||||
"\n}";
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
package io.bitsquare.p2p.storage.data;
|
||||
|
||||
import java.security.PublicKey;
|
||||
|
||||
public interface PubKeyProtectedExpirablePayload extends ExpirablePayload {
|
||||
PublicKey getPubKey();
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
package io.bitsquare.p2p.storage.messages;
|
||||
|
||||
import io.bitsquare.app.Version;
|
||||
import io.bitsquare.p2p.storage.data.ProtectedData;
|
||||
|
||||
public final class AddDataMessage implements DataMessage {
|
||||
// That object is sent over the wire, so we need to take care of version compatibility.
|
||||
private static final long serialVersionUID = Version.NETWORK_PROTOCOL_VERSION;
|
||||
|
||||
public final ProtectedData data;
|
||||
|
||||
public AddDataMessage(ProtectedData data) {
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (!(o instanceof AddDataMessage)) return false;
|
||||
|
||||
AddDataMessage that = (AddDataMessage) o;
|
||||
|
||||
return !(data != null ? !data.equals(that.data) : that.data != null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return data != null ? data.hashCode() : 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "AddDataMessage{" +
|
||||
"data=" + data +
|
||||
'}';
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
package io.bitsquare.p2p.storage.messages;
|
||||
|
||||
import io.bitsquare.p2p.Message;
|
||||
|
||||
public interface BroadcastMessage extends Message {
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
package io.bitsquare.p2p.storage.messages;
|
||||
|
||||
// market interface for messages which manipulate data (add, remove)
|
||||
public interface DataMessage extends BroadcastMessage {
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
package io.bitsquare.p2p.storage.messages;
|
||||
|
||||
import io.bitsquare.app.Version;
|
||||
import io.bitsquare.p2p.Message;
|
||||
import io.bitsquare.p2p.storage.data.ProtectedData;
|
||||
|
||||
import java.util.HashSet;
|
||||
|
||||
public final class DataSetMessage implements Message {
|
||||
// That object is sent over the wire, so we need to take care of version compatibility.
|
||||
private static final long serialVersionUID = Version.NETWORK_PROTOCOL_VERSION;
|
||||
|
||||
public final HashSet<ProtectedData> set;
|
||||
|
||||
public DataSetMessage(HashSet<ProtectedData> set) {
|
||||
this.set = set;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (!(o instanceof DataSetMessage)) return false;
|
||||
|
||||
DataSetMessage that = (DataSetMessage) o;
|
||||
|
||||
return !(set != null ? !set.equals(that.set) : that.set != null);
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return set != null ? set.hashCode() : 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "AllDataMessage{" +
|
||||
"set=" + set +
|
||||
'}';
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
package io.bitsquare.p2p.storage.messages;
|
||||
|
||||
import io.bitsquare.app.Version;
|
||||
import io.bitsquare.p2p.Message;
|
||||
|
||||
public final class GetDataSetMessage implements Message {
|
||||
// That object is sent over the wire, so we need to take care of version compatibility.
|
||||
private static final long serialVersionUID = Version.NETWORK_PROTOCOL_VERSION;
|
||||
|
||||
public final long nonce;
|
||||
|
||||
public GetDataSetMessage(long nonce) {
|
||||
this.nonce = nonce;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
package io.bitsquare.p2p.storage.messages;
|
||||
|
||||
import io.bitsquare.app.Version;
|
||||
import io.bitsquare.p2p.storage.data.ProtectedData;
|
||||
|
||||
public final class RemoveDataMessage implements DataMessage {
|
||||
// That object is sent over the wire, so we need to take care of version compatibility.
|
||||
private static final long serialVersionUID = Version.NETWORK_PROTOCOL_VERSION;
|
||||
|
||||
public final ProtectedData data;
|
||||
|
||||
public RemoveDataMessage(ProtectedData data) {
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (!(o instanceof RemoveDataMessage)) return false;
|
||||
|
||||
RemoveDataMessage that = (RemoveDataMessage) o;
|
||||
|
||||
return !(data != null ? !data.equals(that.data) : that.data != null);
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return data != null ? data.hashCode() : 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "RemoveDataMessage{" +
|
||||
"data=" + data +
|
||||
'}';
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
package io.bitsquare.p2p.storage.messages;
|
||||
|
||||
import io.bitsquare.app.Version;
|
||||
import io.bitsquare.p2p.storage.data.ProtectedMailboxData;
|
||||
|
||||
public final class RemoveMailboxDataMessage implements DataMessage {
|
||||
// That object is sent over the wire, so we need to take care of version compatibility.
|
||||
private static final long serialVersionUID = Version.NETWORK_PROTOCOL_VERSION;
|
||||
|
||||
public final ProtectedMailboxData data;
|
||||
|
||||
public RemoveMailboxDataMessage(ProtectedMailboxData data) {
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (!(o instanceof RemoveMailboxDataMessage)) return false;
|
||||
|
||||
RemoveMailboxDataMessage that = (RemoveMailboxDataMessage) o;
|
||||
|
||||
return !(data != null ? !data.equals(that.data) : that.data != null);
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return data != null ? data.hashCode() : 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "RemoveMailboxDataMessage{" +
|
||||
"data=" + data +
|
||||
'}';
|
||||
}
|
||||
}
|
13
network/src/main/resources/logback.xml
Normal file
13
network/src/main/resources/logback.xml
Normal file
|
@ -0,0 +1,13 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<configuration>
|
||||
<appender name="CONSOLE_APPENDER" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<encoder>
|
||||
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg %xEx%n</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<root level="TRACE">
|
||||
<appender-ref ref="CONSOLE_APPENDER"/>
|
||||
</root>
|
||||
|
||||
</configuration>
|
362
network/src/test/java/io/bitsquare/p2p/P2PServiceTest.java
Normal file
362
network/src/test/java/io/bitsquare/p2p/P2PServiceTest.java
Normal file
|
@ -0,0 +1,362 @@
|
|||
package io.bitsquare.p2p;
|
||||
|
||||
import io.bitsquare.common.crypto.CryptoException;
|
||||
import io.bitsquare.common.crypto.CryptoUtil;
|
||||
import io.bitsquare.common.crypto.KeyRing;
|
||||
import io.bitsquare.common.crypto.KeyStorage;
|
||||
import io.bitsquare.crypto.EncryptionService;
|
||||
import io.bitsquare.p2p.messaging.DecryptedMessageWithPubKey;
|
||||
import io.bitsquare.p2p.messaging.MailboxMessage;
|
||||
import io.bitsquare.p2p.messaging.SealedAndSignedMessage;
|
||||
import io.bitsquare.p2p.messaging.SendMailboxMessageListener;
|
||||
import io.bitsquare.p2p.mocks.MockMailboxMessage;
|
||||
import io.bitsquare.p2p.network.LocalhostNetworkNode;
|
||||
import io.bitsquare.p2p.routing.Routing;
|
||||
import io.bitsquare.p2p.seed.SeedNode;
|
||||
import io.bitsquare.p2p.storage.data.DataAndSeqNr;
|
||||
import io.bitsquare.p2p.storage.data.ProtectedData;
|
||||
import io.bitsquare.p2p.storage.mocks.MockData;
|
||||
import org.junit.*;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.security.*;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
|
||||
// TorNode created. Took 6 sec.
|
||||
// Hidden service created. Took 40-50 sec.
|
||||
// Connection establishment takes about 4 sec.
|
||||
|
||||
// need to define seed node addresses first before using tor version
|
||||
@Ignore
|
||||
public class P2PServiceTest {
|
||||
private static final Logger log = LoggerFactory.getLogger(P2PServiceTest.class);
|
||||
|
||||
boolean useLocalhost = true;
|
||||
private ArrayList<Address> seedNodes;
|
||||
private int sleepTime;
|
||||
private KeyRing keyRing1, keyRing2, keyRing3;
|
||||
private EncryptionService encryptionService1, encryptionService2, encryptionService3;
|
||||
private P2PService p2PService1, p2PService2, p2PService3;
|
||||
private SeedNode seedNode1, seedNode2, seedNode3;
|
||||
|
||||
@Before
|
||||
public void setup() throws InterruptedException, CertificateException, NoSuchAlgorithmException, KeyStoreException, IOException, CryptoException {
|
||||
LocalhostNetworkNode.setSimulateTorDelayTorNode(10);
|
||||
LocalhostNetworkNode.setSimulateTorDelayHiddenService(100);
|
||||
Routing.setMaxConnections(8);
|
||||
|
||||
keyRing1 = new KeyRing(new KeyStorage(new File("temp_keyStorage1")));
|
||||
keyRing2 = new KeyRing(new KeyStorage(new File("temp_keyStorage2")));
|
||||
keyRing3 = new KeyRing(new KeyStorage(new File("temp_keyStorage3")));
|
||||
encryptionService1 = new EncryptionService(keyRing1);
|
||||
encryptionService2 = new EncryptionService(keyRing2);
|
||||
encryptionService3 = new EncryptionService(keyRing3);
|
||||
|
||||
seedNodes = new ArrayList<>();
|
||||
if (useLocalhost) {
|
||||
seedNodes.add(new Address("localhost:8001"));
|
||||
seedNodes.add(new Address("localhost:8002"));
|
||||
seedNodes.add(new Address("localhost:8003"));
|
||||
sleepTime = 100;
|
||||
|
||||
} else {
|
||||
seedNodes.add(new Address("3omjuxn7z73pxoee.onion:8001"));
|
||||
seedNodes.add(new Address("j24fxqyghjetgpdx.onion:8002"));
|
||||
seedNodes.add(new Address("45367tl6unwec6kw.onion:8003"));
|
||||
sleepTime = 1000;
|
||||
}
|
||||
|
||||
seedNode1 = TestUtils.getAndStartSeedNode(8001, encryptionService1, keyRing1, useLocalhost, seedNodes);
|
||||
p2PService1 = seedNode1.getP2PService();
|
||||
p2PService2 = TestUtils.getAndAuthenticateP2PService(8002, encryptionService2, keyRing2, useLocalhost, seedNodes);
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() throws InterruptedException {
|
||||
Thread.sleep(sleepTime);
|
||||
|
||||
if (seedNode1 != null) {
|
||||
CountDownLatch shutDownLatch = new CountDownLatch(1);
|
||||
seedNode1.shutDown(() -> shutDownLatch.countDown());
|
||||
shutDownLatch.await();
|
||||
}
|
||||
if (seedNode2 != null) {
|
||||
CountDownLatch shutDownLatch = new CountDownLatch(1);
|
||||
seedNode2.shutDown(() -> shutDownLatch.countDown());
|
||||
shutDownLatch.await();
|
||||
}
|
||||
if (seedNode3 != null) {
|
||||
CountDownLatch shutDownLatch = new CountDownLatch(1);
|
||||
seedNode3.shutDown(() -> shutDownLatch.countDown());
|
||||
shutDownLatch.await();
|
||||
}
|
||||
if (p2PService1 != null) {
|
||||
CountDownLatch shutDownLatch = new CountDownLatch(1);
|
||||
p2PService1.shutDown(() -> shutDownLatch.countDown());
|
||||
shutDownLatch.await();
|
||||
}
|
||||
if (p2PService2 != null) {
|
||||
CountDownLatch shutDownLatch = new CountDownLatch(1);
|
||||
p2PService2.shutDown(() -> shutDownLatch.countDown());
|
||||
shutDownLatch.await();
|
||||
}
|
||||
if (p2PService3 != null) {
|
||||
CountDownLatch shutDownLatch = new CountDownLatch(1);
|
||||
p2PService3.shutDown(() -> shutDownLatch.countDown());
|
||||
shutDownLatch.await();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAdversaryAttacks() throws InterruptedException, NoSuchAlgorithmException, CertificateException, KeyStoreException, IOException, CryptoException, SignatureException, InvalidKeyException {
|
||||
p2PService3 = TestUtils.getAndAuthenticateP2PService(8003, encryptionService3, keyRing3, useLocalhost, seedNodes);
|
||||
|
||||
MockData origData = new MockData("mockData1", keyRing1.getStorageSignatureKeyPair().getPublic());
|
||||
|
||||
p2PService1.addData(origData);
|
||||
Assert.assertEquals(1, p2PService1.getDataMap().size());
|
||||
Assert.assertEquals(1, p2PService2.getDataMap().size());
|
||||
Assert.assertEquals(1, p2PService3.getDataMap().size());
|
||||
|
||||
|
||||
// p2PService3 is adversary
|
||||
KeyPair msgSignatureKeyPairAdversary = keyRing3.getStorageSignatureKeyPair();
|
||||
|
||||
// try to remove data -> fails
|
||||
Assert.assertFalse(p2PService3.removeData(origData));
|
||||
Thread.sleep(sleepTime);
|
||||
Assert.assertEquals(1, p2PService1.getDataMap().size());
|
||||
Assert.assertEquals(1, p2PService2.getDataMap().size());
|
||||
Assert.assertEquals(1, p2PService3.getDataMap().size());
|
||||
|
||||
|
||||
// try to add manipulated data -> fails
|
||||
Assert.assertFalse(p2PService3.removeData(new MockData("mockData2", origData.publicKey)));
|
||||
Thread.sleep(sleepTime);
|
||||
Assert.assertEquals(1, p2PService1.getDataMap().size());
|
||||
Assert.assertEquals(1, p2PService2.getDataMap().size());
|
||||
Assert.assertEquals(1, p2PService3.getDataMap().size());
|
||||
|
||||
// try to manipulate seq nr. -> fails
|
||||
ProtectedData origProtectedData = p2PService3.getDataMap().values().stream().findFirst().get();
|
||||
ProtectedData protectedDataManipulated = new ProtectedData(origProtectedData.expirablePayload, origProtectedData.ttl, origProtectedData.ownerStoragePubKey, origProtectedData.sequenceNumber + 1, origProtectedData.signature);
|
||||
Assert.assertFalse(p2PService3.removeData(protectedDataManipulated.expirablePayload));
|
||||
Thread.sleep(sleepTime);
|
||||
Assert.assertEquals(1, p2PService1.getDataMap().size());
|
||||
Assert.assertEquals(1, p2PService2.getDataMap().size());
|
||||
Assert.assertEquals(1, p2PService3.getDataMap().size());
|
||||
|
||||
// try to manipulate seq nr. + pubKey -> fails
|
||||
protectedDataManipulated = new ProtectedData(origProtectedData.expirablePayload, origProtectedData.ttl, msgSignatureKeyPairAdversary.getPublic(), origProtectedData.sequenceNumber + 1, origProtectedData.signature);
|
||||
Assert.assertFalse(p2PService3.removeData(protectedDataManipulated.expirablePayload));
|
||||
Thread.sleep(sleepTime);
|
||||
Assert.assertEquals(1, p2PService1.getDataMap().size());
|
||||
Assert.assertEquals(1, p2PService2.getDataMap().size());
|
||||
Assert.assertEquals(1, p2PService3.getDataMap().size());
|
||||
|
||||
// try to manipulate seq nr. + pubKey + sig -> fails
|
||||
int sequenceNumberManipulated = origProtectedData.sequenceNumber + 1;
|
||||
byte[] hashOfDataAndSeqNr = CryptoUtil.getHash(new DataAndSeqNr(origProtectedData.expirablePayload, sequenceNumberManipulated));
|
||||
byte[] signature = CryptoUtil.signStorageData(msgSignatureKeyPairAdversary.getPrivate(), hashOfDataAndSeqNr);
|
||||
protectedDataManipulated = new ProtectedData(origProtectedData.expirablePayload, origProtectedData.ttl, msgSignatureKeyPairAdversary.getPublic(), sequenceNumberManipulated, signature);
|
||||
Assert.assertFalse(p2PService3.removeData(protectedDataManipulated.expirablePayload));
|
||||
Thread.sleep(sleepTime);
|
||||
Assert.assertEquals(1, p2PService1.getDataMap().size());
|
||||
Assert.assertEquals(1, p2PService2.getDataMap().size());
|
||||
Assert.assertEquals(1, p2PService3.getDataMap().size());
|
||||
|
||||
|
||||
// data owner removes -> ok
|
||||
Assert.assertTrue(p2PService1.removeData(origData));
|
||||
Thread.sleep(sleepTime);
|
||||
Assert.assertEquals(0, p2PService1.getDataMap().size());
|
||||
Assert.assertEquals(0, p2PService2.getDataMap().size());
|
||||
Assert.assertEquals(0, p2PService3.getDataMap().size());
|
||||
|
||||
// adversary manage to change data before it is broadcasted to others
|
||||
// data owner is connected only to adversary -> he change data at onMessage and broadcast manipulated data
|
||||
// first he tries to use the orig. pubKey in the data -> fails as pub keys not matching
|
||||
MockData manipulatedData = new MockData("mockData1_manipulated", origData.publicKey);
|
||||
sequenceNumberManipulated = 0;
|
||||
hashOfDataAndSeqNr = CryptoUtil.getHash(new DataAndSeqNr(manipulatedData, sequenceNumberManipulated));
|
||||
signature = CryptoUtil.signStorageData(msgSignatureKeyPairAdversary.getPrivate(), hashOfDataAndSeqNr);
|
||||
protectedDataManipulated = new ProtectedData(origProtectedData.expirablePayload, origProtectedData.ttl, msgSignatureKeyPairAdversary.getPublic(), sequenceNumberManipulated, signature);
|
||||
Assert.assertFalse(p2PService3.addData(protectedDataManipulated.expirablePayload));
|
||||
Thread.sleep(sleepTime);
|
||||
Assert.assertEquals(0, p2PService1.getDataMap().size());
|
||||
Assert.assertEquals(0, p2PService2.getDataMap().size());
|
||||
Assert.assertEquals(0, p2PService3.getDataMap().size());
|
||||
|
||||
// then he tries to use his pubKey but orig data payload -> fails as pub keys nto matching
|
||||
manipulatedData = new MockData("mockData1_manipulated", msgSignatureKeyPairAdversary.getPublic());
|
||||
sequenceNumberManipulated = 0;
|
||||
hashOfDataAndSeqNr = CryptoUtil.getHash(new DataAndSeqNr(manipulatedData, sequenceNumberManipulated));
|
||||
signature = CryptoUtil.signStorageData(msgSignatureKeyPairAdversary.getPrivate(), hashOfDataAndSeqNr);
|
||||
protectedDataManipulated = new ProtectedData(origProtectedData.expirablePayload, origProtectedData.ttl, msgSignatureKeyPairAdversary.getPublic(), sequenceNumberManipulated, signature);
|
||||
Assert.assertFalse(p2PService3.addData(protectedDataManipulated.expirablePayload));
|
||||
Thread.sleep(sleepTime);
|
||||
Assert.assertEquals(0, p2PService1.getDataMap().size());
|
||||
Assert.assertEquals(0, p2PService2.getDataMap().size());
|
||||
Assert.assertEquals(0, p2PService3.getDataMap().size());
|
||||
|
||||
// then he tries to use his pubKey -> now he succeeds, but its same as adding a completely new msg and
|
||||
// payload data has adversary's pubKey so he could hijack the owners data
|
||||
manipulatedData = new MockData("mockData1_manipulated", msgSignatureKeyPairAdversary.getPublic());
|
||||
sequenceNumberManipulated = 0;
|
||||
hashOfDataAndSeqNr = CryptoUtil.getHash(new DataAndSeqNr(manipulatedData, sequenceNumberManipulated));
|
||||
signature = CryptoUtil.signStorageData(msgSignatureKeyPairAdversary.getPrivate(), hashOfDataAndSeqNr);
|
||||
protectedDataManipulated = new ProtectedData(manipulatedData, origProtectedData.ttl, msgSignatureKeyPairAdversary.getPublic(), sequenceNumberManipulated, signature);
|
||||
Assert.assertTrue(p2PService3.addData(protectedDataManipulated.expirablePayload));
|
||||
Thread.sleep(sleepTime);
|
||||
Assert.assertEquals(1, p2PService1.getDataMap().size());
|
||||
Assert.assertEquals(1, p2PService2.getDataMap().size());
|
||||
Assert.assertEquals(1, p2PService3.getDataMap().size());
|
||||
|
||||
// let clean up. he can remove his own data
|
||||
Assert.assertTrue(p2PService3.removeData(manipulatedData));
|
||||
Thread.sleep(sleepTime);
|
||||
Assert.assertEquals(0, p2PService1.getDataMap().size());
|
||||
Assert.assertEquals(0, p2PService2.getDataMap().size());
|
||||
Assert.assertEquals(0, p2PService3.getDataMap().size());
|
||||
|
||||
|
||||
// finally he tries both previous attempts with same data - > same as before
|
||||
manipulatedData = new MockData("mockData1", origData.publicKey);
|
||||
sequenceNumberManipulated = 0;
|
||||
hashOfDataAndSeqNr = CryptoUtil.getHash(new DataAndSeqNr(manipulatedData, sequenceNumberManipulated));
|
||||
signature = CryptoUtil.signStorageData(msgSignatureKeyPairAdversary.getPrivate(), hashOfDataAndSeqNr);
|
||||
protectedDataManipulated = new ProtectedData(origProtectedData.expirablePayload, origProtectedData.ttl, msgSignatureKeyPairAdversary.getPublic(), sequenceNumberManipulated, signature);
|
||||
Assert.assertFalse(p2PService3.addData(protectedDataManipulated.expirablePayload));
|
||||
Thread.sleep(sleepTime);
|
||||
Assert.assertEquals(0, p2PService1.getDataMap().size());
|
||||
Assert.assertEquals(0, p2PService2.getDataMap().size());
|
||||
Assert.assertEquals(0, p2PService3.getDataMap().size());
|
||||
|
||||
manipulatedData = new MockData("mockData1", msgSignatureKeyPairAdversary.getPublic());
|
||||
sequenceNumberManipulated = 0;
|
||||
hashOfDataAndSeqNr = CryptoUtil.getHash(new DataAndSeqNr(manipulatedData, sequenceNumberManipulated));
|
||||
signature = CryptoUtil.signStorageData(msgSignatureKeyPairAdversary.getPrivate(), hashOfDataAndSeqNr);
|
||||
protectedDataManipulated = new ProtectedData(manipulatedData, origProtectedData.ttl, msgSignatureKeyPairAdversary.getPublic(), sequenceNumberManipulated, signature);
|
||||
Assert.assertTrue(p2PService3.addData(protectedDataManipulated.expirablePayload));
|
||||
Thread.sleep(sleepTime);
|
||||
Assert.assertEquals(1, p2PService1.getDataMap().size());
|
||||
Assert.assertEquals(1, p2PService2.getDataMap().size());
|
||||
Assert.assertEquals(1, p2PService3.getDataMap().size());
|
||||
|
||||
// lets reset map
|
||||
Assert.assertTrue(p2PService3.removeData(protectedDataManipulated.expirablePayload));
|
||||
Thread.sleep(sleepTime);
|
||||
Assert.assertEquals(0, p2PService1.getDataMap().size());
|
||||
Assert.assertEquals(0, p2PService2.getDataMap().size());
|
||||
Assert.assertEquals(0, p2PService3.getDataMap().size());
|
||||
|
||||
// owner can add any time his data
|
||||
Assert.assertTrue(p2PService1.addData(origData));
|
||||
Thread.sleep(sleepTime);
|
||||
Assert.assertEquals(1, p2PService1.getDataMap().size());
|
||||
Assert.assertEquals(1, p2PService2.getDataMap().size());
|
||||
Assert.assertEquals(1, p2PService3.getDataMap().size());
|
||||
}
|
||||
|
||||
//@Test
|
||||
public void testSendMailboxMessageToOnlinePeer() throws InterruptedException, CertificateException, NoSuchAlgorithmException, KeyStoreException, IOException, CryptoException {
|
||||
LocalhostNetworkNode.setSimulateTorDelayTorNode(0);
|
||||
LocalhostNetworkNode.setSimulateTorDelayHiddenService(0);
|
||||
|
||||
// send to online peer
|
||||
CountDownLatch latch2 = new CountDownLatch(2);
|
||||
MockMailboxMessage mockMessage = new MockMailboxMessage("MockMailboxMessage", p2PService2.getAddress());
|
||||
p2PService2.addMessageListener((message, connection) -> {
|
||||
log.trace("message " + message);
|
||||
if (message instanceof SealedAndSignedMessage) {
|
||||
try {
|
||||
DecryptedMessageWithPubKey decryptedMessageWithPubKey = encryptionService2.decryptAndVerifyMessage((SealedAndSignedMessage) message);
|
||||
Assert.assertEquals(mockMessage, decryptedMessageWithPubKey.message);
|
||||
Assert.assertEquals(p2PService2.getAddress(), ((MailboxMessage) decryptedMessageWithPubKey.message).getSenderAddress());
|
||||
latch2.countDown();
|
||||
} catch (CryptoException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
p2PService1.sendEncryptedMailboxMessage(
|
||||
p2PService2.getAddress(),
|
||||
keyRing2.getPubKeyRing(),
|
||||
mockMessage,
|
||||
new SendMailboxMessageListener() {
|
||||
@Override
|
||||
public void onArrived() {
|
||||
log.trace("Message arrived at peer.");
|
||||
latch2.countDown();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStoredInMailbox() {
|
||||
log.trace("Message stored in mailbox.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFault() {
|
||||
log.error("onFault");
|
||||
}
|
||||
}
|
||||
);
|
||||
latch2.await();
|
||||
}
|
||||
|
||||
//@Test
|
||||
public void testSendMailboxMessageToOfflinePeer() throws InterruptedException, CertificateException, NoSuchAlgorithmException, KeyStoreException, IOException, CryptoException {
|
||||
LocalhostNetworkNode.setSimulateTorDelayTorNode(0);
|
||||
LocalhostNetworkNode.setSimulateTorDelayHiddenService(0);
|
||||
|
||||
// send msg to offline peer
|
||||
MockMailboxMessage mockMessage = new MockMailboxMessage(
|
||||
"MockMailboxMessage",
|
||||
p2PService2.getAddress()
|
||||
);
|
||||
CountDownLatch latch2 = new CountDownLatch(1);
|
||||
p2PService2.sendEncryptedMailboxMessage(
|
||||
new Address("localhost:8003"),
|
||||
keyRing3.getPubKeyRing(),
|
||||
mockMessage,
|
||||
new SendMailboxMessageListener() {
|
||||
@Override
|
||||
public void onArrived() {
|
||||
log.trace("Message arrived at peer.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStoredInMailbox() {
|
||||
log.trace("Message stored in mailbox.");
|
||||
latch2.countDown();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFault() {
|
||||
log.error("onFault");
|
||||
}
|
||||
}
|
||||
);
|
||||
latch2.await();
|
||||
Thread.sleep(2000);
|
||||
|
||||
|
||||
// start node 3
|
||||
p2PService3 = TestUtils.getAndAuthenticateP2PService(8003, encryptionService3, keyRing3, useLocalhost, seedNodes);
|
||||
Thread.sleep(sleepTime);
|
||||
CountDownLatch latch3 = new CountDownLatch(1);
|
||||
p2PService3.addDecryptedMailboxListener((decryptedMessageWithPubKey, senderAddress) -> {
|
||||
log.debug("decryptedMessageWithPubKey " + decryptedMessageWithPubKey.toString());
|
||||
Assert.assertEquals(mockMessage, decryptedMessageWithPubKey.message);
|
||||
Assert.assertEquals(p2PService2.getAddress(), ((MailboxMessage) decryptedMessageWithPubKey.message).getSenderAddress());
|
||||
latch3.countDown();
|
||||
});
|
||||
latch3.await();
|
||||
}
|
||||
}
|
150
network/src/test/java/io/bitsquare/p2p/TestUtils.java
Normal file
150
network/src/test/java/io/bitsquare/p2p/TestUtils.java
Normal file
|
@ -0,0 +1,150 @@
|
|||
package io.bitsquare.p2p;
|
||||
|
||||
import io.bitsquare.common.crypto.KeyRing;
|
||||
import io.bitsquare.crypto.EncryptionService;
|
||||
import io.bitsquare.p2p.seed.SeedNode;
|
||||
import io.bitsquare.p2p.seed.SeedNodesRepository;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.*;
|
||||
import java.security.*;
|
||||
import java.util.ArrayList;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
|
||||
public class TestUtils {
|
||||
private static final Logger log = LoggerFactory.getLogger(TestUtils.class);
|
||||
|
||||
public static int sleepTime;
|
||||
|
||||
public static KeyPair generateKeyPair() throws NoSuchAlgorithmException {
|
||||
long ts = System.currentTimeMillis();
|
||||
final KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("DSA");
|
||||
keyPairGenerator.initialize(1024);
|
||||
KeyPair keyPair = keyPairGenerator.genKeyPair();
|
||||
log.trace("Generate storageSignatureKeyPair needed {} ms", System.currentTimeMillis() - ts);
|
||||
return keyPair;
|
||||
}
|
||||
|
||||
|
||||
public static byte[] sign(PrivateKey privateKey, Serializable data)
|
||||
throws SignatureException, NoSuchAlgorithmException, InvalidKeyException {
|
||||
Signature sig = Signature.getInstance("SHA1withDSA");
|
||||
sig.initSign(privateKey);
|
||||
sig.update(objectToByteArray(data));
|
||||
return sig.sign();
|
||||
}
|
||||
|
||||
public static byte[] objectToByteArray(Object object) {
|
||||
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||
ObjectOutput out = null;
|
||||
byte[] result = null;
|
||||
try {
|
||||
out = new ObjectOutputStream(bos);
|
||||
out.writeObject(object);
|
||||
result = bos.toByteArray();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
} finally {
|
||||
try {
|
||||
if (out != null) {
|
||||
out.close();
|
||||
}
|
||||
} catch (IOException ex) {
|
||||
// ignore close exception
|
||||
}
|
||||
try {
|
||||
bos.close();
|
||||
} catch (IOException ex) {
|
||||
// ignore close exception
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public static SeedNode getAndStartSeedNode(int port, EncryptionService encryptionService, KeyRing keyRing, boolean useLocalhost, ArrayList<Address> seedNodes) throws InterruptedException {
|
||||
SeedNode seedNode;
|
||||
|
||||
if (useLocalhost) {
|
||||
seedNodes.add(new Address("localhost:8001"));
|
||||
seedNodes.add(new Address("localhost:8002"));
|
||||
seedNodes.add(new Address("localhost:8003"));
|
||||
sleepTime = 100;
|
||||
seedNode = new SeedNode();
|
||||
} else {
|
||||
seedNodes.add(new Address("3omjuxn7z73pxoee.onion:8001"));
|
||||
seedNodes.add(new Address("j24fxqyghjetgpdx.onion:8002"));
|
||||
seedNodes.add(new Address("45367tl6unwec6kw.onion:8003"));
|
||||
sleepTime = 10000;
|
||||
seedNode = new SeedNode();
|
||||
}
|
||||
|
||||
CountDownLatch latch = new CountDownLatch(1);
|
||||
seedNode.createAndStartP2PService(encryptionService, keyRing, port, useLocalhost, seedNodes, new P2PServiceListener() {
|
||||
@Override
|
||||
public void onAllDataReceived() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAuthenticated() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTorNodeReady() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onHiddenServiceReady() {
|
||||
latch.countDown();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetupFailed(Throwable throwable) {
|
||||
}
|
||||
});
|
||||
latch.await();
|
||||
Thread.sleep(sleepTime);
|
||||
return seedNode;
|
||||
}
|
||||
|
||||
public static P2PService getAndAuthenticateP2PService(int port, EncryptionService encryptionService, KeyRing keyRing, boolean useLocalhost, ArrayList<Address> seedNodes) throws InterruptedException {
|
||||
CountDownLatch latch = new CountDownLatch(1);
|
||||
SeedNodesRepository seedNodesRepository = new SeedNodesRepository();
|
||||
if (seedNodes != null && !seedNodes.isEmpty()) {
|
||||
if (useLocalhost)
|
||||
seedNodesRepository.setLocalhostSeedNodeAddresses(seedNodes);
|
||||
else
|
||||
seedNodesRepository.setTorSeedNodeAddresses(seedNodes);
|
||||
}
|
||||
|
||||
P2PService p2PService = new P2PService(seedNodesRepository, port, new File("seed_node_" + port), useLocalhost, encryptionService, keyRing);
|
||||
p2PService.start(new P2PServiceListener() {
|
||||
@Override
|
||||
public void onAllDataReceived() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTorNodeReady() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAuthenticated() {
|
||||
latch.countDown();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onHiddenServiceReady() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetupFailed(Throwable throwable) {
|
||||
|
||||
}
|
||||
});
|
||||
latch.await();
|
||||
Thread.sleep(2000);
|
||||
return p2PService;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
package io.bitsquare.p2p.mocks;
|
||||
|
||||
import io.bitsquare.p2p.Address;
|
||||
import io.bitsquare.p2p.messaging.MailboxMessage;
|
||||
import io.bitsquare.p2p.storage.data.ExpirablePayload;
|
||||
|
||||
public class MockMailboxMessage implements MailboxMessage, ExpirablePayload {
|
||||
public String msg;
|
||||
public Address senderAddress;
|
||||
public long ttl;
|
||||
|
||||
public MockMailboxMessage(String msg, Address senderAddress) {
|
||||
this.msg = msg;
|
||||
this.senderAddress = senderAddress;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (!(o instanceof MockMailboxMessage)) return false;
|
||||
|
||||
MockMailboxMessage that = (MockMailboxMessage) o;
|
||||
|
||||
return !(msg != null ? !msg.equals(that.msg) : that.msg != null);
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return msg != null ? msg.hashCode() : 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "MockData{" +
|
||||
"msg='" + msg + '\'' +
|
||||
'}';
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getTTL() {
|
||||
return ttl;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Address getSenderAddress() {
|
||||
return senderAddress;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
package io.bitsquare.p2p.mocks;
|
||||
|
||||
import io.bitsquare.p2p.Message;
|
||||
import io.bitsquare.p2p.storage.data.ExpirablePayload;
|
||||
|
||||
public class MockMessage implements Message, ExpirablePayload {
|
||||
public String msg;
|
||||
public long ttl;
|
||||
|
||||
public MockMessage(String msg) {
|
||||
this.msg = msg;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (!(o instanceof MockMessage)) return false;
|
||||
|
||||
MockMessage that = (MockMessage) o;
|
||||
|
||||
return !(msg != null ? !msg.equals(that.msg) : that.msg != null);
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return msg != null ? msg.hashCode() : 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "MockData{" +
|
||||
"msg='" + msg + '\'' +
|
||||
'}';
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getTTL() {
|
||||
return ttl;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,84 @@
|
|||
package io.bitsquare.p2p.network;
|
||||
|
||||
import io.bitsquare.p2p.Address;
|
||||
import io.bitsquare.p2p.routing.messages.RequestAuthenticationMessage;
|
||||
import org.junit.Ignore;
|
||||
import org.junit.Test;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
|
||||
// TorNode created. Took 6 sec.
|
||||
// Hidden service created. Took 40-50 sec.
|
||||
// Connection establishment takes about 4 sec.
|
||||
@Ignore
|
||||
public class LocalhostNetworkNodeTest {
|
||||
private static final Logger log = LoggerFactory.getLogger(LocalhostNetworkNodeTest.class);
|
||||
|
||||
@Test
|
||||
public void testMessage() throws InterruptedException, IOException {
|
||||
CountDownLatch msgLatch = new CountDownLatch(2);
|
||||
LocalhostNetworkNode node1 = new LocalhostNetworkNode(9001);
|
||||
node1.addMessageListener((message, connection) -> {
|
||||
log.debug("onMessage node1 " + message);
|
||||
msgLatch.countDown();
|
||||
});
|
||||
CountDownLatch startupLatch = new CountDownLatch(2);
|
||||
node1.start(new SetupListener() {
|
||||
@Override
|
||||
public void onTorNodeReady() {
|
||||
log.debug("onTorNodeReady");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onHiddenServiceReady() {
|
||||
log.debug("onHiddenServiceReady");
|
||||
startupLatch.countDown();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetupFailed(Throwable throwable) {
|
||||
log.debug("onSetupFailed");
|
||||
}
|
||||
});
|
||||
|
||||
LocalhostNetworkNode node2 = new LocalhostNetworkNode(9002);
|
||||
node2.addMessageListener((message, connection) -> {
|
||||
log.debug("onMessage node2 " + message);
|
||||
msgLatch.countDown();
|
||||
});
|
||||
node2.start(new SetupListener() {
|
||||
@Override
|
||||
public void onTorNodeReady() {
|
||||
log.debug("onTorNodeReady 2");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onHiddenServiceReady() {
|
||||
log.debug("onHiddenServiceReady 2");
|
||||
startupLatch.countDown();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetupFailed(Throwable throwable) {
|
||||
log.debug("onSetupFailed 2");
|
||||
}
|
||||
});
|
||||
startupLatch.await();
|
||||
|
||||
node2.sendMessage(new Address("localhost", 9001), new RequestAuthenticationMessage(new Address("localhost", 9002), 1));
|
||||
node1.sendMessage(new Address("localhost", 9002), new RequestAuthenticationMessage(new Address("localhost", 9001), 1));
|
||||
msgLatch.await();
|
||||
|
||||
CountDownLatch shutDownLatch = new CountDownLatch(2);
|
||||
node1.shutDown(() -> {
|
||||
shutDownLatch.countDown();
|
||||
});
|
||||
node2.shutDown(() -> {
|
||||
shutDownLatch.countDown();
|
||||
});
|
||||
shutDownLatch.await();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,185 @@
|
|||
package io.bitsquare.p2p.network;
|
||||
|
||||
import com.google.common.util.concurrent.FutureCallback;
|
||||
import com.google.common.util.concurrent.Futures;
|
||||
import com.google.common.util.concurrent.SettableFuture;
|
||||
import io.bitsquare.p2p.Message;
|
||||
import io.bitsquare.p2p.mocks.MockMessage;
|
||||
import org.junit.Ignore;
|
||||
import org.junit.Test;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
|
||||
// TorNode created. Took 6 sec.
|
||||
// Hidden service created. Took 40-50 sec.
|
||||
// Connection establishment takes about 4 sec.
|
||||
@Ignore
|
||||
public class TorNetworkNodeTest {
|
||||
private static final Logger log = LoggerFactory.getLogger(TorNetworkNodeTest.class);
|
||||
private CountDownLatch latch;
|
||||
|
||||
@Test
|
||||
public void testTorNodeBeforeSecondReady() throws InterruptedException, IOException {
|
||||
latch = new CountDownLatch(1);
|
||||
int port = 9001;
|
||||
TorNetworkNode node1 = new TorNetworkNode(port, new File("torNode_" + port));
|
||||
node1.start(new SetupListener() {
|
||||
@Override
|
||||
public void onTorNodeReady() {
|
||||
log.debug("onReadyForSendingMessages");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onHiddenServiceReady() {
|
||||
log.debug("onReadyForReceivingMessages");
|
||||
latch.countDown();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetupFailed(Throwable throwable) {
|
||||
|
||||
}
|
||||
});
|
||||
latch.await();
|
||||
|
||||
latch = new CountDownLatch(1);
|
||||
int port2 = 9002;
|
||||
TorNetworkNode node2 = new TorNetworkNode(port2, new File("torNode_" + port2));
|
||||
node2.start(new SetupListener() {
|
||||
@Override
|
||||
public void onTorNodeReady() {
|
||||
log.debug("onReadyForSendingMessages");
|
||||
latch.countDown();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onHiddenServiceReady() {
|
||||
log.debug("onReadyForReceivingMessages");
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetupFailed(Throwable throwable) {
|
||||
|
||||
}
|
||||
});
|
||||
latch.await();
|
||||
|
||||
|
||||
latch = new CountDownLatch(2);
|
||||
node1.addMessageListener(new MessageListener() {
|
||||
@Override
|
||||
public void onMessage(Message message, Connection connection) {
|
||||
log.debug("onMessage node1 " + message);
|
||||
latch.countDown();
|
||||
}
|
||||
});
|
||||
SettableFuture<Connection> future = node2.sendMessage(node1.getAddress(), new MockMessage("msg1"));
|
||||
Futures.addCallback(future, new FutureCallback<Connection>() {
|
||||
@Override
|
||||
public void onSuccess(Connection connection) {
|
||||
log.debug("onSuccess ");
|
||||
latch.countDown();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Throwable throwable) {
|
||||
log.debug("onFailure ");
|
||||
}
|
||||
});
|
||||
latch.await();
|
||||
|
||||
|
||||
latch = new CountDownLatch(2);
|
||||
node1.shutDown(() -> {
|
||||
latch.countDown();
|
||||
});
|
||||
node2.shutDown(() -> {
|
||||
latch.countDown();
|
||||
});
|
||||
latch.await();
|
||||
}
|
||||
|
||||
//@Test
|
||||
public void testTorNodeAfterBothReady() throws InterruptedException, IOException {
|
||||
latch = new CountDownLatch(2);
|
||||
int port = 9001;
|
||||
TorNetworkNode node1 = new TorNetworkNode(port, new File("torNode_" + port));
|
||||
node1.start(new SetupListener() {
|
||||
@Override
|
||||
public void onTorNodeReady() {
|
||||
log.debug("onReadyForSendingMessages");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onHiddenServiceReady() {
|
||||
log.debug("onReadyForReceivingMessages");
|
||||
latch.countDown();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetupFailed(Throwable throwable) {
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
int port2 = 9002;
|
||||
TorNetworkNode node2 = new TorNetworkNode(port2, new File("torNode_" + port));
|
||||
node2.start(new SetupListener() {
|
||||
@Override
|
||||
public void onTorNodeReady() {
|
||||
log.debug("onReadyForSendingMessages");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onHiddenServiceReady() {
|
||||
log.debug("onReadyForReceivingMessages");
|
||||
latch.countDown();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetupFailed(Throwable throwable) {
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
latch.await();
|
||||
|
||||
latch = new CountDownLatch(2);
|
||||
node2.addMessageListener(new MessageListener() {
|
||||
@Override
|
||||
public void onMessage(Message message, Connection connection) {
|
||||
log.debug("onMessage node2 " + message);
|
||||
latch.countDown();
|
||||
}
|
||||
});
|
||||
SettableFuture<Connection> future = node1.sendMessage(node2.getAddress(), new MockMessage("msg1"));
|
||||
Futures.addCallback(future, new FutureCallback<Connection>() {
|
||||
@Override
|
||||
public void onSuccess(Connection connection) {
|
||||
log.debug("onSuccess ");
|
||||
latch.countDown();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Throwable throwable) {
|
||||
log.debug("onFailure ");
|
||||
}
|
||||
});
|
||||
latch.await();
|
||||
|
||||
|
||||
latch = new CountDownLatch(2);
|
||||
node1.shutDown(() -> {
|
||||
latch.countDown();
|
||||
});
|
||||
node2.shutDown(() -> {
|
||||
latch.countDown();
|
||||
});
|
||||
latch.await();
|
||||
}
|
||||
}
|
411
network/src/test/java/io/bitsquare/p2p/routing/RoutingTest.java
Normal file
411
network/src/test/java/io/bitsquare/p2p/routing/RoutingTest.java
Normal file
|
@ -0,0 +1,411 @@
|
|||
package io.bitsquare.p2p.routing;
|
||||
|
||||
import io.bitsquare.common.util.Profiler;
|
||||
import io.bitsquare.p2p.Address;
|
||||
import io.bitsquare.p2p.P2PService;
|
||||
import io.bitsquare.p2p.P2PServiceListener;
|
||||
import io.bitsquare.p2p.network.Connection;
|
||||
import io.bitsquare.p2p.network.LocalhostNetworkNode;
|
||||
import io.bitsquare.p2p.seed.SeedNode;
|
||||
import org.junit.*;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
|
||||
// TorNode created. Took 6 sec.
|
||||
// Hidden service created. Took 40-50 sec.
|
||||
// Connection establishment takes about 4 sec.
|
||||
|
||||
// need to define seed node addresses first before using tor version
|
||||
@Ignore
|
||||
public class RoutingTest {
|
||||
private static final Logger log = LoggerFactory.getLogger(RoutingTest.class);
|
||||
|
||||
boolean useLocalhost = true;
|
||||
private CountDownLatch latch;
|
||||
private ArrayList<Address> seedNodes;
|
||||
private int sleepTime;
|
||||
private SeedNode seedNode1, seedNode2, seedNode3;
|
||||
|
||||
@Before
|
||||
public void setup() throws InterruptedException {
|
||||
LocalhostNetworkNode.setSimulateTorDelayTorNode(50);
|
||||
LocalhostNetworkNode.setSimulateTorDelayHiddenService(8);
|
||||
Routing.setMaxConnections(100);
|
||||
|
||||
seedNodes = new ArrayList<>();
|
||||
if (useLocalhost) {
|
||||
//seedNodes.add(new Address("localhost:8001"));
|
||||
// seedNodes.add(new Address("localhost:8002"));
|
||||
seedNodes.add(new Address("localhost:8003"));
|
||||
sleepTime = 100;
|
||||
|
||||
} else {
|
||||
seedNodes.add(new Address("3omjuxn7z73pxoee.onion:8001"));
|
||||
seedNodes.add(new Address("j24fxqyghjetgpdx.onion:8002"));
|
||||
seedNodes.add(new Address("45367tl6unwec6kw.onion:8003"));
|
||||
sleepTime = 1000;
|
||||
}
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() throws InterruptedException {
|
||||
Thread.sleep(sleepTime);
|
||||
|
||||
if (seedNode1 != null) {
|
||||
CountDownLatch shutDownLatch = new CountDownLatch(1);
|
||||
seedNode1.shutDown(() -> shutDownLatch.countDown());
|
||||
shutDownLatch.await();
|
||||
}
|
||||
if (seedNode2 != null) {
|
||||
CountDownLatch shutDownLatch = new CountDownLatch(1);
|
||||
seedNode2.shutDown(() -> shutDownLatch.countDown());
|
||||
shutDownLatch.await();
|
||||
}
|
||||
if (seedNode3 != null) {
|
||||
CountDownLatch shutDownLatch = new CountDownLatch(1);
|
||||
seedNode3.shutDown(() -> shutDownLatch.countDown());
|
||||
shutDownLatch.await();
|
||||
}
|
||||
}
|
||||
|
||||
// @Test
|
||||
public void testSingleSeedNode() throws InterruptedException {
|
||||
LocalhostNetworkNode.setSimulateTorDelayTorNode(0);
|
||||
LocalhostNetworkNode.setSimulateTorDelayHiddenService(0);
|
||||
seedNodes = new ArrayList<>();
|
||||
seedNodes.add(new Address("localhost:8001"));
|
||||
seedNode1 = new SeedNode();
|
||||
latch = new CountDownLatch(2);
|
||||
seedNode1.createAndStartP2PService(null, null, 8001, useLocalhost, seedNodes, new P2PServiceListener() {
|
||||
@Override
|
||||
public void onAllDataReceived() {
|
||||
latch.countDown();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTorNodeReady() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAuthenticated() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onHiddenServiceReady() {
|
||||
latch.countDown();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetupFailed(Throwable throwable) {
|
||||
|
||||
}
|
||||
});
|
||||
P2PService p2PService1 = seedNode1.getP2PService();
|
||||
latch.await();
|
||||
Thread.sleep(500);
|
||||
Assert.assertEquals(0, p2PService1.getRouting().getAllNeighbors().size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test2SeedNodes() throws InterruptedException {
|
||||
LocalhostNetworkNode.setSimulateTorDelayTorNode(0);
|
||||
LocalhostNetworkNode.setSimulateTorDelayHiddenService(0);
|
||||
seedNodes = new ArrayList<>();
|
||||
seedNodes.add(new Address("localhost:8001"));
|
||||
seedNodes.add(new Address("localhost:8002"));
|
||||
|
||||
latch = new CountDownLatch(6);
|
||||
|
||||
seedNode1 = new SeedNode();
|
||||
seedNode1.createAndStartP2PService(null, null, 8001, useLocalhost, seedNodes, new P2PServiceListener() {
|
||||
@Override
|
||||
public void onAllDataReceived() {
|
||||
latch.countDown();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTorNodeReady() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAuthenticated() {
|
||||
latch.countDown();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onHiddenServiceReady() {
|
||||
latch.countDown();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetupFailed(Throwable throwable) {
|
||||
|
||||
}
|
||||
});
|
||||
P2PService p2PService1 = seedNode1.getP2PService();
|
||||
|
||||
Thread.sleep(500);
|
||||
|
||||
seedNode2 = new SeedNode();
|
||||
seedNode2.createAndStartP2PService(null, null, 8002, useLocalhost, seedNodes, new P2PServiceListener() {
|
||||
@Override
|
||||
public void onAllDataReceived() {
|
||||
latch.countDown();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTorNodeReady() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAuthenticated() {
|
||||
latch.countDown();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onHiddenServiceReady() {
|
||||
latch.countDown();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetupFailed(Throwable throwable) {
|
||||
|
||||
}
|
||||
});
|
||||
P2PService p2PService2 = seedNode2.getP2PService();
|
||||
latch.await();
|
||||
Assert.assertEquals(1, p2PService1.getRouting().getAllNeighbors().size());
|
||||
Assert.assertEquals(1, p2PService2.getRouting().getAllNeighbors().size());
|
||||
}
|
||||
|
||||
// @Test
|
||||
public void testAuthentication() throws InterruptedException {
|
||||
log.debug("### start");
|
||||
LocalhostNetworkNode.setSimulateTorDelayTorNode(0);
|
||||
LocalhostNetworkNode.setSimulateTorDelayHiddenService(0);
|
||||
SeedNode seedNode1 = getAndStartSeedNode(8001);
|
||||
log.debug("### seedNode1");
|
||||
Thread.sleep(100);
|
||||
log.debug("### seedNode1 100");
|
||||
Thread.sleep(1000);
|
||||
SeedNode seedNode2 = getAndStartSeedNode(8002);
|
||||
|
||||
// authentication:
|
||||
// node2 -> node1 RequestAuthenticationMessage
|
||||
// node1: close connection
|
||||
// node1 -> node2 ChallengeMessage on new connection
|
||||
// node2: authentication to node1 done if nonce ok
|
||||
// node2 -> node1 GetNeighborsMessage
|
||||
// node1: authentication to node2 done if nonce ok
|
||||
// node1 -> node2 NeighborsMessage
|
||||
|
||||
// first authentication from seedNode2 to seedNode1, then from seedNode1 to seedNode2
|
||||
CountDownLatch latch1 = new CountDownLatch(2);
|
||||
AuthenticationListener routingListener1 = new AuthenticationListener() {
|
||||
@Override
|
||||
public void onConnectionAuthenticated(Connection connection) {
|
||||
log.debug("onConnectionAuthenticated " + connection);
|
||||
latch1.countDown();
|
||||
}
|
||||
};
|
||||
seedNode1.getP2PService().getRouting().addRoutingListener(routingListener1);
|
||||
|
||||
AuthenticationListener routingListener2 = new AuthenticationListener() {
|
||||
@Override
|
||||
public void onConnectionAuthenticated(Connection connection) {
|
||||
log.debug("onConnectionAuthenticated " + connection);
|
||||
latch1.countDown();
|
||||
}
|
||||
};
|
||||
seedNode2.getP2PService().getRouting().addRoutingListener(routingListener2);
|
||||
latch1.await();
|
||||
seedNode1.getP2PService().getRouting().removeRoutingListener(routingListener1);
|
||||
seedNode2.getP2PService().getRouting().removeRoutingListener(routingListener2);
|
||||
|
||||
// wait until Neighbors msg finished
|
||||
Thread.sleep(sleepTime);
|
||||
|
||||
// authentication:
|
||||
// authentication from seedNode3 to seedNode1, then from seedNode1 to seedNode3
|
||||
// authentication from seedNode3 to seedNode2, then from seedNode2 to seedNode3
|
||||
SeedNode seedNode3 = getAndStartSeedNode(8003);
|
||||
CountDownLatch latch2 = new CountDownLatch(3);
|
||||
seedNode1.getP2PService().getRouting().addRoutingListener(new AuthenticationListener() {
|
||||
@Override
|
||||
public void onConnectionAuthenticated(Connection connection) {
|
||||
log.debug("onConnectionAuthenticated " + connection);
|
||||
latch2.countDown();
|
||||
}
|
||||
});
|
||||
seedNode2.getP2PService().getRouting().addRoutingListener(new AuthenticationListener() {
|
||||
@Override
|
||||
public void onConnectionAuthenticated(Connection connection) {
|
||||
log.debug("onConnectionAuthenticated " + connection);
|
||||
latch2.countDown();
|
||||
}
|
||||
});
|
||||
seedNode3.getP2PService().getRouting().addRoutingListener(new AuthenticationListener() {
|
||||
@Override
|
||||
public void onConnectionAuthenticated(Connection connection) {
|
||||
log.debug("onConnectionAuthenticated " + connection);
|
||||
latch2.countDown();
|
||||
}
|
||||
});
|
||||
latch2.await();
|
||||
|
||||
// wait until Neighbors msg finished
|
||||
Thread.sleep(sleepTime);
|
||||
|
||||
|
||||
CountDownLatch shutDownLatch = new CountDownLatch(3);
|
||||
seedNode1.shutDown(() -> shutDownLatch.countDown());
|
||||
seedNode2.shutDown(() -> shutDownLatch.countDown());
|
||||
seedNode3.shutDown(() -> shutDownLatch.countDown());
|
||||
shutDownLatch.await();
|
||||
}
|
||||
|
||||
//@Test
|
||||
public void testAuthenticationWithDisconnect() throws InterruptedException {
|
||||
LocalhostNetworkNode.setSimulateTorDelayTorNode(0);
|
||||
LocalhostNetworkNode.setSimulateTorDelayHiddenService(0);
|
||||
SeedNode seedNode1 = getAndStartSeedNode(8001);
|
||||
SeedNode seedNode2 = getAndStartSeedNode(8002);
|
||||
|
||||
// authentication:
|
||||
// node2 -> node1 RequestAuthenticationMessage
|
||||
// node1: close connection
|
||||
// node1 -> node2 ChallengeMessage on new connection
|
||||
// node2: authentication to node1 done if nonce ok
|
||||
// node2 -> node1 GetNeighborsMessage
|
||||
// node1: authentication to node2 done if nonce ok
|
||||
// node1 -> node2 NeighborsMessage
|
||||
|
||||
// first authentication from seedNode2 to seedNode1, then from seedNode1 to seedNode2
|
||||
CountDownLatch latch1 = new CountDownLatch(2);
|
||||
AuthenticationListener routingListener1 = new AuthenticationListener() {
|
||||
@Override
|
||||
public void onConnectionAuthenticated(Connection connection) {
|
||||
log.debug("onConnectionAuthenticated " + connection);
|
||||
latch1.countDown();
|
||||
}
|
||||
};
|
||||
seedNode1.getP2PService().getRouting().addRoutingListener(routingListener1);
|
||||
|
||||
AuthenticationListener routingListener2 = new AuthenticationListener() {
|
||||
@Override
|
||||
public void onConnectionAuthenticated(Connection connection) {
|
||||
log.debug("onConnectionAuthenticated " + connection);
|
||||
latch1.countDown();
|
||||
}
|
||||
};
|
||||
seedNode2.getP2PService().getRouting().addRoutingListener(routingListener2);
|
||||
latch1.await();
|
||||
|
||||
// shut down node 2
|
||||
Thread.sleep(sleepTime);
|
||||
seedNode1.getP2PService().getRouting().removeRoutingListener(routingListener1);
|
||||
seedNode2.getP2PService().getRouting().removeRoutingListener(routingListener2);
|
||||
CountDownLatch shutDownLatch1 = new CountDownLatch(1);
|
||||
seedNode2.shutDown(() -> shutDownLatch1.countDown());
|
||||
shutDownLatch1.await();
|
||||
|
||||
// restart node 2
|
||||
seedNode2 = getAndStartSeedNode(8002);
|
||||
CountDownLatch latch3 = new CountDownLatch(1);
|
||||
routingListener2 = new AuthenticationListener() {
|
||||
@Override
|
||||
public void onConnectionAuthenticated(Connection connection) {
|
||||
log.debug("onConnectionAuthenticated " + connection);
|
||||
latch3.countDown();
|
||||
}
|
||||
};
|
||||
seedNode2.getP2PService().getRouting().addRoutingListener(routingListener2);
|
||||
latch3.await();
|
||||
|
||||
Thread.sleep(sleepTime);
|
||||
|
||||
CountDownLatch shutDownLatch = new CountDownLatch(2);
|
||||
seedNode1.shutDown(() -> shutDownLatch.countDown());
|
||||
seedNode2.shutDown(() -> shutDownLatch.countDown());
|
||||
shutDownLatch.await();
|
||||
}
|
||||
|
||||
//@Test
|
||||
public void testAuthenticationWithManyNodes() throws InterruptedException {
|
||||
int authentications = 0;
|
||||
int length = 3;
|
||||
SeedNode[] nodes = new SeedNode[length];
|
||||
for (int i = 0; i < length; i++) {
|
||||
SeedNode node = getAndStartSeedNode(8001 + i);
|
||||
nodes[i] = node;
|
||||
|
||||
latch = new CountDownLatch(i * 2);
|
||||
authentications += (i * 2);
|
||||
node.getP2PService().getRouting().addRoutingListener(new AuthenticationListener() {
|
||||
@Override
|
||||
public void onConnectionAuthenticated(Connection connection) {
|
||||
log.debug("onConnectionAuthenticated " + connection);
|
||||
latch.countDown();
|
||||
}
|
||||
});
|
||||
latch.await();
|
||||
Thread.sleep(sleepTime);
|
||||
}
|
||||
|
||||
log.debug("total authentications " + authentications);
|
||||
Profiler.printSystemLoad(log);
|
||||
// total authentications at 8 nodes = 56
|
||||
// total authentications at com nodes = 90, System load (nr. threads/used memory (MB)): 170/20
|
||||
// total authentications at 20 nodes = 380, System load (nr. threads/used memory (MB)): 525/46
|
||||
for (int i = 0; i < length; i++) {
|
||||
nodes[i].getP2PService().getRouting().printConnectedNeighborsMap();
|
||||
nodes[i].getP2PService().getRouting().printReportedNeighborsMap();
|
||||
}
|
||||
|
||||
CountDownLatch shutDownLatch = new CountDownLatch(length);
|
||||
for (int i = 0; i < length; i++) {
|
||||
nodes[i].shutDown(() -> shutDownLatch.countDown());
|
||||
}
|
||||
shutDownLatch.await();
|
||||
}
|
||||
|
||||
private SeedNode getAndStartSeedNode(int port) throws InterruptedException {
|
||||
SeedNode seedNode = new SeedNode();
|
||||
|
||||
latch = new CountDownLatch(1);
|
||||
seedNode.createAndStartP2PService(null, null, port, useLocalhost, seedNodes, new P2PServiceListener() {
|
||||
@Override
|
||||
public void onAllDataReceived() {
|
||||
latch.countDown();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTorNodeReady() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAuthenticated() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onHiddenServiceReady() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetupFailed(Throwable throwable) {
|
||||
|
||||
}
|
||||
});
|
||||
latch.await();
|
||||
Thread.sleep(sleepTime);
|
||||
return seedNode;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,338 @@
|
|||
package io.bitsquare.p2p.storage;
|
||||
|
||||
import io.bitsquare.common.UserThread;
|
||||
import io.bitsquare.common.crypto.CryptoException;
|
||||
import io.bitsquare.common.crypto.CryptoUtil;
|
||||
import io.bitsquare.common.crypto.KeyRing;
|
||||
import io.bitsquare.common.crypto.KeyStorage;
|
||||
import io.bitsquare.common.util.Utilities;
|
||||
import io.bitsquare.crypto.EncryptionService;
|
||||
import io.bitsquare.p2p.Address;
|
||||
import io.bitsquare.p2p.TestUtils;
|
||||
import io.bitsquare.p2p.messaging.SealedAndSignedMessage;
|
||||
import io.bitsquare.p2p.mocks.MockMessage;
|
||||
import io.bitsquare.p2p.network.NetworkNode;
|
||||
import io.bitsquare.p2p.routing.Routing;
|
||||
import io.bitsquare.p2p.storage.data.DataAndSeqNr;
|
||||
import io.bitsquare.p2p.storage.data.ExpirableMailboxPayload;
|
||||
import io.bitsquare.p2p.storage.data.ProtectedData;
|
||||
import io.bitsquare.p2p.storage.data.ProtectedMailboxData;
|
||||
import io.bitsquare.p2p.storage.mocks.MockData;
|
||||
import org.junit.After;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.security.*;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
public class ProtectedDataStorageTest {
|
||||
private static final Logger log = LoggerFactory.getLogger(ProtectedDataStorageTest.class);
|
||||
|
||||
boolean useClearNet = true;
|
||||
private ArrayList<Address> seedNodes = new ArrayList<>();
|
||||
private NetworkNode networkNode1;
|
||||
private Routing routing1;
|
||||
private EncryptionService encryptionService1, encryptionService2;
|
||||
private ProtectedExpirableDataStorage dataStorage1;
|
||||
private KeyPair storageSignatureKeyPair1, storageSignatureKeyPair2;
|
||||
private KeyRing keyRing1, keyRing2;
|
||||
private MockData mockData;
|
||||
private int sleepTime = 100;
|
||||
|
||||
@Before
|
||||
public void setup() throws InterruptedException, NoSuchAlgorithmException, CertificateException, KeyStoreException, IOException, CryptoException, SignatureException, InvalidKeyException {
|
||||
UserThread.executor = Executors.newSingleThreadExecutor();
|
||||
ProtectedExpirableDataStorage.CHECK_TTL_INTERVAL = 10 * 60 * 1000;
|
||||
|
||||
keyRing1 = new KeyRing(new KeyStorage(new File("temp_keyStorage1")));
|
||||
storageSignatureKeyPair1 = keyRing1.getStorageSignatureKeyPair();
|
||||
encryptionService1 = new EncryptionService(keyRing1);
|
||||
networkNode1 = TestUtils.getAndStartSeedNode(8001, encryptionService1, keyRing1, useClearNet, seedNodes).getP2PService().getNetworkNode();
|
||||
routing1 = new Routing(networkNode1, seedNodes);
|
||||
dataStorage1 = new ProtectedExpirableDataStorage(routing1, encryptionService1);
|
||||
|
||||
// for mailbox
|
||||
keyRing2 = new KeyRing(new KeyStorage(new File("temp_keyStorage2")));
|
||||
storageSignatureKeyPair2 = keyRing2.getStorageSignatureKeyPair();
|
||||
encryptionService2 = new EncryptionService(keyRing2);
|
||||
|
||||
mockData = new MockData("mockData", keyRing1.getStorageSignatureKeyPair().getPublic());
|
||||
Thread.sleep(sleepTime);
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() throws InterruptedException, NoSuchAlgorithmException, CertificateException, KeyStoreException, IOException, CryptoException, SignatureException, InvalidKeyException {
|
||||
Thread.sleep(sleepTime);
|
||||
if (dataStorage1 != null) dataStorage1.shutDown();
|
||||
if (routing1 != null) routing1.shutDown();
|
||||
|
||||
if (networkNode1 != null) {
|
||||
CountDownLatch shutDownLatch = new CountDownLatch(1);
|
||||
networkNode1.shutDown(() -> shutDownLatch.countDown());
|
||||
shutDownLatch.await();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAddAndRemove() throws InterruptedException, NoSuchAlgorithmException, CertificateException, KeyStoreException, IOException, CryptoException, SignatureException, InvalidKeyException {
|
||||
ProtectedData data = dataStorage1.getDataWithSignedSeqNr(mockData, storageSignatureKeyPair1);
|
||||
Assert.assertTrue(dataStorage1.add(data, null));
|
||||
Assert.assertEquals(1, dataStorage1.getMap().size());
|
||||
|
||||
int newSequenceNumber = data.sequenceNumber + 1;
|
||||
byte[] hashOfDataAndSeqNr = CryptoUtil.getHash(new DataAndSeqNr(data.expirablePayload, newSequenceNumber));
|
||||
byte[] signature = CryptoUtil.signStorageData(storageSignatureKeyPair1.getPrivate(), hashOfDataAndSeqNr);
|
||||
ProtectedData dataToRemove = new ProtectedData(data.expirablePayload, data.ttl, data.ownerStoragePubKey, newSequenceNumber, signature);
|
||||
Assert.assertTrue(dataStorage1.remove(dataToRemove, null));
|
||||
Assert.assertEquals(0, dataStorage1.getMap().size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExpirableData() throws InterruptedException, NoSuchAlgorithmException, CertificateException, KeyStoreException, IOException, CryptoException, SignatureException, InvalidKeyException {
|
||||
ProtectedExpirableDataStorage.CHECK_TTL_INTERVAL = 10;
|
||||
// CHECK_TTL_INTERVAL is used in constructor of ProtectedExpirableDataStorage so we recreate it here
|
||||
dataStorage1 = new ProtectedExpirableDataStorage(routing1, encryptionService1);
|
||||
mockData.ttl = 50;
|
||||
|
||||
ProtectedData data = dataStorage1.getDataWithSignedSeqNr(mockData, storageSignatureKeyPair1);
|
||||
Assert.assertTrue(dataStorage1.add(data, null));
|
||||
Thread.sleep(5);
|
||||
Assert.assertEquals(1, dataStorage1.getMap().size());
|
||||
// still there
|
||||
Thread.sleep(20);
|
||||
Assert.assertEquals(1, dataStorage1.getMap().size());
|
||||
|
||||
Thread.sleep(40);
|
||||
// now should be removed
|
||||
Assert.assertEquals(0, dataStorage1.getMap().size());
|
||||
|
||||
// add with date in future
|
||||
data = dataStorage1.getDataWithSignedSeqNr(mockData, storageSignatureKeyPair1);
|
||||
int newSequenceNumber = data.sequenceNumber + 1;
|
||||
byte[] hashOfDataAndSeqNr = CryptoUtil.getHash(new DataAndSeqNr(data.expirablePayload, newSequenceNumber));
|
||||
byte[] signature = CryptoUtil.signStorageData(storageSignatureKeyPair1.getPrivate(), hashOfDataAndSeqNr);
|
||||
ProtectedData dataWithFutureDate = new ProtectedData(data.expirablePayload, data.ttl, data.ownerStoragePubKey, newSequenceNumber, signature);
|
||||
dataWithFutureDate.date = new Date(new Date().getTime() + 60 * 60 * sleepTime);
|
||||
// force serialisation (date check is done in readObject)
|
||||
ProtectedData newData = Utilities.byteArrayToObject(Utilities.objectToByteArray(dataWithFutureDate));
|
||||
Assert.assertTrue(dataStorage1.add(newData, null));
|
||||
Thread.sleep(5);
|
||||
Assert.assertEquals(1, dataStorage1.getMap().size());
|
||||
Thread.sleep(50);
|
||||
Assert.assertEquals(0, dataStorage1.getMap().size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMultiAddRemoveProtectedData() throws InterruptedException, NoSuchAlgorithmException, CertificateException, KeyStoreException, IOException, CryptoException, SignatureException, InvalidKeyException {
|
||||
MockData mockData = new MockData("msg1", keyRing1.getStorageSignatureKeyPair().getPublic());
|
||||
ProtectedData data = dataStorage1.getDataWithSignedSeqNr(mockData, storageSignatureKeyPair1);
|
||||
Assert.assertTrue(dataStorage1.add(data, null));
|
||||
|
||||
// remove with not updated seq nr -> failure
|
||||
int newSequenceNumber = 0;
|
||||
byte[] hashOfDataAndSeqNr = CryptoUtil.getHash(new DataAndSeqNr(data.expirablePayload, newSequenceNumber));
|
||||
byte[] signature = CryptoUtil.signStorageData(storageSignatureKeyPair1.getPrivate(), hashOfDataAndSeqNr);
|
||||
ProtectedData dataToRemove = new ProtectedData(data.expirablePayload, data.ttl, data.ownerStoragePubKey, newSequenceNumber, signature);
|
||||
Assert.assertFalse(dataStorage1.remove(dataToRemove, null));
|
||||
|
||||
// remove with too high updated seq nr -> ok
|
||||
newSequenceNumber = 2;
|
||||
hashOfDataAndSeqNr = CryptoUtil.getHash(new DataAndSeqNr(data.expirablePayload, newSequenceNumber));
|
||||
signature = CryptoUtil.signStorageData(storageSignatureKeyPair1.getPrivate(), hashOfDataAndSeqNr);
|
||||
dataToRemove = new ProtectedData(data.expirablePayload, data.ttl, data.ownerStoragePubKey, newSequenceNumber, signature);
|
||||
Assert.assertTrue(dataStorage1.remove(dataToRemove, null));
|
||||
|
||||
// add with updated seq nr below previous -> failure
|
||||
newSequenceNumber = 1;
|
||||
hashOfDataAndSeqNr = CryptoUtil.getHash(new DataAndSeqNr(data.expirablePayload, newSequenceNumber));
|
||||
signature = CryptoUtil.signStorageData(storageSignatureKeyPair1.getPrivate(), hashOfDataAndSeqNr);
|
||||
ProtectedData dataToAdd = new ProtectedData(data.expirablePayload, data.ttl, data.ownerStoragePubKey, newSequenceNumber, signature);
|
||||
Assert.assertFalse(dataStorage1.add(dataToAdd, null));
|
||||
|
||||
// add with updated seq nr over previous -> ok
|
||||
newSequenceNumber = 3;
|
||||
hashOfDataAndSeqNr = CryptoUtil.getHash(new DataAndSeqNr(data.expirablePayload, newSequenceNumber));
|
||||
signature = CryptoUtil.signStorageData(storageSignatureKeyPair1.getPrivate(), hashOfDataAndSeqNr);
|
||||
dataToAdd = new ProtectedData(data.expirablePayload, data.ttl, data.ownerStoragePubKey, newSequenceNumber, signature);
|
||||
Assert.assertTrue(dataStorage1.add(dataToAdd, null));
|
||||
|
||||
// add with same seq nr -> failure
|
||||
newSequenceNumber = 3;
|
||||
hashOfDataAndSeqNr = CryptoUtil.getHash(new DataAndSeqNr(data.expirablePayload, newSequenceNumber));
|
||||
signature = CryptoUtil.signStorageData(storageSignatureKeyPair1.getPrivate(), hashOfDataAndSeqNr);
|
||||
dataToAdd = new ProtectedData(data.expirablePayload, data.ttl, data.ownerStoragePubKey, newSequenceNumber, signature);
|
||||
Assert.assertFalse(dataStorage1.add(dataToAdd, null));
|
||||
|
||||
// add with same data but higher seq nr. -> ok, ignore
|
||||
newSequenceNumber = 4;
|
||||
hashOfDataAndSeqNr = CryptoUtil.getHash(new DataAndSeqNr(data.expirablePayload, newSequenceNumber));
|
||||
signature = CryptoUtil.signStorageData(storageSignatureKeyPair1.getPrivate(), hashOfDataAndSeqNr);
|
||||
dataToAdd = new ProtectedData(data.expirablePayload, data.ttl, data.ownerStoragePubKey, newSequenceNumber, signature);
|
||||
Assert.assertTrue(dataStorage1.add(dataToAdd, null));
|
||||
|
||||
// remove with with same seq nr as prev. ignored -> failed
|
||||
newSequenceNumber = 4;
|
||||
hashOfDataAndSeqNr = CryptoUtil.getHash(new DataAndSeqNr(data.expirablePayload, newSequenceNumber));
|
||||
signature = CryptoUtil.signStorageData(storageSignatureKeyPair1.getPrivate(), hashOfDataAndSeqNr);
|
||||
dataToRemove = new ProtectedData(data.expirablePayload, data.ttl, data.ownerStoragePubKey, newSequenceNumber, signature);
|
||||
Assert.assertFalse(dataStorage1.remove(dataToRemove, null));
|
||||
|
||||
// remove with with higher seq nr -> ok
|
||||
newSequenceNumber = 5;
|
||||
hashOfDataAndSeqNr = CryptoUtil.getHash(new DataAndSeqNr(data.expirablePayload, newSequenceNumber));
|
||||
signature = CryptoUtil.signStorageData(storageSignatureKeyPair1.getPrivate(), hashOfDataAndSeqNr);
|
||||
dataToRemove = new ProtectedData(data.expirablePayload, data.ttl, data.ownerStoragePubKey, newSequenceNumber, signature);
|
||||
Assert.assertTrue(dataStorage1.remove(dataToRemove, null));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAddAndRemoveMailboxData() throws InterruptedException, NoSuchAlgorithmException, CertificateException, KeyStoreException, IOException, CryptoException, SignatureException, InvalidKeyException {
|
||||
// sender
|
||||
MockMessage mockMessage = new MockMessage("MockMessage");
|
||||
SealedAndSignedMessage sealedAndSignedMessage = encryptionService1.encryptAndSignMessage(keyRing1.getPubKeyRing(), mockMessage);
|
||||
ExpirableMailboxPayload expirableMailboxPayload = new ExpirableMailboxPayload(sealedAndSignedMessage,
|
||||
keyRing1.getStorageSignatureKeyPair().getPublic(),
|
||||
keyRing2.getStorageSignatureKeyPair().getPublic());
|
||||
|
||||
ProtectedMailboxData data = dataStorage1.getMailboxDataWithSignedSeqNr(expirableMailboxPayload, storageSignatureKeyPair1, storageSignatureKeyPair2.getPublic());
|
||||
Assert.assertTrue(dataStorage1.add(data, null));
|
||||
Thread.sleep(sleepTime);
|
||||
Assert.assertEquals(1, dataStorage1.getMap().size());
|
||||
|
||||
// receiver (storageSignatureKeyPair2)
|
||||
int newSequenceNumber = data.sequenceNumber + 1;
|
||||
byte[] hashOfDataAndSeqNr = CryptoUtil.getHash(new DataAndSeqNr(data.expirablePayload, newSequenceNumber));
|
||||
|
||||
byte[] signature;
|
||||
ProtectedMailboxData dataToRemove;
|
||||
|
||||
// wrong sig -> fail
|
||||
signature = CryptoUtil.signStorageData(storageSignatureKeyPair1.getPrivate(), hashOfDataAndSeqNr);
|
||||
dataToRemove = new ProtectedMailboxData(expirableMailboxPayload, data.ttl, storageSignatureKeyPair2.getPublic(), newSequenceNumber, signature, storageSignatureKeyPair2.getPublic());
|
||||
Assert.assertFalse(dataStorage1.removeMailboxData(dataToRemove, null));
|
||||
|
||||
// wrong seq nr
|
||||
signature = CryptoUtil.signStorageData(storageSignatureKeyPair2.getPrivate(), hashOfDataAndSeqNr);
|
||||
dataToRemove = new ProtectedMailboxData(expirableMailboxPayload, data.ttl, storageSignatureKeyPair2.getPublic(), data.sequenceNumber, signature, storageSignatureKeyPair2.getPublic());
|
||||
Assert.assertFalse(dataStorage1.removeMailboxData(dataToRemove, null));
|
||||
|
||||
// wrong signingKey
|
||||
signature = CryptoUtil.signStorageData(storageSignatureKeyPair2.getPrivate(), hashOfDataAndSeqNr);
|
||||
dataToRemove = new ProtectedMailboxData(expirableMailboxPayload, data.ttl, data.ownerStoragePubKey, newSequenceNumber, signature, storageSignatureKeyPair2.getPublic());
|
||||
Assert.assertFalse(dataStorage1.removeMailboxData(dataToRemove, null));
|
||||
|
||||
// wrong peerPubKey
|
||||
signature = CryptoUtil.signStorageData(storageSignatureKeyPair2.getPrivate(), hashOfDataAndSeqNr);
|
||||
dataToRemove = new ProtectedMailboxData(expirableMailboxPayload, data.ttl, storageSignatureKeyPair2.getPublic(), newSequenceNumber, signature, storageSignatureKeyPair1.getPublic());
|
||||
Assert.assertFalse(dataStorage1.removeMailboxData(dataToRemove, null));
|
||||
|
||||
// receiver can remove it (storageSignatureKeyPair2) -> all ok
|
||||
Assert.assertEquals(1, dataStorage1.getMap().size());
|
||||
signature = CryptoUtil.signStorageData(storageSignatureKeyPair2.getPrivate(), hashOfDataAndSeqNr);
|
||||
dataToRemove = new ProtectedMailboxData(expirableMailboxPayload, data.ttl, storageSignatureKeyPair2.getPublic(), newSequenceNumber, signature, storageSignatureKeyPair2.getPublic());
|
||||
Assert.assertTrue(dataStorage1.removeMailboxData(dataToRemove, null));
|
||||
|
||||
Assert.assertEquals(0, dataStorage1.getMap().size());
|
||||
}
|
||||
|
||||
|
||||
/*@Test
|
||||
public void testTryToHack() throws InterruptedException, NoSuchAlgorithmException, CertificateException, KeyStoreException, IOException, CryptoException, SignatureException, InvalidKeyException {
|
||||
ProtectedData data = dataStorage1.getDataWithSignedSeqNr(mockData, storageSignatureKeyPair1);
|
||||
Assert.assertTrue(dataStorage1.add(data, null));
|
||||
Thread.sleep(sleepTime);
|
||||
Assert.assertEquals(1, dataStorage1.getMap().size());
|
||||
Assert.assertEquals(1, dataStorage2.getMap().size());
|
||||
|
||||
// hackers key pair is storageSignatureKeyPair2
|
||||
// change seq nr. and signature: fails on both own and peers dataStorage
|
||||
int newSequenceNumber = data.sequenceNumber + 1;
|
||||
byte[] hashOfDataAndSeqNr = cryptoService2.getHash(new DataAndSeqNr(data.expirablePayload, newSequenceNumber));
|
||||
byte[] signature = cryptoService2.signStorageData(storageSignatureKeyPair2.getPrivate(), hashOfDataAndSeqNr);
|
||||
ProtectedData dataToAdd = new ProtectedData(data.expirablePayload, data.ttl, data.ownerStoragePubKey, newSequenceNumber, signature);
|
||||
Assert.assertFalse(dataStorage1.add(dataToAdd, null));
|
||||
Assert.assertFalse(dataStorage2.add(dataToAdd, null));
|
||||
|
||||
// change seq nr. and signature and data pub key. fails on peers dataStorage, succeeds on own dataStorage
|
||||
newSequenceNumber = data.sequenceNumber + 2;
|
||||
hashOfDataAndSeqNr = cryptoService2.getHash(new DataAndSeqNr(data.expirablePayload, newSequenceNumber));
|
||||
signature = cryptoService2.signStorageData(storageSignatureKeyPair2.getPrivate(), hashOfDataAndSeqNr);
|
||||
dataToAdd = new ProtectedData(data.expirablePayload, data.ttl, storageSignatureKeyPair2.getPublic(), newSequenceNumber, signature);
|
||||
Assert.assertTrue(dataStorage2.add(dataToAdd, null));
|
||||
Assert.assertFalse(dataStorage1.add(dataToAdd, null));
|
||||
Thread.sleep(sleepTime);
|
||||
Assert.assertEquals(1, dataStorage2.getMap().size());
|
||||
Thread.sleep(sleepTime);
|
||||
Assert.assertEquals(1, dataStorage1.getMap().size());
|
||||
Assert.assertEquals(data, dataStorage1.getMap().values().stream().findFirst().get());
|
||||
Assert.assertEquals(dataToAdd, dataStorage2.getMap().values().stream().findFirst().get());
|
||||
Assert.assertNotEquals(data, dataToAdd);
|
||||
|
||||
newSequenceNumber = data.sequenceNumber + 3;
|
||||
hashOfDataAndSeqNr = cryptoService1.getHash(new DataAndSeqNr(data.expirablePayload, newSequenceNumber));
|
||||
signature = cryptoService1.signStorageData(storageSignatureKeyPair1.getPrivate(), hashOfDataAndSeqNr);
|
||||
ProtectedData dataToRemove = new ProtectedData(data.expirablePayload, data.ttl, data.ownerStoragePubKey, newSequenceNumber, signature);
|
||||
Assert.assertTrue(dataStorage1.remove(dataToRemove, null));
|
||||
Assert.assertEquals(0, dataStorage1.getMap().size());
|
||||
}*/
|
||||
|
||||
/* //@Test
|
||||
public void testTryToHackMailboxData() throws InterruptedException, NoSuchAlgorithmException, CertificateException, KeyStoreException, IOException, CryptoException, SignatureException, InvalidKeyException {
|
||||
MockMessage mockMessage = new MockMessage("MockMessage");
|
||||
SealedAndSignedMessage sealedAndSignedMessage = cryptoService1.encryptAndSignMessage(keyRing1.getPubKeyRing(), mockMessage);
|
||||
ExpirableMailboxPayload expirableMailboxPayload = new ExpirableMailboxPayload(sealedAndSignedMessage,
|
||||
keyRing1.getStorageSignatureKeyPair().getPublic(),
|
||||
keyRing2.getStorageSignatureKeyPair().getPublic());
|
||||
|
||||
// sender
|
||||
ProtectedMailboxData data = dataStorage1.getMailboxDataWithSignedSeqNr(expirableMailboxPayload, storageSignatureKeyPair1, storageSignatureKeyPair2.getPublic());
|
||||
Assert.assertTrue(dataStorage1.add(data, null));
|
||||
Thread.sleep(sleepTime);
|
||||
Assert.assertEquals(1, dataStorage1.getMap().size());
|
||||
|
||||
// receiver (storageSignatureKeyPair2)
|
||||
int newSequenceNumber = data.sequenceNumber + 1;
|
||||
byte[] hashOfDataAndSeqNr = cryptoService2.getHash(new DataAndSeqNr(expirableMailboxPayload, newSequenceNumber));
|
||||
|
||||
byte[] signature;
|
||||
ProtectedMailboxData dataToRemove;
|
||||
|
||||
// wrong sig -> fail
|
||||
signature = cryptoService2.signStorageData(storageSignatureKeyPair1.getPrivate(), hashOfDataAndSeqNr);
|
||||
dataToRemove = new ProtectedMailboxData(expirableMailboxPayload, data.ttl, storageSignatureKeyPair2.getPublic(), newSequenceNumber, signature, storageSignatureKeyPair2.getPublic());
|
||||
Assert.assertFalse(dataStorage1.removeMailboxData(dataToRemove, null));
|
||||
|
||||
// wrong seq nr
|
||||
signature = cryptoService2.signStorageData(storageSignatureKeyPair2.getPrivate(), hashOfDataAndSeqNr);
|
||||
dataToRemove = new ProtectedMailboxData(expirableMailboxPayload, data.ttl, storageSignatureKeyPair2.getPublic(), data.sequenceNumber, signature, storageSignatureKeyPair2.getPublic());
|
||||
Assert.assertFalse(dataStorage1.removeMailboxData(dataToRemove, null));
|
||||
|
||||
// wrong signingKey
|
||||
signature = cryptoService2.signStorageData(storageSignatureKeyPair2.getPrivate(), hashOfDataAndSeqNr);
|
||||
dataToRemove = new ProtectedMailboxData(expirableMailboxPayload, data.ttl, data.ownerStoragePubKey, newSequenceNumber, signature, storageSignatureKeyPair2.getPublic());
|
||||
Assert.assertFalse(dataStorage1.removeMailboxData(dataToRemove, null));
|
||||
|
||||
// wrong peerPubKey
|
||||
signature = cryptoService2.signStorageData(storageSignatureKeyPair2.getPrivate(), hashOfDataAndSeqNr);
|
||||
dataToRemove = new ProtectedMailboxData(expirableMailboxPayload, data.ttl, storageSignatureKeyPair2.getPublic(), newSequenceNumber, signature, storageSignatureKeyPair1.getPublic());
|
||||
Assert.assertFalse(dataStorage1.removeMailboxData(dataToRemove, null));
|
||||
|
||||
// all ok
|
||||
Assert.assertEquals(1, dataStorage1.getMap().size());
|
||||
signature = cryptoService2.signStorageData(storageSignatureKeyPair2.getPrivate(), hashOfDataAndSeqNr);
|
||||
dataToRemove = new ProtectedMailboxData(expirableMailboxPayload, data.ttl, storageSignatureKeyPair2.getPublic(), newSequenceNumber, signature, storageSignatureKeyPair2.getPublic());
|
||||
Assert.assertTrue(dataStorage1.removeMailboxData(dataToRemove, null));
|
||||
|
||||
Assert.assertEquals(0, dataStorage1.getMap().size());
|
||||
}
|
||||
*/
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
package io.bitsquare.p2p.storage.mocks;
|
||||
|
||||
import io.bitsquare.p2p.storage.data.PubKeyProtectedExpirablePayload;
|
||||
|
||||
import java.security.PublicKey;
|
||||
|
||||
public class MockData implements PubKeyProtectedExpirablePayload {
|
||||
public final String msg;
|
||||
public final PublicKey publicKey;
|
||||
public long ttl;
|
||||
|
||||
public MockData(String msg, PublicKey publicKey) {
|
||||
this.msg = msg;
|
||||
this.publicKey = publicKey;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (!(o instanceof MockData)) return false;
|
||||
|
||||
MockData that = (MockData) o;
|
||||
|
||||
return !(msg != null ? !msg.equals(that.msg) : that.msg != null);
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return msg != null ? msg.hashCode() : 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "MockData{" +
|
||||
"msg='" + msg + '\'' +
|
||||
'}';
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getTTL() {
|
||||
return ttl;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PublicKey getPubKey() {
|
||||
return publicKey;
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue