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.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.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) ) 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; } } } /** * 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 QueryReply.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); } 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 void handlePingReply(PingReply pingReply, ReplyHandler receivingConnection) { //We override the super's method so the receiving connection's //statistics are updated whether or not this is for me. if(receivingConnection instanceof ManagedConnection) { ManagedConnection mc = (ManagedConnection)receivingConnection; mc.updateHorizonStats(pingReply); } super.handlePingReply(pingReply, receivingConnection); } /** 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, 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, 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]); } }