package net.tomp2p.relay; import java.util.ArrayList; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeoutException; import net.tomp2p.connection.Dispatcher; import net.tomp2p.connection.PeerBean; import net.tomp2p.connection.PeerConnection; import net.tomp2p.connection.Responder; import net.tomp2p.futures.BaseFutureAdapter; import net.tomp2p.futures.FutureDone; import net.tomp2p.futures.FuturePeerConnection; import net.tomp2p.futures.FutureResponse; 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.rpc.DispatchHandler; import net.tomp2p.rpc.RPC; import net.tomp2p.utils.Pair; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * This RPC handles two things. First of all, it makes sure that messaging via * reverse connection setup is possible. Second, it is also able to keep the * established {@link PeerConnection} and store it to the {@link PeerBean}. Rcon * means reverse connection. * * @author jonaswagner * */ public class RconRPC extends DispatchHandler { private static final Logger LOG = LoggerFactory.getLogger(RconRPC.class); private final Peer peer; public RconRPC(final Peer peer) { super(peer.peerBean(), peer.connectionBean()); register(RPC.Commands.RCON.getNr()); this.peer = peer; } /** * This method is called from the {@link Dispatcher} and handles the reverse * connection at each step. * * REQUEST_1 = relay rcon forwarding.<br /> * REQUEST_2 = open a TCP channel and transmit {@link PeerConnection}.<br /> * REQUEST_3 = use now open {@link PeerConnection} to transmit original message * (and eventually store the {@link PeerConnection}).<br /> * * @param message * @param peerConnection * @param sign * @param responder */ @Override public void handleResponse(final Message message, final PeerConnection peerConnection, final boolean sign, final Responder responder) throws Exception { LOG.debug("received RconRPC message {}", message); if (message.type() == Message.Type.REQUEST_1 && message.command() == RPC.Commands.RCON.getNr()) { // the message reached the relay peer LOG.debug("handle RconForward for message: {}", message); handleRconForward(message, responder); } else if (message.type() == Message.Type.REQUEST_2 && message.command() == RPC.Commands.RCON.getNr()) { // the message reached the unreachable peer LOG.debug("handle RconSetup for message: {}", message); handleRconSetup(message, responder); } else if (message.type() == Message.Type.REQUEST_3 && message.command() == RPC.Commands.RCON.getNr()) { // the message reached the requesting peer LOG.debug("handle RconAfterconnect for message: {}", message); handleRconAfterconnect(message, responder, peerConnection); } else { LOG.warn("received invalid RconRPC message {}", message); throw new IllegalArgumentException("Message content is wrong"); } } /** * This methods is responsible for forwarding the rconSetupMessage from the * relay to the unreachable Peer. It extracts the already existing {@link PeerConnection} of the * unreachable peer and forwards then a new * message with {@link Type#REQUEST_2}. * * @param message * @param responder * @throws Exception */ private void handleRconForward(final Message message, final Responder responder) throws Exception { // get the relayForwarderRPC via Dispatcher to retrieve the existing peerConnection final Forwarder forwarder = extractRelayForwarder(message); if (forwarder != null) { final Message forwardMessage = createForwardMessage(message, forwarder.unreachablePeerAddress()); forwarder.handleForward(forwardMessage, message, responder); } else { handleFail(message, responder, "No RelayForwarder registered for peerId=" + message.recipient().peerId().toString()); } } /** * This method extracts a registered {@link BaseRelayServer} from the {@link Dispatcher}. This RelayForwarder * can then be used to extract the {@link PeerConnection} to the unreachable Peer we want to contact. * * @param unreachablePeerId the unreachable peer * @return forwarder */ private Forwarder extractRelayForwarder(final Message message) { final Dispatcher dispatcher = peer.connectionBean().dispatcher(); return (Forwarder) dispatcher.searchHandler(Forwarder.class, peer.peerID(), message.recipient().peerId()); } /** * This method creates the message which is sent from the relay peer to the * unreachable peer. * * @param message * @param peerConnection * @return forwardMessage */ private Message createForwardMessage(final Message message, final PeerAddress recipient) { // creates the Message to forward to the unreachable peer Message forwardMessage = createMessage(recipient, RPC.Commands.RCON.getNr(), Message.Type.REQUEST_2); // transmit PeerAddress of reachablePeer final NeighborSet ns = new NeighborSet(1, new ArrayList<PeerAddress>(1)); ns.add(message.sender()); forwardMessage.neighborsSet(ns); if (!message.intList().isEmpty()) { // store the message id for new message to identify the cached message afterwards forwardMessage.intValue(message.intAt(0)); } return forwardMessage; } /** * This method handles the reverse connection setup on the unreachable peer * side. It extracts the {@link PeerAddress} from the reachable peer and * creates a new {@link PeerConnection} to it. Then it informs the reachable * peer that it is ready via a new message with {@link Type#REQUEST_3}. * * @param message * @param responder * @throws TimeoutException */ private void handleRconSetup(final Message message, final Responder responder) throws TimeoutException { if (!message.neighborsSetList().isEmpty()) { // extract the PeerAddress from the reachable peer final PeerAddress originalSender = (PeerAddress) message.neighborsSetList().get(0).neighbors().toArray()[0]; // create new PeerConnectin to the reachable peer final FuturePeerConnection fpc = peer.createPeerConnection(originalSender); fpc.addListener(new BaseFutureAdapter<FuturePeerConnection>() { @Override public void operationComplete(final FuturePeerConnection future) throws Exception { if (future.isSuccess() && future.peerConnection() != null) { // successfully created a connection from unreachable to the requester final PeerConnection peerConnection = future.peerConnection(); final Message readyMessage = createReadyForRequestMessage(message, peerConnection.remotePeer()); LOG.debug("Sending 'RconAfterconnect' message to relay. {}", readyMessage); FutureResponse futureResponse = RelayUtils.send(peerConnection, peer.peerBean(), peer.connectionBean(), readyMessage); futureResponse.addListener(new BaseFutureAdapter<FutureResponse>() { @Override public void operationComplete(final FutureResponse future) throws Exception { // ready message is sent to reachable peer if (future.isSuccess()) { // store the connection if necessary if(message.intList().isEmpty()) { storePeerConnection(message, peerConnection); } // keep the connection open until at least next message arrives responder.response(createResponseMessage(message, Type.OK).keepAlive(true)); } else { LOG.error("Cannot setup the reverse connection to the peer. Reason: {}", future.failedReason()); handleFail(message, responder, "Exception while setting up the reverse connection from the unreachable to the original peer!"); } } }); } else { handleFail(message, responder, "no channel could be established"); } } }); } else { handleFail(message, responder, "the original sender was not transmittet in the neighborsSet!"); } } /** * This method creates a message to indicate that the connection is ready for requests. * It is sent from the unreachable peer to the reachable peer. * * @param message * @param receiver * @return setupMessage */ private Message createReadyForRequestMessage(final Message message, final PeerAddress receiver) { Message readyForRequestMessage = createMessage(receiver, RPC.Commands.RCON.getNr(), Message.Type.REQUEST_3); if (!message.intList().isEmpty()) { // forward the message id to indentify the cached message afterwards readyForRequestMessage.intValue(message.intAt(0)); } // keep the new connection open readyForRequestMessage.keepAlive(true); return readyForRequestMessage; } /** * This method takes the now established {@link PeerConnection} to the * unreachable peer and sends the original created message to it. * * @param message * @param responder * @param peerConnection */ private void handleRconAfterconnect(final Message message, final Responder responder, final PeerConnection peerConnection) { //send back an ok and keep the connection alive for both cases: permanent and immediate reply. Message response = createResponseMessage(message, Type.OK).keepAlive(true); LOG.debug("sending immediate keep alive message back {}", response); FutureDone<Void> doneResponding = responder.response(response); //wait until we sent the response (keep alive), sending before may confuse the decoder doneResponding.addListener(new BaseFutureAdapter<FutureDone<Void>>() { @Override public void operationComplete(FutureDone<Void> future) throws Exception { if (message.intList().isEmpty()) { LOG.debug("This reverse connection is used permanently. Store it!"); storePeerConnection(message, peerConnection); } else { final Map<Integer, Pair<FutureResponse, FutureResponse>> cachedRequests = peer.connectionBean().sender().cachedRequests(); final Pair<FutureResponse, FutureResponse> cachedRequest = cachedRequests.remove(message.intAt(0)); final Message cachedMessage = cachedRequest.element0().request(); LOG.debug("This reverse connection is only used for sending a direct message {}", cachedMessage); // send the message to the unreachable peer through the open channel FutureResponse futureResponse = RelayUtils.send(peerConnection, peer.peerBean(), peer.connectionBean(), cachedMessage); futureResponse.addListener(new BaseFutureAdapter<FutureResponse>() { @Override public void operationComplete(final FutureResponse future) throws Exception { if (future.isSuccess()) { LOG.debug("Successfully transmitted request message {} to unreachablePeer {}", cachedMessage, peerConnection.remotePeer()); peerConnection.close(); //we need to finish both futures, even though the ACK message has not yet arrived. //First we need to finish the internal future, then the one used eg in the DHT cachedRequest.element1().response(future.responseMessage()); cachedRequest.element0().response(future.responseMessage()); } else { //we need to finish both futures, even though the ACK message has not yet arrived. //First we need to finish the internal future, then the one used eg in the DHT cachedRequest.element1().failed("Cannot send the request message", future); cachedRequest.element0().failed("Cannot send the request message", future); handleFail(message, responder, "Cannot send the request message"); } } }); } } }); } /** * This method stores the now open {@link PeerConnection} of the unreachable * peer to the {@link PeerBean}. * * @param message * @param peerConnection */ private void storePeerConnection(final Message message, final PeerConnection peerConnection) { peerConnection.closeFuture().addListener(new BaseFutureAdapter<FutureDone<Void>>() { @Override public void operationComplete(final FutureDone<Void> future) throws Exception { // remove the open PeerConnection to the other Peer from openPeerConnections in the PeerBean LOG.warn("Permanent PeerConnection {} to peer {} has been closed.", peerConnection, message.sender()); peer.peerBean().openPeerConnections().remove(message.sender().peerId()); } }); // put the now open PeerConnection into the openPeerConnections-Map in the PeerBean LOG.debug("Storing peerconnection {} to peer {} permanently: {}", peerConnection, message.sender().peerId()); final ConcurrentHashMap<Number160, PeerConnection> openPeerConnections = peer.peerBean().openPeerConnections(); openPeerConnections.put(message.sender().peerId(), peerConnection); } /** * This method is called if something went wrong while the reverse * connection setup. It responds then with a {@link Type}.EXCEPTION message. * * @param message * @param responder * @param failReason */ private void handleFail(final Message message, final Responder responder, final String failReason) { LOG.error(failReason); responder.response(createResponseMessage(message, Type.EXCEPTION)); } }