package com.limegroup.gnutella; import java.io.IOException; import java.io.InterruptedIOException; import java.io.OutputStream; import java.net.InetAddress; import java.net.Socket; import java.util.Arrays; import java.util.Hashtable; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.Set; import com.limegroup.gnutella.connection.MessageQueue; import com.limegroup.gnutella.connection.PriorityMessageQueue; import com.limegroup.gnutella.connection.SimpleMessageQueue; import com.limegroup.gnutella.filters.SpamFilter; import com.limegroup.gnutella.handshaking.HandshakeResponder; import com.limegroup.gnutella.handshaking.HeaderNames; import com.limegroup.gnutella.handshaking.LeafHandshakeResponder; import com.limegroup.gnutella.handshaking.LeafHeaders; import com.limegroup.gnutella.messages.*; import com.limegroup.gnutella.messages.vendor.*; import com.limegroup.gnutella.search.SearchResultHandler; import com.limegroup.gnutella.security.User; import com.limegroup.gnutella.settings.ConnectionSettings; import com.limegroup.gnutella.util.ManagedThread; import com.limegroup.gnutella.util.StringUtils; import com.util.LOG; /** * A Connection managed by a ConnectionManager. Includes a loopForMessages * method that runs forever (or until an IOException occurs), receiving and * replying to Gnutella messages. ManagedConnection is only instantiated * through a ConnectionManager.<p> * * ManagedConnection provides a sophisticated message buffering mechanism. When * you call send(Message), the message is not actually delivered to the socket; * instead it buffered in an application-level buffer. Periodically, a thread * reads messages from the buffer, writes them to the network, and flushes the * socket buffers. This means that there is no need to manually call flush(). * Furthermore, ManagedConnection provides a simple form of flow control. If * messages are queued faster than they can be written to the network, they are * dropped in the following order: PingRequest, PingReply, QueryRequest, * QueryReply, and PushRequest. See the implementation notes below for more * details.<p> * * All ManagedConnection's have two underlying spam filters: a personal filter * (controls what I see) and a route filter (also controls what I pass along to * others). See SpamFilter for a description. These filters are configured by * the properties in the SettingsManager, but you can change them with * setPersonalFilter and setRouteFilter.<p> * * ManagedConnection maintain a large number of statistics, such as the current * bandwidth for upstream & downstream. ManagedConnection doesn't quite fit the * BandwidthTracker interface, unfortunately. On the query-routing3-branch and * pong-caching CVS branches, these statistics have been bundled into a single * object, reducing the complexity of ManagedConnection.<p> * * ManagedConnection also takes care of various VendorMessage handling, in * particular Hops Flow, UDP ConnectBack, and TCP ConnectBack. See * handleVendorMessage().<p> * * This class implements ReplyHandler to route pongs and query replies that * originated from it.<p> */ public class ManagedConnection extends Connection implements ReplyHandler, PushProxyInterface { /** * The time to wait between route table updates for leaves, * in milliseconds. */ private long LEAF_QUERY_ROUTE_UPDATE_TIME = 1000*60*5; //5 minutes /** * The time to wait between route table updates for Ultrapeers, * in milliseconds. */ private long ULTRAPEER_QUERY_ROUTE_UPDATE_TIME = 1000*60; //1 minute /** The timeout to use when connecting, in milliseconds. This is NOT used * for bootstrap servers. */ private static final int CONNECT_TIMEOUT = 6000; //6 seconds /** The maximum number of times ManagedConnection instances should send UDP * ConnectBack requests. */ private static final int MAX_UDP_CONNECT_BACK_ATTEMPTS = 15; /** The maximum number of times ManagedConnection instances should send TCP * ConnectBack requests. */ private static final int MAX_TCP_CONNECT_BACK_ATTEMPTS = 10; /** Handle to the <tt>ConnectionManager</tt>. */ private ConnectionManager _manager; /** Filter for filtering out messages that are considered spam. */ private volatile SpamFilter _routeFilter = SpamFilter.newRouteFilter(); private volatile SpamFilter _personalFilter = SpamFilter.newPersonalFilter(); /* * IMPLEMENTATION NOTE: this class uses the SACHRIFC algorithm described at * http://www.limewire.com/developer/sachrifc.txt. The basic idea is to use * one queue for each message type. Messages are removed from the queue in * a biased round-robin fashion. This prioritizes some messages types while * preventing any one message type from dominating traffic. Query replies * are further prioritized by "GUID volume", i.e., the number of bytes * already routed for that GUID. Other messages are sorted by time and * removed in a LIFO [sic] policy. This, coupled with timeouts, reduces * latency. */ /** A lock for QRP activity on this connection */ private final Object QRP_LOCK=new Object(); /** A lock to protect _outputQueue. */ private Object _outputQueueLock=new Object(); /** The producer's queues, one priority per mesage type. * INVARIANT: _outputQueue.length==PRIORITIES * LOCKING: obtain _outputQueueLock. */ private MessageQueue[] _outputQueue=new MessageQueue[PRIORITIES]; /** The number of queued messages. Maintained for performance. * INVARIANT: _queued==sum of _outputQueue[i].size() * LOCKING: obtain _outputQueueLock */ private int _queued=0; /** True if the OutputRunner died. For testing only. */ private boolean _runnerDied=false; /** The priority of the last message added to _outputQueue. This is an * optimization to keep OutputRunner from iterating through all priorities. * This value is only a hint and can be legally set to any priority. Hence * no locking is necessary. Package-access for testing purposes only. */ int _lastPriority=0; /** The size of the queue per priority. Larger values tolerate larger bursts * of producer traffic, though they waste more memory. This queue is * slightly larger than the standard to accomodate higher priority * messages, such as queries and query hits. */ private static final int BIG_QUEUE_SIZE = 100; /** The size of the queue per priority. Larger values tolerate larger bursts * of producer traffic, though they waste more memory. This queue is * slightly smaller so that we don't waste too much memory on lower * priority messages. */ private static final int QUEUE_SIZE = 1; /** The max time to keep reply messages and pushes in the queues, in * milliseconds. */ private static int BIG_QUEUE_TIME=10*1000; /** The max time to keep queries, pings, and pongs in the queues, in * milliseconds. Package-access for testing purposes only! */ static int QUEUE_TIME=5*1000; /** The number of different priority levels. */ private static final int PRIORITIES = 8; /** Names for each priority. "Other" includes QRP messages and is NOT * reordered. These numbers do NOT translate directly to priorities; * that's determined by the cycle fields passed to MessageQueue. */ private static final int PRIORITY_WATCHDOG=0; private static final int PRIORITY_PUSH=1; private static final int PRIORITY_QUERY_REPLY=2; private static final int PRIORITY_QUERY=3; //TODO: add requeries private static final int PRIORITY_PING_REPLY=4; private static final int PRIORITY_PING=5; private static final int PRIORITY_OTHER=6; /** * Separate priority for queries that we originate. These are very * high priority because we don't want to drop queries that are * originating from us -- we want to largely bypass the message * queues when we are first sending a query out on the network. */ private static final int PRIORITY_OUR_QUERY=7; /** * The amount of time to wait for a handshake ping in reject connections, in * milliseconds. */ private static final int REJECT_TIMEOUT=500; //0.5 sec /** * The number of messages received. This messages that are eventually * dropped. This stat is synchronized by _outputQueueLock; */ private int _numMessagesSent; /** * The number of messages received. This includes messages that are * eventually dropped. This stat is not synchronized because receiving * is not thread-safe; callers are expected to make sure only one thread * at a time is calling receive on a given connection. */ private int _numMessagesReceived; /** * The number of messages received on this connection either filtered out * or dropped because we didn't know how to route them. */ private int _numReceivedMessagesDropped; /** * The number of messages I dropped because the * output queue overflowed. This happens when the remote host * cannot receive packets as quickly as I am trying to send them. * No synchronization is necessary. */ private int _numSentMessagesDropped; /** * _lastSent/_lastSentDropped and _lastReceived/_lastRecvDropped the values * of _numMessagesSent/_numSentMessagesDropped and * _numMessagesReceived/_numReceivedMessagesDropped at the last call to * getPercentDropped. LOCKING: These are synchronized by this; * finer-grained schemes could be used. */ private int _lastReceived; private int _lastRecvDropped; private int _lastSent; private int _lastSentDropped; /** The next time I should send a query route table to this connection. */ private long _nextQRPForwardTime; /** * The bandwidth trackers for the up/downstream. * These are not synchronized and not guaranteed to be 100% accurate. */ private BandwidthTrackerImpl _upBandwidthTracker= new BandwidthTrackerImpl(); private BandwidthTrackerImpl _downBandwidthTracker= new BandwidthTrackerImpl(); /** True iff this should not be policed by the ConnectionWatchdog, e.g., * because this is a connection to a Clip2 reflector. */ private boolean _isKillable=true; /** * The domain to which this connection is authenticated */ private Set _domains = null; /** Use this if a HopsFlowVM instructs us to stop sending queries below * this certain hops value.... */ private volatile int softMaxHops = -1; /** Use this if a PushProxyAck is received for this MC meaning the remote * Ultrapeer can serve as a PushProxy */ private InetAddress pushProxyAddr = null; /** Use this if a PushProxyAck is received for this MC meaning the remote * Ultrapeer can serve as a PushProxy */ private int pushProxyPort = -1; /** The class wide static counter for the number of udp connect back * request sent. */ private static int _numUDPConnectBackRequests = 0; /** The class wide static counter for the number of tcp connect back * request sent. */ private static int _numTCPConnectBackRequests = 0; /** * Holds the mappings of GUIDs that are being proxied. * We want to construct this lazily.... * GUID.TimedGUID -> GUID * OOB Proxy GUID - > Original GUID */ private Map _guidMap = null; /** * The max lifetime of the GUID (10 minutes). */ private static long TIMED_GUID_LIFETIME = 10 * 60 * 1000; /** * Whether or not horizon counting is enabled from this connection. */ private boolean _horizonEnabled = true; // for bye message test private String _byeReason = null; private byte[] _byeCode = null; public void setByeReason(byte[] code, String reason){ _byeCode = code; _byeReason = reason; } /** * Creates a new outgoing connection to the specified host on the * specified port. * * @param host the address of the host we're connecting to * @param port the port the host is listening on */ public ManagedConnection(String host, int port) { this(host, port, (Properties)(new LeafHeaders(host)), (HandshakeResponder)new LeafHandshakeResponder(host)); } /** * More customizable constructor used for testing. */ static ManagedConnection createTestConnection(String host, int port, Properties props, HandshakeResponder responder) { return new ManagedConnection(host, port, props, responder); } /** * Creates a new <tt>ManagedConnection</tt> with the specified * handshake classes and the specified host and port. */ private ManagedConnection(String host, int port, Properties props, HandshakeResponder responder) { super(host, port, props, responder); _manager = RouterService.getConnectionManager(); } /** * Creates an incoming connection. * ManagedConnections should only be constructed within ConnectionManager. * @requires the word "GNUTELLA " and nothing else has just been read * from socket * @effects wraps a connection around socket and does the rest of the * Gnutella handshake. */ ManagedConnection(Socket socket) { super(socket, (HandshakeResponder)(new LeafHandshakeResponder( socket.getInetAddress().getHostAddress()))); _manager = RouterService.getConnectionManager(); } /** * Override of receive to do ConnectionManager stats and to properly shut * down the connection on IOException */ public Message receive() throws IOException, BadPacketException { Message m = null; try { m = super.receive(); } catch(IOException e) { if( _manager != null ) { //LOG.logSp("remove in receive" + ManagedConnection.this.getAddress() + e.getMessage()); _manager.remove(this); } throw e; } // record received message in stats addReceived(); return m; } /** * Override of receive to do MessageRouter stats and to properly shut * down the connection on IOException */ public Message receive(int timeout) throws IOException, BadPacketException, InterruptedIOException { Message m = null; try { m = super.receive(timeout); } catch(InterruptedIOException ioe) { //we read nothing in this timeframe, //do not remove, just rethrow. throw ioe; } catch(IOException e) { if( _manager != null ) { //LOG.logSp("remove in receive timeout" + ManagedConnection.this.getAddress() + e.getMessage()); _manager.remove(this); } throw e; } // record received message in stats addReceived(); return m; } ////////////////////// Sending, Outgoing Flow Control ////////////////////// /** * Sends a message. This overrides does extra buffering so that Messages * are dropped if the socket gets backed up. Will remove any extended * payloads if the receiving connection does not support GGGEP. Also * updates MessageRouter stats.<p> * * This method IS thread safe. Multiple threads can be in a send call * at the same time for a given connection. * * @requires this is fully constructed * @modifies the network underlying this * @effects send m on the network. Throws IOException if the connection * is already closed. This is thread-safe and guaranteed not to block. */ public void send(Message m) { send(m, calculatePriority(m)); } /** * This is a specialized send method for queries that we originate, * either from ourselves directly, or on behalf of one of our leaves * when we're an Ultrapeer. These queries have a special sending * queue of their own and are treated with a higher priority. * * @param query the <tt>QueryRequest</tt> to send */ public void originateQuery(QueryRequest query) { send(query, PRIORITY_OUR_QUERY); } /** * Sends the message with the specified, pre-calculated priority. * * @param m the <tt>Message</tt> to send * @param priority the priority to send the message with */ private void send(Message m, int priority) { if (! supportsGGEP()) m=m.stripExtendedPayload(); // if Hops Flow is in effect, and this is a QueryRequest, and the // hoppage is too biggage, discardage time... int smh = softMaxHops; if ((smh > -1) && (m instanceof QueryRequest) && (m.getHops() >= smh)) return; repOk(); Assert.that(_outputQueue!=null, "Connection not initialized"); synchronized (_outputQueueLock) { _numMessagesSent++; _outputQueue[priority].add(m); int dropped=_outputQueue[priority].resetDropped(); addSentDropped(dropped); _queued+=1-dropped; _lastPriority=priority; _outputQueueLock.notify(); } repOk(); } /** * Utility method for adding dropped message data. * * @param dropped the number of dropped messages to add */ private void addSentDropped(int dropped) { _numSentMessagesDropped += dropped; } /** * Increments the number of received messages that have been dropped. */ public void addReceivedDropped() { _numReceivedMessagesDropped++; } /** * Increments the stat for the number of messages received. */ public void addReceived() { _numMessagesReceived++; } /** * Returns the send priority for the given message, with higher number for * higher priorities. TODO: this method will eventually be moved to * MessageRouter and account for number of reply bytes. */ private int calculatePriority(Message m) { byte opcode=m.getFunc(); switch (opcode) { case Message.F_QUERY: return PRIORITY_QUERY; case Message.F_QUERY_REPLY: return PRIORITY_QUERY_REPLY; case Message.F_PING_REPLY: return (m.getHops()==0 && m.getTTL()<=2) ? PRIORITY_WATCHDOG : PRIORITY_PING_REPLY; case Message.F_PING: return (m.getHops()==0 && m.getTTL()==1) ? PRIORITY_WATCHDOG : PRIORITY_PING; case Message.F_PUSH: return PRIORITY_PUSH; default: return PRIORITY_OTHER; //includes QRP Tables } } /** * Does nothing. Since this automatically takes care of flushing output * buffers, there is nothing to do. Note that flush() does NOT block for * TCP buffers to be emptied. */ public void flush() throws IOException { } /** * Builds queues and starts the OutputRunner. This is intentionally not * in initialize(), as we do not want to create the queues and start * the OutputRunner for reject connections. */ public void buildAndStartQueues() { //Instantiate queues. TODO: for ultrapeer->leaf connections, we can //save a fair bit of memory by not using buffering at all. But this //requires the CompositeMessageQueue class from nio-branch. _outputQueue[PRIORITY_WATCHDOG] //LIFO, no timeout or priorities = new SimpleMessageQueue(1, Integer.MAX_VALUE, BIG_QUEUE_SIZE, true); _outputQueue[PRIORITY_PUSH] = new PriorityMessageQueue(6, BIG_QUEUE_TIME, BIG_QUEUE_SIZE); _outputQueue[PRIORITY_QUERY_REPLY] = new PriorityMessageQueue(6, BIG_QUEUE_TIME, BIG_QUEUE_SIZE); _outputQueue[PRIORITY_QUERY] = new PriorityMessageQueue(3, QUEUE_TIME, BIG_QUEUE_SIZE); _outputQueue[PRIORITY_PING_REPLY] = new PriorityMessageQueue(1, QUEUE_TIME, QUEUE_SIZE); _outputQueue[PRIORITY_PING] = new PriorityMessageQueue(1, QUEUE_TIME, QUEUE_SIZE); _outputQueue[PRIORITY_OUR_QUERY] = new PriorityMessageQueue(10, BIG_QUEUE_TIME, BIG_QUEUE_SIZE); _outputQueue[PRIORITY_OTHER] //FIFO, no timeout = new SimpleMessageQueue(1, Integer.MAX_VALUE, BIG_QUEUE_SIZE, false); //Start the thread to empty the output queue startOutputRunner(); } /** * Creates and starts an OutputRunner. * Exists as a hook for tests. */ protected void startOutputRunner() { new OutputRunner(); } /** Repeatedly sends all the queued data. */ private class OutputRunner extends ManagedThread { /** how long to wait before forcing a flush */ private final long FLUSH_DELAY; /** the next time we def. need to flush */ private long _nextFlushTime = -1; public OutputRunner() { long timeToWait = ConnectionSettings.FLUSH_DELAY_TIME; if(timeToWait > 0 && isSupernodeSupernodeConnection()) FLUSH_DELAY = timeToWait; else FLUSH_DELAY = 0; setName("OutputRunner"); setDaemon(true); start(); } /** While the connection is not closed, sends all data delay. */ public void managedRun() { //Exceptions are only caught to set the _runnerDied variable //to make testing easier. For non-IOExceptions, Throwable //is caught to notify ErrorService. try { while (true) { repOk(); waitForQueued(); sendQueued(); repOk(); } } catch (IOException e) { if(_manager != null) { _manager.remove(ManagedConnection.this); } //LOG.logSp("remove in managedRun" + ManagedConnection.this.getAddress() + e.getMessage()); _runnerDied=true; } catch(Throwable t) { //LOG.logSp("remove in managedRun t" + ManagedConnection.this.getAddress() + t.getMessage()); //LOG.callStack(t.getStackTrace()); if(_manager != null) { _manager.remove(ManagedConnection.this); } _runnerDied=true; ErrorService.error(t); } } /** * Wait until the queue is (probably) non-empty or closed. * @exception IOException this was closed while waiting */ private final void waitForQueued() throws IOException { //The synchronized statement is outside the while loop to //protect _queued. synchronized (_outputQueueLock) { while (isOpen() && _queued==0) { try { _outputQueueLock.wait(FLUSH_DELAY); // this works even if FLUSH_DELAY == 0 } catch (InterruptedException e) { Assert.that(false, "OutputRunner Interrupted"); } } } if (! isOpen()) throw CONNECTION_CLOSED; } /** Send several queued message of each type. */ private final void sendQueued() throws IOException { //1. For each priority i send as many messages as desired for that //type. As an optimization, we start with the buffer of the last //message sent, wrapping around the buffer. You can also search //from 0 to the end. int start=_lastPriority; int i=start; do { //IMPORTANT: we only obtain _outputQueueLock while touching the //queue, not while actually sending (which can block). MessageQueue queue=_outputQueue[i]; queue.resetCycle(); boolean emptied=false; while (true) { Message m=null; synchronized (_outputQueueLock) { m = queue.removeNext(); int dropped=queue.resetDropped(); addSentDropped(dropped); _queued-=(m==null?0:1)+dropped; //maintain invariant if (_queued==0) emptied=true; if (m==null) break; } //Note that if the ougoing stream is compressed //(isWriteDeflated()), this call may not actually //do anything. This is because the Deflater waits //until an optimal time to start deflating, buffering //up incoming data until that time is reached, or the //data is explicitly flushed. ManagedConnection.super.send(m); } //Optimization: the if statement below is not needed for //correctness but works nicely with the _priorityHint trick. if (emptied) break; i=(i+1)%PRIORITIES; } while (i!=start); //2. Now force data from Connection's BufferedOutputStream into the //kernel's TCP send buffer. It doesn't force TCP to //actually send the data to the network. That is determined //by the receiver's window size and Nagle's algorithm. //Note that if the outgoing stream is compressed //(isWriteDeflated()), then this call may block while the //Deflater deflates the data. // if we're not delaying flushes, don't do any currentTimeMillis() calls if(FLUSH_DELAY == 0) { ManagedConnection.super.flush(); } else if(System.currentTimeMillis() >= _nextFlushTime) { ManagedConnection.super.flush(); _nextFlushTime = System.currentTimeMillis() + FLUSH_DELAY; } } } //end OutputRunner /** * For debugging only: prints to stdout the number of queued messages in * this, by type. */ private void dumpQueueStats() { synchronized (_outputQueueLock) { for (int i=0; i<PRIORITIES; i++) { LOG.info(i+" "+_outputQueue[i].size()); } LOG.info("* "+_queued+"\n"); } } public void close() { //Ensure OutputRunner terminates. synchronized (_outputQueueLock) { // send bye message in case needed and // in some case _outputQueue[PRIORITY_OTHER] is null so block this kind of case here if(_byeCode != null && _outputQueue[PRIORITY_OTHER] != null ) { Assert.that(_byeReason != null, "byeReason must be set when byeCode is set"); ByeRequest byeRequest = new ByeRequest(Message.makeGuid(), ByeRequest.makePayload(_byeReason,_byeCode)); try{ send(byeRequest); flush(); }catch (IOException iox) { } } super.close(); _outputQueueLock.notify(); } // release pointer to our _guidMap so it can be gc()'ed if (_guidMap != null) GuidMapExpirer.removeMap(_guidMap); } ////////////////////////////////////////////////////////////////////////// /** * Implements the reject connection mechanism. Loops until receiving a * handshake ping, responds with the best N pongs, and closes the * connection. Closes the connection if no ping is received within a * reasonable amount of time. Does NOT clean up route tables in the case * of an IOException. */ void loopToReject() { //IMPORTANT: note that we do not use this' send or receive methods. //This is an important optimization to prevent calling //RouteTable.removeReplyHandler when the connection is closed. try { //The first message we get from the remote host should be its //initial ping. However, some clients may start forwarding packets //on the connection before they send the ping. Hence the following //loop. The limit of 10 iterations guarantees that this method //will not run for more than TIMEOUT*10=80 seconds. Thankfully //this happens rarely. for (int i=0; i<10; i++) { Message m=null; try { m=super.receive(REJECT_TIMEOUT); if (m==null) return; //Timeout has occured and we havent received the ping, //so just return }// end of try for BadPacketEception from socket catch (BadPacketException e) { return; //Its a bad packet, just return } if((m instanceof PingRequest) && (m.getHops()==0)) { // this is the only kind of message we will deal with // in Reject Connection // If any other kind of message comes in we drop //SPECIAL CASE: for crawler ping if(m.getTTL() == 2) { handleCrawlerPing((PingRequest)m); return; } }// end of (if m is PingRequest) } // End of while(true) } catch (IOException e) { } finally { close(); } } /** * Handles the crawler ping of Hops=0 & TTL=2, by sending pongs * corresponding to all its neighbors * @param m The ping request received * @exception In case any I/O error occurs while writing Pongs over the * connection */ private void handleCrawlerPing(PingRequest m) throws IOException { //IMPORTANT: note that we do not use this' send or receive methods. //This is an important optimization to prevent calling //RouteTable.removeReplyHandler when the connection is closed. //send the pongs for the Ultrapeer & 0.4 connections List /*<ManagedConnection>*/ nonLeafConnections = _manager.getInitializedConnections(); supersendNeighborPongs(m, nonLeafConnections); //send the pongs for leaves List /*<ManagedConnection>*/ leafConnections = _manager.getInitializedClientConnections(); supersendNeighborPongs(m, leafConnections); //Note that sending its own pong is not necessary, as the crawler has //already connected to this node, and is not sent therefore. //May be sent for completeness though } /** * Uses the super class's send message to send the pongs corresponding * to the list of connections passed. * This prevents calling RouteTable.removeReplyHandler when * the connection is closed. * @param m Th epingrequest received that needs Pongs * @param neigbors List (of ManagedConnection) of neighboring connections * @exception In case any I/O error occurs while writing Pongs over the * connection */ private void supersendNeighborPongs(PingRequest m, List neighbors) throws IOException { for(Iterator iterator = neighbors.iterator(); iterator.hasNext();) { //get the next connection ManagedConnection connection = (ManagedConnection)iterator.next(); //create the pong for this connection //mark the pong if supernode PingReply pr; if(connection.isSupernodeConnection()) { pr = PingReply. createExternal(m.getGUID(), (byte)2, connection.getPort(), connection.getInetAddress().getAddress(), true); } else if(connection.isLeafConnection() || connection.isOutgoing()){ //we know the listening port of the host in this case pr = PingReply. createExternal(m.getGUID(), (byte)2, connection.getPort(), connection.getInetAddress().getAddress(), false); } else{ //Use the port '0' in this case, as we dont know the listening //port of the host pr = PingReply. createExternal(m.getGUID(), (byte)2, 0, connection.getInetAddress().getAddress(), false); } //hop the message, as it is ideally coming from the connected host pr.hop(); //send the message //This is called only during a Reject connection, and thus //it is impossible for the stream to be compressed. //That is a Good Thing (tm) because we're sending such little //data, that the compression may actually hurt. super.send(pr); } //Because we are guaranteed that the stream is not compressed, //this call will not block. super.flush(); } /** * Handles core Gnutella request/reply protocol. This call * will run until the connection is closed. Note that this is called * from the run methods of several different thread implementations * that are inner classes of ConnectionManager. This allows a single * thread to be used for initialization and for the request/reply loop. * * @requires this is initialized * @modifies the network underlying this, manager * @effects receives request and sends appropriate replies. * * @throws IOException passed on from the receive call; failures to forward * or route messages are silently swallowed, allowing the message * loop to continue. */ void loopForMessages() throws IOException { MessageDispatcher dispatcher = MessageDispatcher.instance(); final boolean isSupernodeClientConnection=isSupernodeClientConnection(); while (true) { Message m=null; try { m = receive(); if (m==null) continue; } catch (BadPacketException e) { // Don't increment any message counters here. It's as if // the packet never existed continue; } // Run through the route spam filter and drop accordingly. if (isSpam(m)) { addReceivedDropped(); continue; } //special handling for proxying - note that for //non-SupernodeClientConnections a good compiler will ignore this //code if (isSupernodeClientConnection && (m instanceof QueryRequest)) m = tryToProxy((QueryRequest) m); if (isSupernodeClientConnection && (m instanceof QueryStatusResponse)) m = morphToStopQuery((QueryStatusResponse) m); dispatcher.dispatchTCP(m, this); } } private QueryRequest tryToProxy(QueryRequest query) { // we must have the following qualifications: // 1) Leaf must be sending SuperNode a query (checked in loopForMessages) // 2) Leaf must support Leaf Guidance // 3) Query must not be OOB. // 3.5) The query originator should not disallow proxying. // 4) We must be able to OOB and have great success rate. if (remoteHostSupportsLeafGuidance() < 1) return query; if (query.desiresOutOfBandReplies()) return query; if (query.doNotProxy()) return query; if (!RouterService.isOOBCapable()) return query; // everything is a go - we need to do the following: // 1) mutate the GUID of the query - you should maintain every param of // the query except the new GUID and the OOB minspeed flag // 2) set up mappings between the old guid and the new guid. // after that, everything is set. all you need to do is map the guids // of the replies back to the original guid. also, see if a you get a // QueryStatusResponse message and morph it... // THIS IS SOME MAJOR HOKERY-POKERY!!! // 1) mutate the GUID of the query byte[] origGUID = query.getGUID(); byte[] oobGUID = new byte[origGUID.length]; System.arraycopy(origGUID, 0, oobGUID, 0, origGUID.length); GUID.addressEncodeGuid(oobGUID, RouterService.getAddress(), RouterService.getPort()); query = QueryRequest.createProxyQuery(query, oobGUID); // 2) set up mappings between the guids if (_guidMap == null) { _guidMap = new Hashtable(); GuidMapExpirer.addMapToExpire(_guidMap); } GUID.TimedGUID tGuid = new GUID.TimedGUID(new GUID(oobGUID), TIMED_GUID_LIFETIME); _guidMap.put(tGuid, new GUID(origGUID)); return query; } private QueryStatusResponse morphToStopQuery(QueryStatusResponse resp) { // if the _guidMap is null, we aren't proxying anything.... if (_guidMap == null) return resp; // if we are proxying this query, we should modify the GUID so as // to shut off the correct query final GUID origGUID = resp.getQueryGUID(); GUID oobGUID = null; synchronized (_guidMap) { Iterator entrySetIter = _guidMap.entrySet().iterator(); while (entrySetIter.hasNext()) { Map.Entry entry = (Map.Entry) entrySetIter.next(); if (origGUID.equals(entry)) { oobGUID = ((GUID.TimedGUID)entry.getKey()).getGUID(); break; } } } // if we had a match, then just construct a new one.... if (oobGUID != null) return new QueryStatusResponse(oobGUID, resp.getNumResults()); else return resp; } /** * Utility method for checking whether or not this message is considered * spam. * * @param m the <tt>Message</tt> to check * @return <tt>true</tt> if this is considered spam, otherwise * <tt>false</tt> */ public boolean isSpam(Message m) { return !_routeFilter.allow(m); } // // Begin Message dropping and filtering calls // /** * A callback for the ConnectionManager to inform this connection that a * message was dropped. This happens when a reply received from this * connection has no routing path. */ public void countDroppedMessage() { _numReceivedMessagesDropped++; } /** * A callback for Message Handler implementations to check to see if a * message is considered to be undesirable by the message's receiving * connection. * Messages ignored for this reason are not considered to be dropped, so * no statistics are incremented here. * * @return true if the message is spam, false if it's okay */ public boolean isPersonalSpam(Message m) { return !_personalFilter.allow(m); } /** * @modifies this * @effects sets the underlying routing filter. Note that * most filters are not thread-safe, so they should not be shared * among multiple connections. */ public void setRouteFilter(SpamFilter filter) { _routeFilter = filter; } /** * @modifies this * @effects sets the underlying personal filter. Note that * most filters are not thread-safe, so they should not be shared * among multiple connections. */ public void setPersonalFilter(SpamFilter filter) { _personalFilter = filter; } /** * Returns the domain to which this connection is authenticated * @return the set (of String) of domains to which this connection * is authenticated. Returns * null, in case of unauthenticated connection */ public Set getDomains(){ //Note that this method is not synchronized, and so _domains may //get initialized multiple times (in case multiple threads invoke this //method, before domains is initialized). But thats not a problem as //all the instances will have same values, and all but 1 of them //will get garbage collected if(_domains == null){ //initialize domains _domains = createDomainSet(); } //return the initialized domains return _domains; // return (String[])_domains.toArray(new String[0]); } /** * creates the set (of String) of domains from the properties sent/received * @return the set (of String) of domains */ private Set createDomainSet(){ Set domainSet; //get the domain property //In case of outgoing connection, we received the domains from the //remote host to whom we authenticated, viceversa for incoming //connection String domainsAuthenticated; if(this.isOutgoing()) domainsAuthenticated = getDomainsAuthenticated(); //domainsAuthenticated = getProperty( //HeaderNames.X_DOMAINS_AUTHENTICATED); else domainsAuthenticated = getPropertyWritten( HeaderNames.X_DOMAINS_AUTHENTICATED); //for unauthenticated connections if(domainsAuthenticated == null){ //if no authentication done, initialize to a default domain set domainSet = User.createDefaultDomainSet(); }else{ domainSet = StringUtils.getSetofValues(domainsAuthenticated); } //return the domain set return domainSet; } /** * This method is called when a reply is received for a PingRequest * originating on this Connection. So, just send it back. * If modifying this method, note that receivingConnection may * by null. */ public void handlePingReply(PingReply pingReply, ReplyHandler receivingConnection) { send(pingReply); } /** * This method is called when a reply is received for a QueryRequest * originating on this Connection. So, send it back. * If modifying this method, note that receivingConnection may * by null. */ public void handleQueryReply(QueryReply queryReply, ReplyHandler receivingConnection) { if (_guidMap != null) { // --------------------- // If we are proxying for a query, map back the guid of the reply GUID.TimedGUID tGuid = new GUID.TimedGUID(new GUID(queryReply.getGUID()), TIMED_GUID_LIFETIME); GUID origGUID = (GUID) _guidMap.get(tGuid); if (origGUID != null) { byte prevHops = queryReply.getHops(); queryReply = new QueryReply(origGUID.bytes(), queryReply); queryReply.setTTL((byte)2); // we ttl 1 more than necessary queryReply.setHops(prevHops); } // --------------------- } send(queryReply); } /** * This method is called when a PushRequest is received for a QueryReply * originating on this Connection. So, just send it back. * If modifying this method, note that receivingConnection may * by null. */ public void handlePushRequest(PushRequest pushRequest, ReplyHandler receivingConnection) { send(pushRequest); } protected void handleVendorMessage(VendorMessage vm) { // let Connection do as needed.... super.handleVendorMessage(vm); // now i can process if (vm instanceof HopsFlowVendorMessage) { // update the softMaxHops value so it can take effect.... HopsFlowVendorMessage hops = (HopsFlowVendorMessage) vm; softMaxHops = hops.getHopValue(); } else if (vm instanceof PushProxyAcknowledgement) { // this connection can serve as a PushProxy, so note this.... PushProxyAcknowledgement ack = (PushProxyAcknowledgement) vm; if (Arrays.equals(ack.getGUID(), RouterService.getMessageRouter()._clientGUID)) { pushProxyPort = ack.getListeningPort(); pushProxyAddr = ack.getListeningAddress(); } // else mistake on the server side - the guid should be my client // guid - not really necessary but whatever } else if (vm instanceof MessagesSupportedVendorMessage) { // If this is a ClientSupernodeConnection and the host supports // leaf guidance (because we have to tell them when to stop) // then see if there are any old queries that we can re-originate // on this connection. if(isClientSupernodeConnection() && (remoteHostSupportsLeafGuidance() >= 0)) { SearchResultHandler srh = RouterService.getSearchResultHandler(); List queries = srh.getQueriesToReSend(); for(Iterator i = queries.iterator(); i.hasNext(); ) send((Message)i.next()); } // see if you need a PushProxy - the remoteHostSupportsPushProxy // test incorporates my leaf status in it..... if (remoteHostSupportsPushProxy() > -1) { // get the client GUID and send off a PushProxyRequest GUID clientGUID = new GUID(RouterService.getMessageRouter()._clientGUID); PushProxyRequest req = new PushProxyRequest(clientGUID); send(req); } // do i need to send any ConnectBack messages???? if (!UDPService.instance().canReceiveUnsolicited() && (_numUDPConnectBackRequests < MAX_UDP_CONNECT_BACK_ATTEMPTS) && (remoteHostSupportsUDPRedirect() > -1)) { GUID connectBackGUID = RouterService.getUDPConnectBackGUID(); Message udp = new UDPConnectBackVendorMessage(RouterService.getPort(), connectBackGUID); send(udp); _numUDPConnectBackRequests++; } if (!RouterService.acceptedIncomingConnection() && (_numTCPConnectBackRequests < MAX_TCP_CONNECT_BACK_ATTEMPTS) && (remoteHostSupportsTCPRedirect() > -1)) { Message tcp = new TCPConnectBackVendorMessage(RouterService.getPort()); send(tcp); _numTCPConnectBackRequests++; } } } // // End reply forwarding calls // // // Begin statistics accessors // /** Returns the number of messages sent on this connection */ public int getNumMessagesSent() { return _numMessagesSent; } /** Returns the number of messages received on this connection */ public int getNumMessagesReceived() { return _numMessagesReceived; } /** Returns the number of messages I dropped while trying to send * on this connection. This happens when the remote host cannot * keep up with me. */ public int getNumSentMessagesDropped() { return _numSentMessagesDropped; } /** * The number of messages received on this connection either filtered out * or dropped because we didn't know how to route them. */ public long getNumReceivedMessagesDropped() { return _numReceivedMessagesDropped; } /** * @modifies this * @effects Returns the percentage of messages sent on this * since the last call to getPercentReceivedDropped that were * dropped by this end of the connection. */ public synchronized float getPercentReceivedDropped() { int rdiff = _numMessagesReceived - _lastReceived; int ddiff = _numReceivedMessagesDropped - _lastRecvDropped; float percent=(rdiff==0) ? 0.f : ((float)ddiff/(float)rdiff*100.f); _lastReceived = _numMessagesReceived; _lastRecvDropped = _numReceivedMessagesDropped; return percent; } /** * @modifies this * @effects Returns the percentage of messages sent on this * since the last call to getPercentSentDropped that were * dropped by this end of the connection. This value may be * greater than 100%, e.g., if only one message is sent but * four are dropped during a given time period. */ public synchronized float getPercentSentDropped() { int rdiff = _numMessagesSent - _lastSent; int ddiff = _numSentMessagesDropped - _lastSentDropped; float percent=(rdiff==0) ? 0.f : ((float)ddiff/(float)rdiff*100.f); _lastSent = _numMessagesSent; _lastSentDropped = _numSentMessagesDropped; return percent; } /** * Takes a snapshot of the upstream and downstream bandwidth since the last * call to measureBandwidth. * @see BandwidthTracker#measureBandwidth */ public void measureBandwidth() { _upBandwidthTracker.measureBandwidth( ByteOrder.long2int(getBytesSent())); _downBandwidthTracker.measureBandwidth( ByteOrder.long2int(getBytesReceived())); } /** * Returns the upstream bandwidth between the last two calls to * measureBandwidth. * @see BandwidthTracker#measureBandwidth */ public float getMeasuredUpstreamBandwidth() { float retValue = 0; //initialize to default try { retValue = _upBandwidthTracker.getMeasuredBandwidth(); } catch(InsufficientDataException ide) { return 0; } return retValue; } /** * Returns the downstream bandwidth between the last two calls to * measureBandwidth. * @see BandwidthTracker#measureBandwidth */ public float getMeasuredDownstreamBandwidth() { float retValue = 0; try { retValue = _downBandwidthTracker.getMeasuredBandwidth(); } catch (InsufficientDataException ide) { return 0; } return retValue; } /** * @modifies this * @effects enables or disables updateHorizon. Typically this method * is used to temporarily disable horizon statistics before sending a * ping with a small TTL to make sure a connection is up. */ public synchronized void setHorizonEnabled(boolean enable) { _horizonEnabled=enable; } /** * This method is called when a reply is received by this connection for a * PingRequest that originated from LimeWire. * * @modifies this * @effects adds the statistics from pingReply to this' horizon statistics, * unless horizon statistics have been disabled via setHorizonEnabled(false). * It's possible that the horizon statistics will not actually be updated * until refreshHorizonStats is called. */ public synchronized void updateHorizonStats(PingReply pingReply) { if (! _horizonEnabled) return; HorizonCounter.instance().addPong(pingReply); } // // End statistics accessors // /** Returns the system time that we should next forward a query route table * along this connection. Only valid if isClientSupernodeConnection() is * true. */ public long getNextQRPForwardTime() { return _nextQRPForwardTime; } /** * Increments the next time we should forward query route tables for * this connection. This depends on whether or not this is a connection * to a leaf or to an Ultrapeer. * * @param curTime the current time in milliseconds, used to calculate * the next update time */ public void incrementNextQRPForwardTime(long curTime) { if(isLeafConnection()) { _nextQRPForwardTime = curTime + LEAF_QUERY_ROUTE_UPDATE_TIME; } else { // otherwise, it's an Ultrapeer _nextQRPForwardTime = curTime + ULTRAPEER_QUERY_ROUTE_UPDATE_TIME; } } /** * Returns true if this should not be policed by the ConnectionWatchdog, * e.g., because this is a connection to a Clip2 reflector. Default value: * true. */ public boolean isKillable() { return _isKillable; } /** @return a non-negative integer representing the proxy's port for HTTP * communication, a negative number if PushProxy isn't supported. */ public int getPushProxyPort() { return pushProxyPort; } /** @return the InetAddress of the remote host - only meaningful if * getPushProxyPort() > -1 * @see getPushProxyPort() */ public InetAddress getPushProxyAddress() { return pushProxyAddr; } /** * Tests representation invariants. For performance reasons, this is * private and final. Make protected if ManagedConnection is subclassed. */ private final void repOk() { /* //Check _queued invariant. synchronized (_outputQueueLock) { int sum=0; for (int i=0; i<_outputQueue.length; i++) sum+=_outputQueue[i].size(); Assert.that(sum==_queued, "Expected "+sum+", got "+_queued); } */ } // // overrides Object.toString // public String toString() { // return "ManagedConnection: Ultrapeer: "+isSupernodeConnection()+ // " Leaf: "+isLeafConnection(); // } /*************************************************************************** * UNIT TESTS: tests/com/limegroup/gnutella/ManagedConnectionTest **************************************************************************/ /** FOR TESTING PURPOSES ONLY! */ void stopOutputRunner() { //Ensure OutputRunner terminates. synchronized (_outputQueueLock) { super._closed=true; //doesn't close socket _outputQueueLock.notify(); } //Wait for OutputRunner to terminate while (! _runnerDied) { Thread.yield(); } //Make it alive again (except for runner) _runnerDied=false; super._closed=false; } /** FOR TESTING PURPOSES ONLY! */ boolean runnerDied() { return _runnerDied; } public Object getQRPLock() { return QRP_LOCK; } /** * set preferencing for the responder * (The preference of the Responder is used when creating the response * (in Connection.java: conclude..)) */ public void setLocalePreferencing(boolean b) { RESPONSE_HEADERS.setLocalePreferencing(b); } public void reply(Message m){ send(m); } /** Class-wide expiration mechanism for all ManagedConnections. * Only expires on-demand. */ private static class GuidMapExpirer implements Runnable { private static List toExpire = new LinkedList(); private static boolean scheduled = false; public GuidMapExpirer() {}; public static synchronized void addMapToExpire(Map expiree) { // schedule it on demand if (!scheduled) { RouterService.schedule(new GuidMapExpirer(), 0, TIMED_GUID_LIFETIME); scheduled = true; } toExpire.add(expiree); } public static synchronized void removeMap(Map expiree) { toExpire.remove(expiree); } public void run() { synchronized (GuidMapExpirer.class) { // iterator through all the maps.... Iterator iter = toExpire.iterator(); while (iter.hasNext()) { Map currMap = (Map) iter.next(); synchronized (currMap) { Iterator keyIter = currMap.keySet().iterator(); // and expire as many entries as possible.... while (keyIter.hasNext()) if (((GUID.TimedGUID) keyIter.next()).shouldExpire()) keyIter.remove(); } } } } } }