package com.limegroup.gnutella; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.OutputStream; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.Socket; import java.net.UnknownHostException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.TreeMap; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ExecutorService; 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.collection.FixedsizeHashMap; import org.limewire.collection.NoMoreStorageException; import org.limewire.concurrent.ExecutorsHelper; import org.limewire.concurrent.ManagedThread; import org.limewire.core.settings.ConnectionSettings; import org.limewire.core.settings.DownloadSettings; import org.limewire.core.settings.FilterSettings; import org.limewire.core.settings.MessageSettings; import org.limewire.core.settings.SearchSettings; import org.limewire.i18n.I18nMarker; import org.limewire.inspection.Inspectable; import org.limewire.inspection.InspectablePrimitive; import org.limewire.inspection.InspectionHistogram; import org.limewire.inspection.InspectionPoint; import org.limewire.io.GUID; import org.limewire.io.IOUtils; import org.limewire.io.IpPort; import org.limewire.io.NetworkUtils; import org.limewire.lifecycle.ServiceRegistry; import org.limewire.lifecycle.ServiceStage; import org.limewire.net.SocketsManager; import org.limewire.net.SocketsManager.ConnectType; import org.limewire.security.AddressSecurityToken; import org.limewire.security.MACCalculatorRepositoryManager; import org.limewire.security.SecurityToken; import org.limewire.service.ErrorService; import org.limewire.setting.evt.SettingEvent; import org.limewire.setting.evt.SettingListener; import org.limewire.util.Base32; import org.limewire.util.ByteUtils; import org.limewire.util.Objects; import org.limewire.util.StringUtils; import com.google.inject.Inject; import com.google.inject.Provider; import com.google.inject.name.Named; import com.limegroup.gnutella.auth.ContentManager; import com.limegroup.gnutella.connection.Connection; import com.limegroup.gnutella.connection.ConnectionLifecycleEvent; import com.limegroup.gnutella.connection.ConnectionLifecycleListener; import com.limegroup.gnutella.connection.RoutedConnection; import com.limegroup.gnutella.dht.DHTManager; import com.limegroup.gnutella.filters.URNFilter; import com.limegroup.gnutella.guess.GUESSEndpoint; import com.limegroup.gnutella.guess.OnDemandUnicaster; import com.limegroup.gnutella.library.FileViewManager; import com.limegroup.gnutella.messagehandlers.DualMessageHandler; import com.limegroup.gnutella.messagehandlers.InspectionRequestHandler; import com.limegroup.gnutella.messagehandlers.LimeACKHandler; import com.limegroup.gnutella.messagehandlers.MessageHandler; import com.limegroup.gnutella.messagehandlers.OOBHandler; import com.limegroup.gnutella.messagehandlers.UDPCrawlerPingHandler; import com.limegroup.gnutella.messages.BadPacketException; import com.limegroup.gnutella.messages.Message; import com.limegroup.gnutella.messages.OutgoingQueryReplyFactory; import com.limegroup.gnutella.messages.PingReply; import com.limegroup.gnutella.messages.PingReplyFactory; import com.limegroup.gnutella.messages.PingRequest; import com.limegroup.gnutella.messages.PingRequestFactory; import com.limegroup.gnutella.messages.PushRequest; import com.limegroup.gnutella.messages.QueryReply; import com.limegroup.gnutella.messages.QueryReplyFactory; import com.limegroup.gnutella.messages.QueryRequest; import com.limegroup.gnutella.messages.QueryRequestFactory; import com.limegroup.gnutella.messages.StaticMessages; import com.limegroup.gnutella.messages.vendor.ContentResponse; import com.limegroup.gnutella.messages.vendor.DHTContactsMessage; import com.limegroup.gnutella.messages.vendor.HeadPing; import com.limegroup.gnutella.messages.vendor.HeadPong; import com.limegroup.gnutella.messages.vendor.HeadPongFactory; import com.limegroup.gnutella.messages.vendor.HopsFlowVendorMessage; import com.limegroup.gnutella.messages.vendor.InspectionRequest; import com.limegroup.gnutella.messages.vendor.LimeACKVendorMessage; import com.limegroup.gnutella.messages.vendor.PushProxyAcknowledgement; import com.limegroup.gnutella.messages.vendor.PushProxyRequest; import com.limegroup.gnutella.messages.vendor.QueryStatusResponse; import com.limegroup.gnutella.messages.vendor.ReplyNumberVendorMessage; import com.limegroup.gnutella.messages.vendor.SimppRequestVM; import com.limegroup.gnutella.messages.vendor.SimppVM; import com.limegroup.gnutella.messages.vendor.TCPConnectBackRedirect; import com.limegroup.gnutella.messages.vendor.TCPConnectBackVendorMessage; import com.limegroup.gnutella.messages.vendor.UDPConnectBackRedirect; import com.limegroup.gnutella.messages.vendor.UDPConnectBackVendorMessage; import com.limegroup.gnutella.messages.vendor.UDPCrawlerPing; import com.limegroup.gnutella.messages.vendor.UpdateRequest; import com.limegroup.gnutella.messages.vendor.UpdateResponse; import com.limegroup.gnutella.messages.vendor.VendorMessage; import com.limegroup.gnutella.routing.PatchTableMessage; import com.limegroup.gnutella.routing.QRPUpdater; import com.limegroup.gnutella.routing.QueryRouteTable; import com.limegroup.gnutella.routing.ResetTableMessage; import com.limegroup.gnutella.routing.RouteTableMessage; import com.limegroup.gnutella.search.QueryDispatcher; import com.limegroup.gnutella.search.QueryHandler; import com.limegroup.gnutella.search.QueryHandlerFactory; import com.limegroup.gnutella.search.ResultCounter; import com.limegroup.gnutella.search.SearchResultHandler; import com.limegroup.gnutella.simpp.SimppManager; import com.limegroup.gnutella.util.LimeWireUtils; import com.limegroup.gnutella.version.UpdateHandler; /** * One of the three classes that make up the core of the backend. This * class' job is to direct the routing of messages and to count those message * as they pass through. To do so, it aggregates a ConnectionManager that * maintains a list of connections. */ //@Singleton // not here because it's not constructed, subclass is! public abstract class MessageRouterImpl implements MessageRouter { private static final Log LOG = LogFactory.getLog(MessageRouterImpl.class); /** * Constant for the number of old connections to use when forwarding * traffic from old connections. */ private static final int OLD_CONNECTIONS_TO_USE = 15; /** * The GUID we attach to QueryReplies to allow PushRequests in * responses. */ protected byte[] _clientGUID; /** * The maximum size for <tt>RouteTable</tt>s. */ private int MAX_ROUTE_TABLE_SIZE = 50000; //actually 100,000 entries /** * Maps PingRequest GUIDs to PingReplyHandlers. Stores 2-4 minutes, * typically around 2500 entries, but never more than 100,000 entries. */ @InspectionPoint("ping routing table dump") private RouteTable _pingRouteTable = new RouteTable(2*60, MAX_ROUTE_TABLE_SIZE); /** * Maps QueryRequest GUIDs to QueryReplyHandlers. Stores 5-10 minutes, * typically around 13000 entries, but never more than 100,000 entries. */ @InspectionPoint("query routing table dump") private RouteTable _queryRouteTable = new RouteTable(5*60, MAX_ROUTE_TABLE_SIZE); /** * Maps QueryReply client GUIDs to PushRequestHandlers. Stores 7-14 * minutes, typically around 3500 entries, but never more than 100,000 * entries. */ @InspectionPoint("push routing table dump") private RouteTable _pushRouteTable = new RouteTable(7*60, MAX_ROUTE_TABLE_SIZE); /** * Maps HeadPong guids to the originating pingers. Short-lived since * we expect replies from our leaves quickly. */ @InspectionPoint("headpong routing table dump") private RouteTable _headPongRouteTable = new RouteTable(10, MAX_ROUTE_TABLE_SIZE); /** * The amount of time after which to expire an OOBSession. */ private static final long OOB_SESSION_EXPIRE_TIME = 2 * 60 * 1000; /** Time between sending HopsFlow messages. */ private static final long HOPS_FLOW_INTERVAL = 15 * 1000; // 15 seconds private final BypassedResultsCache _bypassedResultsCache; /** * Keeps track of what hosts we have recently tried to connect back to via * UDP. The size is limited and once the size is reached, no more connect * back attempts will be honored. */ private static final FixedsizeHashMap<String, String> _udpConnectBacks = new FixedsizeHashMap<String, String>(200); /** * The maximum numbers of ultrapeers to forward a UDPConnectBackRedirect * message to, per forward. */ private static final int MAX_UDP_CONNECTBACK_FORWARDS = 5; /** * Keeps track of what hosts we have recently tried to connect back to via * TCP. The size is limited and once the size is reached, no more connect * back attempts will be honored. */ private static final FixedsizeHashMap<String, String> _tcpConnectBacks = new FixedsizeHashMap<String, String>(200); /** * The maximum numbers of ultrapeers to forward a TCPConnectBackRedirect * message to, per forward. */ private static final int MAX_TCP_CONNECTBACK_FORWARDS = 5; /** * The processingqueue to add tcpconnectback socket connections to. */ private static final ExecutorService TCP_CONNECT_BACKER = ExecutorsHelper.newProcessingQueue("TCPConnectBack"); /** * A handle to the thread that deals with QRP Propagation */ private final QRPPropagator QRP_PROPAGATOR = new QRPPropagator(); /** * Variable for the most recent <tt>QueryRouteTable</tt> created * for this node. If this node is an Ultrapeer, the routing * table will include the tables from its leaves. */ private QueryRouteTable _lastQueryRouteTable; /** * The maximum number of response to send to a query that has * a "high" number of hops. */ private static final int HIGH_HOPS_RESPONSE_LIMIT = 10; /** Keeps track of Listeners of GUIDs. */ private volatile Map<byte[], List<MessageListener>> _messageListeners = Collections.emptyMap(); /** * Lock that registering & unregistering listeners can hold * while replacing the listeners map / lists. */ private final Object MESSAGE_LISTENER_LOCK = new Object(); /** * The time we last received a request for a query key. */ private long _lastQueryKeyTime; /** Handlers for TCP messages. */ private ConcurrentMap<Class<? extends Message>, MessageHandler> messageHandlers = new ConcurrentHashMap<Class<? extends Message>, MessageHandler>(30, 0.75f, 3); /** Handler for UDP messages. */ private ConcurrentMap<Class<? extends Message>, MessageHandler> udpMessageHandlers = new ConcurrentHashMap<Class<? extends Message>, MessageHandler>(15, 0.75f, 3); /** Handler for TCP messages. */ private ConcurrentMap<Class<? extends Message>, MessageHandler> multicastMessageHandlers = new ConcurrentHashMap<Class<? extends Message>, MessageHandler>(5, 0.75f, 3); /** The length of time a multicast guid should stay alive. */ private static final long MULTICAST_GUID_EXPIRE_TIME = 60 * 1000; /** How long to remember cached udp reply handlers. */ private static final int UDP_REPLY_CACHE_TIME = 60 * 1000; @InspectionPoint("guid tracker") @SuppressWarnings("unused") private final GUIDTracker guidTracker = new GUIDTracker(); @InspectionPoint("dropped replies") private final DroppedReplyCounter droppedReplyCounter = new DroppedReplyCounter(); @InspectionPoint("duplicate queries") private final DuplicateQueryCounter duplicateQueryCounter = new DuplicateQueryCounter(); @InspectionPoint("last hop counter for ultrapeers") private final LastHopCounter lastHopCounter = new LastHopCounter(); @InspectablePrimitive("leaf connection qrp hits") private final InspectionHistogram<Integer> leafQRPHits = new InspectionHistogram<Integer>(); protected final NetworkManager networkManager; protected final QueryRequestFactory queryRequestFactory; protected final QueryHandlerFactory queryHandlerFactory; protected final OnDemandUnicaster onDemandUnicaster; protected final HeadPongFactory headPongFactory; protected final PingReplyFactory pingReplyFactory; protected final ConnectionManager connectionManager; protected final ReplyHandler forMeReplyHandler; protected final QueryUnicaster queryUnicaster; protected final FileViewManager fileManager; protected final ContentManager contentManager; protected final DHTManager dhtManager; protected final UploadManager uploadManager; protected final DownloadManager downloadManager; protected final UDPService udpService; protected final SearchResultHandler searchResultHandler; protected final SocketsManager socketsManager; protected final HostCatcher hostCatcher; protected final QueryReplyFactory queryReplyFactory; protected final StaticMessages staticMessages; protected final Provider<MessageDispatcher> messageDispatcher; protected final MulticastService multicastService; protected final QueryDispatcher queryDispatcher; protected final Provider<ActivityCallback> activityCallback; protected final ConnectionServices connectionServices; protected final ScheduledExecutorService backgroundExecutor; protected final Provider<PongCacher> pongCacher; protected final Provider<SimppManager> simppManager; protected final Provider<UpdateHandler> updateHandler; protected final UDPReplyHandlerCache udpReplyHandlerCache; protected final GuidMap multicastGuidMap; private final Provider<InspectionRequestHandler> inspectionRequestHandlerFactory; private final Provider<UDPCrawlerPingHandler> udpCrawlerPingHandlerFactory; private final Provider<OOBHandler> oobHandlerFactory; private final Provider<MACCalculatorRepositoryManager> MACCalculatorRepositoryManager; protected final Provider<LimeACKHandler> limeAckHandler; protected final QRPUpdater qrpUpdater; private final URNFilter urnFilter; private final SpamServices spamServices; private final PingRequestFactory pingRequestFactory; private final MessageHandlerBinder messageHandlerBinder; private final ConnectionListener connectionListener = new ConnectionListener(); private final OutgoingQueryReplyFactory outgoingQueryReplyFactory; /** * Creates a MessageRouter. Must call initialize before using. */ @Inject // TODO: Create a MessageRouterController, // or split MessageRouter into a number of different classes, // instead of passing all of these... protected MessageRouterImpl(NetworkManager networkManager, QueryRequestFactory queryRequestFactory, QueryHandlerFactory queryHandlerFactory, OnDemandUnicaster onDemandUnicaster, HeadPongFactory headPongFactory, PingReplyFactory pingReplyFactory, ConnectionManager connectionManager, @Named("forMeReplyHandler") ReplyHandler forMeReplyHandler, QueryUnicaster queryUnicaster, FileViewManager fileManager, ContentManager contentManager, DHTManager dhtManager, UploadManager uploadManager, DownloadManager downloadManager, UDPService udpService, SearchResultHandler searchResultHandler, SocketsManager socketsManager, HostCatcher hostCatcher, QueryReplyFactory queryReplyFactory, StaticMessages staticMessages, Provider<MessageDispatcher> messageDispatcher, MulticastService multicastService, QueryDispatcher queryDispatcher, Provider<ActivityCallback> activityCallback, ConnectionServices connectionServices, ApplicationServices applicationServices, @Named("backgroundExecutor") ScheduledExecutorService backgroundExecutor, Provider<PongCacher> pongCacher, Provider<SimppManager> simppManager, Provider<UpdateHandler> updateHandler, GuidMapManager guidMapManager, UDPReplyHandlerCache udpReplyHandlerCache, Provider<InspectionRequestHandler> inspectionRequestHandlerFactory, Provider<UDPCrawlerPingHandler> udpCrawlerPingHandlerFactory, PingRequestFactory pingRequestFactory, MessageHandlerBinder messageHandlerBinder, Provider<OOBHandler> oobHandlerFactory, Provider<MACCalculatorRepositoryManager> MACCalculatorRepositoryManager, Provider<LimeACKHandler> limeACKHandler, OutgoingQueryReplyFactory outgoingQueryReplyFactory, QRPUpdater qrpUpdater, URNFilter urnFilter, SpamServices spamServices) { this.networkManager = networkManager; this.queryRequestFactory = queryRequestFactory; this.queryHandlerFactory = queryHandlerFactory; this.onDemandUnicaster = onDemandUnicaster; this.headPongFactory = headPongFactory; this.pingReplyFactory = pingReplyFactory; this.connectionManager = connectionManager; this.forMeReplyHandler = forMeReplyHandler; this.queryUnicaster = queryUnicaster; this.fileManager = fileManager; this.contentManager = contentManager; this.dhtManager = dhtManager; this.uploadManager = uploadManager; this.downloadManager = downloadManager; this.udpService = udpService; this.searchResultHandler = searchResultHandler; this.socketsManager = socketsManager; this.hostCatcher = hostCatcher; this.queryReplyFactory = queryReplyFactory; this.staticMessages = staticMessages; this.messageDispatcher = messageDispatcher; this.multicastService = multicastService; this.queryDispatcher = queryDispatcher; this.activityCallback = activityCallback; this.connectionServices = connectionServices; this.backgroundExecutor = backgroundExecutor; this.pongCacher = pongCacher; this.simppManager = simppManager; this.updateHandler = updateHandler; this.udpCrawlerPingHandlerFactory = udpCrawlerPingHandlerFactory; this.pingRequestFactory = pingRequestFactory; this.messageHandlerBinder = messageHandlerBinder; this.outgoingQueryReplyFactory = outgoingQueryReplyFactory; this.multicastGuidMap = guidMapManager.getMap(); this.udpReplyHandlerCache = udpReplyHandlerCache; this.inspectionRequestHandlerFactory = inspectionRequestHandlerFactory; this.oobHandlerFactory = oobHandlerFactory; this.MACCalculatorRepositoryManager = MACCalculatorRepositoryManager; this.limeAckHandler = limeACKHandler; this.qrpUpdater = qrpUpdater; this.urnFilter = urnFilter; this.spamServices = spamServices; _clientGUID = applicationServices.getMyGUID(); _bypassedResultsCache = new BypassedResultsCache(activityCallback, downloadManager); } /** Sets a new handler to the given handlerMap, for the given class. */ private boolean setHandler(ConcurrentMap<Class<? extends Message>, MessageHandler> handlerMap, Class<? extends Message> clazz, MessageHandler handler) { if (handler != null) { MessageHandler old = handlerMap.put(clazz, handler); if(old != null) { LOG.warn("Ejecting old handler: " + old + " for clazz: " + clazz); } return true; } else { return handlerMap.remove(clazz) != null; } } /** * Adds the given handler to the handlerMap for the given class. * If a handler already existed, this will construct a DualMessageHandler * so that both are handlers are notified. * * @param handlerMap * @param clazz * @param handler */ private void addHandler(ConcurrentMap<Class<? extends Message>, MessageHandler> handlerMap, Class<? extends Message> clazz, MessageHandler handler) { MessageHandler existing = handlerMap.get(clazz); if(existing != null) { // non-blocking addition -- continue trying until we succesfully // replace the prior handler w/ a dual version of that handler while(true) { MessageHandler dual = new DualMessageHandler(handler, existing); if(handlerMap.replace(clazz, existing, dual)) break; existing = handlerMap.get(clazz); dual = new DualMessageHandler(handler, existing); } } else { setHandler(handlerMap, clazz, handler); } } /* (non-Javadoc) * @see com.limegroup.gnutella.MessageRouter#setMessageHandler(java.lang.Class, com.limegroup.gnutella.messagehandlers.MessageHandler) */ public void setMessageHandler(Class<? extends Message> clazz, MessageHandler handler) { setHandler(messageHandlers, clazz, handler); } /* (non-Javadoc) * @see com.limegroup.gnutella.MessageRouter#addMessageHandler(java.lang.Class, com.limegroup.gnutella.messagehandlers.MessageHandler) */ public void addMessageHandler(Class<? extends Message> clazz, MessageHandler handler) { addHandler(messageHandlers, clazz, handler); } /* (non-Javadoc) * @see com.limegroup.gnutella.MessageRouter#getMessageHandler(java.lang.Class) */ public MessageHandler getMessageHandler(Class<? extends Message> clazz) { return messageHandlers.get(clazz); } /* (non-Javadoc) * @see com.limegroup.gnutella.MessageRouter#setUDPMessageHandler(java.lang.Class, com.limegroup.gnutella.messagehandlers.MessageHandler) */ public void setUDPMessageHandler(Class<? extends Message> clazz, MessageHandler handler) { setHandler(udpMessageHandlers, clazz, handler); } /* (non-Javadoc) * @see com.limegroup.gnutella.MessageRouter#addUDPMessageHandler(java.lang.Class, com.limegroup.gnutella.messagehandlers.MessageHandler) */ public void addUDPMessageHandler(Class<? extends Message> clazz, MessageHandler handler) { addHandler(udpMessageHandlers, clazz, handler); } /* (non-Javadoc) * @see com.limegroup.gnutella.MessageRouter#getUDPMessageHandler(java.lang.Class) */ public MessageHandler getUDPMessageHandler(Class<? extends Message> clazz) { return udpMessageHandlers.get(clazz); } /* (non-Javadoc) * @see com.limegroup.gnutella.MessageRouter#setMulticastMessageHandler(java.lang.Class, com.limegroup.gnutella.messagehandlers.MessageHandler) */ public void setMulticastMessageHandler(Class<? extends Message> clazz, MessageHandler handler) { setHandler(multicastMessageHandlers, clazz, handler); } /* (non-Javadoc) * @see com.limegroup.gnutella.MessageRouter#addMulticastMessageHandler(java.lang.Class, com.limegroup.gnutella.messagehandlers.MessageHandler) */ public void addMulticastMessageHandler(Class<? extends Message> clazz, MessageHandler handler) { addHandler(multicastMessageHandlers, clazz, handler); } /* (non-Javadoc) * @see com.limegroup.gnutella.MessageRouter#getMulticastMessageHandler(java.lang.Class) */ public MessageHandler getMulticastMessageHandler(Class<? extends Message> clazz) { return multicastMessageHandlers.get(clazz); } @Inject void register(ServiceRegistry registry) { registry.register(this).in(ServiceStage.EARLY); } public void initialize() { } public String getServiceName() { return I18nMarker.marktr("Message Routing"); } /* (non-Javadoc) * @see com.limegroup.gnutella.MessageRouter#initialize() */ public void start() { // TODO listener leaking, we should have a shutdown event connectionManager.addEventListener(connectionListener); QRP_PROPAGATOR.start(); // schedule a runner to clear guys we've connected back to backgroundExecutor.scheduleWithFixedDelay(new ConnectBackExpirer(), 10 * CLEAR_TIME, 10 * CLEAR_TIME, TimeUnit.MILLISECONDS); // schedule a runner to send hops-flow messages backgroundExecutor.scheduleWithFixedDelay(new HopsFlowManager(uploadManager, connectionManager), HOPS_FLOW_INTERVAL*10, HOPS_FLOW_INTERVAL, TimeUnit.MILLISECONDS); backgroundExecutor.scheduleWithFixedDelay(new UDPReplyCleaner(), UDP_REPLY_CACHE_TIME, UDP_REPLY_CACHE_TIME, TimeUnit.MILLISECONDS); // runner to clean up OOB sessions OOBHandler oobHandler = oobHandlerFactory.get(); backgroundExecutor.scheduleWithFixedDelay(oobHandler, CLEAR_TIME, CLEAR_TIME, TimeUnit.MILLISECONDS); // handler for inspection requests InspectionRequestHandler inspectionHandler = inspectionRequestHandlerFactory.get(); messageHandlerBinder.bind(this); setMessageHandler(PingRequest.class, new PingRequestHandler()); setMessageHandler(PingReply.class, new PingReplyHandler()); setMessageHandler(QueryRequest.class, new QueryRequestHandler()); setMessageHandler(QueryReply.class, new QueryReplyHandler()); setMessageHandler(ResetTableMessage.class, new ResetTableHandler()); setMessageHandler(PatchTableMessage.class, new PatchTableHandler()); setMessageHandler(TCPConnectBackVendorMessage.class, new TCPConnectBackHandler()); setMessageHandler(UDPConnectBackVendorMessage.class, new UDPConnectBackHandler()); setMessageHandler(TCPConnectBackRedirect.class, new TCPConnectBackRedirectHandler()); setMessageHandler(UDPConnectBackRedirect.class, new UDPConnectBackRedirectHandler()); setMessageHandler(PushProxyRequest.class, new PushProxyRequestHandler()); setMessageHandler(QueryStatusResponse.class, new QueryStatusResponseHandler()); setMessageHandler(HeadPing.class, new HeadPingHandler()); setMessageHandler(SimppRequestVM.class, new SimppRequestVMHandler()); setMessageHandler(SimppVM.class, new SimppVMHandler()); setMessageHandler(UpdateRequest.class, new UpdateRequestHandler()); setMessageHandler(UpdateResponse.class, new UpdateResponseHandler()); setMessageHandler(HeadPong.class, new HeadPongHandler()); setMessageHandler(DHTContactsMessage.class, new DHTContactsMessageHandler()); setMessageHandler(VendorMessage.class, new VendorMessageHandler()); setMessageHandler(InspectionRequest.class, inspectionHandler); setUDPMessageHandler(QueryRequest.class, new UDPQueryRequestHandler()); setUDPMessageHandler(QueryReply.class, new UDPQueryReplyHandler(oobHandler)); setUDPMessageHandler(PingRequest.class, new UDPPingRequestHandler()); setUDPMessageHandler(PingReply.class, new UDPPingReplyHandler()); LimeACKHandler ackHandler = limeAckHandler.get(); ackHandler.start(); setUDPMessageHandler(LimeACKVendorMessage.class, ackHandler); setUDPMessageHandler(ReplyNumberVendorMessage.class, oobHandler); setUDPMessageHandler(UDPCrawlerPing.class, udpCrawlerPingHandlerFactory.get()); setUDPMessageHandler(HeadPing.class, new UDPHeadPingHandler()); setUDPMessageHandler(UpdateRequest.class, new UDPUpdateRequestHandler()); setUDPMessageHandler(ContentResponse.class, new UDPContentResponseHandler()); setUDPMessageHandler(InspectionRequest.class, inspectionHandler); setMulticastMessageHandler(QueryRequest.class, new MulticastQueryRequestHandler()); //setMulticastMessageHandler(QueryReply.class, new MulticastQueryReplyHandler()); setMulticastMessageHandler(PingRequest.class, new MulticastPingRequestHandler()); //setMulticastMessageHandler(PingReply.class, new MulticastPingReplyHandler()); } public void stop() { connectionManager.removeEventListener(connectionListener); } /* (non-Javadoc) * @see com.limegroup.gnutella.MessageRouter#originateQueryGUID(byte[]) */ public void originateQueryGUID(byte[] guid) { _queryRouteTable.routeReply(guid, forMeReplyHandler); } /* (non-Javadoc) * @see com.limegroup.gnutella.MessageRouter#queryKilled(com.limegroup.gnutella.GUID) */ public void queryKilled(GUID guid) throws IllegalArgumentException { if (guid == null) throw new IllegalArgumentException("Input GUID is null!"); _bypassedResultsCache.queryKilled(guid); } /* (non-Javadoc) * @see com.limegroup.gnutella.MessageRouter#downloadFinished(com.limegroup.gnutella.GUID) */ public void downloadFinished(GUID guid) throws IllegalArgumentException { if (guid == null) throw new IllegalArgumentException("Input GUID is null!"); _bypassedResultsCache.downloadFinished(guid); } /* (non-Javadoc) * @see com.limegroup.gnutella.MessageRouter#getQueryLocs(com.limegroup.gnutella.GUID) */ public Set<GUESSEndpoint> getQueryLocs(GUID guid) { return _bypassedResultsCache.getQueryLocs(guid); } /* (non-Javadoc) * @see com.limegroup.gnutella.MessageRouter#getPingRouteTableDump() */ public String getPingRouteTableDump() { return _pingRouteTable.toString(); } /* (non-Javadoc) * @see com.limegroup.gnutella.MessageRouter#getQueryRouteTableDump() */ public String getQueryRouteTableDump() { return _queryRouteTable.toString(); } /* (non-Javadoc) * @see com.limegroup.gnutella.MessageRouter#getPushRouteTableDump() */ public String getPushRouteTableDump() { return _pushRouteTable.toString(); } /* (non-Javadoc) * @see com.limegroup.gnutella.MessageRouter#removeConnection(com.limegroup.gnutella.ReplyHandler) */ private void removeConnection(ReplyHandler rh) { queryDispatcher.removeReplyHandler(rh); _pingRouteTable.removeReplyHandler(rh); _queryRouteTable.removeReplyHandler(rh); _pushRouteTable.removeReplyHandler(rh); _headPongRouteTable.removeReplyHandler(rh); } /* (non-Javadoc) * @see com.limegroup.gnutella.MessageRouter#handleMessage(com.limegroup.gnutella.messages.Message, com.limegroup.gnutella.RoutedConnection) */ public void handleMessage(Message msg, ReplyHandler receivingConnection) { // Increment hops and decrease TTL. msg.hop(); MessageHandler msgHandler = getMessageHandler(msg.getHandlerClass()); if (msgHandler != null) { msgHandler.handleMessage(msg, null, receivingConnection); } else if (msg instanceof VendorMessage) { msgHandler = getMessageHandler(VendorMessage.class); if (msgHandler != null) { msgHandler.handleMessage(msg, null, receivingConnection); } } //This may trigger propogation of query route tables. We do this AFTER //any handshake pings. Otherwise we'll think all clients are old //clients. //forwardQueryRouteTables(); notifyMessageListener(msg, receivingConnection); } /** * Notifies any message listeners of this message's guid about the message. * This holds no locks. */ private final void notifyMessageListener(Message msg, ReplyHandler handler) { List<MessageListener> all = _messageListeners.get(msg.getGUID()); if(all != null) { for(MessageListener next : all) { next.processMessage(msg, handler); } } } /* (non-Javadoc) * @see com.limegroup.gnutella.MessageRouter#handleUDPMessage(com.limegroup.gnutella.messages.Message, java.net.InetSocketAddress) */ public void handleUDPMessage(Message msg, InetSocketAddress addr) { if(LOG.isTraceEnabled()) { LOG.trace("Handling UDP message " + msg + " from " + addr); } // Increment hops and decrement TTL. msg.hop(); if(msg instanceof QueryReply) { // check to see if it was from the multicast map. byte[] origGUID = multicastGuidMap.getOriginalGUID(msg.getGUID()); if(origGUID != null) { msg = queryReplyFactory.createQueryReply(origGUID, (QueryReply)msg); ((QueryReply)msg).setMulticastAllowed(true); } } ReplyHandler replyHandler = udpReplyHandlerCache.getUDPReplyHandler(addr); MessageHandler msgHandler = getUDPMessageHandler(msg.getHandlerClass()); if (msgHandler != null) { msgHandler.handleMessage(msg, addr, replyHandler); } else if (msg instanceof VendorMessage) { msgHandler = getUDPMessageHandler(VendorMessage.class); if (msgHandler != null) { msgHandler.handleMessage(msg, addr, replyHandler); } } notifyMessageListener(msg, replyHandler); } /* (non-Javadoc) * @see com.limegroup.gnutella.MessageRouter#handleMulticastMessage(com.limegroup.gnutella.messages.Message, java.net.InetSocketAddress) */ public void handleMulticastMessage(Message msg, InetSocketAddress addr) { // Use this assert for testing only -- it is a dangerous assert // to have in the field, as not all messages currently set the // network int appropriately. // If someone sends us messages we're not prepared to handle, // this could cause widespreaad AssertFailures // Assert.that(msg.isMulticast(), // "non multicast message in handleMulticastMessage: " + msg); // no multicast messages should ever have been // set with a TTL greater than 1. if (msg.getTTL() > 1) { return; } // Increment hops and decrement TTL. msg.hop(); if (NetworkUtils.isLocalAddress(addr.getAddress()) && !ConnectionSettings.ALLOW_MULTICAST_LOOPBACK.getValue()) { return; } ReplyHandler replyHandler = udpReplyHandlerCache.getUDPReplyHandler(addr); MessageHandler msgHandler = getMulticastMessageHandler(msg.getHandlerClass()); if (msgHandler != null) { msgHandler.handleMessage(msg, addr, replyHandler); } else if (msg instanceof VendorMessage) { msgHandler = getMulticastMessageHandler(VendorMessage.class); if (msgHandler != null) { msgHandler.handleMessage(msg, addr, replyHandler); } } notifyMessageListener(msg, replyHandler); } /** * Returns true if the Query has a valid AddressSecurityToken. false if it isn't present * or valid. */ protected boolean hasValidQueryKey(InetAddress ip, int port, QueryRequest qr) { AddressSecurityToken qk = qr.getQueryKey(); if (qk == null) return false; return qk.isFor(ip, port); } /** * Sends an ack back to the GUESS client node. */ protected void sendAcknowledgement(InetSocketAddress addr, byte[] guid) { Endpoint host = connectionManager.getConnectedGUESSUltrapeer(); PingReply reply; if(host != null) { try { reply = pingReplyFactory.createGUESSReply(guid, (byte)1, host); } catch(UnknownHostException e) { reply = createPingReply(guid); } } else { reply = createPingReply(guid); } // No GUESS endpoints existed and our IP/port was invalid. if( reply == null ) return; udpService.send(reply, addr.getAddress(), addr.getPort()); } /** * Creates a new <tt>PingReply</tt> from the set of cached * GUESS endpoints, or a <tt>PingReply</tt> for localhost * if no GUESS endpoints are available. */ private PingReply createPingReply(byte[] guid) { GUESSEndpoint endpoint = queryUnicaster.getUnicastEndpoint(); if(endpoint == null) { if(networkManager.isIpPortValid()) return pingReplyFactory.create(guid, (byte)1); else return null; } else { return pingReplyFactory.createGUESSReply(guid, (byte)1, endpoint.getPort(), endpoint.getInetAddress().getAddress()); } } /** * Checks the routing table to see if the request has already been seen. * If not, calls handlePingRequest. */ final void handlePingRequestPossibleDuplicate( PingRequest request, ReplyHandler handler) { if(_pingRouteTable.tryToRouteReply(request.getGUID(), handler) != null) handlePingRequest(request, handler); } /** * Checks the routing table to see if the request has already been seen. * If not, calls handlePingRequest. */ final void handleUDPPingRequestPossibleDuplicate( PingRequest request, ReplyHandler handler, InetSocketAddress addr) { if(_pingRouteTable.tryToRouteReply(request.getGUID(), handler) != null) handleUDPPingRequest(request, handler, addr); } /** * Checks the routing table to see if the request has already been seen. * If not, calls handleQueryRequest. */ final void handleQueryRequestPossibleDuplicate( QueryRequest request, ReplyHandler receivingConnection) { // With the new handling of probe queries (TTL 1, Hops 0), we have a few // new options: // 1) If we have a probe query.... // a) If you have never seen it before, put it in the route table and // set the ttl appropriately // b) If you have seen it before, then just count it as a duplicate // 2) If it isn't a probe query.... // a) Is it an extension of a probe? If so re-adjust the TTL. // b) Is it a 'normal' query (no probe extension or already extended)? // Then check if it is a duplicate: // 1) If it a duplicate, just count it as one // 2) If it isn't, put it in the route table but no need to setTTL // we msg.hop() before we get here.... // hops may be 1 or 2 because we may be probing with a leaf query.... final boolean isProbeQuery = ((request.getTTL() == 0) && ((request.getHops() == 1) || (request.getHops() == 2))); ResultCounter counter = _queryRouteTable.tryToRouteReply(request.getGUID(), receivingConnection); if(counter != null) { // query is new (probe or normal) // 1a: set the TTL of the query so it can be potentially extended if (isProbeQuery) _queryRouteTable.setTTL(counter, (byte)1); // 1a and 2b2 // if a new probe or a new request, do everything (so input true // below) handleQueryRequest(request, receivingConnection, counter, true); } else if (!isProbeQuery) {// probe extension? if (wasProbeQuery(request)) { // rebroadcast out but don't locally evaluate.... handleQueryRequest(request, receivingConnection, counter, false); } else { // 2b1: not a correct extension, so call it a duplicate.... tallyDupQuery(request); } } else if (isProbeQuery) { // 1b: duplicate probe tallyDupQuery(request); } } private boolean wasProbeQuery(QueryRequest request) { // if the current TTL is large enough and the old TTL was 1, then this // was a probe query.... // NOTE: that i'm setting the ttl to be the actual ttl of the query. i // could set it to some max value, but since we only allow TTL 1 queries // to be extended, it isn't a big deal what i set it to. in fact, i'm // setting the ttl to the correct value if we had full expanding rings // of queries. return ((request.getTTL() > 0) && _queryRouteTable.getAndSetTTL(request.getGUID(), (byte)1, (byte)(request.getTTL()+1))); } private void tallyDupQuery(QueryRequest request) { duplicateQueryCounter.duplicateQuery(request); } /** * Special handler for UDP queries. Checks the routing table to see if * the request has already been seen, handling it if not. * * @param query the UDP <tt>QueryRequest</tt> * @param handler the <tt>ReplyHandler</tt> that will handle the reply * @return false if it was a duplicate, true if it was not. */ final boolean handleUDPQueryRequestPossibleDuplicate(QueryRequest request, ReplyHandler handler) { ResultCounter counter = _queryRouteTable.tryToRouteReply(request.getGUID(), handler); if(counter != null) { handleQueryRequest(request, handler, counter, true); return true; } return false; } /** * Handles pings from the network. With the addition of pong caching, this * method will either respond with cached pongs, or it will ignore the ping * entirely if another ping has been received from this connection very * recently. If the ping is TTL=1, we will always process it, as it may * be a hearbeat ping to make sure the connection is alive and well. * * @param ping the ping to handle * @param handler the <tt>ReplyHandler</tt> instance that sent the ping */ final private void handlePingRequest(PingRequest ping, ReplyHandler handler) { // Send it along if it's a heartbeat ping or if we should allow new // pings on this connection. if(ping.isHeartbeat() || handler.allowNewPings()) { respondToPingRequest(ping, handler); } } /** * Responds to a UDP ping or query key request. */ protected void handleUDPPingRequest(PingRequest pingRequest, ReplyHandler handler, InetSocketAddress addr) { if (pingRequest.isQueryKeyRequest()) sendQueryKeyPong(pingRequest, addr); else respondToUDPPingRequest(pingRequest, addr, handler); } /** * Generates an AddressSecurityToken for the source (described by addr) * and sends it in an AddressSecurityToken pong */ protected void sendQueryKeyPong(PingRequest pr, InetSocketAddress addr) { // check if we're getting bombarded long now = System.currentTimeMillis(); if (now - _lastQueryKeyTime < SearchSettings.QUERY_KEY_DELAY.getValue()) return; _lastQueryKeyTime = now; // after find more sources and OOB queries, everyone can dole out query // keys.... // generate a AddressSecurityToken (quite quick - current impl. (DES) is super // fast! InetAddress address = addr.getAddress(); int port = addr.getPort(); AddressSecurityToken key = new AddressSecurityToken(address, port, MACCalculatorRepositoryManager.get()); // respond with Pong with QK, as GUESS requires.... if (networkManager.isIpPortValid()) { PingReply reply = pingReplyFactory.createQueryKeyReply(pr.getGUID(), (byte)1, key); udpService.send(reply, addr.getAddress(), addr.getPort()); } } protected void handleUDPPingReply(PingReply reply, ReplyHandler handler, InetAddress address, int port) { if (reply.getQueryKey() != null) { // this is a PingReply in reply to my AddressSecurityToken Request - //consume the Pong and return, don't process as usual.... onDemandUnicaster.handleQueryKeyPong(reply); return; } // do not process the pong if different from the host // described in the reply if(reply.getPort() == port && reply.getInetAddress().equals(address)) handlePingReply(reply, handler); } /** * The default handler for QueryRequests. This implementation updates * stats, does the broadcast, and generates a response. * * You can customize behavior in three ways: * 1. Override. You can assume that duplicate messages * (messages with the same GUID that arrived via different paths) have * already been filtered. If you want stats updated, you'll * have to call super.handleQueryRequest. * 2. Override broadcastQueryRequest. This allows you to use the default * handling framework and just customize request routing. * 3. Implement respondToQueryRequest. This allows you to use the default * handling framework and just customize responses. * * @param locallyEvaluate false if you don't want to send the query to * leaves and yourself, true otherwise.... */ public void handleQueryRequest(QueryRequest request, ReplyHandler handler, ResultCounter counter, boolean locallyEvaluate) { // Apply the personal filter to decide whether the callback // should be informed of the query if(!spamServices.isPersonalSpam(request)) { activityCallback.get().handleQuery(request, handler.getAddress(), handler.getPort()); } // if it's a request from a leaf and we GUESS, send it out via GUESS -- // otherwise, broadcast it if it still has TTL //if(handler.isSupernodeClientConnection() && // RouterService.isGUESSCapable()) //unicastQueryRequest(request, handler); //else if(request.getTTL() > 0) { updateMessage(request, handler); if(handler.isSupernodeClientConnection() && counter != null) { LOG.trace("Query request from leaf"); if(request.desiresOutOfBandReplies()) { LOG.trace("Leaf wants OOB replies"); // this query came from a leaf - so check if it desires OOB // responses and make sure that the IP it advertises is legit - // if it isn't drop away.... // no need to check the port - if you are attacking yourself you // got problems String remoteAddr = handler.getInetAddress().getHostAddress(); String myAddress = NetworkUtils.ip2string(networkManager.getAddress()); if(request.getReplyAddress().equals(remoteAddr)) { LOG.trace("OOB address is leaf's address"); ; // continue below, everything looks good } else if(request.getReplyAddress().equals(myAddress) && networkManager.isOOBCapable()) { LOG.trace("OOB address is my address"); // i am proxying - maybe i should check my success rate but // whatever... ; } else { LOG.trace("OOB address is wrong, dropping query"); return; } } // don't send it to leaves here -- the dynamic querier will // handle that locallyEvaluate = false; // do respond with files that we may have, though respondToQueryRequest(request, _clientGUID, handler); multicastQueryRequest(request); if(handler.isGoodLeaf()) { sendDynamicQuery(queryHandlerFactory.createHandlerForNewLeaf( request, handler, counter)); } else { sendDynamicQuery(queryHandlerFactory.createHandlerForOldLeaf( request, handler, counter)); } } else if(request.getTTL() > 0 && connectionServices.isSupernode()) { // send the request to intra-Ultrapeer connections -- this does // not send the request to leaves if(handler.isGoodUltrapeer()) { // send it to everyone forwardQueryToUltrapeers(request, handler); } else { // otherwise, only send it to some connections forwardLimitedQueryToUltrapeers(request, handler); } } if (locallyEvaluate) { // always forward any queries to leaves -- this only does // anything when this node's an Ultrapeer forwardQueryRequestToLeaves(request, handler); // if (I'm firewalled AND the source is firewalled) AND // NOT(he can do a FW transfer and so can i) then don't reply... if ((request.isFirewalledSource() && !networkManager.acceptedIncomingConnection()) && !(request.canDoFirewalledTransfer() && networkManager.canDoFWT()) ) return; respondToQueryRequest(request, _clientGUID, handler); } } /** * Adds the sender of the reply message to an internal cache under the guid of the message * if the sender can receive unsolicited UDP. * * @see {@link #getQueryLocs(GUID)} for retrieval */ public boolean addBypassedSource(ReplyNumberVendorMessage reply, ReplyHandler handler) { //if the reply cannot receive unsolicited udp, there is no point storing it if (!reply.canReceiveUnsolicited()) { return false; } GUESSEndpoint ep = new GUESSEndpoint(handler.getInetAddress(), handler.getPort()); return _bypassedResultsCache.addBypassedSource(new GUID(reply.getGUID()), ep); } /** * Adds the sender of the reply message to an internal cache under the guid of the message * if the sender can receive unsolicited UDP. * * @see {@link #getQueryLocs(GUID)} for retrieval */ public boolean addBypassedSource(QueryReply reply, ReplyHandler handler) { if (reply.isFirewalled()) return false; GUESSEndpoint ep = new GUESSEndpoint(handler.getInetAddress(), handler.getPort()); return _bypassedResultsCache.addBypassedSource(new GUID(reply.getGUID()), ep); } /* (non-Javadoc) * @see com.limegroup.gnutella.MessageRouter#getNumOOBToRequest(com.limegroup.gnutella.messages.vendor.ReplyNumberVendorMessage) */ public int getNumOOBToRequest(ReplyNumberVendorMessage reply) { GUID qGUID = new GUID(reply.getGUID()); int numResults = searchResultHandler.getNumResultsForQuery(qGUID); if (numResults < 0) // this may be a proxy query numResults = queryDispatcher.getLeafResultsForQuery(qGUID); if (numResults < 0 || numResults > QueryHandler.ULTRAPEER_RESULTS) { return -1; } return reply.getNumResults(); } /* (non-Javadoc) * @see com.limegroup.gnutella.MessageRouter#isQueryAlive(com.limegroup.gnutella.GUID) */ public boolean isQueryAlive(GUID guid) { return _queryRouteTable.getReplyHandler(guid.bytes()) != null; } /* (non-Javadoc) * @see com.limegroup.gnutella.MessageRouter#isHostUnicastQueried(com.limegroup.gnutella.GUID, org.limewire.io.IpPort) */ public boolean isHostUnicastQueried(GUID guid, IpPort host) { return onDemandUnicaster.isHostQueriedForGUID(guid, host); } /** * Forwards the UDPConnectBack to neighboring peers * as a UDPConnectBackRedirect request. */ protected void handleUDPConnectBackRequest(UDPConnectBackVendorMessage udp, Connection source) { GUID guidToUse = udp.getConnectBackGUID(); int portToContact = udp.getConnectBackPort(); InetAddress sourceAddr = source.getInetAddress(); Message msg = new UDPConnectBackRedirect(guidToUse, sourceAddr, portToContact); int sentTo = 0; List<RoutedConnection> peers = new ArrayList<RoutedConnection>(connectionManager.getInitializedConnections()); Collections.shuffle(peers); for(RoutedConnection currMC : peers) { if(sentTo >= MAX_UDP_CONNECTBACK_FORWARDS) break; if(currMC == source) continue; if (currMC.getConnectionCapabilities().remoteHostSupportsUDPRedirect() >= 0) { currMC.send(msg); sentTo++; } } } /** * Sends a ping to the person requesting the connectback request. */ protected void handleUDPConnectBackRedirect(UDPConnectBackRedirect udp, Connection source) { // only allow other UPs to send you this message.... if (!source.getConnectionCapabilities().isSupernodeSupernodeConnection()) return; GUID guidToUse = udp.getConnectBackGUID(); int portToContact = udp.getConnectBackPort(); InetAddress addrToContact = udp.getConnectBackAddress(); // only connect back if you aren't connected to the host - that is the // whole point of redirect after all.... Endpoint endPoint = new Endpoint(addrToContact.getAddress(), portToContact); if (connectionManager.isConnectedTo(endPoint.getAddress())) return; // keep track of who you tried connecting back too, don't do it too // much.... String addrString = addrToContact.getHostAddress(); if (!shouldServiceRedirect(_udpConnectBacks,addrString)) return; // mutating twice restores the original guid UDPService.mutateGUID(guidToUse.bytes(), addrToContact, portToContact); PingRequest pr = pingRequestFactory.createPingRequest(guidToUse.bytes(), (byte)1, (byte)0); udpService.send(pr, addrToContact, portToContact); } /** * @param map the map that keeps track of recent redirects * @param key the key which we would (have) store(d) in the map * @return whether we should service the redirect request * @modifies the map */ private boolean shouldServiceRedirect(FixedsizeHashMap<String, String> map, String key) { synchronized(map) { String placeHolder = map.get(key); if (placeHolder == null) { try { map.put(key, key); return true; } catch (NoMoreStorageException nomo) { return false; // we've done too many connect backs, stop.... } } else return false; // we've connected back to this guy recently.... } } /** * Forwards the request to neighboring Ultrapeers as a * TCPConnectBackRedirect message. */ protected void handleTCPConnectBackRequest(TCPConnectBackVendorMessage tcp, Connection source) { final int portToContact = tcp.getConnectBackPort(); InetAddress sourceAddr = source.getInetAddress(); Message msg = new TCPConnectBackRedirect(sourceAddr, portToContact); int sentTo = 0; List<RoutedConnection> peers = new ArrayList<RoutedConnection>(connectionManager.getInitializedConnections()); Collections.shuffle(peers); for(RoutedConnection currMC : peers) { if(sentTo >= MAX_TCP_CONNECTBACK_FORWARDS) break; if(currMC == source) continue; if (currMC.getConnectionCapabilities().remoteHostSupportsTCPRedirect() >= 0) { currMC.send(msg); sentTo++; } } } /** * Basically, just get the correct parameters, create a Socket, and * send a "/n/n". */ protected void handleTCPConnectBackRedirect(TCPConnectBackRedirect tcp, Connection source) { // only allow other UPs to send you this message.... if (!source.getConnectionCapabilities().isSupernodeSupernodeConnection()) return; final int portToContact = tcp.getConnectBackPort(); final String addrToContact =tcp.getConnectBackAddress().getHostAddress(); // only connect back if you aren't connected to the host - that is the // whole point of redirect after all.... Endpoint endPoint = new Endpoint(addrToContact, portToContact); if (connectionManager.isConnectedTo(endPoint.getAddress())) return; // keep track of who you tried connecting back too, don't do it too // much.... if (!shouldServiceRedirect(_tcpConnectBacks,addrToContact)) return; TCP_CONNECT_BACKER.execute(new Runnable() { public void run() { Socket sock = null; try { try { sock = socketsManager.connect(new InetSocketAddress(addrToContact, portToContact), 6000, ConnectType.TLS); sock.setSoTimeout(6000); OutputStream os = sock.getOutputStream(); os.write(StringUtils.toAsciiBytes("CONNECT BACK\r\n\r\n")); os.flush(); } catch (IOException noTls) { IOUtils.close(sock); sock = socketsManager.connect(new InetSocketAddress(addrToContact, portToContact), 12000, ConnectType.PLAIN); sock.setSoTimeout(12000); OutputStream os = sock.getOutputStream(); os.write(StringUtils.toAsciiBytes("CONNECT BACK\r\n\r\n")); os.flush(); } if(LOG.isTraceEnabled()) LOG.trace("Succesful connectback to: " + addrToContact); try { Thread.sleep(500); // let the other side get it. } catch(InterruptedException ignored) { LOG.warn("Interrupted connectback", ignored); } } catch (IOException ignored) { LOG.warn("IOX during connectback", ignored); } finally { IOUtils.close(sock); } } }); } /** * 1) confirm that the connection is Ultrapeer to Leaf, then send your * listening port in a PushProxyAcknowledgement. * 2) Also cache the client's client GUID. */ protected void handlePushProxyRequest(PushProxyRequest ppReq, RoutedConnection source) { if (source.isSupernodeClientConnection() && networkManager.isIpPortValid()) { String stringAddr = NetworkUtils.ip2string(networkManager.getAddress()); InetAddress addr = null; try { addr = InetAddress.getByName(stringAddr); } catch(UnknownHostException uhe) { ErrorService.error(uhe); // impossible } if(_pushRouteTable.routeReply(ppReq.getClientGUID().bytes(), source) != null) { source.setPushProxyFor(true); PushProxyAcknowledgement ack = new PushProxyAcknowledgement(addr,networkManager.getPort(), ppReq.getClientGUID()); source.send(ack); } } } /** This method should be invoked when this node receives a * QueryStatusResponse message from the wire. If this node is an * Ultrapeer, we should update the Dynamic Querier about the status of * the leaf's query. */ protected void handleQueryStatus(QueryStatusResponse resp, RoutedConnection leaf) { // message only makes sense if i'm a UP and the sender is a leaf if (!leaf.isSupernodeClientConnection()) return; GUID queryGUID = resp.getQueryGUID(); int numResults = resp.getNumResults(); // get the QueryHandler and update the stats.... queryDispatcher.updateLeafResultsForQuery(queryGUID, numResults); } /* (non-Javadoc) * @see com.limegroup.gnutella.MessageRouter#sendPingRequest(com.limegroup.gnutella.messages.PingRequest, com.limegroup.gnutella.RoutedConnection) */ public void sendPingRequest(PingRequest request, RoutedConnection connection) { Objects.nonNull(request, "ping"); Objects.nonNull(connection, "connection"); _pingRouteTable.routeReply(request.getGUID(), forMeReplyHandler); connection.send(request); } /* (non-Javadoc) * @see com.limegroup.gnutella.MessageRouter#broadcastPingRequest(com.limegroup.gnutella.messages.PingRequest) */ public void broadcastPingRequest(PingRequest ping) { Objects.nonNull(ping, "ping"); _pingRouteTable.routeReply(ping.getGUID(), forMeReplyHandler); broadcastPingRequest(ping, forMeReplyHandler, connectionManager); } /* (non-Javadoc) * @see com.limegroup.gnutella.MessageRouter#sendDynamicQuery(com.limegroup.gnutella.messages.QueryRequest) */ public void sendDynamicQuery(QueryRequest query) { Objects.nonNull(query, "query"); // get the result counter so we can track the number of results ResultCounter counter = _queryRouteTable.routeReply(query.getGUID(), forMeReplyHandler); if(connectionServices.isSupernode()) { QueryHandler qh = queryHandlerFactory.createHandlerForMe(query, counter); sendDynamicQuery(qh); } else { originateLeafQuery(query); } // always send the query to your multicast people originateMulticastQuery(query); } /** * Originates a multicast query from this host. * This will alter the GUID of the query and store it in a mapping * of new -> old GUID. When replies come in, if they have the new GUID, * they are reset to be the old one and the multicast flag is allowed. * * @param query * @return the newGUID that the multicast query is using. */ protected void originateMulticastQuery(QueryRequest query) { byte[] newGUID = GUID.makeGuid(); QueryRequest mquery = queryRequestFactory.createMulticastQuery(newGUID, query); multicastGuidMap.addMapping(query.getGUID(), newGUID, MULTICAST_GUID_EXPIRE_TIME); multicastQueryRequest(mquery); } /** * Initiates a dynamic query. Only Ultrapeer should call this method, * as this technique relies on fairly high numbers of connections to * dynamically adjust the TTL based on the number of results received, * the number of remaining connections, etc. * * @param qh the <tt>QueryHandler</tt> instance that generates * queries for this dynamic query */ private void sendDynamicQuery(QueryHandler qh) { Objects.nonNull(qh, "query handler"); queryDispatcher.addQuery(qh); } /** * Broadcasts the ping request to all initialized connections that * are not the receivingConnection, setting up the routing * to the designated PingReplyHandler. This is called from the default * handlePingRequest and the default broadcastPingRequest(PingRequest). If * there are more than 3 initialized connections, most will be skipped. * Unstable connections are skipped, and leaves only send their own pings * to their ultrapeers. * * If different (smarter) broadcasting functionality is desired, override * as desired. If you do, note that receivingConnection may be null (for * requests originating here). */ private void broadcastPingRequest(PingRequest request, ReplyHandler receivingConnection, ConnectionManager manager) { // Note the use of initializedConnections only. // Note that we have zero allocations here. List<RoutedConnection> list = manager.getInitializedConnections(); boolean randomlyForward = list.size() > 3; for(RoutedConnection mc : list) { if(mc == receivingConnection || !mc.isStable()) continue; if(receivingConnection == forMeReplyHandler || !mc.getConnectionCapabilities().isClientSupernodeConnection()) { double percentToIgnore = mc.supportsPongCaching() ? 0.7 : 0.9; if(!randomlyForward || Math.random() >= percentToIgnore) mc.send(request); } } } /* (non-Javadoc) * @see com.limegroup.gnutella.MessageRouter#forwardQueryRequestToLeaves(com.limegroup.gnutella.messages.QueryRequest, com.limegroup.gnutella.ReplyHandler) */ public final void forwardQueryRequestToLeaves(QueryRequest query, ReplyHandler handler) { if(!connectionServices.isSupernode()) return; //use query routing to route queries to client connections //send queries only to the clients from whom query routing //table has been received List<RoutedConnection> list = connectionManager.getInitializedClientConnections(); List<RoutedConnection> hitConnections = new ArrayList<RoutedConnection>(); for(RoutedConnection mc : list) { if(mc == handler) continue; if(mc.shouldForwardQuery(query)) { hitConnections.add(mc); } } //forward only to a quarter of the leaves in case the query is //very popular. if(list.size() > 8 && (double)hitConnections.size()/(double)list.size() > .8) { int startIndex = (int) Math.floor( Math.random() * hitConnections.size() * 0.75); hitConnections = hitConnections.subList(startIndex, startIndex+hitConnections.size()/4); } for(RoutedConnection mc : hitConnections) { // sendRoutedQueryToHost is not called because // we have already ensured it hits the routing table // by filling up the 'hitsConnection' list. mc.send(query); } leafQRPHits.count(hitConnections.size()); } /** * Factored-out method that sends a query to a connection that supports * query routing. The query is only forwarded if there's a hit in the * query routing entries. * * @param query the <tt>QueryRequest</tt> to potentially forward * @param mc the <tt>RoutedConnection</tt> to forward the query to * @param handler the <tt>ReplyHandler</tt> that will be entered into * the routing tables to handle any replies * @return <tt>true</tt> if the query was sent, otherwise <tt>false</tt> */ private boolean sendRoutedQueryToHost(QueryRequest query, RoutedConnection mc, ReplyHandler handler) { if (mc.shouldForwardQuery(query)) { //A new client with routing entry, or one that hasn't started //sending the patch. mc.send(query); return true; } return false; } /** * Adds the QueryRequest to the unicaster module. Not much work done here, * see QueryUnicaster for more details. */ protected void unicastQueryRequest(QueryRequest query, ReplyHandler conn) { // set the TTL on outgoing udp queries to 1 query.setTTL((byte)1); queryUnicaster.addQuery(query, conn); } /** * Send the query to the multicast group. */ protected void multicastQueryRequest(QueryRequest query) { // set the TTL on outgoing udp queries to 1 query.setTTL((byte)1); multicastService.send(query); } /** * Broadcasts the query request to all initialized connections that * are not the receivingConnection, setting up the routing * to the designated QueryReplyHandler. This is called from teh default * handleQueryRequest and the default broadcastQueryRequest(QueryRequest) * * If different (smarter) broadcasting functionality is desired, override * as desired. If you do, note that receivingConnection may be null (for * requests originating here). */ // default access for testing void forwardQueryToUltrapeers(QueryRequest query, ReplyHandler handler) { // Note the use of initializedConnections only. // Note that we have zero allocations here. //Broadcast the query to other connected nodes (ultrapeers or older //nodes), but DON'T forward any queries not originating from me //along leaf to ultrapeer connections. List<RoutedConnection> list = connectionManager.getInitializedConnections(); int forwarded = 0; for(RoutedConnection mc : list) { forwarded += forwardQueryToUltrapeer(query, handler, mc) ? 1 : 0; } if (query.getTTL() == 1) lastHopCounter.countMessage(query.getHops(), forwarded); } /** * Performs a limited broadcast of the specified query. This is * useful, for example, when receiving queries from old-style * connections that we don't want to forward to all connected * Ultrapeers because we don't want to overly magnify the query. * * @param query the <tt>QueryRequest</tt> instance to forward * @param handler the <tt>ReplyHandler</tt> from which we received * the query */ // default access for testing void forwardLimitedQueryToUltrapeers(QueryRequest query, ReplyHandler handler) { //Broadcast the query to other connected nodes (ultrapeers or older //nodes), but DON'T forward any queries not originating from me //along leaf to ultrapeer connections. List<RoutedConnection> list = connectionManager.getInitializedConnections(); int limit = list.size(); int connectionsNeededForOld = OLD_CONNECTIONS_TO_USE; for(int i=0; i<limit; i++) { // if we've already queried enough old connections for // an old-style query, break out if(connectionsNeededForOld == 0) break; RoutedConnection mc = list.get(i); // if the query is comiing from an old connection, try to // send it's traffic to old connections. Only send it to // new connections if we only have a minimum number left if(mc.isGoodUltrapeer() && (limit-i) > connectionsNeededForOld) { continue; } forwardQueryToUltrapeer(query, handler, mc); // decrement the connections to use connectionsNeededForOld--; } } /** * Forwards the specified query to the specified Ultrapeer. This * encapsulates all necessary logic for forwarding queries to * Ultrapeers, for example handling last hop Ultrapeers specially * when the receiving Ultrapeer supports Ultrapeer query routing, * meaning that we check it's routing tables for a match before sending * the query. * * @param query the <tt>QueryRequest</tt> to forward * @param handler the <tt>ReplyHandler</tt> that sent the query * @param ultrapeer the Ultrapeer to send the query to */ // default access for testing boolean forwardQueryToUltrapeer(QueryRequest query, ReplyHandler handler, RoutedConnection ultrapeer) { // don't send a query back to the guy who sent it if(ultrapeer == handler) return false; // make double-sure we don't send a query received // by a leaf to other Ultrapeers if(ultrapeer.getConnectionCapabilities().isClientSupernodeConnection()) return false; // make sure that the ultrapeer understands feature queries. if(query.isFeatureQuery() && !ultrapeer.getConnectionCapabilities().getRemoteHostSupportsFeatureQueries()) return false; // is this the last hop for the query?? boolean lastHop = query.getTTL() == 1; // if it's the last hop to an Ultrapeer that sends // query route tables, route it. if(lastHop && ultrapeer.isUltrapeerQueryRoutingConnection()) { return sendRoutedQueryToHost(query, ultrapeer, handler); } else { // otherwise, just send it out ultrapeer.send(query); } return true; } /** * Originate a new query from this leaf node. * * @param qr the <tt>QueryRequest</tt> to send */ // default access for testing void originateLeafQuery(QueryRequest qr) { List<RoutedConnection> list = connectionManager.getInitializedConnections(); // only send to at most 4 Ultrapeers, as we could have more // as a result of race conditions - also, don't send what is new // requests down too many connections final int max = qr.isWhatIsNewRequest() ? 2 : 3; int start = !qr.isWhatIsNewRequest() ? 0 : (int) (Math.floor(Math.random()*(list.size()-1))); int limit = Math.min(max, list.size()); final boolean wantsOOB = qr.desiresOutOfBandReplies(); if(wantsOOB) LOG.trace("Query asks for OOB replies"); else LOG.trace("Query does not ask for OOB replies"); for(int i=start; i<start+limit; i++) { RoutedConnection mc = list.get(i); QueryRequest qrToSend = qr; if(wantsOOB && mc.getConnectionCapabilities().remoteHostSupportsLeafGuidance() < 0) { LOG.trace("Unmarking OOB query"); qrToSend = queryRequestFactory.unmarkOOBQuery(qr); } mc.originateQuery(qrToSend); } } /* (non-Javadoc) * @see com.limegroup.gnutella.MessageRouter#originateQuery(com.limegroup.gnutella.messages.QueryRequest, com.limegroup.gnutella.RoutedConnection) */ public boolean sendInitialQuery(QueryRequest query, RoutedConnection mc) { Objects.nonNull(query, "query"); Objects.nonNull(mc, "connection"); // if this is a feature query & the other side doesn't // support it, then don't send it // This is an optimization of network traffic, and doesn't // necessarily need to exist. We could be shooting ourselves // in the foot by not sending this, rendering Feature Searches // inoperable for some users connected to bad Ultrapeers. if (query.isFeatureQuery() && !mc.getConnectionCapabilities().getRemoteHostSupportsFeatureQueries()) { return false; } query.originate(); // ensure it gets put in the right priority. mc.send(query); return true; } /** * Respond to the ping request. Implementations typically will either * do nothing (if they don't think a response is appropriate) or call * sendPingReply(PingReply). * This method is called from the default handlePingRequest. */ protected abstract void respondToPingRequest(PingRequest request, ReplyHandler handler); /** * Responds to a ping received over UDP -- implementations * handle this differently from pings received over TCP, as it is * assumed that the requester only wants pongs from other nodes * that also support UDP messaging. * * @param request the <tt>PingRequest</tt> to service * @param addr the <tt>InetSocketAddress</tt> containing the ping * @param handler the <tt>ReplyHandler</tt> instance from which the * ping was received and to which pongs should be sent */ protected abstract void respondToUDPPingRequest(PingRequest request, InetSocketAddress addr, ReplyHandler handler); /** * Respond to the query request. Implementations typically will either * do nothing (if they don't think a response is appropriate) or call * sendQueryReply(QueryReply). * This method is called from the default handleQueryRequest. */ protected abstract boolean respondToQueryRequest(QueryRequest queryRequest, byte[] clientGUID, ReplyHandler handler); /** * The default handler for PingRequests. This implementation uses the * ping route table to route a ping reply. If an appropriate route * doesn't exist, records the error statistics. On sucessful routing, * the PingReply count is incremented.<p> * * In all cases, the ping reply is recorded into the host catcher.<p> * * Override as desired, but you probably want to call super.handlePingReply * if you do. */ protected void handlePingReply(PingReply reply, ReplyHandler handler) { //update hostcatcher (even if the reply isn't for me) boolean newAddress = hostCatcher.add(reply); if(newAddress && !reply.isUDPHostCache()) { pongCacher.get().addPong(reply); } //First route to originator in usual manner. ReplyHandler replyHandler = _pingRouteTable.getReplyHandler(reply.getGUID()); if (replyHandler != null) { replyHandler.handlePingReply(reply, handler); } else { handler.countDroppedMessage(); } boolean supportsUnicast = reply.supportsUnicast(); //Then, if a marked pong from an Ultrapeer that we've never seen before, //send to all leaf connections except replyHandler (which may be null), //irregardless of GUID. The leafs will add the address then drop the //pong as they have no routing entry. Note that if Ultrapeers are very //prevalent, this may consume too much bandwidth. //Also forward any GUESS pongs to all leaves. if (newAddress && (reply.isUltrapeer() || supportsUnicast)) { List<RoutedConnection> list = connectionManager.getInitializedClientConnections(); for(RoutedConnection c : list) { assert c != null : "null c."; if (c!=handler && c!=replyHandler && c.allowNewPongs()) { c.handlePingReply(reply, handler); } } } } /* (non-Javadoc) * @see com.limegroup.gnutella.MessageRouter#handleQueryReply(com.limegroup.gnutella.messages.QueryReply, com.limegroup.gnutella.ReplyHandler) */ public void handleQueryReply(QueryReply queryReply, ReplyHandler handler) { Objects.nonNull(queryReply, "query reply"); Objects.nonNull(handler, "reply handler"); // check the altloc count if (!altCountOk(queryReply)) return; //For flow control reasons, we keep track of the bytes routed for this //GUID. Replies with less volume have higher priorities (i.e., lower //numbers). int classC = ByteUtils.beb2int(queryReply.getIPBytes(), 0); RouteTable.ReplyRoutePair rrp = _queryRouteTable.getReplyHandler(queryReply.getGUID(), queryReply.getTotalLength(), queryReply.getUniqueResultCount(), queryReply.getPartialResultCount(), classC, !urnFilter.isBlacklisted(queryReply)); if(rrp != null) { queryReply.setPriority(rrp.getBytesRouted()); // Prepare a routing for a PushRequest, which works // here like a QueryReplyReply // Note the use of getClientGUID() here, not getGUID() _pushRouteTable.routeReply(queryReply.getClientGUID(), handler); //Simple flow control: don't route this message along other //connections if we've already routed too many replies for this //GUID. Note that replies destined for me all always delivered to //the GUI. ReplyHandler rh = rrp.getReplyHandler(); // remember more stats _queryRouteTable.countHopsTTLNet(queryReply); // if this reply is for us, remember even more stats if (rh == forMeReplyHandler) _queryRouteTable.timeStampResults(queryReply); if(!shouldDropReply(rrp, rh, queryReply)) { rh.handleQueryReply(queryReply, handler); // also add to the QueryUnicaster for accounting - basically, // most results will not be relevant, but since it is a simple // HashSet lookup, it isn't a prohibitive expense... queryUnicaster.handleQueryReply(queryReply); } else { LOG.trace("Dropping reply"); handler.countDroppedMessage(); } } else { LOG.trace("Dropping reply with no route table entry"); handler.countDroppedMessage(); } } private boolean altCountOk(QueryReply qr) { try { for (Response r : qr.getResultsAsList()) { if (r.getLocations().size() > FilterSettings.MAX_ALTS_PER_RESPONSE.getValue()) return false; } } catch (BadPacketException bpe) { // may get routed to someone who can parse it } return true; } /** * Checks if the <tt>QueryReply</tt> should be dropped for various reasons. * * Reason 1) The reply has already routed enough traffic. Based on per-TTL * hard limits for the number of bytes routed for the given reply guid. * This algorithm favors replies that don't have as far to go on the * network -- i.e., low TTL hits have more liberal limits than high TTL * hits. This ensures that hits that are closer to the query originator * -- hits for which we've already done most of the work, are not * dropped unless we've routed a really large number of bytes for that * guid. This method also checks that hard number of results that have * been sent for this GUID. If this number is greater than a specified * limit, we simply drop the reply. * * Reason 2) The reply was meant for me -- DO NOT DROP. * * Reason 3) The TTL is 0, drop. * * @param rrp the <tt>ReplyRoutePair</tt> containing data about what's * been routed for this GUID * @param ttl the time to live of the query hit * @return <tt>true if the reply should be dropped, otherwise <tt>false</tt> */ private boolean shouldDropReply(RouteTable.ReplyRoutePair rrp, ReplyHandler rh, QueryReply qr) { byte ttl = qr.getTTL(); // Reason 2 -- The reply is meant for me, do not drop it. if( rh == forMeReplyHandler ) return false; // Reason 3 -- drop if TTL is 0. if( ttl == 0 ) { droppedReplyCounter.dropTTL0(); return true; } // Reason 1 ... int resultsRouted = rrp.getResultsRouted(); // drop the reply if we've already sent more than the specified number // of results for this GUID // FIXME: could this cause spam to kill queries? if(resultsRouted > 100) { droppedReplyCounter.tooManyResults((short)resultsRouted); return true; } int bytesRouted = rrp.getBytesRouted(); // send replies with ttl above 2 if we've routed under 50K if(ttl > 2 && bytesRouted < 50 * 1024) return false; // send replies with ttl 1 if we've routed under 200K if(ttl == 1 && bytesRouted < 200 * 1024) return false; // send replies with ttl 2 if we've routed under 100K if(ttl == 2 && bytesRouted < 100 * 1024) return false; droppedReplyCounter.ttlByteDrop(ttl, bytesRouted); // if none of the above conditions holds true, drop the reply // FIXME: could this cause spam to kill queries? return true; } /** * If we get and SimppRequest, get the payload we need from the * SimppManager and send the simpp bytes the the requestor in a SimppVM. */ private void handleSimppRequest(final SimppRequestVM simppReq, final ReplyHandler handler ) { byte[] data = simppReq.isOldRequest() ? simppManager.get().getOldUpdateResponse() : simppManager.get().getSimppBytes(); if(data != null && data.length > 0 ) { SimppVM simppVM = SimppVM.createSimppResponse(simppReq, data); try { handler.handleSimppVM(simppVM); } catch(IOException iox) { return; } } } /** * Passes on the SimppVM to the SimppManager which will verify it and * make sure we it's newer than the one we know about, and then make changes * to the settings as necessary, and cause new CapabilityVMs to be sent down * all connections. */ private void handleSimppVM(SimppVM simppVM, ReplyHandler handler) { if(simppVM.isNewVersion()) { simppManager.get().checkAndUpdate(handler, simppVM.getData()); } } /** * Handles an update request by sending a response. */ private void handleUpdateRequest(UpdateRequest req, ReplyHandler handler ) { byte[] data = req.isOldRequest() ? updateHandler.get().getOldUpdateResponse() : updateHandler.get().getLatestBytes(); if(data != null) { UpdateResponse msg = UpdateResponse.createUpdateResponse(data,req); handler.reply(msg); } } /** * Passes the request onto the update manager. */ private void handleUpdateResponse(UpdateResponse resp, ReplyHandler handler) { if(resp.isNewVersion()) { updateHandler.get().handleNewData(resp.getUpdate(), handler); } } /** Handles a ContentResponse msg -- passing it to the ContentManager. */ private void handleContentResponse(ContentResponse msg, ReplyHandler handler) { contentManager.handleContentResponse(msg); } /** * Returns the appropriate handler from the _pushRouteTable. * This enforces that requests for my clientGUID will return * FOR_ME_REPLY_HANDLER, even if it's not in the table. Will also * iterate through leaves if they are not in the table. */ public ReplyHandler getPushHandler(byte[] guid) { ReplyHandler replyHandler = _pushRouteTable.getReplyHandler(guid); if(replyHandler != null) { return replyHandler; } else if(Arrays.equals(_clientGUID, guid)) { return forMeReplyHandler; } else { if (!connectionServices.isSupernode()) { return null; } for (RoutedConnection leaf : connectionManager.getInitializedClientConnections()) { if (leaf.isPushProxyFor() && Arrays.equals(leaf.getClientGUID(), guid)) { return leaf; } } return null; } } /** * Uses the ping route table to send a PingReply to the appropriate * connection. Since this is used for PingReplies orginating here, no * stats are updated. */ protected void sendPingReply(PingReply pong, ReplyHandler handler) { Objects.nonNull(pong, "pong"); Objects.nonNull(handler, "reply handler"); handler.handlePingReply(pong, null); } /** * Uses the query route table to send a QueryReply to the appropriate * connection. Since this is used for QueryReplies orginating here, no * stats are updated. * @throws IOException if no appropriate route exists. */ protected void sendQueryReply(QueryReply queryReply) throws IOException { Objects.nonNull(queryReply, "query reply"); //For flow control reasons, we keep track of the bytes routed for this //GUID. Replies with less volume have higher priorities (i.e., lower //numbers). RouteTable.ReplyRoutePair rrp = _queryRouteTable.getReplyHandler(queryReply.getGUID(), queryReply.getTotalLength(), queryReply.getResultCount(), queryReply.getPartialResultCount()); if(rrp != null) { queryReply.setPriority(rrp.getBytesRouted()); rrp.getReplyHandler().handleQueryReply(queryReply, null); } else throw new IOException("no route for reply"); } /* (non-Javadoc) * @see com.limegroup.gnutella.MessageRouter#sendPushRequest(com.limegroup.gnutella.messages.PushRequest) */ public void sendPushRequest(PushRequest push) throws IOException { Objects.nonNull(push, "push"); // Note the use of getClientGUID() here, not getGUID() ReplyHandler replyHandler = getPushHandler(push.getClientGUID()); if(replyHandler != null) replyHandler.handlePushRequest(push, forMeReplyHandler); else throw new IOException("no route for push"); } /* (non-Javadoc) * @see com.limegroup.gnutella.MessageRouter#sendMulticastPushRequest(com.limegroup.gnutella.messages.PushRequest) */ public void sendMulticastPushRequest(PushRequest push) { Objects.nonNull(push, "push"); // must have a TTL of 1 assert push.getTTL() == 1 : "multicast push ttl not 1"; multicastService.send(push); } /* (non-Javadoc) * @see com.limegroup.gnutella.MessageRouter#responsesToQueryReplies(com.limegroup.gnutella.Response[], com.limegroup.gnutella.messages.QueryRequest) */ public Iterable<QueryReply> responsesToQueryReplies(Response[] responses, QueryRequest queryRequest) { return responsesToQueryReplies(responses, queryRequest, 10, null); } /** * Converts the passed responses to QueryReplies. Each QueryReply can * accomodate atmost 255 responses. Not all the responses may get included * in QueryReplies in case the query request came from a far away host. * <p> * NOTE: This method doesnt have any side effect, * and does not modify the state of this object * @param responses The responses to be converted * @param queryRequest The query request corresponding to which we are * generating query replies. * @param REPLY_LIMIT the maximum number of responses to have in each reply. * @param security token might be null * @return Iterable of QueryReply */ public Iterable<QueryReply> responsesToQueryReplies(Response[] responses, QueryRequest queryRequest, final int REPLY_LIMIT, SecurityToken securityToken) { int numResponses = responses.length; int numHops = queryRequest.getHops(); // limit the responses if we're not delivering this // out-of-band and we have a lot of responses if(REPLY_LIMIT > 1 && numHops > 2 && numResponses > HIGH_HOPS_RESPONSE_LIMIT) { int j = (int)(Math.random() * numResponses) % (numResponses - HIGH_HOPS_RESPONSE_LIMIT); Response[] newResponses = new Response[HIGH_HOPS_RESPONSE_LIMIT]; for(int i=0; i<10; i++, j++) { newResponses[i] = responses[j]; } responses = newResponses; numResponses = responses.length; } // decrement the number of responses we have left return outgoingQueryReplyFactory.createReplies(responses, queryRequest, securityToken, REPLY_LIMIT); } /** * Handles a message to reset the query route table for the given * connection. * * @param rtm the <tt>ResetTableMessage</tt> for resetting the query * route table * @param mc the <tt>RoutedConnection</tt> for which the query route * table should be reset */ private void handleResetTableMessage(ResetTableMessage rtm, RoutedConnection mc) { // if it's not from a leaf or an Ultrapeer advertising // QRP support, ignore it if(!isQRPConnection(mc)) return; // reset the query route table for this connection synchronized (mc.getQRPLock()) { mc.resetQueryRouteTable(rtm); } // if this is coming from a leaf, make sure we update // our tables so that the dynamic querier has correct // data if(mc.isLeafConnection()) { _lastQueryRouteTable = createRouteTable(); } } /** * Handles a message to patch the query route table for the given * connection. * * @param rtm the <tt>PatchTableMessage</tt> for patching the query * route table * @param mc the <tt>RoutedConnection</tt> for which the query route * table should be patched */ private void handlePatchTableMessage(PatchTableMessage ptm, RoutedConnection mc) { // if it's not from a leaf or an Ultrapeer advertising // QRP support, ignore it if(!isQRPConnection(mc)) return; // patch the query route table for this connection synchronized(mc.getQRPLock()) { mc.patchQueryRouteTable(ptm); } // if this is coming from a leaf, make sure we update // our tables so that the dynamic querier has correct // data if(mc.isLeafConnection()) { // Leaves should not send full tables if(mc.getRoutedConnectionStatistics().getQueryRouteTablePercentFull() == 100) { if(LOG.isWarnEnabled()) LOG.warn("Leaf " + mc + " sent full query routing table"); mc.close(); } _lastQueryRouteTable = createRouteTable(); } } private void updateMessage(QueryRequest request, ReplyHandler handler) { if (SearchSettings.SEND_LIME_RESPONSES.getBoolean() && request.isQueryForLW() && staticMessages.getLimeReply() != null) { QueryReply qr = queryReplyFactory.createQueryReply(request.getGUID(), staticMessages.getLimeReply()); qr.setHops((byte)0); qr.setTTL((byte)(request.getHops()+1)); try { sendQueryReply(qr); } catch (IOException ignored) {} } if (!(handler instanceof Connection)) return; } /** * Utility method for checking whether or not the given connection * is able to pass QRP messages. * * @param c the <tt>Connection</tt> to check * @return <tt>true</tt> if this is a QRP-enabled connection, * otherwise <tt>false</tt> */ private static boolean isQRPConnection(Connection c) { if (c.getConnectionCapabilities().isSupernodeClientConnection()) return true; if (c.getConnectionCapabilities().isUltrapeerQueryRoutingConnection()) return true; return false; } /** Thread the processing of QRP Table delivery. */ private class QRPPropagator extends ManagedThread { public QRPPropagator() { setName("QRPPropagator"); setDaemon(true); } /** While the connection is not closed, sends all data delay. */ @Override public void run() { try { while (true) { // Check for any scheduled QRP table propagations // every 10 seconds Thread.sleep(10*1000); forwardQueryRouteTables(); } } catch(Throwable t) { ErrorService.error(t); } } } /** * Sends updated query routing tables to all connections which haven't * been updated in a while. You can call this method as often as you want; * it takes care of throttling. * @modifies connections */ // default access for testing void forwardQueryRouteTables() { //Check the time to decide if it needs an update. long time = System.currentTimeMillis(); //For all connections to new hosts c needing an update... List<RoutedConnection> list = connectionManager.getInitializedConnections(); QueryRouteTable table = null; List<RouteTableMessage> patches = null; QueryRouteTable lastSent = null; for(RoutedConnection c : list) { // continue if I'm an Ultrapeer and the node on the // other end doesn't support Ultrapeer-level query // routing if(connectionServices.isSupernode()) { // only skip it if it's not an Ultrapeer query routing // connection if(!c.isUltrapeerQueryRoutingConnection()) { continue; } } // otherwise, I'm a leaf, and don't send routing // tables if it's not a connection to an Ultrapeer // or if query routing is not enabled on the connection else if (!(c.getConnectionCapabilities().isClientSupernodeConnection() && c.getConnectionCapabilities().isQueryRoutingEnabled())) { continue; } // See if it is time for this connections QRP update // This call is safe since only this thread updates time if (time<c.getRoutedConnectionStatistics().getNextQRPForwardTime()) continue; c.getRoutedConnectionStatistics().incrementNextQRPForwardTime(time); // Create a new query route table if we need to if (table == null) { table = createRouteTable(); // Ignores busy leaves _lastQueryRouteTable = table; } //..and send each piece. // Because we tend to send the same list of patches to lots of // Connections, we can reuse the list of RouteTableMessages // between those connections if their last sent // table is exactly the same. // This allows us to only reduce the amount of times we have // to call encode. // (This if works for 'null' sent tables too) if( lastSent == c.getRoutedConnectionStatistics().getQueryRouteTableSent() ) { // if we have not constructed the patches yet, then do so. if( patches == null ) patches = table.encode(lastSent, true); } // If they aren't the same, we have to encode a new set of // patches for this connection. else { lastSent = c.getRoutedConnectionStatistics().getQueryRouteTableSent(); patches = table.encode(lastSent, true); } // If sending QRP tables is turned off, don't send them. if(!ConnectionSettings.SEND_QRP.getValue()) { return; } for(RouteTableMessage next : patches) c.send(next); c.getRoutedConnectionStatistics().setQueryRouteTableSent(table); } } /* (non-Javadoc) * @see com.limegroup.gnutella.MessageRouter#getQueryRouteTable() */ public QueryRouteTable getQueryRouteTable() { return _lastQueryRouteTable; } /** * Creates a query route table appropriate for forwarding to connection c. * This will not include information from c. * @requires queryUpdateLock held */ //default access for testing QueryRouteTable createRouteTable() { QueryRouteTable ret = qrpUpdater.getQRT(); // Add leaves' files if we're an Ultrapeer. if(connectionServices.isSupernode()) { addQueryRoutingEntriesForLeaves(ret); } return ret; } /** * Adds all query routing tables of leaves to the query routing table for * this node for propagation to other Ultrapeers at 1 hop. * * Added "busy leaf" support to prevent a busy leaf from having its QRT * table added to the Ultrapeer's last-hop QRT table. This should reduce * BW costs for UPs with busy leaves. * * @param qrt the <tt>QueryRouteTable</tt> to add to */ private void addQueryRoutingEntriesForLeaves(QueryRouteTable qrt) { List<RoutedConnection> leaves = connectionManager.getInitializedClientConnections(); for(RoutedConnection mc : leaves) { synchronized (mc.getQRPLock()) { // Don't include busy leaves if( !mc.isBusyLeaf() ){ QueryRouteTable qrtr = mc.getRoutedConnectionStatistics().getQueryRouteTableReceived(); if(qrtr != null) { qrt.addAll(qrtr); } } } } } /* (non-Javadoc) * @see com.limegroup.gnutella.MessageRouter#registerMessageListener(byte[], com.limegroup.gnutella.MessageListener) */ public void registerMessageListener(byte[] guid, MessageListener ml) { ml.registered(guid); synchronized(MESSAGE_LISTENER_LOCK) { Map<byte[], List<MessageListener>> listeners = new TreeMap<byte[], List<MessageListener>>(GUID.GUID_BYTE_COMPARATOR); listeners.putAll(_messageListeners); List<MessageListener> all = listeners.get(guid); if(all == null) { all = new ArrayList<MessageListener>(1); all.add(ml); } else { List<MessageListener> temp = new ArrayList<MessageListener>(all.size() + 1); temp.addAll(all); all = temp; all.add(ml); } listeners.put(guid, Collections.unmodifiableList(all)); _messageListeners = Collections.unmodifiableMap(listeners); } } /* (non-Javadoc) * @see com.limegroup.gnutella.MessageRouter#unregisterMessageListener(byte[], com.limegroup.gnutella.MessageListener) */ public void unregisterMessageListener(byte[] guid, MessageListener ml) { boolean removed = false; synchronized(MESSAGE_LISTENER_LOCK) { List<MessageListener> all = _messageListeners.get(guid); if(all != null) { all = new ArrayList<MessageListener>(all); if(all.remove(ml)) { removed = true; Map<byte[], List<MessageListener>> listeners = new TreeMap<byte[], List<MessageListener>>(GUID.GUID_BYTE_COMPARATOR); listeners.putAll(_messageListeners); if(all.isEmpty()) listeners.remove(guid); else listeners.put(guid, Collections.unmodifiableList(all)); _messageListeners = Collections.unmodifiableMap(listeners); } } } if(removed) ml.unregistered(guid); } /** * Replies to a head ping sent from the given ReplyHandler. */ private void handleHeadPing(HeadPing ping, ReplyHandler handler) { if (DownloadSettings.DROP_HEADPINGS.getValue()) return; GUID clientGUID = ping.getClientGuid(); ReplyHandler pingee; if(clientGUID != null) pingee = getPushHandler(clientGUID.bytes()); else pingee = forMeReplyHandler; // handle ourselves. //drop the ping if no entry for the given clientGUID if (pingee == null) return; //don't bother routing if this is intended for me. // TODO: Clean up ReplyHandler interface so we aren't // afraid to use it like it's intended. // That way, we can do pingee.handleHeadPing(ping) // and not need this anti-OO instanceof check. if (pingee instanceof ForMeReplyHandler) { // If it's for me, reply directly to the person who sent it. HeadPong pong = headPongFactory.create(ping); handler.reply(pong); // } else { // Otherwise, remember who sent it and forward it on. //remember where to send the pong to. //the pong will have the same GUID as the ping. // Note that this uses the messageGUID, not the clientGUID _headPongRouteTable.routeReply(ping.getGUID(), handler); //and send off the routed ping if ( !(handler instanceof Connection) || ((Connection)handler).getConnectionCapabilities().supportsVMRouting()) pingee.reply(ping); else pingee.reply(new HeadPing(ping)); } } /** * Pure testing method. */ Map<byte[], List<MessageListener>> getMessageListenerMap() { return _messageListeners; } /** * Handles a pong received from the given handler. */ private void handleHeadPong(HeadPong pong, ReplyHandler handler) { ReplyHandler forwardTo = _headPongRouteTable.getReplyHandler(pong.getGUID()); // TODO: Clean up ReplyHandler interface so we're not afraid // to use it correctly. // Ideally, we'd do forwardTo.handleHeadPong(pong) // instead of this instanceof check // if this pong is for me, process it as usual (not implemented yet) if (forwardTo != null && !(forwardTo instanceof ForMeReplyHandler)) { forwardTo.reply(pong); _headPongRouteTable.removeReplyHandler(forwardTo); } } private void handleDHTContactsMessage(DHTContactsMessage msg, ReplyHandler handler) { dhtManager.handleDHTContactsMessage(msg); } /* (non-Javadoc) * @see com.limegroup.gnutella.MessageRouter#forwardInspectionRequestToLeaves(com.limegroup.gnutella.messages.vendor.InspectionRequest) */ public void forwardInspectionRequestToLeaves(InspectionRequest ir) { if (!connectionManager.isSupernode()) return; // only inspection requests with return address are forwarded. if (ir.getReturnAddress() == null) return; for (RoutedConnection mc : connectionManager.getInitializedClientConnections()) { if (mc.getConnectionCapabilities().remoteHostSupportsInspections() >= ir.getVersion()) mc.send(ir); } } /** * Should only be used for testing. */ RouteTable getPushRouteTable() { return _pushRouteTable; } RouteTable getHeadPongRouteTable() { return _headPongRouteTable; } /** Expires the UDP-Reply cache. */ private class UDPReplyCleaner implements Runnable { public void run() { messageDispatcher.get().dispatch(new Runnable() { public void run() { udpReplyHandlerCache.clear(); } }); } } /** This is run to clear out the registry of connect back attempts... * Made package access for easy test access. */ static class ConnectBackExpirer implements Runnable { public void run() { _tcpConnectBacks.clear(); _udpConnectBacks.clear(); } } private static class HopsFlowManager implements Runnable { private final UploadManager uploadManager; private final ConnectionManager connectionManager; public HopsFlowManager(UploadManager uploadManager, ConnectionManager connectionManager) { this.uploadManager = uploadManager; this.connectionManager = connectionManager; } /* in case we don't want any queries any more */ private static final byte BUSY_HOPS_FLOW = 0; /* in case we want to reenable queries */ private static final byte FREE_HOPS_FLOW = 5; /* small optimization: send only HopsFlowVendorMessages if the busy state changed */ @InspectablePrimitive("last upload slot busy state") private static boolean _oldBusyState = false; public void run() { // only leafs should use HopsFlow if (connectionManager.isSupernode()) return; // busy hosts don't want to receive any queries, if this node is not // busy, we need to reset the HopsFlow value boolean isBusy = !uploadManager.mayBeServiceable(); // state changed? don't bother the ultrapeer with information // that it already knows. we need to inform new ultrapeers, though. final List<RoutedConnection> connections = connectionManager.getInitializedConnections(); final HopsFlowVendorMessage hops = new HopsFlowVendorMessage(isBusy ? BUSY_HOPS_FLOW : FREE_HOPS_FLOW); if (isBusy == _oldBusyState) { for(RoutedConnection c : connections) { // Yes, we may tell a new ultrapeer twice, but // without a buffer of some kind, we might forget // some ultrapeers. The clean solution would be // to remember the hops-flow value in the connection. if (c != null && c.getConnectionTime() + 1.25 * HOPS_FLOW_INTERVAL > System.currentTimeMillis() && c.getConnectionCapabilities().isClientSupernodeConnection() ) c.send(hops); } } else { _oldBusyState = isBusy; for(RoutedConnection c : connections) { if (c != null && c.getConnectionCapabilities().isClientSupernodeConnection()) c.send(hops); } } } } /* (non-Javadoc) * @see com.limegroup.gnutella.MessageRouter#getOOBExpireTime() */ public long getOOBExpireTime() { return OOB_SESSION_EXPIRE_TIME; } /* * =================================================== * "REGULAR" HANDLER * =================================================== */ private class PingRequestHandler implements MessageHandler { public void handleMessage(Message msg, InetSocketAddress addr, ReplyHandler handler) { handlePingRequestPossibleDuplicate((PingRequest)msg, handler); } } private class PingReplyHandler implements MessageHandler { public void handleMessage(Message msg, InetSocketAddress addr, ReplyHandler handler) { handlePingReply((PingReply)msg, handler); } } private class QueryRequestHandler implements MessageHandler { public void handleMessage(Message msg, InetSocketAddress addr, ReplyHandler handler) { handleQueryRequestPossibleDuplicate( (QueryRequest)msg, (RoutedConnection)handler); } } private class QueryReplyHandler implements MessageHandler { public void handleMessage(Message msg, InetSocketAddress addr, ReplyHandler handler) { // if someone sent a TCP QueryReply with the MCAST header, // that's bad, so ignore it. QueryReply qmsg = (QueryReply)msg; handleQueryReply(qmsg, handler); } } private class ResetTableHandler implements MessageHandler { public void handleMessage(Message msg, InetSocketAddress addr, ReplyHandler handler) { handleResetTableMessage((ResetTableMessage)msg, (RoutedConnection)handler); } } private class PatchTableHandler implements MessageHandler { public void handleMessage(Message msg, InetSocketAddress addr, ReplyHandler handler) { handlePatchTableMessage((PatchTableMessage)msg, (RoutedConnection)handler); } } private class TCPConnectBackHandler implements MessageHandler { public void handleMessage(Message msg, InetSocketAddress addr, ReplyHandler handler) { handleTCPConnectBackRequest((TCPConnectBackVendorMessage) msg, (RoutedConnection)handler); } } private class UDPConnectBackHandler implements MessageHandler { public void handleMessage(Message msg, InetSocketAddress addr, ReplyHandler handler) { handleUDPConnectBackRequest((UDPConnectBackVendorMessage) msg, (RoutedConnection)handler); } } private class TCPConnectBackRedirectHandler implements MessageHandler { public void handleMessage(Message msg, InetSocketAddress addr, ReplyHandler handler) { handleTCPConnectBackRedirect((TCPConnectBackRedirect) msg, (RoutedConnection)handler); } } private class UDPConnectBackRedirectHandler implements MessageHandler { public void handleMessage(Message msg, InetSocketAddress addr, ReplyHandler handler) { handleUDPConnectBackRedirect((UDPConnectBackRedirect) msg, (RoutedConnection)handler); } } private class PushProxyRequestHandler implements MessageHandler { public void handleMessage(Message msg, InetSocketAddress addr, ReplyHandler handler) { handlePushProxyRequest((PushProxyRequest) msg, (RoutedConnection)handler); } } private class QueryStatusResponseHandler implements MessageHandler { public void handleMessage(Message msg, InetSocketAddress addr, ReplyHandler handler) { handleQueryStatus((QueryStatusResponse) msg, (RoutedConnection)handler); } } private class HeadPingHandler implements MessageHandler { public void handleMessage(Message msg, InetSocketAddress addr, ReplyHandler handler) { //TODO: add the statistics recording code handleHeadPing((HeadPing)msg, handler); } } private class SimppRequestVMHandler implements MessageHandler { public void handleMessage(Message msg, InetSocketAddress addr, ReplyHandler handler) { handleSimppRequest((SimppRequestVM)msg, handler); } } private class SimppVMHandler implements MessageHandler { public void handleMessage(Message msg, InetSocketAddress addr, ReplyHandler handler) { handleSimppVM((SimppVM)msg, handler); } } private class UpdateRequestHandler implements MessageHandler { public void handleMessage(Message msg, InetSocketAddress addr, ReplyHandler handler) { handleUpdateRequest((UpdateRequest)msg, handler); } } private class UpdateResponseHandler implements MessageHandler { public void handleMessage(Message msg, InetSocketAddress addr, ReplyHandler handler) { handleUpdateResponse((UpdateResponse)msg, handler); } } private class HeadPongHandler implements MessageHandler { public void handleMessage(Message msg, InetSocketAddress addr, ReplyHandler handler) { handleHeadPong((HeadPong)msg, handler); } } private class DHTContactsMessageHandler implements MessageHandler { public void handleMessage(Message msg, InetSocketAddress addr, ReplyHandler handler) { handleDHTContactsMessage((DHTContactsMessage)msg, handler); } } public static class VendorMessageHandler implements MessageHandler { public void handleMessage(Message msg, InetSocketAddress addr, ReplyHandler handler) { Connection c = (Connection)handler; c.handleVendorMessage((VendorMessage)msg); } } /* * =================================================== * UDP HANDLER * =================================================== */ private class UDPQueryRequestHandler implements MessageHandler { public void handleMessage(Message msg, InetSocketAddress addr, ReplyHandler handler) { InetAddress address = addr.getAddress(); int port = addr.getPort(); // TODO: compare AddressSecurityToken with old generation params. if it matches //send a new one generated with current params if (hasValidQueryKey(address, port, (QueryRequest) msg)) { sendAcknowledgement(addr, msg.getGUID()); // a TTL above zero may indicate a malicious client, as UDP // messages queries should not be sent with TTL above 1. //if(msg.getTTL() > 0) return; handleUDPQueryRequestPossibleDuplicate((QueryRequest)msg, handler); } } } private class UDPPingRequestHandler implements MessageHandler { public void handleMessage(Message msg, InetSocketAddress addr, ReplyHandler handler) { handleUDPPingRequestPossibleDuplicate((PingRequest)msg, handler, addr); } } private class UDPPingReplyHandler implements MessageHandler { public void handleMessage(Message msg, InetSocketAddress addr, ReplyHandler handler) { handleUDPPingReply((PingReply)msg, handler, addr.getAddress(), addr.getPort()); } } private class UDPHeadPingHandler implements MessageHandler { public void handleMessage(Message msg, InetSocketAddress addr, ReplyHandler handler) { //TODO: add the statistics recording code handleHeadPing((HeadPing)msg, handler); } } private class UDPUpdateRequestHandler implements MessageHandler { public void handleMessage(Message msg, InetSocketAddress addr, ReplyHandler handler) { handleUpdateRequest((UpdateRequest)msg, handler); } } private class UDPContentResponseHandler implements MessageHandler { public void handleMessage(Message msg, InetSocketAddress addr, ReplyHandler handler) { handleContentResponse((ContentResponse)msg, handler); } } /* * =================================================== * MULTICAST HANDLER * =================================================== */ public class MulticastQueryRequestHandler implements MessageHandler { public void handleMessage(Message msg, InetSocketAddress addr, ReplyHandler handler) { handleUDPQueryRequestPossibleDuplicate((QueryRequest) msg, handler); } } public class MulticastQueryReplyHandler implements MessageHandler { public void handleMessage(Message msg, InetSocketAddress addr, ReplyHandler handler) { handleQueryReply((QueryReply)msg, handler); } } public class MulticastPingRequestHandler implements MessageHandler { public void handleMessage(Message msg, InetSocketAddress addr, ReplyHandler handler) { handleUDPPingRequestPossibleDuplicate((PingRequest)msg, handler, addr); } } public class MulticastPingReplyHandler implements MessageHandler { public void handleMessage(Message msg, InetSocketAddress addr, ReplyHandler handler) { handleUDPPingReply((PingReply)msg, handler, addr.getAddress(), addr.getPort()); } } /** * This class handles UDP query replies and forwards them to the * {@link OOBHandler} if they are not replies to multicast or unicast * queries. */ public class UDPQueryReplyHandler implements MessageHandler { private final OOBHandler oobHandler; public UDPQueryReplyHandler(OOBHandler oobHandler) { this.oobHandler = oobHandler; } public void handleMessage(Message msg, InetSocketAddress addr, ReplyHandler handler) { QueryReply reply = (QueryReply)msg; if (reply.isReplyToMulticastQuery() || isHostUnicastQueried(new GUID(reply.getGUID()), handler)) { handleQueryReply(reply, handler); } else { oobHandler.handleMessage(msg, addr, handler); } } } /** * tracks information about messages with a specified guid. */ private class GUIDTracker implements Inspectable, MessageListener, SettingListener { private final List<Map<String,Object>> l = Collections.synchronizedList(new ArrayList<Map<String,Object>>()); private volatile long start; private volatile byte[] lastGuid; public GUIDTracker() { MessageSettings.TRACKING_GUID.addSettingListener(this); } @Override public Object inspect() { Map<String,Object> ret = new HashMap<String,Object>(); ret.put("start",start); if (lastGuid != null) ret.put("guid", Base32.encode(lastGuid)); ret.put("messages",l); return ret; } public void processMessage(Message m, ReplyHandler handler) { Map<String,Object> data = new HashMap<String,Object>(); data.put("ver",1); data.put("time", System.currentTimeMillis()); data.put("source",handler.getInetAddress()+":"+handler.getPort()); data.put("type",m.getClass().getName()); data.put("hops",m.getHops()); data.put("ttl",m.getTTL()); data.put("net",m.getNetwork()); data.put("len",m.getLength()); // why not try { ByteArrayOutputStream baos = new ByteArrayOutputStream(); m.writeQuickly(baos); data.put("pay",baos.toByteArray()); } catch (IOException impossible){} l.add(data); } public void registered(final byte[] guid) { start = System.currentTimeMillis(); lastGuid = guid; // don't keep history for ever. backgroundExecutor.schedule(new Runnable() { public void run () { byte [] last = lastGuid; if (last != null && Arrays.equals(guid,last)) unregisterMessageListener(guid, GUIDTracker.this); } }, 600, TimeUnit.SECONDS); } public void unregistered(byte[] guid) { start = 0; lastGuid = null; l.clear(); // this is the only list manipulation outside of messagedispatch thread } public void settingChanged(SettingEvent evt) { if (evt.getEventType() != SettingEvent.EventType.VALUE_CHANGED || evt.getSetting() != MessageSettings.TRACKING_GUID) return; byte [] last = lastGuid; if (last != null) unregisterMessageListener(last, this); String newGuid = MessageSettings.TRACKING_GUID.get(); if (newGuid.length() == 0) return; byte[] guid = Base32.decode(newGuid); registerMessageListener(guid, this); } } /** * Counts reasons why a reply would be dropped. */ private static class DroppedReplyCounter implements Inspectable { // buffer storage is actually inited lazily private final Buffer<Short> tooManyResults = new Buffer<Short>(50); private final Buffer<Byte> ttls = new Buffer<Byte>(50); private final Buffer<Integer> bytesRouted = new Buffer<Integer>(50); private long totalDropped, droppedTTL0; @Override public synchronized Object inspect() { byte [] tooManyResultsByte = new byte[tooManyResults.getSize() * 2]; for (int i = 0; i < tooManyResults.getSize(); i++) ByteUtils.short2beb(tooManyResults.get(i), tooManyResultsByte, i * 2); byte [] ttlsByte = new byte[ttls.getSize()]; for (int i = 0; i < ttls.getSize(); i++) ttlsByte[i] = ttls.get(i); byte [] bytesRoutedByte = new byte[bytesRouted.getSize() * 4]; for (int i = 0; i < bytesRouted.getSize(); i++) ByteUtils.int2beb(bytesRouted.get(i), bytesRoutedByte, i * 4); Map<String,Object> ret = new HashMap<String,Object>(); ret.put("tooMany",tooManyResultsByte); ret.put("ttls",ttlsByte); ret.put("bytes",bytesRoutedByte); ret.put("total",totalDropped); ret.put("ttl0",droppedTTL0); return ret; } public synchronized void tooManyResults(short numResults) { if (!LimeWireUtils.isBetaRelease()) return; totalDropped++; tooManyResults.add(numResults); } public synchronized void ttlByteDrop(byte ttl, int bytes) { if (!LimeWireUtils.isBetaRelease()) return; totalDropped++; ttls.add(ttl); bytesRouted.add(bytes); } public synchronized void dropTTL0() { // not checking isBeta since these fields are already allocated totalDropped++; droppedTTL0++; } } private static class LastHopCounter implements Inspectable { private final Map<Byte,InspectionHistogram<Integer>> lastHopRouted = new HashMap<Byte,InspectionHistogram<Integer>>(); @Override public synchronized Object inspect() { Map<String,Object> ret = new HashMap<String,Object>(); for (byte b : lastHopRouted.keySet()) ret.put(String.valueOf(b),lastHopRouted.get(b).inspect()); return ret; } public synchronized void countMessage(byte hops, int connections) { InspectionHistogram<Integer> histogram = lastHopRouted.get(hops); if (histogram == null) { histogram = new InspectionHistogram<Integer>(); lastHopRouted.put(hops, histogram); } histogram.count(connections); } } private static class DuplicateQueryCounter implements Inspectable { private final Buffer<Long> duplicateTimes = new Buffer<Long>(100); private final Buffer<Byte> duplicateHops = new Buffer<Byte>(100); private final Buffer<Byte> duplicateTTLs = new Buffer<Byte>(100); private long totalDropped; private final Buffer<GUID> lastNGUIDs = new Buffer<GUID>(100); private final Map<GUID, Integer> counts = new HashMap<GUID,Integer>(); private int highestEver; public synchronized void duplicateQuery(QueryRequest r) { totalDropped++; if (!LimeWireUtils.isBetaRelease()) return; duplicateTimes.add(System.currentTimeMillis()); duplicateHops.add(r.getHops()); duplicateTTLs.add(r.getTTL()); GUID g = new GUID(r.getGUID()); Integer num = counts.get(g); if (num != null) { num++; highestEver = Math.max(highestEver, num); counts.put(g,num); return; } num = 1; counts.put(g, num); GUID ejected = lastNGUIDs.add(g); if (ejected != null) counts.remove(ejected); } @Override public synchronized Object inspect() { Map<String,Object> ret = new HashMap<String,Object>(); ret.put("total",totalDropped); ret.put("highest",highestEver); byte [] ttlsByte = new byte[duplicateTTLs.getSize()]; for (int i = 0; i < duplicateTTLs.getSize(); i++) ttlsByte[i] = duplicateTTLs.get(i); byte [] hopsByte = new byte[duplicateHops.getSize()]; for (int i = 0; i < duplicateHops.getSize(); i++) hopsByte[i] = duplicateHops.get(i); byte [] timesByte = new byte[duplicateTimes.getSize() * 8]; for (int i = 0; i < duplicateTimes.getSize(); i++) ByteUtils.long2beb(duplicateTimes.get(i), timesByte, i * 8); ret.put("ttls",ttlsByte); ret.put("hops",hopsByte); ret.put("times",timesByte); ret.put("hist", new HashMap<GUID,Integer>(counts)); return ret; } } private class ConnectionListener implements ConnectionLifecycleListener { public void handleConnectionLifecycleEvent(ConnectionLifecycleEvent evt) { if (evt.isConnectionClosedEvent()) { removeConnection(evt.getConnection()); } } } }