/* * MessageRouter.java * * Created on Mar 19, 2010, 9:37:26 AM * * Description: Provides a message router that connects with remote message routers over the internet using SSL * transport. OpenChord provides the naming service. * * Copyright (C) Mar 19, 2010 reed. * * This program is free software; you can redistribute it and/or modify it under the terms * of the GNU General Public License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * This program 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 General Public License for more details. * * You should have received a copy of the GNU General Public License along with this program; * if not, write to the Free Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. */ package org.texai.ahcs.router; import java.io.IOException; import net.sbbi.upnp.messages.UPNPResponseException; import org.texai.ahcsSupport.RoleInfo; import de.uniba.wiai.lspi.chord.com.Endpoint; import de.uniba.wiai.lspi.chord.console.command.entry.Key; import de.uniba.wiai.lspi.chord.data.URL; import de.uniba.wiai.lspi.chord.service.Chord; import de.uniba.wiai.lspi.chord.service.ServiceException; import de.uniba.wiai.lspi.chord.service.impl.ChordImpl; import java.io.Serializable; import java.net.InetAddress; import java.net.InetSocketAddress; import java.security.cert.X509Certificate; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; import net.jcip.annotations.ThreadSafe; import net.sbbi.upnp.impls.InternetGatewayDevice; import net.sbbi.upnp.messages.ActionResponse; import org.apache.log4j.Level; import org.apache.log4j.Logger; import org.jboss.netty.channel.Channel; import org.jboss.netty.channel.ChannelFuture; import org.jboss.netty.channel.ChannelFutureListener; import org.jboss.netty.channel.ChannelHandlerContext; import org.jboss.netty.channel.ExceptionEvent; import org.jboss.netty.channel.MessageEvent; import org.openrdf.model.URI; import org.texai.ahcs.AlbusHCSMessageHandlerFactory; import org.texai.ahcsSupport.AlbusMessageDispatcher; import org.texai.ahcsSupport.Message; import org.texai.ahcsSupport.NodeRuntime; import org.texai.chord.com.ssl.MessageRouterProvider; import org.texai.chord.com.ssl.RequestMessage; import org.texai.chord.com.ssl.ResponseMessage; import org.texai.chord.com.ssl.SSLEndpoint; import org.texai.chord.com.ssl.SSLProxy; import org.texai.network.netty.ConnectionUtils; import org.texai.network.netty.handler.AbstractAlbusHCSMessageHandler; import org.texai.util.NetworkUtils; import org.texai.util.TexaiException; import org.texai.x509.X509Utils; /** Provides a message router that connects with remote message routers over the Internet using SSL * transport. OpenChord provides the naming service. * * @author reed */ @ThreadSafe public final class MessageRouter extends AbstractAlbusHCSMessageHandler implements AlbusMessageDispatcher { /** the logger */ private static final Logger LOGGER = Logger.getLogger(MessageRouter.class); /** the UPNP discovery timeout of 5 seconds */ private static final int UPNP_DISCOVERY_TIMEOUT = 3000; /** the role/channel dictionary, role id string --> channel to peer message router */ private final Map<String, Channel> roleChannelDictionary = new HashMap<>(); /** the Chord proxy dictionary, proxy URI string --> proxy */ private final Map<String, SSLProxy> proxyDictionary = new HashMap<>(); /** the X.509 certificate dictionary, role id string --> X.509 certificate */ private final Map<String, X509Certificate> x509CertificateDictionary = new HashMap<>(); /** the role id strings of local roles that are registered for communication with remote roles */ private final Set<String> localRoles = new HashSet<>(); /** the node runtime */ private final NodeRuntime nodeRuntime; /** the Chord endpoint */ private SSLEndpoint sslEndpoint; /** the Chord implementation */ private Chord chord; /** the host address as presented to the Internet, e.g. texai.dyndns.org */ private String externalHostName; /** the host address as presented to the LAN, e.g. turing */ private final int externalPort; /** the message router external IP address */ private String externalIPAddress; // /** the effective external IP address and port, which is most likely mapped to an internal IP address and port */ // private InetSocketAddress effectiveSocketAddress; /** the Internet gateway device */ private InternetGatewayDevice internetGatewayDevice; /** the local URL */ private URL localURL; /** the indicator whether this router has joined the Chord network */ private AtomicBoolean isJoinedChordNetwork = new AtomicBoolean(false); /** the lock for retrieving role information from the Chord distributed hash table */ private final Object roleInfoRetrievalLock = new Object(); // the indicator whether to perform dynamic port forwarding of the NAT router private static final boolean DO_DYNAMIC_PORT_FORWARDING = true; // TODO close unused channels after an inactivity period /** Constructs a new MessageRouter instance. * * @param nodeRuntime the node runtime * @param internalPort the internal port * @param externalPort the external port * @param localURL the URL of this node in the Chord network * @param bootstrapURL the bootstrap URL, or null if this is the first node in the Chord network */ public MessageRouter( final NodeRuntime nodeRuntime, final int internalPort, final int externalPort, final URL localURL, final URL bootstrapURL) { //Preconditions assert nodeRuntime != null : "nodeRuntime must not be null"; assert internalPort > 0 : "internalPort must be positive"; assert externalPort > 0 : "externalPort must be positive"; assert localURL != null : "localURL must not be null"; this.nodeRuntime = nodeRuntime; this.externalPort = externalPort; this.localURL = localURL; if (DO_DYNAMIC_PORT_FORWARDING && NetworkUtils.isPrivateNetworkAddress(NetworkUtils.getLocalHostAddress())) { // there is a NAT router between this host and the internet boolean isOK = configureSSLServerPortForwarding(internalPort, externalPort); if (!isOK) { LOGGER.warn("cannot forward a server port to this host using UPnP"); } } else { // there is no need to configure a NAT router between this host and the internet externalHostName = NetworkUtils.getHostName(); externalIPAddress = NetworkUtils.getLocalHostAddress().getHostAddress(); } // initialize the secure random number so that does not delay the first time establishing an SSL server X509Utils.getSecureRandom(); // accept SSL connections and Albus messages final AlbusHCSMessageHandlerFactory albusHCSMessageHandlerFactory = new AlbusHCSMessageHandlerFactory(this); ConnectionUtils.createPortUnificationServer( internalPort, nodeRuntime.getX509SecurityInfo(), // sslHandshakeCompletedListener albusHCSMessageHandlerFactory, null, // bitTorrentHandlerFactory null, // httpRequestHandlerFactory nodeRuntime.getExecutor(), // bossExecutor nodeRuntime.getExecutor()); // workerExecutor LOGGER.warn("message router is ready to accept SSL connections ..."); LOGGER.warn(" internet-facing IP address: " + externalHostName + ":" + externalPort); LOGGER.warn(" LAN IP address: " + nodeRuntime.getInternalHostName() + ":" + internalPort); joinChordNetwork(bootstrapURL); // insert this message router's role information final RoleInfo roleInfo = new RoleInfo( nodeRuntime.getRoleId(), nodeRuntime.getX509SecurityInfo().getCertPath(), nodeRuntime.getX509SecurityInfo().getPrivateKey(), // node runtime's private key signs the role info nodeRuntime.getLocalAreaNetworkID(), externalHostName, externalPort, nodeRuntime.getInternalHostName(), internalPort); registerRoleForRemoteCommunications(roleInfo); // verify that role info can be retrieved from the chord network final RoleInfo retrievedRoleInfo = getRoleInfo(nodeRuntime.getRoleId()); assert retrievedRoleInfo != null; assert retrievedRoleInfo.equals(roleInfo); LOGGER.info("message router initialized"); } /** Join the Chord network. * * @param bootstrapURL the bootstrap URL, or null if this is the first node in the Chord network */ private void joinChordNetwork(final URL bootstrapURL) { //Preconditions assert localURL != null : "localURL must not be null"; assert nodeRuntime.getX509SecurityInfo() != null : "node runtime X.509 security info must not be null"; LOGGER.info("joining the Chord network"); // the number of bytes of displayed IDs System.getProperties().put("de.uniba.wiai.lspi.chord.data.ID.number.of.displayed.bytes", String.valueOf(4)); // the representation chosen when displaying IDs. 0 = binary, 1 = decimal, 2 = hexadecimal System.getProperties().put("de.uniba.wiai.lspi.chord.data.ID.displayed.representation", String.valueOf(2)); // the number of successors, which must be greater or equal to 1 System.getProperties().put("de.uniba.wiai.lspi.chord.service.impl.ChordImpl.successors", String.valueOf(2)); // the number of threads for asynchronous executions System.getProperties().put("de.uniba.wiai.lspi.chord.service.impl.ChordImpl.AsyncThread.no", String.valueOf(Runtime.getRuntime().availableProcessors())); // the log properties file location System.getProperties().put("log4j.properties.file", System.getProperty("user.home") + "/TexaiLauncher-1.0/log4j.properties"); // the time in seconds until the stabilize task is started for the first time System.getProperties().put("de.uniba.wiai.lspi.chord.service.impl.ChordImpl.StabilizeTask.start", String.valueOf(0)); // the time in seconds between two invocations of the stabilize task System.getProperties().put("de.uniba.wiai.lspi.chord.service.impl.ChordImpl.StabilizeTask.interval", String.valueOf(12)); // the time in seconds until the fix finger task is started for the first time System.getProperties().put("de.uniba.wiai.lspi.chord.service.impl.ChordImpl.FixFingerTask.start", String.valueOf(0)); // the time in seconds between two invocations of the fix finger task System.getProperties().put("de.uniba.wiai.lspi.chord.service.impl.ChordImpl.FixFingerTask.interval", String.valueOf(12)); // the time in seconds until the check predecessor task is started for the first time System.getProperties().put("de.uniba.wiai.lspi.chord.service.impl.ChordImpl.CheckPredecessorTask.start", String.valueOf(0)); // the time in seconds between two invocations of the check predecessor task System.getProperties().put("de.uniba.wiai.lspi.chord.service.impl.ChordImpl.CheckPredecessorTask.interval", String.valueOf(12)); // inject dependencies final MessageRouterProvider messageRouterProvider = new MessageRouterProvider(); messageRouterProvider.setMessageRouter(this); SSLEndpoint.setMessageRouterProvider(messageRouterProvider); SSLProxy.setMessageRouterProvider(messageRouterProvider); SSLEndpoint.setX509SecurityInfo(nodeRuntime.getX509SecurityInfo()); // logging Logger.getLogger(SSLEndpoint.class).setLevel(Level.INFO); Logger.getLogger(SSLProxy.class).setLevel(Level.INFO); chord = new ChordImpl(); try { if (bootstrapURL == null || bootstrapURL.equals(localURL)) { LOGGER.info("creating first node in the Chord network with " + localURL); chord.create(localURL); } else { LOGGER.info("joining the Chord network with " + localURL + ", bootstrap node at " + bootstrapURL); chord.join(localURL, bootstrapURL); } } catch (ServiceException ex) { throw new TexaiException(ex); } sslEndpoint = (SSLEndpoint) Endpoint.getEndpoint(localURL); LOGGER.info("created Chord endpoint " + sslEndpoint); assert sslEndpoint != null; isJoinedChordNetwork.set(true); } /** Leaves the chord network. */ public void leaveChordNetwork() { LOGGER.warn("leaving the Chord network"); try { chord.leave(); } catch (ServiceException ex) { LOGGER.warn("exception when leaving the Chord network: " + ex.getMessage()); } } /** Uses Universal Plug and Play to configure the NAT router to forward the given port to this host. * * @param internalPort the port for this message router * @param externalPort the NAT port to forward to this host * @return whether there is no UPnP router or whether mapping succeeded */ private boolean configureSSLServerPortForwarding(final int internalPort, final int externalPort) { //Preconditions assert internalPort > 0 : "internalPort must be positive"; assert externalPort > 0 : "externalPort must be positive"; final boolean isMapped; LOGGER.info("configuring the NAT to forward port " + externalPort + " to the message router internal port " + internalPort); try { final InternetGatewayDevice[] internetGatewayDevices = InternetGatewayDevice.getDevices(UPNP_DISCOVERY_TIMEOUT); if (internetGatewayDevices == null) { LOGGER.warn("no UPnP router found"); isMapped = false; } else { // use the the first device found internetGatewayDevice = internetGatewayDevices[0]; LOGGER.info("found device " + internetGatewayDevice.getIGDRootDevice().getModelDescription()); externalIPAddress = internetGatewayDevice.getExternalIPAddress(); LOGGER.info("external IP address: " + externalIPAddress); externalHostName = externalIPAddress.toString(); // open the port final InetAddress localHostAddress = NetworkUtils.getLocalHostAddress(); LOGGER.info("local host address: " + localHostAddress.getHostAddress()); // assume that localHostIP is something other than 127.0.0.1 isMapped = internetGatewayDevice.addPortMapping( "Texai SSL message router", // description null, // remote host internalPort, externalPort, localHostAddress.getHostAddress(), 0, // lease duration in seconds, 0 for an infinite time "TCP"); // protocol if (isMapped) { LOGGER.info("port " + externalPort + " mapped to " + localHostAddress.getHostAddress()); final ActionResponse actionResponse = internetGatewayDevice.getSpecificPortMappingEntry( null, // remoteHost externalPort, // external port "TCP"); // protocol LOGGER.info("mapping info:\n" + actionResponse); } else { LOGGER.info("port " + externalPort + " cannot be mapped at " + internetGatewayDevice.getIGDRootDevice().getModelDescription()); for (int i = 0; i < internetGatewayDevice.getNatMappingsCount(); i++) { final ActionResponse actionResponse = internetGatewayDevice.getGenericPortMappingEntry(i); LOGGER.info("index " + i + " mapping info:\n" + actionResponse); } } } } catch (IOException | UPNPResponseException ex) { throw new TexaiException(ex); } try { // wait for discovery listener thread to finish Thread.sleep(1000); } catch (InterruptedException ex) { throw new TexaiException(ex); } return isMapped; } /** Registers the given SSL proxy. * * @param sslProxy the given SSL proxy */ @Override public void registerSSLProxy(final Object sslProxy) { //Preconditions assert sslProxy != null : "sslProxy must not be null"; final SSLProxy sslProxy1 = (SSLProxy) sslProxy; LOGGER.info("registering SSLProxy, " + sslProxy1.getLocalNodeURL().toString() + " --> " + sslProxy); synchronized (proxyDictionary) { proxyDictionary.put(sslProxy1.getLocalNodeURL().toString(), sslProxy1); } } /** Unregisters the given SSL proxy. * * @param sslProxy the given SSL proxy */ public void unregisterSSLProxy(final SSLProxy sslProxy) { //Preconditions assert sslProxy != null : "sslProxy must not be null"; LOGGER.info("unregistering SSLProxy " + sslProxy); synchronized (proxyDictionary) { proxyDictionary.remove(sslProxy.getLocalNodeURL().toString()); } } /** Catches a channel exception. * * @param channelHandlerContext the channel handler context * @param exceptionEvent the exception event */ @Override public void exceptionCaught(final ChannelHandlerContext channelHandlerContext, final ExceptionEvent exceptionEvent) { //Preconditions assert channelHandlerContext != null : "channelHandlerContext must not be null"; assert exceptionEvent != null : "exceptionEvent must not be null"; throw new TexaiException(exceptionEvent.getCause()); } /** Registers the role for remote communications. * * @param roleInfo the role information */ public void registerRoleForRemoteCommunications(final RoleInfo roleInfo) { //Preconditions assert roleInfo != null : "roleInfo must not be null"; final String roleIdString = roleInfo.getRoleId().toString(); LOGGER.info("registering role " + roleIdString); synchronized (localRoles) { localRoles.add(roleIdString); } try { LOGGER.info("inserting into chord " + roleIdString + "-->" + roleInfo); chord.insert(new Key(roleIdString), roleInfo); } catch (ServiceException ex) { throw new TexaiException(ex); } } /** Unregisters the role for remote communications. * * @param roleInfo the role information */ public void unregisterRoleForRemoteCommunications(final RoleInfo roleInfo) { //Preconditions assert roleInfo != null : "roleInfo must not be null"; final String roleIdString = roleInfo.getRoleId().toString(); LOGGER.info("unregistering role " + roleIdString); synchronized (localRoles) { localRoles.remove(roleIdString); } try { chord.remove(new Key(roleIdString), roleInfo); } catch (ServiceException ex) { throw new TexaiException(ex); } } /** Receives a Netty message object from a remote message router peer. The received message is * verified before relaying to the local node runtime. * * @param channelHandlerContext the channel handler context * @param messageEvent the message event */ @Override public void messageReceived( final ChannelHandlerContext channelHandlerContext, final MessageEvent messageEvent) { //Preconditions assert messageEvent != null : "messageEvent must not be null"; assert messageEvent.getMessage() instanceof Message; final Message message = (Message) messageEvent.getMessage(); if (LOGGER.isDebugEnabled()) { LOGGER.debug("***** received from remote message router: " + message); } final String senderRoleIdString = message.getSenderRoleId().toString(); synchronized (roleChannelDictionary) { Channel channel = roleChannelDictionary.get(senderRoleIdString); if (channel == null || channel != channelHandlerContext.getChannel()) { // record the incoming message channel so that it can be used for outbound messages to the same peer roleChannelDictionary.put(senderRoleIdString, channelHandlerContext.getChannel()); } } dispatchAlbusMessage(message); } /** Receives a message sent from the Chord endpoint or from a local Chord proxy, whose recipient is a remote Chord * endpoint. * * @param message the Albus message */ @Override public void dispatchAlbusMessage(final Message message) { //Preconditions assert message != null : "message must not be null"; if (LOGGER.isDebugEnabled()) { LOGGER.debug("dispatching message " + message); LOGGER.debug(" reply-with: " + message.getReplyWith()); } if (sslEndpoint == null) { sslEndpoint = (SSLEndpoint) Endpoint.getEndpoint(localURL); } if (LOGGER.isDebugEnabled()) { LOGGER.debug(" my sslEndpoint: " + sslEndpoint); } LOGGER.info(""); final String recipientRoleIdString = message.getRecipientRoleId().toString(); if (RequestMessage.isRequestMessage(message)) { // This is an inbound Chord request message sent between message routers. The endpoint handles incoming Chord requests. verifyMessage(message); // will not block if (sslEndpoint.getURL().toString().equals(recipientRoleIdString)) { LOGGER.info("<====== dispatching inbound Chord request message to endpoint: " + message); // use a separate thread for the SSLEndpoint because it might block via a call to SSLProxy nodeRuntime.getExecutor().execute(new AlbusMessageDispatchRunner( sslEndpoint, // albusMessageDispatcher message)); } else { // outbound message LOGGER.info("======> dispatching outbound Chord request message to remote router: " + message); routeAlbusMessageToPeerRouter(message); } } else if (ResponseMessage.isResponseMessage(message)) { // This is an inbound Chord response message sent between message routers. The proxy handles incoming Chord responses. verifyMessage(message); // will not block synchronized (proxyDictionary) { if (LOGGER.isDebugEnabled()) { LOGGER.debug("proxyDictionary: " + proxyDictionary); } // because proxies have the URL of the remote node, the recipient id here is the same as the sender if (recipientRoleIdString.equals(localURL.toString())) { if (proxyDictionary.containsKey(recipientRoleIdString)) { final SSLProxy sslProxy = proxyDictionary.get(recipientRoleIdString); LOGGER.info("<====== dispatching inbound Chord response message to proxy: " + message); // use a separate thread for the SSLProxy because it might block nodeRuntime.getExecutor().execute(new AlbusMessageDispatchRunner( sslProxy, // albusMessageDispatcher message)); } else { assert false : "SSL proxy not found for " + message; } } else { LOGGER.info("======> dispatching outbound Chord response message to remote router: " + message); routeAlbusMessageToPeerRouter(message); } } } else { // Albus message sent between roles via their respective message routers final boolean isLocalRole; synchronized (localRoles) { if (LOGGER.isDebugEnabled()) { LOGGER.debug("recipientRoleIdString: " + recipientRoleIdString); LOGGER.debug("localRoles: "); } isLocalRole = localRoles.contains(recipientRoleIdString); } if (isLocalRole) { // route to local node runtime LOGGER.info("<====== dispatching inbound role message " + message); // use a separate thread for the role message because it might block, e.g. to retrieve the sender's X509 certificate nodeRuntime.getExecutor().execute(new AlbusMessageDispatchRunner( nodeRuntime, // albusMessageDispatcher message)); } else { LOGGER.info("======> dispatching outbound role message " + message); synchronized (localRoles) { // if the sending role has not otherwise been registered for remote communications, allow incoming messages on the LAN to reach it final String senderRoleIdString = message.getSenderRoleId().toString(); if (!localRoles.contains(senderRoleIdString)) { localRoles.add(senderRoleIdString); } } routeAlbusMessageToPeerRouter(message); } } if (LOGGER.isDebugEnabled()) { LOGGER.debug(" dispatch completed"); } } /** Provides a thread to execute the SSLProxy or SSL endpoint that handles an inbound Chord message, and which might then subsequently * block the I/O thread while awaiting a conversational response from a Chord network peer. The parent thread is an * AbstractAlbusHCSMessageHandler and must not block. */ static class AlbusMessageDispatchRunner implements Runnable { /** the SSL proxy, SSL endpoint, or node runtime */ private final AlbusMessageDispatcher albusMessageDispatcher; /** the message */ private final Message message; /** Constructs a new AlbusMessageDispatchRunner instance. * * @param albusMessageDispatcher the SSL proxy, SSL endpoint, or node runtime * @param message the message */ AlbusMessageDispatchRunner( final AlbusMessageDispatcher albusMessageDispatcher, final Message message) { //Preconditions assert albusMessageDispatcher != null : "albusMessageDispatcher must not be null"; assert message != null : "message must not be null"; this.albusMessageDispatcher = albusMessageDispatcher; this.message = message; } /** Executes this runnable. */ @Override public void run() { albusMessageDispatcher.dispatchAlbusMessage(message); } } /** Retrieves the role information from the Chord network using the given role id as the key. * * @param chordRoleId the given role id * @return the role information from the Chord network */ public RoleInfo getRoleInfo(final URI chordRoleId) { //Preconditions assert chordRoleId != null : "chordRoleId must not be null"; final String chordRoleIdString = chordRoleId.toString(); final Set<Serializable> roleInfos; try { LOGGER.info("retrieving role information from chord for: " + chordRoleId + ", string: " + chordRoleIdString); String threadName = Thread.currentThread().getName(); Thread.currentThread().setName("role-info-retriever-thread"); synchronized (roleInfoRetrievalLock) { roleInfos = chord.retrieve(new Key(chordRoleIdString)); } LOGGER.info("role information objects retrieved " + roleInfos); Thread.currentThread().setName(threadName); } catch (ServiceException ex) { throw new TexaiException(ex); } if (roleInfos.isEmpty()) { throw new TexaiException("unknown router/role information for " + chordRoleIdString); } else if (roleInfos.size() != 1) { throw new TexaiException(" non-unique router/role information for " + chordRoleIdString + " --> " + roleInfos); } final RoleInfo roleInfo = (RoleInfo) roleInfos.toArray()[0]; //Postconditions roleInfo.verify(); return roleInfo; } /** Verifies a message sent between roles in the Albus hierarchical control system network, throwing an exception * if the message's digital signature fails verification. * * @param message the message */ private void verifyMessage(final Message message) { //Preconditions assert message != null : "message must not be null"; if (message.isChordOperation()) { if (LOGGER.isDebugEnabled()) { LOGGER.debug("non-verifiable Chord operation message " + message); } return; } final String senderRoleIdString = message.getSenderRoleId().toString(); X509Certificate x509Certificate = x509CertificateDictionary.get(senderRoleIdString); if (x509Certificate == null) { LOGGER.info("sender's X.509 certificate not found, requesting it from the Chord network"); final Key key = new Key(senderRoleIdString); final Set<Serializable> roleInfos; try { roleInfos = chord.retrieve(key); } catch (ServiceException ex) { throw new TexaiException(ex); } if (roleInfos.isEmpty()) { throw new TexaiException("sending role does not have a registered X.509 certificate " + senderRoleIdString); } assert roleInfos.size() == 1; final RoleInfo roleInfo = (RoleInfo) roleInfos.toArray()[0]; assert roleInfo.getRoleId().toString().equals(senderRoleIdString); x509Certificate = roleInfo.getRoleX509Certificate(); assert X509Utils.getUUID(x509Certificate).toString().equals(senderRoleIdString) : "message: " + message + "\nX509 certificate ...\n" + x509Certificate; x509CertificateDictionary.put(senderRoleIdString, x509Certificate); LOGGER.info("sender's X.509 certificate obtained from the Chord network"); } //Postconditions message.verify(x509Certificate); LOGGER.info("verified message"); } /** Routes the given message to the responsible peer message router. * * @param message the Albus message */ private void routeAlbusMessageToPeerRouter(final Message message) { //Preconditions assert message != null : "message must not be null"; final String recipientRoleIdString = message.getRecipientRoleId().toString(); Channel channel; RoleInfo roleInfo = null; if (!message.getRecipientRoleId().getLocalName().isEmpty()) { synchronized (roleChannelDictionary) { channel = roleChannelDictionary.get(recipientRoleIdString); } if (channel == null) { // retrieve the role information from the Chord DHT before routing the message // to routeAlbusMessageToPeerRouter - avoiding deadlock on synchronized resources roleInfo = getRoleInfo(message.getRecipientRoleId()); assert roleInfo != null : "roleInfo not found in Chord DHT for " + message.getRecipientRoleId(); } } synchronized (roleChannelDictionary) { channel = roleChannelDictionary.get(recipientRoleIdString); if (channel == null) { // open a channel between this message router and the peer message router that services the recipient role if (LOGGER.isDebugEnabled()) { LOGGER.debug("finding a channel ..."); } if (message.getRecipientRoleId().getLocalName().isEmpty()) { // the message is a Chord operation, e.g. the receipient is http://mccarthy.local:5048/ if (LOGGER.isDebugEnabled()) { LOGGER.debug(" creating a channel to route Chord operation"); LOGGER.debug(" from node runtime: " + nodeRuntime.getX509SecurityInfo().getX509Certificate().getSubjectDN()); } channel = ConnectionUtils.openAlbusHCSConnection( NetworkUtils.makeInetSocketAddress(recipientRoleIdString), // inetSocketAddress nodeRuntime.getX509SecurityInfo(), // albusHCSMessageHandler this, // sslHandshakeCompletedListener nodeRuntime.getExecutor(), // bossExecutor nodeRuntime.getExecutor()); // workerExecutor } else { // the message is between two roles LOGGER.info(" routing message beteen two roles"); assert roleInfo != null; final InetSocketAddress inetSocketAddress; if (roleInfo.getLocalAreaNetworkID().equals(nodeRuntime.getLocalAreaNetworkID())) { // both roles are hosted on the same LAN - so use its internal LAN address inetSocketAddress = new InetSocketAddress(roleInfo.getInternalHostName(), roleInfo.getInternalPort()); } else { // the remote role is hosted on another LAN - so use its external, i.e. NAT-mapped, address inetSocketAddress = new InetSocketAddress(roleInfo.getExternalHostName(), roleInfo.getExternalPort()); } // search for an existing channel to the target node runtime that was established for some other role for (final Channel channel1 : roleChannelDictionary.values()) { final InetSocketAddress inetSocketAddress1 = (InetSocketAddress) channel1.getRemoteAddress(); if (inetSocketAddress1.equals(inetSocketAddress)) { channel = channel1; LOGGER.info(" found existing channel to " + inetSocketAddress); break; } } if (channel == null) { // no channel to the target node runtime exists yet, so create one if (LOGGER.isDebugEnabled()) { LOGGER.debug("creating a channel to route between two roles"); } channel = ConnectionUtils.openAlbusHCSConnection( inetSocketAddress, nodeRuntime.getX509SecurityInfo(), // albusHCSMessageHandler this, // sslHandshakeCompletedListener nodeRuntime.getExecutor(), // bossExecutor nodeRuntime.getExecutor()); // workerExecutor } } assert channel != null; roleChannelDictionary.put(recipientRoleIdString, channel); } if (!channel.isBound()) { LOGGER.info("peer has shutdown " + message.getRecipientRoleId()); return; } assert channel.isConnected() : "channel must be connected"; assert channel.isReadable() : "channel must be readable"; assert channel.isWritable() : "channel must be writable"; if (LOGGER.isDebugEnabled()) { LOGGER.debug("transmitting outbound message on channel " + channel); } synchronized (channel) { final ChannelFuture channelFuture = channel.write(message); if (LOGGER.isDebugEnabled()) { channelFuture.addListener(new ChannelFutureListener() { public void operationComplete(final ChannelFuture future) { if (LOGGER.isDebugEnabled()) { LOGGER.debug(" message transmission completed"); } } }); } } } } /** Gets the NAT's external IP address. * * @return the NAT's external IP address */ public String getExternalIPAddress() { return externalIPAddress; } /** Gets the host address as presented to the Internet, e.g. texai.dyndns.org. * * @return the host address as presented to the Internet */ public String getExternalHostName() { return externalHostName; } /** Finalizes the message router and releases its resources. */ public void finalization() { LOGGER.warn("releasing resources held by the message router"); if (isJoinedChordNetwork.get()) { leaveChordNetwork(); } // removes NAT mapping try { final boolean isUnmapped = internetGatewayDevice.deletePortMapping(null, externalPort, "TCP"); if (isUnmapped) { LOGGER.warn("Port " + externalPort + " unmapped"); } } catch (IOException | UPNPResponseException ex) { throw new TexaiException(ex); } } }