package net.tomp2p.relay;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SignatureException;
import java.security.spec.InvalidKeySpecException;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import io.netty.buffer.ByteBuf;
import net.tomp2p.connection.Dispatcher;
import net.tomp2p.connection.PeerConnection;
import net.tomp2p.connection.Responder;
import net.tomp2p.connection.SignatureFactory;
import net.tomp2p.futures.BaseFutureAdapter;
import net.tomp2p.futures.FutureDone;
import net.tomp2p.futures.FuturePeerConnection;
import net.tomp2p.futures.FutureResponse;
import net.tomp2p.holep.HolePRPC;
import net.tomp2p.message.Buffer;
import net.tomp2p.message.Message;
import net.tomp2p.message.Message.Type;
import net.tomp2p.message.NeighborSet;
import net.tomp2p.p2p.Peer;
import net.tomp2p.peers.Number160;
import net.tomp2p.peers.PeerAddress;
import net.tomp2p.peers.PeerSocketAddress;
import net.tomp2p.peers.PeerStatistic;
import net.tomp2p.rpc.DispatchHandler;
import net.tomp2p.rpc.RPC;
import net.tomp2p.rpc.RPC.Commands;
public class RelayRPC extends DispatchHandler {
private static final Logger LOG = LoggerFactory.getLogger(RelayRPC.class);
private final Peer peer;
// holds the server for each client
//private final Map<Number160, BaseRelayServer> servers;
// holds the client for each server
//private ConcurrentHashMap<Number160, BaseRelayClient> clients;
/**
* This variable is needed, because a relay overwrites every RPC of an
* unreachable peer with another RPC called {@link RelayForwarderRPC}. This
* variable is forwarded to the {@link RelayForwarderRPC} in order to
* guarantee the existence of a {@link RconRPC}. Without this variable, no
* reverse connections would be possible.
*
* @author jonaswagner
*/
private final RconRPC rconRPC;
/**
* This variable is needed, because a relay overwrites every RPC of an
* unreachable peer with another RPC called {@link RelayForwarderRPC}. This
* variable is forwarded to the {@link RelayForwarderRPC} in order to
* guarantee the existence of a {@link HolePRPC}. Without this variable, no
* hole punch connections would be possible.
*
* @author jonaswagner
*/
private final HolePRPC holePunchRPC;
/**
* Register the RelayRPC. After the setup, the peer is ready to act as a
* relay if asked by an unreachable peer.
*
* @param peer
* The peer to register the RelayRPC
* @param rconRPC the reverse connection RPC
* @return
*/
final private int bufferTimeoutSeconds;
final private int bufferSize;
final private int heartBeatMillis;
final private int idleTCP;
public RelayRPC(Peer peer, RconRPC rconRPC, HolePRPC holePRPC, int bufferTimeoutSeconds, int bufferSize, int heartBeatMillis, int idleTCP) {
super(peer.peerBean(), peer.connectionBean());
this.peer = peer;
//this.servers = new ConcurrentHashMap<Number160, BaseRelayServer>();
//this.clients = new ConcurrentHashMap<Number160, BaseRelayClient>();
this.rconRPC = rconRPC;
this.holePunchRPC = holePRPC;
this.bufferTimeoutSeconds = bufferTimeoutSeconds;
this.bufferSize = bufferSize;
this.heartBeatMillis = heartBeatMillis;
this.idleTCP = idleTCP;
// register this handler
register(RPC.Commands.RELAY.getNr());
}
public FutureDone<PeerConnection> sendSetupMessage(final PeerAddress candidate) {
final FutureDone<PeerConnection> futureDone = new FutureDone<PeerConnection>();
final Message message = createMessage(candidate, RPC.Commands.RELAY.getNr(), Type.REQUEST_1);
// depend on the relay type whether to keep the connection open or close it after the setup.
message.keepAlive(true);
LOG.debug("Setting up relay connection to peer {}, message {}", candidate, message);
final FuturePeerConnection fpc = peer.createPeerConnection(candidate, heartBeatMillis, idleTCP);
fpc.addListener(new BaseFutureAdapter<FuturePeerConnection>() {
public void operationComplete(final FuturePeerConnection futurePeerConnection) throws Exception {
if (futurePeerConnection.isSuccess()) {
// successfully created a connection to the relay peer
final PeerConnection peerConnection = futurePeerConnection.object();
// send the message
FutureResponse response = RelayUtils.send(peerConnection, peer.peerBean(), peer.connectionBean(), message);
response.addListener(new BaseFutureAdapter<FutureResponse>() {
public void operationComplete(FutureResponse future) throws Exception {
if (future.isSuccess()) {
LOG.debug("Peer {} accepted relay request", candidate);
futureDone.done(peerConnection);
} else {
LOG.debug("Peer {} denied relay request", candidate);
futureDone.failed(future);
}
}
});
} else {
LOG.debug("Unable to setup a connection to relay peer {}", candidate);
futureDone.failed(futurePeerConnection);
}
}
});
return futureDone;
}
public FutureResponse sendPeerMap(final PeerAddress relayPeer, PeerConnection peerConnection,
final List<Map<Number160, PeerStatistic>> map) {
final Message message = createMessage(relayPeer, RPC.Commands.RELAY.getNr(), Type.REQUEST_3);
NeighborSet ns = new NeighborSet(255, RelayUtils.flatten(map));
message.neighborsSet(ns);
LOG.debug("send neighbors " + ns);
// append relay-type specific data (if necessary)
//relayConfig.prepareMapUpdateMessage(message);
message.keepAlive(true);
FutureResponse response = RelayUtils.send(peerConnection, peer.peerBean(), peer.connectionBean(), message);
return response;
}
/**
* Receive a message at the relay server and the relay client
*/
@Override
public void handleResponse(final Message message, PeerConnection peerConnection, final boolean sign, Responder responder)
throws Exception {
LOG.debug("Received RPC message {}", message);
if (message.type() == Type.REQUEST_1 && message.command() == RPC.Commands.RELAY.getNr()) {
// The relay server received a setup request from an unreachable peer
handleSetup(message, peerConnection, responder);
} else if (message.type() == Type.REQUEST_2 && message.command() == RPC.Commands.RELAY.getNr()) {
// The unreachable peer receives wrapped messages from the relay
handlePiggyBackedMessage(message, responder);
} else if (message.type() == Type.REQUEST_3 && message.command() == RPC.Commands.RELAY.getNr()) {
// the relay server receives the update of the routing table regularly from the unreachable peer
handleMap(message, responder);
} else if (message.type() == Type.REQUEST_4 && message.command() == RPC.Commands.RELAY.getNr()) {
// An unreachable peer requests the buffer at the relay peer
// or a buffer is transmitted to the unreachable peer directly
handleBuffer(message);
responder.response(createResponseMessage(message, Type.OK));
} else {
throw new IllegalArgumentException("Message content is wrong");
}
}
public Peer peer() {
return this.peer;
}
/**
* Convenience method
*
* @return the signature factory
*/
private SignatureFactory signatureFactory() {
return connectionBean().channelServer().channelServerConfiguration().signatureFactory();
}
/**
* Convenience method
*
* @return the dispatcher of this peer
*/
private Dispatcher dispatcher() {
return peer().connectionBean().dispatcher();
}
/**
* @return all unreachable peers currently connected to this relay node
*/
/*public Set<PeerAddress> unreachablePeers() {
Set<PeerAddress> unreachablePeers = new HashSet<PeerAddress>(servers.size());
for (BaseRelayServer forwarder : servers.values()) {
unreachablePeers.add(forwarder.unreachablePeerAddress());
}
return unreachablePeers;
}*/
/**
* Add a client to the list
*/
/*public void addClient(BaseRelayClient connection) {
clients.put(connection.relayAddress().peerId(), connection);
}*/
/**
* Remove a client from the list
*/
/*public void removeClient(BaseRelayClient connection) {
clients.remove(connection.relayAddress().peerId());
}*/
/**
* Handle the setup where an unreachable peer connects to this one
*/
private void handleSetup(Message message, final PeerConnection unreachablePeerConnectionOrig, final Responder responder) {
final Number160 unreachablePeerId = unreachablePeerConnectionOrig.remotePeer().peerId();
//add myself as relay
Collection<PeerSocketAddress> psa = unreachablePeerConnectionOrig.remotePeer().relays();
psa.addAll(peer().peerAddress().relays());
//TODO: fix setup
//final PeerConnection unreachablePeerConnectionCopy = unreachablePeerConnectionOrig.remotePeer(
// unreachablePeerConnectionOrig.remotePeer().withRelays(psa));
final PeerConnection unreachablePeerConnectionCopy = unreachablePeerConnectionOrig;
//now we can add this peer to the map, as we have now set the flag
//its TCP, we have a connection to this peer, so mark it as first hand
peerBean().notifyPeerFound(unreachablePeerConnectionCopy.remotePeer(), null, unreachablePeerConnectionCopy, null);
final Forwarder forwarder = new Forwarder(peer, unreachablePeerConnectionCopy, message.sender().slow(), bufferTimeoutSeconds, bufferSize);
for (Commands command : RPC.Commands.values()) {
if (command == RPC.Commands.RCON) {
// We must register the rconRPC for every unreachable peer that
// we serve as a relay. Without this registration, no reverse
// connection setup is possible.
LOG.debug("register rcon for {}, {}",command.toString(), message);
dispatcher().registerIoHandler(peer.peerID(), unreachablePeerId, rconRPC, command.getNr());
} else if (command == RPC.Commands.HOLEP) {
// We must register the holePunchRPC for every unreachable peer that
// we serve as a relay. Without this registration, no reverse
// connection setup is possible.
LOG.debug("register holepunching for {}, {}",command.toString(), message);
dispatcher().registerIoHandler(peer.peerID(), unreachablePeerId, holePunchRPC, command.getNr());
} else if (command == RPC.Commands.RELAY) {
// Register this class to handle all relay messages (currently used when a slow message
// arrives)
LOG.debug("register this for {}, {}",command.toString(), message);
dispatcher().registerIoHandler(peer.peerID(), unreachablePeerId, this, command.getNr());
} else {
LOG.debug("register forwarder for {}, {}",command.toString(), message);
dispatcher().registerIoHandler(peer.peerID(), unreachablePeerId, forwarder, command.getNr());
}
}
responder.response(createResponseMessage(message, Type.OK));
}
/**
* The unreachable peer received an envelope message with another message inside (piggypacked)
*/
private void handlePiggyBackedMessage(Message message, final Responder responderToRelay) throws Exception {
// TODO: check if we have right setup
// this contains the real sender
PeerSocketAddress peerSocketAddress = message.peerSocket4Address(0);
final InetSocketAddress sender;
if (peerSocketAddress != null) {
sender = peerSocketAddress.createTCPSocket();
} else {
sender = new InetSocketAddress(0);
}
Buffer requestBuffer = message.buffer(0);
Message realMessage = RelayUtils.decodeRelayedMessage(requestBuffer.buffer(), message.recipientSocket(), sender,
signatureFactory());
realMessage.restoreContentReferences();
LOG.debug("Received message from relay peer: {}", realMessage);
final Message envelope = createResponseMessage(message, Type.OK);
final Responder responder = new Responder() {
// TODO: add reply leak handler
@Override
public FutureDone<Void> response(Message responseMessage) {
final FutureDone<Void> futureDone = new FutureDone<Void>();
LOG.debug("Send reply message to relay peer: {}", responseMessage);
try {
envelope.buffer(RelayUtils.encodeMessage(responseMessage, signatureFactory()));
} catch (Exception e) {
LOG.error("Cannot piggyback the response", e);
futureDone.failed("Cannot piggyback the response");
failed(Type.EXCEPTION, e.getMessage());
return futureDone;
}
responderToRelay.response(envelope);
futureDone.done();
return futureDone;
}
@Override
public void failed(Type type, String reason) {
responderToRelay.failed(type, reason);
}
@Override
public void responseFireAndForget() {
responderToRelay.responseFireAndForget();
}
};
DispatchHandler dispatchHandler = dispatcher().associatedHandler(realMessage);
if (dispatchHandler == null) {
responder.failed(Type.EXCEPTION, "handler not found, probably not relaying peer anymore");
} else {
dispatchHandler.handleResponse(realMessage, null, false, responder);
}
}
/**
* Updates the peer map of an unreachable peer on the relay peer, so that
* the relay peer can respond to neighbor RPC on behalf of the unreachable
* peer
*
* @param message
* @param responder
*/
private void handleMap(Message message, Responder responder) {
LOG.debug("Handle foreign map update {}", message);
final Forwarder forwarder = dispatcher().searchHandler(Forwarder.class, peer.peerAddress().peerId(), message.sender().peerId());
if (forwarder != null) {
Collection<PeerAddress> map = message.neighborsSet(0).neighbors();
Message response = createResponseMessage(message, Type.OK);
List<Message> buffered = forwarder.buffered();
if(buffered != null) {
ByteBuf bb = RelayUtils.composeMessageBuffer(buffered, peer.connectionBean().resourceConfiguration().signatureFactory());
response.buffer(new Buffer(bb));
}
forwarder.setPeerMap(RelayUtils.unflatten(map, message.sender()), message, response);
responder.response(response);
} else {
LOG.error("No forwarder for peer {} found. Need to setup relay first");
responder.response(createResponseMessage(message, Type.NOT_FOUND));
}
}
public void handleBuffer(final Message message) throws InvalidKeyException, NoSuchAlgorithmException, InvalidKeySpecException, SignatureException, IOException {
//the unreachable peer gets the buffered messages
if(message.bufferList().isEmpty()) {
return;
}
List<Message> buffered = RelayUtils.decomposeCompositeBuffer(
message.buffer(0).buffer(), message.recipientSocket(),
message.senderSocket(), peer.connectionBean().resourceConfiguration().signatureFactory());
LOG.debug("got {} messages", buffered.size());
for(Message msg:buffered) {
DispatchHandler dh = connectionBean().dispatcher().associatedHandler(msg);
//TODO: add a custom responder and respond to the address found in the message
dh.forwardMessage(msg, null, createResponder(peer, msg.sender().ipv4Socket()));
}
}
private static Responder createResponder(final Peer peer, final PeerSocketAddress sender) {
return new Responder() {
@Override
public void responseFireAndForget() {}
@Override
public FutureDone<Void> response(final Message responseMessage) {
responseMessage.recipient(responseMessage.recipient().withIPSocket(sender));
LOG.debug("send late reply {}", responseMessage);
final FutureResponse fr = RelayUtils.connectAndSend(peer, responseMessage);
final FutureDone<Void> fd = new FutureDone<Void>();
fr.addListener(new BaseFutureAdapter<FutureResponse>() {
@Override
public void operationComplete(FutureResponse future)
throws Exception {
fd.done();
}
});
return fd;
}
@Override
public void failed(Type type, String reason) {
LOG.error("could not sent to peer. {}", reason);
}
};
}
}