package com.limegroup.gnutella;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.InetSocketAddress;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.Collections;
import java.util.Collection;
import com.limegroup.gnutella.messages.FeatureSearchData;
import com.limegroup.gnutella.messages.IPPortCombo;
import com.limegroup.gnutella.messages.PingReply;
import com.limegroup.gnutella.messages.PingRequest;
import com.limegroup.gnutella.messages.QueryReply;
import com.limegroup.gnutella.messages.QueryRequest;
import com.limegroup.gnutella.messages.vendor.ReplyNumberVendorMessage;
import com.limegroup.gnutella.settings.ChatSettings;
import com.limegroup.gnutella.settings.ConnectionSettings;
import com.limegroup.gnutella.statistics.ReceivedMessageStat;
import com.limegroup.gnutella.statistics.RoutedQueryStat;
import com.limegroup.gnutella.util.DataUtils;
import com.limegroup.gnutella.util.NetworkUtils;
import com.limegroup.gnutella.util.IpPort;
import com.limegroup.gnutella.xml.LimeXMLDocumentHelper;
import com.limegroup.gnutella.xml.LimeXMLUtils;
/**
* This class is the message routing implementation for TCP messages.
*/
public class StandardMessageRouter extends MessageRouter {
/**
* 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
*/
protected void respondToPingRequest(PingRequest ping,
ReplyHandler handler) {
//If this wasn't a handshake or crawler ping, check if we can accept
//incoming connection for old-style unrouted connections, ultrapeers, or
//leaves. TODO: does this mean leaves always respond to pings?
int hops = (int)ping.getHops();
int ttl = (int)ping.getTTL();
if ( (hops+ttl > 2)
&& !_manager.allowAnyConnection())
return;
// Only send pongs for ourself if we have a valid address & port.
if(NetworkUtils.isValidAddress(RouterService.getAddress()) &&
NetworkUtils.isValidPort(RouterService.getPort())) {
//SPECIAL CASE: for crawler ping
// TODO:: this means that we can never send TTL=2 pings without
// them being interpreted as from the crawler!!
if(hops ==1 && ttl==1) {
handleCrawlerPing(ping, handler);
return;
//Note that the while handling crawler ping, we dont send our
//own pong, as that is unnecessary, since crawler already has
//our address.
}
// handle heartbeat pings specially -- bypass pong caching code
if(ping.isHeartbeat()) {
sendPingReply(PingReply.create(ping.getGUID(), (byte)1),
handler);
return;
}
//send its own ping in all the cases
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(RouterService.getConnectionManager().hasFreeSlots() ||
Statistics.instance().calculateDailyUptime() > 60*30) {
PingReply pr =
PingReply.create(ping.getGUID(), (byte)newTTL);
sendPingReply(pr, handler);
}
}
List pongs = PongCacher.instance().getBestPongs(ping.getLocale());
Iterator iter = pongs.iterator();
byte[] guid = ping.getGUID();
InetAddress pingerIP = handler.getInetAddress();
while(iter.hasNext()) {
PingReply pr = (PingReply)iter.next();
if(pr.getInetAddress().equals(pingerIP))
continue;
sendPingReply(pr.mutateGUID(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
*/
protected void respondToUDPPingRequest(PingRequest request,
InetSocketAddress addr,
ReplyHandler handler) {
if(!RouterService.isIpPortValid())
return;
IpPort ipport = null;
if (request.requestsIP()) {
try {
ipport = new IPPortCombo(
addr.getAddress().getHostAddress(),
addr.getPort());
} catch(IOException tooBad) { }
}
byte[] data = request.getSupportsCachedPongData();
Collection hosts = Collections.EMPTY_LIST;
if(data != null) {
boolean isUltrapeer =
data.length >= 1 &&
(data[0] & PingRequest.SCP_ULTRAPEER_OR_LEAF_MASK) ==
PingRequest.SCP_ULTRAPEER;
hosts = RouterService.getPreferencedHosts(
isUltrapeer,
request.getLocale(),
ConnectionSettings.NUM_RETURN_PONGS.getValue());
}
PingReply reply;
if (ipport != null)
reply = PingReply.create(request.getGUID(), (byte)1, ipport, hosts);
else
reply = PingReply.create(request.getGUID(), (byte)1, hosts);
sendPingReply(reply, handler);
}
/**
* Handles the crawler ping of Hops=0 & TTL=2, by sending pongs
* corresponding to all its leaves
* @param m The ping request received
* @param handler the <tt>ReplyHandler</tt> that should handle any
* replies
*/
private void handleCrawlerPing(PingRequest m, ReplyHandler handler) {
//TODO: why is this any different than the standard pong? In other
//words, why no ultrapong marking, proper address calculation, etc?
//send the pongs for leaves
List /*<ManagedConnection>*/ leafConnections
= _manager.getInitializedClientConnections();
for(Iterator iterator = leafConnections.iterator();
iterator.hasNext();) {
//get the next connection
ManagedConnection connection = (ManagedConnection)iterator.next();
//create the pong for this connection
PingReply pr =
PingReply.createExternal(m.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);
}
//pongs for the neighbors will be sent by neighbors themselves
//as ping will be broadcasted to them (since TTL=2)
}
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;
if (queryRequest.isWhatIsNewRequest())
ReceivedMessageStat.WHAT_IS_NEW_QUERY_MESSAGES.incrementStat();
// 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(!RouterService.getUploadManager().isServiceable() ) {
return false;
}
// Ensure that we have a valid IP & Port before we send the response.
// Otherwise the QueryReply will fail on creation.
if( !NetworkUtils.isValidPort(RouterService.getPort()) ||
!NetworkUtils.isValidAddress(RouterService.getAddress()))
return false;
// Run the local query
Response[] responses =
RouterService.getFileManager().query(queryRequest);
if( RouterService.isShieldedLeaf() && queryRequest.isTCP() ) {
if( responses != null && responses.length > 0 )
RoutedQueryStat.LEAF_HIT.incrementStat();
else
RoutedQueryStat.LEAF_FALSE_POSITIVE.incrementStat();
}
return sendResponses(responses, queryRequest, handler);
}
//This method needs to be public because the Peer-Server code uses it.
public 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;
// 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) &&
RouterService.canReceiveSolicited() &&
RouterService.getUploadManager().isServiceable() &&
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 (bufferResponsesForLaterDelivery(query, responses)) {
// special out of band handling....
InetAddress addr = null;
try {
addr = InetAddress.getByName(query.getReplyAddress());
} catch (UnknownHostException uhe) {}
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;
ReplyNumberVendorMessage vm =
new ReplyNumberVendorMessage(new GUID(query.getGUID()),
resultCount);
UDPService.instance().send(vm, addr, port);
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
Iterator /*<QueryReply>*/iterator=responsesToQueryReplies(responses,
query);
//send the query replies
try {
while(iterator.hasNext()) {
QueryReply queryReply = (QueryReply)iterator.next();
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());
}
/**
* Creates a <tt>List</tt> of <tt>QueryReply</tt> instances with
* compressed XML data, if requested.
*
* @return a new <tt>List</tt> of <tt>QueryReply</tt> instances
*/
protected List createQueryReply(byte[] guid, byte ttl,
long speed, Response[] res,
byte[] clientGUID,
boolean busy, boolean uploaded,
boolean measuredSpeed,
boolean isFromMcast,
boolean isFWTransfer) {
List queryReplies = new ArrayList();
QueryReply queryReply = null;
// pick the right address & port depending on multicast & fwtrans
// if we cannot find a valid address & port, exit early.
int port = -1;
byte[] ip = null;
// first try using multicast addresses & ports, but if they're
// invalid, fallback to non multicast.
if(isFromMcast) {
ip = RouterService.getNonForcedAddress();
port = RouterService.getNonForcedPort();
if(!NetworkUtils.isValidPort(port) ||
!NetworkUtils.isValidAddress(ip))
isFromMcast = false;
}
if(!isFromMcast) {
// see if we have a valid FWTrans address. if not, fall back.
if(isFWTransfer) {
port = UDPService.instance().getStableUDPPort();
ip = RouterService.getExternalAddress();
if(!NetworkUtils.isValidAddress(ip)
|| !NetworkUtils.isValidPort(port))
isFWTransfer = false;
}
// if we still don't have a valid address here, exit early.
if(!isFWTransfer) {
ip = RouterService.getAddress();
port = RouterService.getPort();
if(!NetworkUtils.isValidAddress(ip) ||
!NetworkUtils.isValidPort(port))
return Collections.EMPTY_LIST;
}
}
// get the xml collection string...
String xmlCollectionString =
LimeXMLDocumentHelper.getAggregateString(res);
if (xmlCollectionString == null)
xmlCollectionString = "";
byte[] xmlBytes = null;
try {
xmlBytes = xmlCollectionString.getBytes("UTF-8");
} catch(UnsupportedEncodingException ueex) {//no support for utf-8??
//all implementations of java must support utf8 encoding
//here we will allow this QueryReply to be sent out
//with xml being empty rather than not allowing the
//Query to be sent out
//therefore we won't throw a IllegalArgumentException but we will
//show it so the error will be sent to Bug servlet
ErrorService.error
(ueex,
"encountered UnsupportedEncodingException in creation of QueryReply : xmlCollectionString : "
+ xmlCollectionString);
}
// get the *latest* push proxies if we have not accepted an incoming
// connection in this session
boolean notIncoming = !RouterService.acceptedIncomingConnection();
Set proxies =
(notIncoming ?
_manager.getPushProxies() : null);
// it may be too big....
if (xmlBytes.length > QueryReply.XML_MAX_SIZE) {
// ok, need to partition responses up once again and send out
// multiple query replies.....
List splitResps = new LinkedList();
splitAndAddResponses(splitResps, res);
while (!splitResps.isEmpty()) {
Response[] currResps = (Response[]) splitResps.remove(0);
String currXML =
LimeXMLDocumentHelper.getAggregateString(currResps);
byte[] currXMLBytes = null;
try {
currXMLBytes = currXML.getBytes("UTF-8");
} catch(UnsupportedEncodingException ueex) {
//all implementations of java must support utf8 encoding
//so if we get here there was something really wrong
//we will show the error but treat as if the currXML was
//empty (see the try catch for uee earlier)
ErrorService.error
(ueex,
"encountered UnsupportedEncodingException : currXML "
+ currXML);
currXMLBytes = "".getBytes();
}
if ((currXMLBytes.length > QueryReply.XML_MAX_SIZE) &&
(currResps.length > 1))
splitAndAddResponses(splitResps, currResps);
else {
// create xml bytes if possible...
byte[] xmlCompressed = null;
if ((currXML != null) && (!currXML.equals("")))
xmlCompressed = LimeXMLUtils.compress(currXMLBytes);
else //there is no XML
xmlCompressed = DataUtils.EMPTY_BYTE_ARRAY;
// create the new queryReply
queryReply = new QueryReply(guid, ttl, port, ip, speed,
currResps, _clientGUID,
xmlCompressed, notIncoming,
busy, uploaded,
measuredSpeed,
ChatSettings.CHAT_ENABLED.getValue(),
isFromMcast, isFWTransfer,
proxies);
queryReplies.add(queryReply);
}
}
}
else { // xml is small enough, no problem.....
// get xml bytes if possible....
byte[] xmlCompressed = null;
if (xmlCollectionString!=null && !xmlCollectionString.equals(""))
xmlCompressed =
LimeXMLUtils.compress(xmlBytes);
else //there is no XML
xmlCompressed = DataUtils.EMPTY_BYTE_ARRAY;
// create the new queryReply
queryReply = new QueryReply(guid, ttl, port, ip, speed, res,
_clientGUID, xmlCompressed,
notIncoming, busy, uploaded,
measuredSpeed,
ChatSettings.CHAT_ENABLED.getValue(),
isFromMcast, isFWTransfer,
proxies);
queryReplies.add(queryReply);
}
return queryReplies;
}
/** @return Simply splits the input array into two (almost) equally sized
* arrays.
*/
private Response[][] splitResponses(Response[] in) {
int middle = in.length/2;
Response[][] retResps = new Response[2][];
retResps[0] = new Response[middle];
retResps[1] = new Response[in.length-middle];
for (int i = 0; i < middle; i++)
retResps[0][i] = in[i];
for (int i = 0; i < (in.length-middle); i++)
retResps[1][i] = in[i+middle];
return retResps;
}
private void splitAndAddResponses(List addTo, Response[] toSplit) {
Response[][] splits = splitResponses(toSplit);
addTo.add(splits[0]);
addTo.add(splits[1]);
}
}