package com.limegroup.gnutella.dht; import java.io.IOException; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.SocketAddress; import java.security.KeyPair; import java.security.PublicKey; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Set; import java.util.concurrent.Future; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.ThreadFactory; import java.util.concurrent.TimeUnit; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.limewire.collection.Buffer; import org.limewire.collection.FixedSizeLIFOSet; import org.limewire.collection.FixedSizeLIFOSet.EjectionPolicy; import org.limewire.concurrent.ManagedThread; import org.limewire.core.settings.DHTSettings; import org.limewire.io.IpPort; import org.limewire.mojito.KUID; import org.limewire.mojito.MojitoDHT; import org.limewire.mojito.routing.Contact; import org.limewire.mojito.routing.Vendor; import org.limewire.mojito.routing.Version; import org.limewire.mojito.routing.RouteTable.RouteTableEvent; import org.limewire.mojito.routing.RouteTable.RouteTableListener; import org.limewire.mojito.util.ContactUtils; import org.limewire.mojito.util.HostFilter; import org.limewire.security.SignatureVerifier; import org.limewire.service.ErrorService; import com.limegroup.gnutella.connection.ConnectionLifecycleEvent; import com.limegroup.gnutella.connection.RoutedConnection; import com.limegroup.gnutella.dht.DHTEvent.Type; import com.limegroup.gnutella.dht.DHTManager.DHTMode; import com.limegroup.gnutella.dht.db.AbstractAltLocValue; import com.limegroup.gnutella.dht.db.AbstractPushProxiesValue; import com.limegroup.gnutella.messages.vendor.DHTContactsMessage; import com.limegroup.gnutella.util.EventDispatcher; /** * The controller for the LimeWire DHT. A node should connect to the DHT only * if it has previously been designated as capable by the <tt>NodeAssigner</tt> * or if it is forced to. Once the node is a DHT node * (if <tt>EXCLUDE_ULTRAPEERS</tt> is set to true) * it should not try to connect as an Ultrapeer. * <p> * The <code>NodeAssigner</code> should be the only class to have the authority * to initialize the DHT and connect to the network. * <p> * This controller can be in one of the four following states: * <ul> * <li> not running. * <li> running and bootstrapping: the DHT is trying to bootstrap. * <li> running and waiting: the DHT has failed the bootstrap and is waiting * for additional bootstrap hosts. * <li> running and bootstrapped. * </ul> * Nodes are bootstrap in the following order: * <ol> * <li>If we have received hosts from the Gnutella network, try them. * <li>Else try the persisted routing table (stored contacts from the last session). * <li>Else try the SIMPP list. * <li>Else start the node fetcher and wait for hosts coming from the network. * </ol> * <strong>Warning:</strong> The methods in this class are NOT synchronized. * <p> * The current implementation is specific to the Mojito DHT. */ public abstract class AbstractDHTController implements DHTController { private static final String PUBLIC_KEY = "GCBADOBQQIASYBQHFKDERTRYAQATBAQBD4BIDAIA7V7VHAI5OUJCSUW7JKOC53HE473BDN2SHTXUIAGDDY7YBNSREZUUKXKAEJI7WWJ5RVMPVP6F6W5DB5WLTNKWZV4BHOAB2NDP6JTGBN3LTFIKLJE7T7UAI6YQELBE7O5J277LPRQ37A5VPZ6GVCTBKDYE7OB7NU6FD3BQENKUCNNBNEJS6Z27HLRLMHLSV37SEIBRTHORJAA4OAQVACLWAUEPCURQXTFSSK4YFIXLQQF7AWA46UBIDAIA67Q2BBOWTM655S54VNODNOCXXF4ZJL537I5OVAXZK5GAWPIHQJTVCWKXR25NIWKP4ZYQOEEBQC2ESFTREPUEYKAWCO346CJSRTEKNYJ4CZ5IWVD4RUUOBI5ODYV3HJTVSFXKG7YL7IQTKYXR7NRHUAJEHPGKJ4N6VBIZBCNIQPP6CWXFT4DJFC3GL2AHWVJFMQAUYO76Z5ESUA4BQUAAFAMBADVUPOYXTTF3IOLCXBEDV7ZPCCW2MDZIM2T2JHUA4HLLGRQCING7PUOJOHCC6CNALSXXYXH3HZHKA5TGV4LC2WGYK5UEYY6BYQBKDQ6RV5JE2XPBJPRT5E5SWDGD2PJPOE34ZSSKBYLQCWQEMN46HZGH75DAIDATD3S3FLETRCHRNWWK6JT7TC4DELTHMAMTZPLTCPYDLPX2T5A"; protected final Log LOG = LogFactory.getLog(getClass()); /** * The instance of the DHT. */ protected final MojitoDHT dht; /** * The DHT bootstrapper instance. */ protected final DHTBootstrapper bootstrapper; /** * The random node adder. */ private final RandomNodeAdder dhtNodeAdder = new RandomNodeAdder(); /** * The forwarder of nodes to passive leafs. */ private final NodeForwarder nodeForwarder = new NodeForwarder(); /** * The DHT event dispatcher. */ private final EventDispatcher<DHTEvent, DHTEventListener> dispatcher; /** * The mode of this DHTController. */ private final DHTMode mode; /** * Get and save the current RouteTable version. */ private final int routeTableVersion; private final DHTControllerFacade dhtControllerFacade; public AbstractDHTController(Vendor vendor, Version version, EventDispatcher<DHTEvent, DHTEventListener> dispatcher, final DHTMode mode, DHTControllerFacade dhtControllerFacade) { this.dhtControllerFacade = dhtControllerFacade; switch(mode) { case ACTIVE: routeTableVersion = DHTSettings.ACTIVE_DHT_ROUTETABLE_VERSION.getValue(); break; case PASSIVE: routeTableVersion = DHTSettings.PASSIVE_DHT_ROUTETABLE_VERSION.getValue(); break; default: routeTableVersion = -1; } this.dispatcher = dispatcher; this.mode = mode; this.dht = createMojitoDHT(vendor, version); assert (dht != null); dht.setMessageDispatcher(dhtControllerFacade.getMessageDispatcherFactory()); dht.setMACCalculatorRepositoryManager(dhtControllerFacade.getMACCalculatorRespositoryManager()); dht.setSecurityTokenProvider(dhtControllerFacade.getSecurityTokenProvider()); dht.getDHTExecutorService().setThreadFactory(new ThreadFactory() { public Thread newThread(Runnable runnable) { return new ManagedThread(runnable); } }); dht.setHostFilter(new FilterDelegate()); dht.getDHTValueFactoryManager().addValueFactory( AbstractAltLocValue.ALT_LOC, dhtControllerFacade.getAltLocValueFactory()); dht.getDHTValueFactoryManager().addValueFactory( AbstractPushProxiesValue.PUSH_PROXIES, dhtControllerFacade.getPushProxyValueFactory()); PublicKey publicKey = SignatureVerifier.readKey(PUBLIC_KEY, "DSA"); KeyPair keyPair = new KeyPair(publicKey, null); dht.setKeyPair(keyPair); dht.getStorableModelManager().addStorableModel( AbstractAltLocValue.ALT_LOC, dhtControllerFacade.getAltLocModel()); this.bootstrapper = dhtControllerFacade.getDHTBootstrapper(this); // If we're an Ultrapeer we want to notify our firewalled // leafs about every new Contact if (dhtControllerFacade.isActiveSupernode()) { dht.getRouteTable().addRouteTableListener(new RouteTableListener() { public void handleRouteTableEvent(RouteTableEvent event) { switch(event.getEventType()) { case ADD_ACTIVE_CONTACT: case ADD_CACHED_CONTACT: case UPDATE_CONTACT: Contact node = event.getContact(); if (mode == DHTMode.ACTIVE || !dht.getLocalNodeID().equals(node.getNodeID())) { nodeForwarder.addContact(node); } break; } } }); } DHTSettings.DHT_NODE_ID.set(dht.getLocalNodeID().toHexString()); } /** * Returns the current RouteTable version. */ protected final int getRouteTableVersion() { return routeTableVersion; } /** * A factory method to create MojitoDHTs. */ protected abstract MojitoDHT createMojitoDHT(Vendor vendor, Version version); public DHTMode getDHTMode() { return mode; } public List<IpPort> getActiveDHTNodes(int maxNodes) { return Collections.emptyList(); } public void handleConnectionLifecycleEvent(ConnectionLifecycleEvent evt) { } /** * Start the Mojito DHT and connects it to the network in either passive * mode, or active mode (if we are a Gnutella leaf node). For the node to * be either passive or active mode, it must meet certain bandwidth settings * and must be able to receive solicited UDP (for example, non-firewalled). * <p> * The start preconditions are the following: * <p> * We are not already connected, AND * <ul> * <li>we are not currently running, or * <li>we want to force a connection (FORCE_DHT_CONNECT is true) */ public void start() { if (isRunning() || (!DHTSettings.FORCE_DHT_CONNECT.getValue() && !dhtControllerFacade.isConnected())) { return; } if(LOG.isDebugEnabled()) { LOG.debug("Initializing the DHT"); } try { InetAddress addr = InetAddress.getByAddress(dhtControllerFacade.getAddress()); int port = dhtControllerFacade.getPort(); if (LOG.isDebugEnabled()) { LOG.debug("binding dht to: " + new InetSocketAddress(addr, port)); } dht.bind(new InetSocketAddress(addr, port)); dht.start(); if (dhtControllerFacade.isActiveSupernode()) nodeForwarder.start(); // Bootstrap only if we're not a passive leaf node if (getDHTMode() != DHTMode.PASSIVE_LEAF) { bootstrapper.bootstrap(); } dispatcher.dispatchEvent(new DHTEvent(this, Type.STARTING)); } catch (IOException err) { LOG.error("IOException", err); ErrorService.error(err); } } /** * Shuts down the DHT. If this is an active node, it sends the updated * capabilities to its ultrapeers and stores the node's route table to a * file (active.mojito). Otherwise, as a passive node, it saves a list * of Most Recently Seen (MRS) nodes to bootstrap for the next session * as long as there was at least two <code>Contact</code>s in the route * table. * <p> * The persisted route table (stored contacts from the last session) is * used as a secondary means to bootstrap, if the node didn't receive any * hosts through the Gnutella network to bootstrap. */ public void stop() { LOG.debug("Shutting down DHT Controller"); bootstrapper.stop(); dhtNodeAdder.stop(); nodeForwarder.stop(); dht.close(); dispatcher.dispatchEvent(new DHTEvent(this, Type.STOPPED)); } /** * If this node is not bootstrapped, passes the given hostAddress * to the DHT bootstrapper. * If it is already bootstrapped, this randomly tries to add the node * to the DHT routing table. * * @param hostAddress the SocketAddress of the DHT host. * @param addToDHTNodeAdder true to add to the random node adder if the DHT * is bootstrapped. */ protected void addActiveDHTNode(SocketAddress hostAddress, boolean addToDHTNodeAdder) { if(!dht.isBootstrapped()){ bootstrapper.addBootstrapHost(hostAddress); } else if(addToDHTNodeAdder){ dhtNodeAdder.addDHTNode(hostAddress); dhtNodeAdder.start(); } } public void addActiveDHTNode(SocketAddress hostAddress) { addActiveDHTNode(hostAddress, true); } public void addPassiveDHTNode(SocketAddress hostAddress) { if (!dht.isBootstrapped()) { bootstrapper.addPassiveNode(hostAddress); } } public void addContact(Contact node) { if (getDHTMode() == DHTMode.PASSIVE_LEAF) { getMojitoDHT().getRouteTable().add(node); } } /** * Returns a list of the Most Recently Seen nodes from the Mojito * routing table. * * @param numNodes the number of nodes to return * @param excludeLocal true to exclude the local node * @return a list of DHT <tt>IpPorts</tt> */ protected List<IpPort> getMRSNodes(int numNodes, boolean excludeLocal) { Collection<Contact> nodes = ContactUtils.sort( dht.getRouteTable().getActiveContacts(), numNodes + 1); //it will add the local node! KUID localNode = dht.getLocalNodeID(); List<IpPort> ipps = new ArrayList<IpPort>(); for(Contact cn : nodes) { if(excludeLocal && cn.getNodeID().equals(localNode)) { continue; } ipps.add(new IpPortRemoteContact(cn)); } return ipps; } public boolean isRunning() { return dht.isRunning(); } public boolean isBootstrapped() { return dht.isBootstrapped(); } public boolean isWaitingForNodes() { return bootstrapper.isWaitingForNodes(); } public MojitoDHT getMojitoDHT() { return dht; } /** * Sends the updated <code>CapabilitiesVM</code> to our connections. This * is used when a node has successfully bootstrapped to the network and wants * to notify its Gnutella peers that they can now bootstrap off of him. */ public void sendUpdatedCapabilities() { LOG.debug("Sending updated capabilities to our connections"); dhtControllerFacade.updateCapabilities(); dhtControllerFacade.sendUpdatedCapabilities(); if (isRunning()) dispatcher.dispatchEvent(new DHTEvent(this, Type.CONNECTED)); } /** * A helper class to easily go back and forth * from the DHT's RemoteContact to Gnutella's IpPort. */ private static class IpPortRemoteContact implements IpPort { private InetSocketAddress addr; public IpPortRemoteContact(Contact node) { if(!(node.getContactAddress() instanceof InetSocketAddress)) { throw new IllegalArgumentException("Contact not instance of InetSocketAddress"); } addr = (InetSocketAddress) node.getContactAddress(); } @Override public String getAddress() { return getInetAddress().getHostAddress(); } @Override public InetAddress getInetAddress() { return addr.getAddress(); } @Override public int getPort() { return addr.getPort(); } @Override public InetSocketAddress getInetSocketAddress() { return addr; } } /** * Used to fight against possible DHT clusters by periodically sending * a Mojito ping to the last MAX_SIZE DHT nodes seen in the Gnutella * network. It is effectively randomly adding them to the DHT routing table. */ class RandomNodeAdder implements Runnable { private static final int MAX_SIZE = 30; private final Set<SocketAddress> dhtNodes; private ScheduledFuture<?> timerTask; private boolean isRunning; public RandomNodeAdder() { dhtNodes = new FixedSizeLIFOSet<SocketAddress>(MAX_SIZE, EjectionPolicy.FIFO); } public synchronized void start() { if(isRunning) { return; } long delay = DHTSettings.DHT_NODE_ADDER_DELAY.getValue(); timerTask = dhtControllerFacade.scheduleWithFixedDelay(this, delay, delay, TimeUnit.MILLISECONDS); isRunning = true; } synchronized void addDHTNode(SocketAddress address) { dhtNodes.add(address); } public void run() { List<SocketAddress> nodes = null; synchronized (this) { if(!isRunning()) { return; } nodes = new ArrayList<SocketAddress>(dhtNodes); dhtNodes.clear(); } synchronized(dht) { for(SocketAddress addr : nodes) { if(LOG.isDebugEnabled()) { LOG.debug("RandomNodeAdder pinging: "+ addr); } dht.ping(addr); } } } synchronized boolean isRunning() { return isRunning; } synchronized void stop() { if(timerTask != null) { // TODO: should this attempt to cancel the running task, or not? timerTask.cancel(true); } dhtNodes.clear(); isRunning = false; } } private class FilterDelegate implements HostFilter { public boolean allow(SocketAddress addr) { return dhtControllerFacade.allow(addr); } } /** * Forwards contacts to passive leafs. */ private class NodeForwarder implements Runnable { /** * Contacts to forward to passive leafs. */ private final Buffer<Contact> contactsToForward = new Buffer<Contact>(10); /** * Future that actually forwards. */ private volatile Future<?> forwarderFuture; void start() { forwarderFuture = dhtControllerFacade.scheduleWithFixedDelay(this, 60, 60, TimeUnit.SECONDS); } synchronized void addContact(Contact contact) { contactsToForward.add(contact); } public void run() { if (!DHTSettings.ENABLE_PASSIVE_LEAF_DHT_MODE.getValue() || !isRunning()) { return; } Collection<Contact> contacts; synchronized(this) { if (contactsToForward.isEmpty()) return; contacts = new ArrayList<Contact>(10); for(Contact c : contactsToForward) contacts.add(c); // do not erase contacts - can be re-forwarded to new connections } DHTContactsMessage msg = new DHTContactsMessage(contacts); List<RoutedConnection> list = dhtControllerFacade.getInitializedClientConnections(); for (RoutedConnection mc : list) { if (mc.isPushProxyFor() && mc.getConnectionCapabilities().remoteHostIsPassiveLeafNode() > -1) { mc.send(msg); } } } void stop() { Future<?> f = forwarderFuture; if (f != null) f.cancel(false); } } }