package com.limegroup.gnutella; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.InterruptedIOException; import java.io.OutputStream; import java.net.InetAddress; import java.net.Socket; import java.net.UnknownHostException; import java.util.Enumeration; import java.util.Properties; import java.util.zip.Deflater; import java.util.zip.Inflater; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import com.limegroup.gnutella.connection.GnetConnectObserver; import com.limegroup.gnutella.handshaking.BadHandshakeException; import com.limegroup.gnutella.handshaking.BlockingIncomingHandshaker; import com.limegroup.gnutella.handshaking.BlockingOutgoingHandshaker; import com.limegroup.gnutella.handshaking.HandshakeResponder; import com.limegroup.gnutella.handshaking.HandshakeResponse; import com.limegroup.gnutella.handshaking.Handshaker; import com.limegroup.gnutella.handshaking.HeaderNames; import com.limegroup.gnutella.handshaking.NoGnutellaOkException; import com.limegroup.gnutella.io.ConnectObserver; import com.limegroup.gnutella.messages.BadPacketException; import com.limegroup.gnutella.messages.Message; import com.limegroup.gnutella.messages.vendor.CapabilitiesVM; import com.limegroup.gnutella.messages.vendor.HeaderUpdateVendorMessage; import com.limegroup.gnutella.messages.vendor.MessagesSupportedVendorMessage; import com.limegroup.gnutella.messages.vendor.SimppVM; import com.limegroup.gnutella.messages.vendor.StatisticVendorMessage; import com.limegroup.gnutella.messages.vendor.VendorMessage; import com.limegroup.gnutella.settings.ConnectionSettings; import com.limegroup.gnutella.settings.StringSetting; import com.limegroup.gnutella.statistics.BandwidthStat; import com.limegroup.gnutella.statistics.CompressionStat; import com.limegroup.gnutella.statistics.ConnectionStat; import com.limegroup.gnutella.statistics.HandshakingStat; import com.limegroup.gnutella.util.CompressingOutputStream; import com.limegroup.gnutella.util.IOUtils; import com.limegroup.gnutella.util.IpPort; import com.limegroup.gnutella.util.NetworkUtils; import com.limegroup.gnutella.util.Sockets; import com.limegroup.gnutella.util.ThreadFactory; import com.limegroup.gnutella.util.UncompressingInputStream; /** * A Gnutella messaging connection. Provides handshaking functionality and * routines for reading and writing of Gnutella messages. A connection is * either incoming (created from a Socket) or outgoing (created from an * address). This class does not provide sophisticated buffering or routing * logic; use ManagedConnection for that. <p> * * You will note that the constructors don't actually involve the network and * hence never throw exceptions or block. <b>To actual initialize a connection, * you must call initialize().</b> While this is somewhat awkward, it is * intentional. It makes it easier, for example, for the GUI to show * uninitialized connections.<p> * * <tt>Connection</tt> supports only 0.6 handshakes. Gnutella 0.6 connections * have a list of properties read and written during the handshake sequence. * Typical property/value pairs might be "Query-Routing: 0.3" or "User-Agent: * LimeWire".<p> * * This class augments the basic 0.6 handshaking mechanism to allow * authentication via "401" messages. Authentication interactions can take * multiple rounds.<p> * * This class supports reading and writing streams using 'deflate' compression. * The HandshakeResponser is what actually determines whether or not * deflate will be used. This class merely looks at what the responses are in * order to set up the appropriate streams. Compression is implemented by * chaining the input and output streams, meaning that even if an extending * class implements getInputStream() and getOutputStream(), the actual input * and output stream used may not be an instance of the expected class. * However, the information is still chained through the appropriate stream.<p> * * The amount of bytes written and received are maintained by this class. This * is necessary because of compression and decompression are considered * implementation details in this class.<p> * * Finally, <tt>Connection</tt> also handles setting the SOFT_MAX_TTL on a * per-connection basis. The SOFT_MAX TTL is the limit for hops+TTL on all * incoming traffic, with the exception of query hits. If an incoming * message has hops+TTL greater than SOFT_MAX, we set the TTL to * SOFT_MAX-hops. We do this on a per-connection basis because on newer * connections that understand X-Max-TTL, we can regulate the TTLs they * send us. This helps prevent malicious hosts from using headers like * X-Max-TTL to simply get connections. This way, they also have to abide * by the contract of the X-Max-TTL header, illustrated by sending lower * TTL traffic generally. */ public class Connection implements IpPort { private static final Log LOG = LogFactory.getLog(Connection.class); /** * Lock for maintaining accurate data for when to allow ping forwarding. */ private final Object PING_LOCK = new Object(); /** * Lock for maintaining accurate data for when to allow pong forwarding. */ private final Object PONG_LOCK = new Object(); /** * The underlying socket, its address, and input and output streams. sock, * in, and out are null iff this is in the unconnected state. For thread * synchronization reasons, it is important that this only be modified by * the send(m) and receive() methods. */ private final String _host; private int _port; protected Socket _socket; private InputStream _in; private OutputStream _out; private final boolean OUTGOING; /** * The Inflater to use for inflating read streams, initialized * in initialize() if the connection told us it's sending with * a Content-Encoding of deflate. * Definitions: * Inflater.getTotalOut -- The number of UNCOMPRESSED bytes * Inflater.getTotalIn -- The number of COMPRESSED bytes */ protected Inflater _inflater; /** * The Deflater to use for deflating written streams, initialized * in initialize() if we told the connection we're sending with * a Content-Encoding of deflate. * Note that this is the same as '_out', but is assigned here * as the appropriate type so we don't have to cast when we * want to measure the compression savings. * Definitions: * Deflater.getTotalOut -- The number of COMPRESSED bytes * Deflater.getTotalIn -- The number of UNCOMPRESSED bytes */ protected Deflater _deflater; /** * The number of bytes sent to the output stream. */ private volatile long _bytesSent; /** * The number of bytes recieved from the input stream. */ private volatile long _bytesReceived; /** * The number of compressed bytes sent to the stream. * This is effectively the same as _deflater.getTotalOut(), * but must be cached because Deflater's behaviour is undefined * after end() has been called on it, which is done when this * connection is closed. */ private volatile long _compressedBytesSent; /** * The number of compressed bytes read from the stream. * This is effectively the same as _inflater.getTotalIn(), * but must be cached because Inflater's behaviour is undefined * after end() has been called on it, which is done when this * connection is closed. */ private volatile long _compressedBytesReceived; /** The possibly non-null VendorMessagePayload which describes what * VendorMessages the guy on the other side of this connection supports. */ protected MessagesSupportedVendorMessage _messagesSupported = null; /** The possibly non-null VendorMessagePayload which describes what * Capabilities the guy on the other side of this connection supports. */ protected CapabilitiesVM _capabilities = null; /** * Trigger an opening connection to close after it opens. This * flag is set in shutdown() and then checked in initialize() * to insure the _socket.close() happens if shutdown is called * asynchronously before initialize() completes. Note that the * connection may have been remotely closed even if _closed==true. * Protected (instead of private) for testing purposes only. * This also protects us from calling methods on the Inflater/Deflater * objects after end() has been called on them. */ protected volatile boolean _closed=false; /** The <tt>HandshakeResponse</tt> wrapper for all headers we read from the remote side. */ private HandshakeResponse _headersRead = HandshakeResponse.createEmptyResponse(); /** The <tt>HandshakeResponse</tt> wrapper for all headers we wrote to the remote side. */ private HandshakeResponse _headersWritten = HandshakeResponse.createEmptyResponse(); /** * The time in milliseconds since 1970 that this connection was * established. */ private long _connectionTime = Long.MAX_VALUE; /** * The "soft max" ttl to use for this connection. */ private byte _softMax; /** * Variable for the next time to allow a ping. Volatile to avoid * multiple threads caching old data for the ping time. */ private volatile long _nextPingTime = Long.MIN_VALUE; /** * Variable for the next time to allow a pong. Volatile to avoid * multiple threads caching old data for the pong time. */ private volatile long _nextPongTime = Long.MIN_VALUE; /** * Cache the 'connection closed' exception, so we have to allocate * one for every closed connection. */ protected static final IOException CONNECTION_CLOSED = new IOException("connection closed"); /** * Creates an uninitialized outgoing Gnutella 0.6 connection with the * desired outgoing properties. * * @param host the name of the host to connect to * @param port the port of the remote host * @param requestHeaders the headers to be sent after "GNUTELLA CONNECT" * @param responder a function returning the headers to be sent * after the server's "GNUTELLA OK". Typically this returns only * vendor-specific properties. * @throws <tt>NullPointerException</tt> if any of the arguments are * <tt>null</tt> * @throws <tt>IllegalArgumentException</tt> if the port is invalid */ public Connection(String host, int port) { if(host == null) throw new NullPointerException("null host"); if(!NetworkUtils.isValidPort(port)) throw new IllegalArgumentException("illegal port: "+port); _host = host; _port = port; OUTGOING = true; ConnectionStat.OUTGOING_CONNECTION_ATTEMPTS.incrementStat(); } /** * Creates an uninitialized incoming 0.6 Gnutella connection. If the * client is attempting to connect using an 0.4 handshake, it is * rejected. * * @param socket the socket accepted by a ServerSocket. The word * "GNUTELLA " and nothing else must have been read from the socket. * @param responder the headers to be sent in response to the client's * "GNUTELLA CONNECT". * @throws <tt>NullPointerException</tt> if any of the arguments are * <tt>null</tt> */ public Connection(Socket socket) { if(socket == null) throw new NullPointerException("null socket"); //Get the address in dotted-quad format. It's important not to do a //reverse DNS lookup here, as that can block. And on the Mac, it blocks //your entire system! _host = socket.getInetAddress().getHostAddress(); _port = socket.getPort(); _socket = socket; OUTGOING = false; ConnectionStat.INCOMING_CONNECTION_ATTEMPTS.incrementStat(); } /** Call this method when the Connection has been initialized and accepted * as 'long-lived'. */ protected void postInit() { try { // TASK 1 - Send a MessagesSupportedVendorMessage if necessary.... if(_headersRead.supportsVendorMessages() > 0) { send(MessagesSupportedVendorMessage.instance()); send(CapabilitiesVM.instance()); } } catch (IOException ioe) { } } /** * Call this method if you want to send your neighbours a message with your * updated capabilities. */ protected void sendUpdatedCapabilities() { try { if(_headersRead.supportsVendorMessages() > 0) send(CapabilitiesVM.instance()); } catch (IOException iox) { } } /** * Call this method when you want to handle us to handle a VM. We may.... */ protected void handleVendorMessage(VendorMessage vm) { if (vm instanceof MessagesSupportedVendorMessage) _messagesSupported = (MessagesSupportedVendorMessage) vm; if (vm instanceof CapabilitiesVM) _capabilities = (CapabilitiesVM) vm; if (vm instanceof HeaderUpdateVendorMessage) { HeaderUpdateVendorMessage huvm = (HeaderUpdateVendorMessage)vm; Properties props = _headersRead.props(); props.putAll(huvm.getProperties()); _headersRead = HandshakeResponse.createResponse(props); } } /** * Initializes this without timeout; exactly like initialize(0). * @see initialize(int) */ public void initialize(Properties requestHeaders, HandshakeResponder responder) throws IOException, NoGnutellaOkException, BadHandshakeException { initialize(requestHeaders, responder, 0, null); } /** * Initializes this without a timeout, using the given ConnectObserver. */ public void initialize(Properties requestHeaders, HandshakeResponder responder, GnetConnectObserver observer) throws IOException, NoGnutellaOkException, BadHandshakeException { initialize(requestHeaders, responder, 0, observer); } /** * Initialize the connection by doing the handshake. Throws IOException * if we were unable to establish a normal messaging connection for * any reason. Do not call send or receive if this happens. * * @param timeout for outgoing connections, the timeout in milliseconds * to use in establishing the socket, or 0 for no timeout. If the * platform does not support native timeouts, it will be emulated with * threads. * @exception IOException we were unable to connect to the host * @exception NoGnutellaOkException one of the participants responded * with an error code other than 200 OK (possibly after several rounds * of 401's) * @exception BadHandshakeException some other problem establishing * the connection, e.g., the server responded with HTTP, closed the * the connection during handshaking, etc. */ protected void initialize(Properties requestHeaders, HandshakeResponder responder, int timeout, GnetConnectObserver observer) throws IOException, NoGnutellaOkException, BadHandshakeException { if(isOutgoing()) { if(observer != null) { _socket = Sockets.connect(_host, _port, timeout, createAsyncConnectObserver(requestHeaders, responder, observer)); } else { _socket=Sockets.connect(_host, _port, timeout); preHandshakeInitialize(requestHeaders, responder, observer); } } else { preHandshakeInitialize(requestHeaders, responder, observer); } } /** * Constructs the ConnectObserver that will be used to continue the connection process asynchronously. */ protected ConnectObserver createAsyncConnectObserver(Properties requestHeaders, HandshakeResponder responder, GnetConnectObserver observer) { return new Connector(requestHeaders, responder, observer); } /** * Finishes the initialization process. This blocks during handshaking. * * @throws IOException * @throws NoGnutellaOkException * @throws BadHandshakeException */ protected void preHandshakeInitialize(Properties requestHeaders, HandshakeResponder responder, GnetConnectObserver observer) throws IOException, NoGnutellaOkException, BadHandshakeException { // Check to see if close() was called while the socket was initializing if (_closed) { _socket.close(); throw CONNECTION_CLOSED; } // Check to see if this is an attempt to connect to ourselves InetAddress localAddress = _socket.getLocalAddress(); if (ConnectionSettings.LOCAL_IS_PRIVATE.getValue() && _socket.getInetAddress().equals(localAddress) && _port == ConnectionSettings.PORT.getValue()) { throw new IOException("Connection to self"); } // Notify the acceptor of our address. RouterService.getAcceptor().setAddress(localAddress); performHandshake(requestHeaders, responder, observer); } /** * Delegates to the Handshaker to perform the handshake, and then calls * postHandshakeInitialize. * * @throws IOException * @throws BadHandshakeException * @throws NoGnutellaOkException */ protected void performHandshake(Properties requestHeaders, HandshakeResponder responder, GnetConnectObserver observer) throws IOException, BadHandshakeException, NoGnutellaOkException { Handshaker shaker = createHandshaker(requestHeaders, responder); try { shaker.shake(); } catch (NoGnutellaOkException e) { setHeaders(shaker); close(); throw e; } catch (IOException e) { setHeaders(shaker); close(); throw new BadHandshakeException(e); } postHandshakeInitialize(shaker); } /** Constructs the Handshaker object. */ protected Handshaker createHandshaker(Properties requestHeaders, HandshakeResponder responder) throws IOException { try { _in = getInputStream(); _out = getOutputStream(); if (_in == null) throw new IOException("null input stream"); else if(_out == null) throw new IOException("null output stream"); } catch (Exception e) { //Apparently Socket.getInput/OutputStream throws //NullPointerException if the socket is closed. (See Sun bug //4091706.) Unfortunately the socket may have been closed after the //the check above, e.g., if the user pressed disconnect. So we //catch NullPointerException here--and any other weird possible //exceptions. An alternative is to obtain a lock before doing these //calls, but we are afraid that getInput/OutputStream may be a //blocking operation. Just to be safe, we also check that in/out //are not null. close(); throw new IOException("could not establish connection"); } if(isOutgoing()) return new BlockingOutgoingHandshaker(requestHeaders, responder, _socket, _in, _out); else return new BlockingIncomingHandshaker(responder, _socket, _in, _out); } /** * Sets the headers read & written. * * @param shaker */ protected void setHeaders(Handshaker shaker) { _headersWritten = shaker.getWrittenHeaders(); _headersRead = shaker.getReadHeaders(); } /** * Sets up the connection for post-handshake info. * @param shaker */ protected void postHandshakeInitialize(Handshaker shaker) { setHeaders(shaker); _connectionTime = System.currentTimeMillis(); // Now set the soft max TTL that should be used on this connection. // The +1 on the soft max for "good" connections is because the message // may come from a leaf, and therefore can have an extra hop. // "Good" connections are connections with features such as // intra-Ultrapeer QRP passing. _softMax = ConnectionSettings.SOFT_MAX.getValue(); if (isGoodUltrapeer() || isGoodLeaf()) { // we give these an extra hop because they might be sending // us traffic from their leaves _softMax++; } // wrap the streams with inflater/deflater // These calls must be delayed until absolutely necessary (here) // because the native construction for Deflater & Inflater // allocate buffers outside of Java's memory heap, preventing // Java from fully knowing when/how to GC. The call to end() // (done explicitly in the close() method of this class, and // implicitly in the finalization of the Deflater & Inflater) // releases these buffers. if (isWriteDeflated()) { _deflater = new Deflater(); _out = createDeflatedOutputStream(_out); } if (isReadDeflated()) { _inflater = new Inflater(); _in = createInflatedInputStream(_in); } } /** Creates the output stream for deflating */ protected OutputStream createDeflatedOutputStream(OutputStream out) { return new CompressingOutputStream(out, _deflater); } /** Creates the input stream for inflating */ protected InputStream createInflatedInputStream(InputStream in) { return new UncompressingInputStream(in, _inflater); } /** * Determines whether this connection is capable of asynchronous messaging. */ public boolean isAsynchronous() { return _socket.getChannel() != null; } /** * Accessor for whether or not this connection has been initialized. * Several methods of this class require that the connection is * initialized, particularly that the socket is established. These * methods should verify that the connection is initialized before * being called. * * @return <tt>true</tt> if the connection has been initialized and * the socket established, otherwise <tt>false</tt> */ public boolean isInitialized() { return _socket != null; } /** * Returns the stream to use for writing to s. * If the message supports asynchronous messaging, we don't need * to buffer it, because it's already buffered internally. Note, however, * that buffering it would not be wrong, because we can always flush * the buffered data. */ protected OutputStream getOutputStream() throws IOException { if(isAsynchronous()) return _socket.getOutputStream(); else return new BufferedOutputStream(_socket.getOutputStream()); } /** * Returns the stream to use for reading from s. * If this supports asynchronous messaging, the stream itself is returned, * because the underlying stream is already buffered. This is also done * to ensure that when we switch to using asynch message processing, no * bytes are left within the BufferedInputStream's buffer. * * Otherwise (it isn't asynchronous-capable), we enforce a buffer around the stream. * * Subclasses may override to decorate the stream. */ protected InputStream getInputStream() throws IOException { if(isAsynchronous()) return _socket.getInputStream(); else return new BufferedInputStream(_socket.getInputStream()); } ///////////////////////////////////////////////////////////////////////// /** * Used to determine whether the connection is incoming or outgoing. */ public boolean isOutgoing() { return OUTGOING; } /** A tiny allocation optimization; see Message.read(InputStream,byte[]). */ private final byte[] HEADER_BUF=new byte[23]; /** * Receives a message. This method is NOT thread-safe. Behavior is * undefined if two threads are in a receive call at the same time for a * given connection. * * If this is an asynchronous read-deflated connection, this will set up * the UncompressingInputStream the first time this is called. * * @requires this is fully initialized * @effects exactly like Message.read(), but blocks until a * message is available. A half-completed message * results in InterruptedIOException. */ protected Message receive() throws IOException, BadPacketException { if(isAsynchronous() && isReadDeflated() && !(_in instanceof UncompressingInputStream)) _in = new UncompressingInputStream(_in, _inflater); //On the Macintosh, sockets *appear* to return the same ping reply //repeatedly if the connection has been closed remotely. This prevents //connections from dying. The following works around the problem. Note //that Message.read may still throw IOException below. //See note on _closed for more information. if (_closed) throw CONNECTION_CLOSED; Message m = null; while (m == null) { m = readAndUpdateStatistics(); } return m; } /** * Receives a message with timeout. This method is NOT thread-safe. * Behavior is undefined if two threads are in a receive call at the same * time for a given connection. * * If this is an asynchronous read-deflated connection, this will set up * the UncompressingInputStream the first time this is called. * * @requires this is fully initialized * @effects exactly like Message.read(), but throws InterruptedIOException * if timeout!=0 and no message is read after "timeout" milliseconds. In * this case, you should terminate the connection, as half a message may * have been read. */ public Message receive(int timeout) throws IOException, BadPacketException, InterruptedIOException { if(isAsynchronous() && isReadDeflated() && !(_in instanceof UncompressingInputStream)) _in = new UncompressingInputStream(_in, _inflater); //See note in receive(). if (_closed) throw CONNECTION_CLOSED; //temporarily change socket timeout. int oldTimeout=_socket.getSoTimeout(); _socket.setSoTimeout(timeout); try { Message m = readAndUpdateStatistics(); if (m==null) { throw new InterruptedIOException("null message read"); } return m; } finally { _socket.setSoTimeout(oldTimeout); } } /** * Reads a message from the network and updates the appropriate statistics. */ private Message readAndUpdateStatistics() throws IOException, BadPacketException { // The try/catch block is necessary for two reasons... // See the notes in Connection.close above the calls // to end() on the Inflater/Deflater and close() // on the Input/OutputStreams for the details. Message msg = Message.read(_in, HEADER_BUF, Message.N_TCP, _softMax); updateReadStatistics(msg); return msg; } /** * Updates the read statistics. */ protected void updateReadStatistics(Message msg) throws IOException { // _bytesReceived must be set differently // when compressed because the inflater will // read more input than a single message, // making it appear as if the deflated input // was actually larger. if( isReadDeflated() ) { try { long newIn = _inflater.getTotalIn(); long newOut = _inflater.getTotalOut(); CompressionStat.GNUTELLA_UNCOMPRESSED_DOWNSTREAM.addData((int)(newOut - _bytesReceived)); CompressionStat.GNUTELLA_COMPRESSED_DOWNSTREAM.addData((int)(newIn - _compressedBytesReceived)); _compressedBytesReceived = newIn; _bytesReceived = newOut; } catch(NullPointerException npe) { // Inflater is broken and can throw an NPE if it was ended // at an odd time. throw CONNECTION_CLOSED; } } else if(msg != null) { _bytesReceived += msg.getTotalLength(); } } /** * Optimization -- reuse the header buffer since sending will only be * done on one thread. */ private final byte[] OUT_HEADER_BUF = new byte[23]; /** * Sends a message. The message may be buffered, so call flush() to * guarantee that the message is sent synchronously. This method is NOT * thread-safe. Behavior is undefined if two threads are in a send call * at the same time for a given connection. * * @requires this is fully initialized * @modifies the network underlying this * @effects send m on the network. Throws IOException if problems * arise. */ public void send(Message m) throws IOException { if(LOG.isTraceEnabled()) LOG.trace("Connection (" + toString() + ") is sending message: " + m); // The try/catch block is necessary for two reasons... // See the notes in Connection.close above the calls // to end() on the Inflater/Deflater and close() // on the Input/OutputStreams for the details. try { m.write(_out, OUT_HEADER_BUF); updateWriteStatistics(m); } catch(NullPointerException e) { throw CONNECTION_CLOSED; } } /** * Flushes any buffered messages sent through the send method. */ public void flush() throws IOException { // The try/catch block is necessary for two reasons... // See the notes in Connection.close above the calls // to end() on the Inflater/Deflater and close() // on the Input/OutputStreams for the details. try { _out.flush(); // we must update the write statistics again, // because flushing forces the deflater to deflate. updateWriteStatistics(null); } catch(NullPointerException npe) { throw CONNECTION_CLOSED; } } /** * Updates the write statistics. * @param m the possibly null message to add to the bytes sent */ protected void updateWriteStatistics(Message m) { if( isWriteDeflated() ) { long newIn = _deflater.getTotalIn(); long newOut = _deflater.getTotalOut(); CompressionStat.GNUTELLA_UNCOMPRESSED_UPSTREAM.addData((int)(newIn - _bytesSent)); CompressionStat.GNUTELLA_COMPRESSED_UPSTREAM.addData((int)(newOut - _compressedBytesSent)); _bytesSent = newIn; _compressedBytesSent = newOut; } else if( m != null) { _bytesSent += m.getTotalLength(); } } /** * Returns the number of bytes sent on this connection. * If the outgoing stream is compressed, the return value indicates * the compressed number of bytes sent. */ public long getBytesSent() { if(isWriteDeflated()) return _compressedBytesSent; else return _bytesSent; } /** * Returns the number of uncompressed bytes sent on this connection. * If the outgoing stream is not compressed, this is effectively the same * as calling getBytesSent() */ public long getUncompressedBytesSent() { return _bytesSent; } /** * Returns the number of bytes received on this connection. * If the incoming stream is compressed, the return value indicates * the number of compressed bytes received. */ public long getBytesReceived() { if(isReadDeflated()) return _compressedBytesReceived; else return _bytesReceived; } /** * Returns the number of uncompressed bytes read on this connection. * If the incoming stream is not compressed, this is effectively the same * as calling getBytesReceived() */ public long getUncompressedBytesReceived() { return _bytesReceived; } /** * Returns the percentage saved through compressing the outgoing data. * The value may be slightly off until the output stream is flushed, * because the value of the compressed bytes is not calculated until * then. */ public float getSentSavedFromCompression() { if( !isWriteDeflated() || _bytesSent == 0 ) return 0; return 1-((float)_compressedBytesSent/(float)_bytesSent); } /** * Returns the percentage saved from having the incoming data compressed. */ public float getReadSavedFromCompression() { if( !isReadDeflated() || _bytesReceived == 0 ) return 0; return 1-((float)_compressedBytesReceived/(float)_bytesReceived); } /** * Returns the IP address of the remote host as a string. * * @return the IP address of the remote host as a string */ public String getAddress() { return _host; } /** * Accessor for the port number this connection is listening on. Note that * this is NOT the port of the socket itself. For incoming connections, * the getPort method of the java.net.Socket class returns the ephemeral * port that the host connected with. This port, however, is the port the * remote host is listening on for new connections, which we set using * Gnutella connection headers in the case of incoming connections. For * outgoing connections, this is the port we used to connect to them -- * their listening port. * * @return the listening port for the remote host */ public int getPort() { return _port; } /** * Sets the port where the conected node listens at, not the one * got from socket */ void setListeningPort(int port){ if (!NetworkUtils.isValidPort(port)) throw new IllegalArgumentException("invalid port: "+port); this._port = port; } /** * Returns the address of the foreign host this is connected to. * @exception IllegalStateException this is not initialized */ public InetAddress getInetAddress() throws IllegalStateException { if(_socket == null) { throw new IllegalStateException("Not initialized"); } return _socket.getInetAddress(); } /** * Accessor for the <tt>Socket</tt> for this connection. * * @return the <tt>Socket</tt> for this connection * @throws IllegalStateException if this connection is not yet * initialized */ public Socket getSocket() throws IllegalStateException { if(_socket == null) { throw new IllegalStateException("Not initialized"); } return _socket; } /** * Returns the time this connection was established, in milliseconds * since January 1, 1970. * * @return the time this connection was established */ public long getConnectionTime() { return _connectionTime; } /** * Accessor for the soft max TTL to use for this connection. * * @return the soft max TTL for this connection */ public byte getSoftMax() { return _softMax; } /** * Checks whether this connection is considered a stable connection, * meaning it has been up for enough time to be considered stable. * * @return <tt>true</tt> if the connection is considered stable, * otherwise <tt>false</tt> */ public boolean isStable() { return isStable(System.currentTimeMillis()); } /** * Checks whether this connection is considered a stable connection, * by comparing the time it was established with the <tt>millis</tt> * argument. * * @return <tt>true</tt> if the connection is considered stable, * otherwise <tt>false</tt> */ public boolean isStable(long millis) { return (millis - getConnectionTime())/1000 > 5; } /** @return -1 if the message isn't supported, else the version number * supported. */ public int supportsVendorMessage(byte[] vendorID, int selector) { if (_messagesSupported != null) return _messagesSupported.supportsMessage(vendorID, selector); return -1; } /** * @return whether this connection supports routing of vendor messages * (i.e. will not drop a VM that has ttl <> 1 and hops > 0) */ public boolean supportsVMRouting() { if (_headersRead != null) return _headersRead.supportsVendorMessages() >= 0.2; return false; } /** @return -1 if the message isn't supported, else the version number * supported. */ public int remoteHostSupportsUDPConnectBack() { if (_messagesSupported != null) return _messagesSupported.supportsUDPConnectBack(); return -1; } /** @return -1 if the message isn't supported, else the version number * supported. */ public int remoteHostSupportsTCPConnectBack() { if (_messagesSupported != null) return _messagesSupported.supportsTCPConnectBack(); return -1; } /** @return -1 if the message isn't supported, else the version number * supported. */ public int remoteHostSupportsUDPRedirect() { if (_messagesSupported != null) return _messagesSupported.supportsUDPConnectBackRedirect(); return -1; } /** @return -1 if the message isn't supported, else the version number * supported. */ public int remoteHostSupportsTCPRedirect() { if (_messagesSupported != null) return _messagesSupported.supportsTCPConnectBackRedirect(); return -1; } /** @return -1 if UDP crawling is supported, else the version number * supported. */ public int remoteHostSupportsUDPCrawling() { if (_messagesSupported != null) return _messagesSupported.supportsUDPCrawling(); return -1; } /** @return -1 if the message isn't supported, else the version number * supported. */ public int remoteHostSupportsHopsFlow() { if (_messagesSupported != null) return _messagesSupported.supportsHopsFlow(); return -1; } /** @return -1 if the message isn't supported, else the version number * supported. */ public int remoteHostSupportsPushProxy() { if ((_messagesSupported != null) && isClientSupernodeConnection()) return _messagesSupported.supportsPushProxy(); return -1; } /** @return -1 if the message isn't supported, else the version number * supported. */ public int remoteHostSupportsLeafGuidance() { if (_messagesSupported != null) return _messagesSupported.supportsLeafGuidance(); return -1; } public int remoteHostSupportsHeaderUpdate() { if (_messagesSupported != null) return _messagesSupported.supportsHeaderUpdate(); return -1; } /** * Return whether or not the remote host supports feature queries. */ public boolean getRemoteHostSupportsFeatureQueries() { if(_capabilities != null) return _capabilities.supportsFeatureQueries() > 0; return false; } /** @return the maximum selector of capability supported, else -1 if no * support. */ public int getRemoteHostFeatureQuerySelector() { if (_capabilities != null) return _capabilities.supportsFeatureQueries(); return -1; } /** @return true if the capability is supported. */ public boolean remoteHostSupportsWhatIsNew() { if (_capabilities != null) return _capabilities.supportsWhatIsNew(); return false; } /** * Gets the remote host's 'update' version. */ public int getRemoteHostUpdateVersion() { if(_capabilities != null) return _capabilities.supportsUpdate(); else return -1; } /** * Returns whether or not this connection represents a local address. * * @return <tt>true</tt> if this connection is a local address, * otherwise <tt>false</tt> */ protected boolean isLocal() { return NetworkUtils.isLocalAddress(_socket.getInetAddress()); } /** * Returns the value of the given outgoing (written) connection property, or * null if no such property. For example, getProperty("X-Supernode") tells * whether I am a supernode or a leaf node. If I wrote a property multiple * time during connection, returns the latest. */ public String getPropertyWritten(String name) { return _headersWritten.props().getProperty(name); } /** * @return true until close() is called on this Connection */ public boolean isOpen() { return !_closed; } /** * Closes the Connection's socket and thus the connection itself. */ public void close() { if(_closed) return; // Setting this flag insures that the socket is closed if this // method is called asynchronously before the socket is initialized. _closed = true; if(_socket != null) { try { _socket.close(); } catch(IOException e) {} } // tell the inflater & deflater that we're done with them. // These calls are dangerous, because we don't know that the // stream isn't currently deflating or inflating, and the access // to the deflater/inflater is not synchronized (it shouldn't be). // This can lead to NPE's popping up in unexpected places. // Fortunately, the calls aren't explicitly necessary because // when the deflater/inflaters are garbage-collected they will call // end for us. if( _deflater != null ) _deflater.end(); if( _inflater != null ) _inflater.end(); // closing _in (and possibly _out too) can cause NPE's // in Message.read (and possibly other places), // because BufferedInputStream can't handle // the case where one thread is reading from the stream and // another closes it. // See BugParade ID: 4505257 IOUtils.close(_in); IOUtils.close(_out); } /** Returns the vendor string reported by this connection, i.e., * the USER_AGENT property, or null if it wasn't set. * @return the vendor string, or null if unknown */ public String getUserAgent() { return _headersRead.getUserAgent(); } /** * Returns whether or not the remote host is a LimeWire (or derivative) */ public boolean isLimeWire() { return _headersRead.isLimeWire(); } public boolean isOldLimeWire() { return _headersRead.isOldLimeWire(); } /** * Returns true if the outgoing stream is deflated. * * @return true if the outgoing stream is deflated. */ public boolean isWriteDeflated() { return _headersWritten.isDeflateEnabled(); } /** * Returns true if the incoming stream is deflated. * * @return true if the incoming stream is deflated. */ public boolean isReadDeflated() { return _headersRead.isDeflateEnabled(); } // inherit doc comment public boolean isGoodUltrapeer() { return _headersRead.isGoodUltrapeer(); } // inherit doc comment public boolean isGoodLeaf() { return _headersRead.isGoodLeaf(); } // inherit doc comment public boolean supportsPongCaching() { return _headersRead.supportsPongCaching(); } /** * Returns whether or not we should allow new pings on this connection. If * we have recently received a ping, we will likely not allow the second * ping to go through to avoid flooding the network with ping traffic. * * @return <tt>true</tt> if new pings are allowed along this connection, * otherwise <tt>false</tt> */ public boolean allowNewPings() { synchronized(PING_LOCK) { long curTime = System.currentTimeMillis(); // don't allow new pings if the connection could drop any second if(!isStable(curTime)) return false; if(curTime < _nextPingTime) { return false; } _nextPingTime = System.currentTimeMillis() + 2500; return true; } } /** * Returns whether or not we should allow new pongs on this connection. If * we have recently received a pong, we will likely not allow the second * pong to go through to avoid flooding the network with pong traffic. * In practice, this is only used to limit pongs sent to leaves. * * @return <tt>true</tt> if new pongs are allowed along this connection, * otherwise <tt>false</tt> */ public boolean allowNewPongs() { synchronized(PONG_LOCK) { long curTime = System.currentTimeMillis(); // don't allow new pongs if the connection could drop any second if(!isStable(curTime)) return false; if(curTime < _nextPongTime) { return false; } int interval; // if the connection is young, give it a lot of pongs, otherwise // be more conservative if(curTime - getConnectionTime() < 10000) { interval = 300; } else { interval = 12000; } _nextPongTime = curTime + interval; return true; } } /** * Returns the number of intra-Ultrapeer connections this node maintains. * * @return the number of intra-Ultrapeer connections this node maintains */ public int getNumIntraUltrapeerConnections() { return _headersRead.getNumIntraUltrapeerConnections(); } // implements ReplyHandler interface -- inherit doc comment public boolean isHighDegreeConnection() { return _headersRead.isHighDegreeConnection(); } /** * Returns whether or not this connection is to an Ultrapeer that * supports query routing between Ultrapeers at 1 hop. * * @return <tt>true</tt> if this is an Ultrapeer connection that * exchanges query routing tables with other Ultrapeers at 1 hop, * otherwise <tt>false</tt> */ public boolean isUltrapeerQueryRoutingConnection() { return _headersRead.isUltrapeerQueryRoutingConnection(); } /** * Returns whether or not this connections supports "probe" queries, * or queries sent at TTL=1 that should not block the send path * of subsequent, higher TTL queries. * * @return <tt>true</tt> if this connection supports probe queries, * otherwise <tt>false</tt> */ public boolean supportsProbeQueries() { return _headersRead.supportsProbeQueries(); } /** * Accessor for whether or not this connection has received any * headers. * * @return <tt>true</tt> if this connection has finished initializing * and therefore has headers, otherwise <tt>false</tt> */ public boolean receivedHeaders() { return _headersRead != HandshakeResponse.createEmptyResponse(); } /** * Accessor for the <tt>HandshakeResponse</tt> instance containing all * of the Gnutella connection headers passed by this node. * * @return the <tt>HandshakeResponse</tt> instance containing all of * the Gnutella connection headers passed by this node */ public HandshakeResponse headers() { return _headersRead; } /** * Accessor for the LimeWire version reported in the connection headers * for this node. */ public String getVersion() { return _headersRead.getVersion(); } /** Returns true iff this connection wrote "Ultrapeer: false". * This does NOT necessarily mean the connection is shielded. */ public boolean isLeafConnection() { return _headersRead.isLeaf(); } /** Returns true iff this connection wrote "Supernode: true". */ public boolean isSupernodeConnection() { return _headersRead.isUltrapeer(); } /** * Returns true iff the connection is an Ultrapeer and I am a leaf, i.e., * if I wrote "X-Ultrapeer: false", this connection wrote * "X-Ultrapeer: true" (not necessarily in that order). <b>Does * NOT require that QRP is enabled</b> between the two; the Ultrapeer * could be using reflector indexing, for example. */ public boolean isClientSupernodeConnection() { //Is remote host a supernode... if (! isSupernodeConnection()) return false; //...and am I a leaf node? String value=getPropertyWritten(HeaderNames.X_ULTRAPEER); if (value==null) return false; else return !Boolean.valueOf(value).booleanValue(); } /** * Returns true iff the connection is an Ultrapeer and I am a Ultrapeer, * ie: if I wrote "X-Ultrapeer: true", this connection wrote * "X-Ultrapeer: true" (not necessarily in that order). <b>Does * NOT require that QRP is enabled</b> between the two; the Ultrapeer * could be using reflector indexing, for example. */ public boolean isSupernodeSupernodeConnection() { //Is remote host a supernode... if (! isSupernodeConnection()) return false; //...and am I a leaf node? String value=getPropertyWritten(HeaderNames.X_ULTRAPEER); if (value==null) return false; else return Boolean.valueOf(value).booleanValue(); } /** * Returns whether or not this connection is to a client supporting * GUESS. * * @return <tt>true</tt> if the node on the other end of this * connection supports GUESS, <tt>false</tt> otherwise */ public boolean isGUESSCapable() { return _headersRead.isGUESSCapable(); } /** * Returns whether or not this connection is to a ultrapeer supporting * GUESS. * * @return <tt>true</tt> if the node on the other end of this * Ultrapeer connection supports GUESS, <tt>false</tt> otherwise */ public boolean isGUESSUltrapeer() { return _headersRead.isGUESSUltrapeer(); } /** Returns true iff this connection is a temporary connection as per the headers. */ public boolean isTempConnection() { return _headersRead.isTempConnection(); } /** Returns true iff I am a supernode shielding the given connection, i.e., * if I wrote "X-Ultrapeer: true" and this connection wrote * "X-Ultrapeer: false, and <b>both support query routing</b>. */ public boolean isSupernodeClientConnection() { //Is remote host a supernode... if (! isLeafConnection()) return false; //...and am I a supernode? String value=getPropertyWritten( HeaderNames.X_ULTRAPEER); if (value==null) return false; else if (!Boolean.valueOf(value).booleanValue()) return false; //...and do both support QRP? return isQueryRoutingEnabled(); } /** Returns true if this supports GGEP'ed messages. GGEP'ed messages (e.g., * big pongs) should only be sent along connections for which * supportsGGEP()==true. */ public boolean supportsGGEP() { return _headersRead.supportsGGEP(); } /** * Sends the StatisticVendorMessage down the connection */ public void handleStatisticVM(StatisticVendorMessage svm) throws IOException { send(svm); } /** * Sends the SimppVM down the connection */ public void handleSimppVM(SimppVM simppVM) throws IOException { send(simppVM); } /** True if the remote host supports query routing (QRP). This is only * meaningful in the context of leaf-ultrapeer relationships. */ boolean isQueryRoutingEnabled() { return _headersRead.isQueryRoutingEnabled(); } // overrides Object.toString public String toString() { return "CONNECTION: host=" + _host + " port=" + _port; } /** * access the locale pref. of the connected servent */ public String getLocalePref() { return _headersRead.getLocalePref(); } /** * A ConnectObserver to finish the initialization process prior * to handing the connection to the underlying ConnectObserver. */ private class Connector implements ConnectObserver, Runnable { private final Properties requestHeaders; private final HandshakeResponder responder; private final GnetConnectObserver observer; Connector(Properties requestHeaders, HandshakeResponder responder, GnetConnectObserver observer) { this.requestHeaders = requestHeaders; this.responder = responder; this.observer = observer; } // unused. public void handleIOException(IOException iox) {} /** * The connection couldn't be created. */ public void shutdown() { observer.shutdown(); } /** We got a connection. */ public void handleConnect(Socket socket) { ThreadFactory.startThread(this, "Handshaking"); } /** Does the handshaking & completes the connection process. */ public void run() { try { preHandshakeInitialize(requestHeaders, responder, observer); observer.handleConnect(); } catch(NoGnutellaOkException ex) { observer.handleNoGnutellaOk(ex.getCode(), ex.getMessage()); } catch(BadHandshakeException ex) { observer.handleBadHandshake(); } catch(IOException iox) { observer.shutdown(); } } } // Technically, a Connection object can be equal in various ways... // Connections can be said to be equal if the pipe the information is // travelling through is the same. // Or they can be equal if the remote host is the same, even if the // two connections are on different channels. // Ideally, our equals method would use the second option, however // this has problems with tests because of the setup of various // tests, connecting multiple connection objects to a central // testing Ultrapeer, uncorrectly labelling each connection // as equal. // Using pipe equality (by socket) also fails because // the socket doesn't exist for outgoing connections until // the connection is established, but the equals method is used // before then. // Until necessary, the equals & hashCode methods are therefore // commented out and sameness equality is being used. // public boolean equals(Object o) { // return super.equals(o); // } // // public int hashCode() { // return super.hashCode(); // } }