package com.limegroup.gnutella; import java.net.InetAddress; import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; import java.util.Hashtable; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; import java.util.Vector; import java.util.concurrent.ScheduledExecutorService; 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.concurrent.ThreadExecutor; import org.limewire.core.settings.ConnectionSettings; import org.limewire.core.settings.SearchSettings; import org.limewire.inject.EagerSingleton; import org.limewire.io.GUID; import org.limewire.io.NetworkUtils; import org.limewire.lifecycle.Service; import org.limewire.security.AddressSecurityToken; import org.limewire.util.Objects; import com.google.inject.Inject; import com.google.inject.Provider; import com.google.inject.name.Named; import com.limegroup.gnutella.guess.GUESSEndpoint; import com.limegroup.gnutella.messages.PingReply; import com.limegroup.gnutella.messages.PingRequest; import com.limegroup.gnutella.messages.PingRequestFactory; import com.limegroup.gnutella.messages.QueryReply; import com.limegroup.gnutella.messages.QueryRequest; import com.limegroup.gnutella.messages.QueryRequestFactory; /** * This class runs a single thread which sends unicast UDP queries to a master * list of unicast-enabled hosts every n milliseconds. It interacts with * HostCatcher to find unicast-enabled hosts. It also allows for stopping of * individual queries by reply counts. */ @EagerSingleton public final class QueryUnicaster implements Service { private static final Log LOG = LogFactory.getLog(QueryUnicaster.class); /** The time in between successive unicast queries. */ public static final int ITERATION_TIME = 100; // 1/10th of a second... /** The number of Endpoints where you should start sending pings to them. */ public static final int MIN_ENDPOINTS = 25; /** The max number of unicast pongs to store. */ //public static final int MAX_ENDPOINTS = 2000; public static final int MAX_ENDPOINTS = 30; /** One hour in milliseconds. */ public static final long ONE_HOUR = 1000 * 60 * 60; // 60 minutes /** Actually sends any QRs via unicast UDP messages. */ private final Thread _querier; /** * The map of Queries I need to send every iteration. * The map is from GUID to QueryBundle. The following invariant is * maintained: * GUID -> QueryBundle where GUID == QueryBundle._qr.getGUID() */ private final Map<GUID, QueryBundle> _queries; /** * Maps leaf connections to the queries they've spawned. * The map is from ReplyHandler to a Set (of GUIDs). */ private final Map<ReplyHandler, Set<GUID>> _querySets; /** * The unicast enabled hosts I should contact for queries. Add to the * front, remove from the end. Therefore, the OLDEST entries are at the * end. */ private final LinkedList<GUESSEndpoint> _queryHosts; /** * The Set of QueryKeys to be used for Queries. */ private final Map<GUESSEndpoint, QueryKeyBundle> _queryKeys; /** The fixed size list of endpoints i've pinged. */ private final Buffer<GUESSEndpoint> _pingList; /** A List of query GUIDS to purge. */ private final List<GUID> _qGuidsToRemove; /** The last time I sent a broadcast ping. */ private long _lastPingTime = 0; /** * How many test pings have been sent out to determine * whether or not we can accept incoming connections. */ private int _testUDPPingsSent = 0; /** * Records whether or not someone has called init on me.... */ private boolean _initialized = false; private final NetworkManager networkManager; private final QueryRequestFactory queryRequestFactory; private final ScheduledExecutorService backgroundExecutor; private final Provider<MessageRouter> messageRouter; private final Provider<UDPService> udpService; private final PingRequestFactory pingRequestFactory; @Inject public QueryUnicaster(NetworkManager networkManager, QueryRequestFactory queryRequestFactory, @Named("backgroundExecutor") ScheduledExecutorService backgroundExecutor, Provider<MessageRouter> messageRouter, Provider<UDPService> udpService, PingRequestFactory pingRequestFactory) { this.networkManager = networkManager; this.queryRequestFactory = queryRequestFactory; this.backgroundExecutor = backgroundExecutor; this.messageRouter = messageRouter; this.udpService = udpService; this.pingRequestFactory = pingRequestFactory; _queries = new Hashtable<GUID, QueryBundle>(); _queryHosts = new LinkedList<GUESSEndpoint>(); _queryKeys = new Hashtable<GUESSEndpoint, QueryKeyBundle>(); _pingList = new Buffer<GUESSEndpoint>(25); _querySets = new Hashtable<ReplyHandler, Set<GUID>>(); _qGuidsToRemove = new Vector<GUID>(); // start service... _querier = ThreadExecutor.newManagedThread(new Runnable() { public void run() { queryLoop(); } }); _querier.setName("QueryUnicaster"); _querier.setDaemon(true); } /** Returns the number of Queries unicasted by this guy... */ int getQueryNumber() { return _queries.size(); } /** * Returns a List of unicast Endpoints. These Endpoints are the NEWEST * we've seen. */ public List<GUESSEndpoint> getUnicastEndpoints() { List<GUESSEndpoint> retList = new ArrayList<GUESSEndpoint>(); synchronized (_queryHosts) { int size = _queryHosts.size(); if (size > 0) { int max = (size > 10 ? 10 : size); for (int i = 0; i < max; i++) retList.add(_queryHosts.get(i)); } } return retList; } /** * Returns a <tt>GUESSEndpoint</tt> from the current cache of * GUESS endpoints. * * @return a <tt>GUESSEndpoint</tt> from the list of GUESS hosts * to query, or <tt>null</tt> if there are no available hosts * to return */ public GUESSEndpoint getUnicastEndpoint() { synchronized(_queryHosts) { if(_queryHosts.isEmpty()) return null; else return _queryHosts.getFirst(); } } @Inject void register(org.limewire.lifecycle.ServiceRegistry registry) { registry.register(this); } /** * Starts the query unicaster thread. */ public synchronized void start() { if (!_initialized) { _querier.start(); QueryKeyExpirer expirer = new QueryKeyExpirer(); backgroundExecutor.scheduleWithFixedDelay(expirer, 0, 3 * ONE_HOUR, TimeUnit.MILLISECONDS); // Expire query keys every 3 hours _initialized = true; } } public String getServiceName() { return org.limewire.i18n.I18nMarker.marktr("Directed Querier"); } public void initialize() { } public void stop() { } /** * The main work to be done. * If there are queries, get a unicast enabled UP, and send each Query to * it. Then sleep and try some more later... */ private void queryLoop() { while (true) { try { waitForQueries(); GUESSEndpoint toQuery = getUnicastHost(); // no query key to use in my query! if (!_queryKeys.containsKey(toQuery)) { // send a AddressSecurityToken Request PingRequest pr = pingRequestFactory.createQueryKeyRequest(); udpService.get().send(pr,toQuery.getInetAddress(), toQuery.getPort()); // DO NOT RE-ADD ENDPOINT - we'll do that if we get a // AddressSecurityToken Reply!! continue; // try another up above.... } AddressSecurityToken addressSecurityToken = _queryKeys.get(toQuery)._queryKey; purgeGuidsInternal(); // in case any were added while asleep boolean currentHostUsed = false; synchronized (_queries) { for(QueryBundle currQB : _queries.values()) { if (currQB._hostsQueried.size() > QueryBundle.MAX_QUERIES) // query is now stale.... _qGuidsToRemove.add(new GUID(currQB._qr.getGUID())); else if (currQB._hostsQueried.contains(toQuery)) ; // don't send another.... else { InetAddress ip = toQuery.getInetAddress(); QueryRequest qrToSend = queryRequestFactory.createQueryKeyQuery(currQB._qr, addressSecurityToken); udpService.get().send(qrToSend, ip, toQuery.getPort()); currentHostUsed = true; currQB._hostsQueried.add(toQuery); } } } // add the current host back to the list if it was not used for // any query if(!currentHostUsed) { addUnicastEndpoint(toQuery); } // purge stale queries, hold lock so you don't miss any... synchronized (_qGuidsToRemove) { purgeGuidsInternal(); _qGuidsToRemove.clear(); } Thread.sleep(ITERATION_TIME); } catch (InterruptedException ignored) {} } } /** * A quick purging of query GUIDS from the _queries Map. The * queryLoop uses this to so it doesn't have to hold the _queries * lock for too long. */ private void purgeGuidsInternal() { synchronized (_qGuidsToRemove) { for(GUID currGuid : _qGuidsToRemove) _queries.remove(currGuid); } } private void waitForQueries() throws InterruptedException { LOG.trace("Waiting for queries"); synchronized (_queries) { if (_queries.isEmpty()) { // i'll be notifed when stuff is added... _queries.wait(); } } if(LOG.isTraceEnabled()) LOG.trace("Got " + _queries.size() + " queries"); } /** * @return true if the query was added (maybe false if it existed). * @param query The Query to add, to start unicasting. * @param reference The originating connection. OK if NULL. */ public boolean addQuery(QueryRequest query, ReplyHandler reference) { LOG.trace("Adding query"); boolean added = false; GUID guid = new GUID(query.getGUID()); // first map the QueryBundle using the guid.... synchronized (_queries) { if (!_queries.containsKey(guid)) { QueryBundle qb = new QueryBundle(query); _queries.put(guid, qb); _queries.notifyAll(); added = true; } } // return if this node originated the query if (reference == null) return added; // then record the guid in the set of leaf's queries... synchronized (_querySets) { Set<GUID> guids = _querySets.get(reference); if (guids == null) { guids = new HashSet<GUID>(); _querySets.put(reference, guids); } guids.add(guid); } return added; } /** Just feed me ExtendedEndpoints - I'll check if I could use them or not. */ public void addUnicastEndpoint(InetAddress address, int port) { if (!SearchSettings.GUESS_ENABLED.getValue()) return; if (notMe(address, port) && NetworkUtils.isValidPort(port) && NetworkUtils.isValidAddress(address)) { GUESSEndpoint endpoint = new GUESSEndpoint(address, port); addUnicastEndpoint(endpoint); } } /** Adds the <tt>GUESSEndpoint</tt> instance to the host data. * * @param endpoint the <tt>GUESSEndpoint</tt> to add */ private void addUnicastEndpoint(GUESSEndpoint endpoint) { synchronized (_queryHosts) { if (_queryHosts.size() == MAX_ENDPOINTS) { LOG.trace("Evicting old unicast host to make room"); _queryHosts.removeLast(); } LOG.trace("Adding new unicast host"); _queryHosts.addFirst(endpoint); _queryHosts.notify(); // Consider sending a test ping if(udpService.get().isListening() && !networkManager.isGUESSCapable() && _testUDPPingsSent < 10 && !(ConnectionSettings.LOCAL_IS_PRIVATE.getValue() && NetworkUtils.isCloseIP(networkManager.getAddress(), endpoint.getInetAddress().getAddress()))) { LOG.info("Sending a UDP test ping"); byte[] guid = udpService.get().getSolicitedGUID().bytes(); PingRequest pr = pingRequestFactory.createPingRequest(guid, (byte)1, (byte)0); udpService.get().send(pr, endpoint.getInetAddress(), endpoint.getPort()); _testUDPPingsSent++; } } } /** * Returns whether or not the Endpoint refers to me! True if it doesn't, * false if it does (NOT not me == me). */ private boolean notMe(InetAddress address, int port) { if((port == networkManager.getPort()) && Arrays.equals(address.getAddress(), networkManager.getAddress())) { return false; } return true; } /** * Gets rid of a Query according to ReplyHandler. * Use this if a leaf connection dies and you want to stop the query. */ void purgeQuery(ReplyHandler reference) { LOG.trace("Purging query by ReplyHandler"); if (reference == null) return; synchronized (_querySets) { Set<GUID> guids = _querySets.remove(reference); if (guids == null) return; for(GUID guid : guids) purgeQuery(guid); } } /** * Gets rid of a Query according to GUID. Use this if a leaf connection * dies and you want to stop the query. */ void purgeQuery(GUID queryGUID) { LOG.trace("Purging query by GUID"); _qGuidsToRemove.add(queryGUID); } /** Feed me QRs so I can keep track of stuff. */ public void handleQueryReply(QueryReply qr) { addResults(new GUID(qr.getGUID()), qr.getResultCount()); } /** Feed me AddressSecurityToken pongs so I can query people.... * pre: pr.getQueryKey() != null */ public void handleQueryKeyPong(PingReply pr) { Objects.nonNull(pr, "pong"); AddressSecurityToken qk = pr.getQueryKey(); Objects.nonNull(qk, "query key"); InetAddress address = pr.getInetAddress(); int port = pr.getPort(); GUESSEndpoint endpoint = new GUESSEndpoint(address, port); _queryKeys.put(endpoint, new QueryKeyBundle(qk)); addUnicastEndpoint(endpoint); } /** * Add results to a query so we can invalidate it when enough results are * received. */ private void addResults(GUID queryGUID, int numResultsToAdd) { LOG.trace("Adding results to query"); synchronized (_queries) { QueryBundle qb = _queries.get(queryGUID); if (qb != null) {// add results if possible... qb._numResults += numResultsToAdd; // This code moved from queryLoop() since that ftn. blocks before // removing stale queries, when out of hosts to query. if(qb._numResults > QueryBundle.MAX_RESULTS) { LOG.trace("Query has enough results, removing"); synchronized(_qGuidsToRemove) { _qGuidsToRemove.add(new GUID(qb._qr.getGUID())); purgeGuidsInternal(); _qGuidsToRemove.clear(); } } } } } /** May block if no hosts exist. */ private GUESSEndpoint getUnicastHost() throws InterruptedException { LOG.trace("Waiting for hosts"); synchronized (_queryHosts) { while (_queryHosts.isEmpty()) { // don't sent too many pings if (System.currentTimeMillis() - _lastPingTime > 20000) { // first send a Ping, hopefully we'll get some pongs.... byte ttl = ConnectionSettings.TTL.getValue(); PingRequest pr = pingRequestFactory.createPingRequest(ttl); LOG.info("Broadcasting a ping"); messageRouter.get().broadcastPingRequest(pr); _lastPingTime = System.currentTimeMillis(); } // now wait, what else can we do? _queryHosts.wait(); } } if(LOG.isTraceEnabled()) LOG.trace("Got " + _queryHosts.size() + " hosts"); if (_queryHosts.size() < MIN_ENDPOINTS) { // send a ping to the guy you are popping if cache too small GUESSEndpoint toReturn = _queryHosts.removeLast(); // if i haven't pinged him 'recently', then ping him... synchronized (_pingList) { if (!_pingList.contains(toReturn)) { LOG.trace("Pinging unicast host before removing"); PingRequest pr = pingRequestFactory.createPingRequest((byte)1); InetAddress ip = toReturn.getInetAddress(); udpService.get().send(pr, ip, toReturn.getPort()); _pingList.add(toReturn); } } return toReturn; } return _queryHosts.removeLast(); } /** removes all Unicast Endpoints, reset associated members */ void resetUnicastEndpointsAndQueries() { LOG.trace("Resetting unicast hosts and queries"); synchronized (_queries) { _queries.clear(); _queries.notifyAll(); } synchronized (_queryHosts) { _queryHosts.clear(); _queryHosts.notifyAll(); } synchronized (_queryKeys) { _queryKeys.clear(); _queryKeys.notifyAll(); } synchronized (_pingList) { _pingList.clear(); _pingList.notifyAll(); } _lastPingTime=0; _testUDPPingsSent=0; } private static class QueryBundle { public static final int MAX_RESULTS = 250; public static final int MAX_QUERIES = 1000; final QueryRequest _qr; // the number of results received per Query... int _numResults = 0; /** The Set of Endpoints queried for this Query. */ final Set<GUESSEndpoint> _hostsQueried = new HashSet<GUESSEndpoint>(); public QueryBundle(QueryRequest qr) { _qr = qr; } // overrides toString to provide more information @Override public String toString() { return "QueryBundle: "+_qr; } } private static class QueryKeyBundle { public static final long QUERY_KEY_LIFETIME = 2 * ONE_HOUR; // 2 hours final long _birthTime; final AddressSecurityToken _queryKey; public QueryKeyBundle(AddressSecurityToken qk) { _queryKey = qk; _birthTime = System.currentTimeMillis(); } /** Returns true if this AddressSecurityToken hasn't been updated in a while and * should be expired. */ public boolean shouldExpire() { if (System.currentTimeMillis() - _birthTime >= QUERY_KEY_LIFETIME) return true; return false; } @Override public String toString() { return "{QueryKeyBundle: " + _queryKey + " BirthTime = " + _birthTime; } } /** * Schedule this class to run every so often and rid the Map of Bundles that * are stale. */ private class QueryKeyExpirer implements Runnable { public void run() { synchronized (_queryKeys) { Iterator<QueryKeyBundle> iter = _queryKeys.values().iterator(); while(iter.hasNext()) { if(iter.next().shouldExpire()) iter.remove(); } } } } }