package com.limegroup.gnutella;
import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.UnknownHostException;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
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.core.settings.ConnectionSettings;
import org.limewire.core.settings.MessageSettings;
import org.limewire.inject.EagerSingleton;
import org.limewire.inspection.InspectionPoint;
import org.limewire.io.GUID;
import org.limewire.io.IpPort;
import org.limewire.io.IpPortImpl;
import org.limewire.io.NetworkUtils;
import org.limewire.net.SocketsManager;
import org.limewire.security.MACCalculatorRepositoryManager;
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.RoutedConnection;
import com.limegroup.gnutella.dht.DHTManager;
import com.limegroup.gnutella.filters.URNFilter;
import com.limegroup.gnutella.guess.OnDemandUnicaster;
import com.limegroup.gnutella.library.FileViewManager;
import com.limegroup.gnutella.library.SharedFilesKeywordIndex;
import com.limegroup.gnutella.messagehandlers.InspectionRequestHandler;
import com.limegroup.gnutella.messagehandlers.LimeACKHandler;
import com.limegroup.gnutella.messagehandlers.OOBHandler;
import com.limegroup.gnutella.messagehandlers.UDPCrawlerPingHandler;
import com.limegroup.gnutella.messages.FeatureSearchData;
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.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.Message.MessageCounter;
import com.limegroup.gnutella.messages.vendor.HeadPongFactory;
import com.limegroup.gnutella.messages.vendor.ReplyNumberVendorMessage;
import com.limegroup.gnutella.messages.vendor.ReplyNumberVendorMessageFactory;
import com.limegroup.gnutella.routing.QRPUpdater;
import com.limegroup.gnutella.search.QueryDispatcher;
import com.limegroup.gnutella.search.QueryHandlerFactory;
import com.limegroup.gnutella.search.SearchResultHandler;
import com.limegroup.gnutella.simpp.SimppManager;
import com.limegroup.gnutella.version.UpdateHandler;
/**
* This class is the message routing implementation for TCP messages.
*/
@EagerSingleton
public class StandardMessageRouter extends MessageRouterImpl {
private static final Log LOG = LogFactory.getLog(StandardMessageRouter.class);
private final Statistics statistics;
private final ReplyNumberVendorMessageFactory replyNumberVendorMessageFactory;
@InspectionPoint("false positive queries")
private final MessageCounter falsePositives = new Message.MessageCounter(50);
@InspectionPoint("not serviced queries")
private final MessageCounter notServiced = new Message.MessageCounter(500);
@InspectionPoint("ignored busy queries")
private final MessageCounter ignoredBusy = new Message.MessageCounter(500);
private final SharedFilesKeywordIndex sharedFilesKeywordIndex;
@Inject
public StandardMessageRouter(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,
Statistics statistics,
ReplyNumberVendorMessageFactory replyNumberVendorMessageFactory,
PingRequestFactory pingRequestFactory,
MessageHandlerBinder messageHandlerBinder,
Provider<OOBHandler> oobHandlerFactory,
Provider<MACCalculatorRepositoryManager> MACCalculatorRepositoryManager,
Provider<LimeACKHandler> limeACKHandler,
OutgoingQueryReplyFactory outgoingQueryReplyFactory,
SharedFilesKeywordIndex sharedFilesKeywordIndex,
QRPUpdater qrpUpdater, URNFilter urnFilter,
SpamServices spamServices) {
super(networkManager, queryRequestFactory, queryHandlerFactory,
onDemandUnicaster, headPongFactory, pingReplyFactory,
connectionManager, forMeReplyHandler, queryUnicaster,
fileManager, contentManager, dhtManager, uploadManager,
downloadManager, udpService, searchResultHandler,
socketsManager, hostCatcher, queryReplyFactory, staticMessages,
messageDispatcher, multicastService, queryDispatcher,
activityCallback, connectionServices, applicationServices,
backgroundExecutor, pongCacher, simppManager, updateHandler,
guidMapManager, udpReplyHandlerCache,
inspectionRequestHandlerFactory, udpCrawlerPingHandlerFactory,
pingRequestFactory, messageHandlerBinder, oobHandlerFactory,
MACCalculatorRepositoryManager, limeACKHandler,
outgoingQueryReplyFactory, qrpUpdater, urnFilter,
spamServices);
this.statistics = statistics;
this.replyNumberVendorMessageFactory = replyNumberVendorMessageFactory;
this.sharedFilesKeywordIndex = sharedFilesKeywordIndex;
}
/**
* Responds to a Gnutella ping with cached pongs. This does special handling
* for both "heartbeat" pings that were sent to ensure that the connection
* is still live as well as for pings from a crawler.
*
* @param ping the <tt>PingRequest</tt> to respond to
* @param handler the <tt>ReplyHandler</tt> to send any pongs to
*/
@Override
protected void respondToPingRequest(PingRequest ping,
ReplyHandler handler) {
// The ping has already been hopped
byte hops = ping.getHops();
byte ttl = ping.getTTL();
// If hops + ttl > 2 this is not a heartbeat or a crawler ping. Check
// if we can accept an incoming connection for old-style unrouted
// connections, ultrapeers, or leaves.
if(hops + ttl > 2 && !connectionManager.allowAnyConnection()) {
if(LOG.isDebugEnabled())
LOG.debug("Not responding to ordinary ping (1) " + ping);
return;
}
// Only send pongs for ourself if we have a valid address & port.
if(networkManager.isIpPortValid()) {
// If hops == 1 and ttl == 1 this is a crawler ping. We don't send
// our own pong since the crawler already knows our address, but
// we send the addresses of our leaves.
if(hops == 1 && ttl == 1) {
if(LOG.isDebugEnabled())
LOG.debug("Responding to crawler ping " + ping);
handleCrawlerPing(ping, handler);
return;
}
// If hops == 1 and ttl == 0 this is a heartbeat ping. Bypass
// the pong caching code and reply. TODO: why does this require a
// valid address and port? Don't we want to respond to heartbeat
// pings on LAN connections?
if(ping.isHeartbeat()) {
if(LOG.isDebugEnabled())
LOG.debug("Responding to heartbeat ping " + ping);
sendPingReply(pingReplyFactory.create(ping.getGUID(), (byte)1),
handler);
return;
}
// TODO: why would hops + ttl be less than 3 at this point? We've
// already dealt with crawler and heartbeat pings. And why is the
// ttl of the pong greater than the hop count of the ping?
int newTTL = hops+1;
if ( (hops+ttl) <=2)
newTTL = 1;
// send our own pong if we have free slots or if our average
// daily uptime is more than 1/2 hour
if(connectionManager.hasFreeSlots() ||
statistics.calculateDailyUptime() > 60*30) {
if(LOG.isDebugEnabled())
LOG.debug("Responding to ordinary ping " + ping);
PingReply pr =
pingReplyFactory.create(ping.getGUID(), (byte)newTTL);
sendPingReply(pr, handler);
} else {
if(LOG.isDebugEnabled())
LOG.debug("Not responding to ordinary ping (2) " + ping);
}
}
List<PingReply> pongs = pongCacher.get().getBestPongs(ping.getLocale());
byte[] guid = ping.getGUID();
InetAddress pingerIP = handler.getInetAddress();
for(PingReply pr : pongs) {
if(pr.getInetAddress().equals(pingerIP))
continue;
sendPingReply(pingReplyFactory.mutateGUID(pr, guid), handler);
}
}
/**
* Responds to a ping request received over a UDP port. This is
* handled differently from all other ping requests. Instead of
* responding with cached pongs, we respond with a pong from our node.
*
* @param request the <tt>PingRequest</tt> to service
* @param addr the <tt>InetSocketAddress</tt> containing the IP
* and port of the client node
* @param handler the <tt>ReplyHandler</tt> that should handle any
* replies
*/
@Override
protected void respondToUDPPingRequest(PingRequest request,
InetSocketAddress addr,
ReplyHandler handler) {
if(!networkManager.isIpPortValid())
return;
List<IpPort> dhthosts = Collections.emptyList();
int maxHosts = ConnectionSettings.NUM_RETURN_PONGS.getValue();
if (request.requestsDHTIPP() && dhtManager.isRunning()) {
dhthosts = dhtManager.getActiveDHTNodes(maxHosts);
}
int numDHTHosts = dhthosts.size();
byte[] data = request.getSupportsCachedPongData();
Collection<IpPort> gnuthosts = Collections.emptyList();
if(data != null){
boolean isUltrapeer =
data.length >= 1 &&
(data[0] & PingRequest.SCP_ULTRAPEER_OR_LEAF_MASK) ==
PingRequest.SCP_ULTRAPEER;
int dhtFraction = ConnectionSettings.DHT_TO_GNUT_HOSTS_PONG.getValue();
int maxDHTHosts = Math.round(((float)dhtFraction/100)*maxHosts);
gnuthosts = connectionServices.getPreferencedHosts(
isUltrapeer,
request.getLocale(),
maxHosts - Math.min(numDHTHosts, maxDHTHosts));
//remove extra dht hosts
dhthosts = dhthosts.subList(0, Math.min(numDHTHosts,maxHosts - gnuthosts.size()));
}
PingReply reply;
if (request.requestsIP())
reply = pingReplyFactory.create(request.getGUID(), (byte)1, new IpPortImpl(addr), gnuthosts, dhthosts);
else
reply = pingReplyFactory.create(request.getGUID(), (byte)1, gnuthosts, dhthosts);
if(LOG.isDebugEnabled()) {
LOG.debug("Responding to UDPPingRequest "+(request.requestsDHTIPP()?"with DHTIPP ":"") +
"from : "+ addr + " with Gnutella hosts: \n"+ gnuthosts
+ "\n and DHT hosts: \n" + dhthosts);
}
sendPingReply(reply, handler);
}
/**
* Responds to a crawler ping with hops 0 and ttl 2 (before hopping) by
* sending a pong for each leaf. Ultrapeer neighbours will send their own
* pongs when the ping is forwarded to them. TODO: where is the ping
* forwarded to them?
* @param ping the ping request received
* @param handler the <tt>ReplyHandler</tt> that should handle any
* replies
*/
private void handleCrawlerPing(PingRequest ping, ReplyHandler handler) {
//send the pongs for leaves
List<RoutedConnection> leafConnections =
connectionManager.getInitializedClientConnections();
for(RoutedConnection connection : leafConnections) {
//create the pong for this connection
PingReply pr =
pingReplyFactory.createExternal(ping.getGUID(), (byte)2,
connection.getPort(),
connection.getInetAddress().getAddress(),
false);
//hop the message, as it is ideally coming from the connected host
pr.hop();
sendPingReply(pr, handler);
}
}
@Override
protected boolean respondToQueryRequest(QueryRequest queryRequest,
byte[] clientGUID,
ReplyHandler handler) {
//Only respond if we understand the actual feature, if it had a feature.
if(!FeatureSearchData.supportsFeature(queryRequest.getFeatureSelector()))
return false;
// Only send results if we're not busy. Note that this ignores
// queue slots -- we're considered busy if all of our "normal"
// slots are full. This allows some spillover into our queue that
// is necessary because we're always returning more total hits than
// we have slots available.
if(!uploadManager.mayBeServiceable() ) {
ignoredBusy.countMessage(queryRequest);
return false;
}
// Ensure that we have a valid IP & Port before we send the response.
// Otherwise the QueryReply will fail on creation.
if(!networkManager.isIpPortValid())
return false;
// Run the local query
Response[] responses = sharedFilesKeywordIndex.query(queryRequest);
if (responses.length == 0)
falsePositives.countMessage(queryRequest);
return sendResponses(responses, queryRequest, handler);
}
private boolean sendResponses(Response[] responses, QueryRequest query,
ReplyHandler handler) {
// if either there are no responses or, the
// response array came back null for some reason,
// exit this method
if ( (responses == null) || ((responses.length < 1)) )
return false;
if (!uploadManager.isServiceable()) {
notServiced.countMessage(query);
return false;
}
// Here we can do a couple of things - if the query wants
// out-of-band replies we should do things differently. else just
// send it off as usual. only send out-of-band if you can
// receive solicited udp AND not servicing too many
// uploads AND not connected to the originator of the query
if (query.desiresOutOfBandReplies() &&
!isConnectedTo(query, handler) &&
networkManager.canReceiveSolicited() &&
NetworkUtils.isValidAddressAndPort(query.getReplyAddress(), query.getReplyPort())) {
// send the replies out-of-band - we need to
// 1) buffer the responses
// 2) send a ReplyNumberVM with the number of responses
if (limeAckHandler.get().bufferResponsesForLaterDelivery(query, responses)) {
// special out of band handling....
InetAddress addr = null;
try {
addr = InetAddress.getByName(query.getReplyAddress());
} catch (UnknownHostException uhe) {}
final int port = query.getReplyPort();
if(addr != null) {
// send a ReplyNumberVM to the host - he'll ACK you if he
// wants the whole shebang
int resultCount =
(responses.length > 255) ? 255 : responses.length;
final ReplyNumberVendorMessage vm = query.desiresOutOfBandRepliesV3() ?
replyNumberVendorMessageFactory.createV3ReplyNumberVendorMessage(new GUID(query.getGUID()), resultCount) :
replyNumberVendorMessageFactory.createV2ReplyNumberVendorMessage(new GUID(query.getGUID()), resultCount);
udpService.send(vm, addr, port);
if (MessageSettings.OOB_REDUNDANCY.getValue() &&
query.desiresOutOfBandRepliesV3()) {
final InetAddress addrf = addr;
backgroundExecutor.schedule(new Runnable() {
public void run () {
udpService.send(vm, addrf, port);
}
}, 100, TimeUnit.MILLISECONDS);
}
return true;
}
} else {
// else i couldn't buffer the responses due to busy-ness, oh, scrap
// them.....
return false;
}
}
// send the replies in-band
// -----------------------------
//convert responses to QueryReplies
Iterable<QueryReply> iterable = responsesToQueryReplies(responses,
query);
//send the query replies
try {
for(QueryReply queryReply : iterable)
sendQueryReply(queryReply);
} catch (IOException e) {
// if there is an error, do nothing..
}
// -----------------------------
return true;
}
/** Returns whether or not we are connected to the originator of this query.
* PRE: assumes query.desiresOutOfBandReplies == true
*/
private final boolean isConnectedTo(QueryRequest query,
ReplyHandler handler) {
return query.matchesReplyAddress(handler.getInetAddress().getAddress());
}
}