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 com.util.LOG; import com.limegroup.gnutella.handshaking.BadHandshakeException; import com.limegroup.gnutella.handshaking.HandshakeResponder; import com.limegroup.gnutella.handshaking.HandshakeResponse; import com.limegroup.gnutella.handshaking.HeaderNames; import com.limegroup.gnutella.handshaking.NoGnutellaOkException; import com.limegroup.gnutella.messages.BadPacketException; import com.limegroup.gnutella.messages.Message; import com.limegroup.gnutella.messages.vendor.HeaderUpdateVendorMessage; import com.limegroup.gnutella.messages.vendor.MessagesSupportedVendorMessage; import com.limegroup.gnutella.messages.vendor.VendorMessage; import com.limegroup.gnutella.settings.ConnectionSettings; import com.limegroup.gnutella.util.CompressingOutputStream; import com.limegroup.gnutella.util.IpPort; import com.limegroup.gnutella.util.NetworkUtils; import com.limegroup.gnutella.util.Sockets; import com.limegroup.gnutella.util.UncompressingInputStream; import com.limegroup.gnutella.messages.ByeRequest; /** * 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 { /** * 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; private 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 */ private 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 */ private 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; /** * 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 headers read from the connection. */ private final Properties HEADERS_READ = new Properties(); /** * The <tt>HandshakeResponse</tt> wrapper for the connection headers. */ private volatile HandshakeResponse _headers = HandshakeResponse.createEmptyResponse(); /** * The <tt>HandshakeResponse</tt> wrapper for written connection headers. */ private HandshakeResponse _headersWritten = HandshakeResponse.createEmptyResponse(); /** For outgoing Gnutella 0.6 connections, the properties written * after "GNUTELLA CONNECT". Null otherwise. */ private final Properties REQUEST_HEADERS; /** * For outgoing Gnutella 0.6 connections, a function calculating the * properties written after the server's "GNUTELLA OK". For incoming * Gnutella 0.6 connections, the properties written after the client's * "GNUTELLA CONNECT". * Non-final so that the responder can be garbage collected after we've * concluded the responding (by setting to null). */ protected HandshakeResponder RESPONSE_HEADERS; /** The list of all properties written during the handshake sequence, * analogous to HEADERS_READ. This is needed because * RESPONSE_HEADERS lazily calculates properties according to what it * read. */ private final Properties HEADERS_WRITTEN = new Properties(); /** * Gnutella 0.6 connect string. */ private String GNUTELLA_CONNECT_06 = "GNUTELLA CONNECT/0.6"; /** * Gnutella 0.6 accept connection string. */ public static final String GNUTELLA_OK_06 = "GNUTELLA/0.6 200 OK"; public static final String GNUTELLA_06 = "GNUTELLA/0.6"; public static final String _200_OK = " 200 OK"; public static final String GNUTELLA_06_200 = "GNUTELLA/0.6 200"; public static final String CONNECT="CONNECT/"; /** End of line for Gnutella 0.6 */ public static final String CRLF="\r\n"; /** * Time to wait for inut from user at the remote end. (in milliseconds) */ public static final int USER_INPUT_WAIT_TIME = 2 * 60 * 1000; //2 min /** * The number of times we will respond to a given challenge * from the other side, or otherwise, during connection handshaking */ public static final int MAX_HANDSHAKE_ATTEMPTS = 5; /** * 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, possibly reverting to Gnutella 0.4 if * needed. * * If properties1 and properties2 are null, forces connection at the 0.4 * level. This is a bit of a hack to make implementation in this and * subclasses easier; outside classes are discouraged from using it. * * @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 responseHeaders 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, Properties requestHeaders, HandshakeResponder responseHeaders) { if(host == null) { throw new NullPointerException("null host"); } if(!NetworkUtils.isValidPort(port)) { throw new IllegalArgumentException("illegal port: "+port); } if(requestHeaders == null) { throw new NullPointerException("null request headers"); } if(responseHeaders == null) { throw new NullPointerException("null response headers"); } _host = host; _port = port; OUTGOING = true; REQUEST_HEADERS = requestHeaders; RESPONSE_HEADERS = responseHeaders; } /** * 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 responseHeaders 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, HandshakeResponder responseHeaders) { if(socket == null) { throw new NullPointerException("null socket"); } if(responseHeaders == null) { throw new NullPointerException("null response headers"); } //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; RESPONSE_HEADERS = responseHeaders; REQUEST_HEADERS = null; } /** 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(_headers.supportsVendorMessages()) { send(MessagesSupportedVendorMessage.instance()); } } catch (IOException ioe) { //LOG.logSp("postInit" + this.getAddress() + ioe.getMessage()); } } /** * 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 HeaderUpdateVendorMessage) { HeaderUpdateVendorMessage huvm = (HeaderUpdateVendorMessage)vm; HEADERS_READ.putAll(huvm.getProperties()); try { _headers = HandshakeResponse.createResponse(HEADERS_READ); }catch(IOException ignored){} } } /** * Initializes this without timeout; exactly like initialize(0). * @see initialize(int) */ public void initialize() throws IOException, NoGnutellaOkException, BadHandshakeException { initialize(4000); } /** * 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. */ public void initialize(int timeout) throws IOException, NoGnutellaOkException, BadHandshakeException { if(isOutgoing()) _socket=Sockets.connect(_host, _port, timeout); // 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 = null; try { localAddress = _socket.getLocalAddress(); } catch (Exception e) { LOG.error("getLocalAddress " + e.getMessage()); } if (localAddress != null) { if ( _socket.getInetAddress().equals(localAddress) && _port == ConnectionSettings.PORT) { throw new IOException("Connection to self"); } else { LOG.error("connection before set our address"); // Notify the acceptor of our address. RouterService.getAcceptor().setAddress(localAddress); } } 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"); } LOG.error("connection before gnutella processing"); try { //In all the line reading code below, we are somewhat lax in //distinguishing between '\r' and '\n'. Who cares? if(isOutgoing()) initializeOutgoing(); else initializeIncoming(); _headersWritten = HandshakeResponse.createResponse(HEADERS_WRITTEN); _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 = Message.SOFT_MAX; 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()) { // _out = new ZOutputStream(_out, JZlib.Z_NO_COMPRESSION); //_deflater = new Deflater(); //_out = new CompressingOutputStream(_out, _deflater); } if(isReadDeflated()) { _inflater = new Inflater(); _in = new UncompressingInputStream(_in, _inflater); //_in = new ZInputStream(_in); } // remove the reference to the RESPONSE_HEADERS, since we'll no // longer be responding. // This does not need to be in a finally clause, because if an // exception was thrown, the connection will be removed anyway. RESPONSE_HEADERS = null; } catch (NoGnutellaOkException e) { close(); throw e; } catch (IOException e) { close(); throw new BadHandshakeException(e); } } /** * 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; } /** * Sends and receives handshake strings for outgoing connections, * throwing exception if any problems. * * @exception NoGnutellaOkException one of the participants responded * with an error code other than 200 OK (possibly after several rounds * of 401's) * @exception IOException any other error. */ private void initializeOutgoing() throws IOException { //1. Send "GNUTELLA CONNECT/0.6" and headers writeLine(GNUTELLA_CONNECT_06+CRLF); sendHeaders(REQUEST_HEADERS); //conclude the handshake (This may involve exchange of //information multiple times with the host at the other end). concludeOutgoingHandshake(); } /** * Responds to the responses/challenges from the host on the other * end of the connection, till a conclusion reaches. Handshaking may * involve multiple steps. * * @exception NoGnutellaOkException one of the participants responded * with an error code other than 200 OK (possibly after several rounds * of 401's) * @exception IOException any other error. */ private void concludeOutgoingHandshake() throws IOException { //This step may involve handshaking multiple times so as //to support challenge/response kind of behaviour for(int i=0; i < MAX_HANDSHAKE_ATTEMPTS; i++) { //2. Read "GNUTELLA/0.6 200 OK" String connectLine = readLine(); Assert.that(connectLine != null, "null connectLine"); if (! connectLine.startsWith(GNUTELLA_06)) { throw new IOException("Bad connect string"); } //3. Read the Gnutella headers. readHeaders(Constants.TIMEOUT); //Terminate abnormally if we read something other than 200 or 401. HandshakeResponse theirResponse = HandshakeResponse.createRemoteResponse( connectLine.substring(GNUTELLA_06.length()).trim(), HEADERS_READ); _headers = theirResponse; Assert.that(theirResponse != null, "null theirResponse"); int code = theirResponse.getStatusCode(); if (code != HandshakeResponse.OK && code != HandshakeResponse.UNAUTHORIZED_CODE) { if(code == HandshakeResponse.SLOTS_FULL) { throw NoGnutellaOkException.SERVER_REJECT; } else { throw NoGnutellaOkException.createServerUnknown(code); } } //4. Write "GNUTELLA/0.6" plus response code, such as "200 OK", // and headers. Assert.that(RESPONSE_HEADERS != null, "null RESPONSE_HEADERS"); HandshakeResponse ourResponse = RESPONSE_HEADERS.respond(theirResponse, true); Assert.that(ourResponse != null, "null ourResponse"); writeLine(GNUTELLA_06 + " " + ourResponse.getStatusLine() + CRLF); sendHeaders(ourResponse.props()); code = ourResponse.getStatusCode(); //Consider termination... if(code == HandshakeResponse.OK) { if(HandshakeResponse.OK_MESSAGE.equals( ourResponse.getStatusMessage())){ //a) Terminate normally if we wrote "200 OK". return; } else { //b) Continue loop if we wrote "200 AUTHENTICATING". continue; } } else { //c) Terminate abnormally if we wrote anything else. if(code == HandshakeResponse.SLOTS_FULL) { throw NoGnutellaOkException.CLIENT_REJECT; } else if(code == HandshakeResponse.LOCALE_NO_MATCH) { //if responder's locale preferencing was set //and didn't match the locale this code is used. //(currently in use by the dedicated connectionfetcher) throw NoGnutellaOkException.CLIENT_REJECT_LOCALE; } else { throw NoGnutellaOkException.createClientUnknown(code); } } } //If we didn't successfully return out of the method, throw an exception //to indicate that handshaking didn't reach any conclusion. The values //here are kind of a hack. throw NoGnutellaOkException.UNRESOLVED_SERVER; } /** * Sends and receives handshake strings for incoming connections, * throwing exception if any problems. * * @exception NoGnutellaOkException one of the participants responded * with an error code other than 200 OK (possibly after several rounds * of 401's) * @exception IOException if there's an unexpected connect string or * any other problem */ private void initializeIncoming() throws IOException { //Dispatch based on first line read. Remember that "GNUTELLA " has //already been read by Acceptor. Hence we are looking for "CONNECT/0.6" String connectString = readLine(); if (notLessThan06(connectString)) { //1. Read headers (connect line has already been read) readHeaders(); //Conclude the handshake (This may involve exchange of information //multiple times with the host at the other end). concludeIncomingHandshake(); } else { throw new IOException("Unexpected connect string: "+connectString); } } /** * Responds to the handshake from the host on the other * end of the connection, till a conclusion reaches. Handshaking may * involve multiple steps. * * @exception NoGnutellaOkException one of the participants responded * with an error code other than 200 OK (possibly after several rounds * of 401's) * @exception IOException any other error. May wish to retry at 0.4 */ private void concludeIncomingHandshake() throws IOException { //Respond to the handshake. This step may involve handshaking multiple //times so as to support challenge/response kind of behaviour for(int i=0; i < MAX_HANDSHAKE_ATTEMPTS; i++){ //2. Send our response and headers. // is this an incoming connection from the crawler?? boolean isCrawler = _headers.isCrawler(); //Note: in the following code, it appears that we're ignoring //the response code written by the initiator of the connection. //However, you can prove that the last code was always 200 OK. //See initializeIncoming and the code at the bottom of this //loop. HandshakeResponse ourResponse = RESPONSE_HEADERS.respond(_headers, false); writeLine(GNUTELLA_06 + " " + ourResponse.getStatusLine() + CRLF); sendHeaders(ourResponse.props()); // if it was the crawler, leave early. if(isCrawler) { // read one response, just to make sure they got ours. readLine(); throw new IOException("crawler"); } //Our response should be either OK or UNAUTHORIZED for the handshake //to proceed. int code = ourResponse.getStatusCode(); if((code != HandshakeResponse.OK) && (code != HandshakeResponse.UNAUTHORIZED_CODE)) { if(code == HandshakeResponse.SLOTS_FULL) { throw NoGnutellaOkException.CLIENT_REJECT; } else { throw NoGnutellaOkException.createClientUnknown(code); } } //3. read the response from the other side. If we asked the other //side to authenticate, give more time so as to receive user input String connectLine; if(ourResponse.getStatusCode() == HandshakeResponse.UNAUTHORIZED_CODE){ connectLine = readLine(USER_INPUT_WAIT_TIME); readHeaders(USER_INPUT_WAIT_TIME); _headers = HandshakeResponse.createResponse(HEADERS_READ); } else{ connectLine = readLine(); readHeaders(); } if (! connectLine.startsWith(GNUTELLA_06)) { throw new IOException("Bad connect string"); } HandshakeResponse theirResponse = HandshakeResponse.createRemoteResponse( connectLine.substring(GNUTELLA_06.length()).trim(), HEADERS_READ); //Decide whether to proceed. code = ourResponse.getStatusCode(); if(code == HandshakeResponse.OK) { if(theirResponse.getStatusCode() == HandshakeResponse.OK) { //a) If we wrote 200 and they wrote 200 OK, stop normally. return; } } else { Assert.that(code==HandshakeResponse.UNAUTHORIZED_CODE, "Response code: "+code); if(theirResponse.getStatusCode()==HandshakeResponse.OK) //b) If we wrote 401 and they wrote "200...", keep looping. continue; } //c) Terminate abnormally throw NoGnutellaOkException. createServerUnknown(theirResponse.getStatusCode()); } //If we didn't successfully return out of the method, throw an exception //to indicate that handshaking didn't reach any conclusion. throw NoGnutellaOkException.UNRESOLVED_CLIENT; } /** Returns true iff line ends with "CONNECT/N", where N * is a number greater than or equal "0.6". */ private static boolean notLessThan06(String line) { int i=line.indexOf(CONNECT); if (i<0) return false; try { Float F = new Float(line.substring(i+CONNECT.length())); float f= F.floatValue(); return f>=0.6f; } catch (NumberFormatException e) { return false; } } /** * Writes the properties in props to network, including the blank line at * the end. Throws IOException if there are any problems. * @param props The headers to be sent. Note: null argument is * acceptable, if no headers need to be sent (still the trailer will * be sent * @modifies network */ private void sendHeaders(Properties props) throws IOException { if(props != null) { Enumeration names=props.propertyNames(); while (names.hasMoreElements()) { String key=(String)names.nextElement(); String value=props.getProperty(key); // Overwrite any domainname with true IP address if ( HeaderNames.REMOTE_IP.equals(key) ) value=getInetAddress().getHostAddress(); if (value==null) value=""; writeLine(key+": "+value+CRLF); HEADERS_WRITTEN.put(key, value); } } //send the trailer writeLine(CRLF); } /** * Reads the properties from the network into HEADERS_READ, throwing * IOException if there are any problems. * @modifies network */ private void readHeaders() throws IOException { readHeaders(Constants.TIMEOUT); _headers = HandshakeResponse.createResponse(HEADERS_READ); } /** * Reads the properties from the network into HEADERS_READ, throwing * IOException if there are any problems. * @param timeout The time to wait on the socket to read data before * IOException is thrown * @return The line of characters read * @modifies network * @exception IOException if the characters cannot be read within * the specified timeout */ private void readHeaders(int timeout) throws IOException { //TODO: limit number of headers read while (true) { //This doesn't distinguish between \r and \n. That's fine. String line=readLine(timeout); if (line==null) throw new IOException("unexpected end of file"); //unexpected EOF if (line.equals("")) return; //blank line ==> done int i=line.indexOf(':'); if (i<0) continue; //ignore lines without ':' String key=line.substring(0, i); String value=line.substring(i+1).trim(); if (HeaderNames.REMOTE_IP.equals(key)) changeAddress(value); HEADERS_READ.put(key, value); } } /** * Determines if the address should be changed and changes it if * necessary. */ private void changeAddress(final String v) { InetAddress ia = null; try { ia = InetAddress.getByName(v); } catch(UnknownHostException uhe) { return; // invalid. } // invalid or private, exit if(!NetworkUtils.isValidAddress(ia) || NetworkUtils.isPrivateAddress(ia)) return; // Otherwise, if our current address is invalid, change. else if(!NetworkUtils.isValidAddress(RouterService.getAddress())) { // will auto-call addressChanged. RouterService.getAcceptor().setAddress(ia); } RouterService.getAcceptor().setExternalAddress(ia); } /** * Writes s to out, with no trailing linefeeds. Called only from * initialize(). * @requires _socket, _out are properly set up */ private void writeLine(String s) throws IOException { if(s == null || s.equals("")) { throw new NullPointerException("null or empty string: "+s); } //TODO: character encodings? byte[] bytes=s.getBytes(); _out.write(bytes); _out.flush(); } /** * Reads and returns one line from the network. A line is defined as a * maximal sequence of characters without '\n', with '\r''s removed. If the * characters cannot be read within TIMEOUT milliseconds (as defined by the * property manager), throws IOException. This includes EOF. * @return The line of characters read * @requires _socket is properly set up * @modifies network * @exception IOException if the characters cannot be read within * the specified timeout */ private String readLine() throws IOException { return readLine(Constants.TIMEOUT); } /** * Reads and returns one line from the network. A line is defined as a * maximal sequence of characters without '\n', with '\r''s removed. If the * characters cannot be read within the specified timeout milliseconds, * throws IOException. This includes EOF. * @param timeout The time to wait on the socket to read data before * IOException is thrown * @return The line of characters read * @requires _socket is properly set up * @modifies network * @exception IOException if the characters cannot be read within * the specified timeout */ private String readLine(int timeout) throws IOException { int oldTimeout=_socket.getSoTimeout(); // _in.read can throw an NPE if we closed the connection, // so we must catch NPE and throw the CONNECTION_CLOSED. try { _socket.setSoTimeout(timeout); String line=(new ByteReader(_in)).readLine(); if (line==null) throw new IOException("read null line"); return line; } catch(NullPointerException npe) { throw CONNECTION_CLOSED; } finally { //Restore socket timeout. _socket.setSoTimeout(oldTimeout); } } /** * Returns the stream to use for writing to s. * By default this is a BufferedOutputStream. * Subclasses may override to decorate the stream. */ protected OutputStream getOutputStream() throws IOException { return new BufferedOutputStream(_socket.getOutputStream()); } /** * Returns the stream to use for reading from s. * By default this is a BufferedInputStream. * Subclasses may override to decorate the stream. */ protected InputStream getInputStream() throws IOException { 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. * * @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 { //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) { //LOG.logSp("receive already close " + this.getAddress()); 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. * * @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 { //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 { //int pCompressed = 0, pUncompressed = 0; // LOG.logSp("in readAndUpdateStatistics" + this.getAddress()); // 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 = null; try { /* if(isReadDeflated()) { pCompressed = _inflater.getTotalIn(); pUncompressed = _inflater.getTotalOut(); } */ // DO THE ACTUAL READ msg = Message.read(_in, HEADER_BUF, Message.N_TCP, _softMax); //LOG.logSp("Connection (" + toString() + // ") read message: " + msg); // _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() && _inflater != null) { _compressedBytesReceived = _inflater.getTotalIn(); _bytesReceived = _inflater.getTotalOut(); } else if(msg != null) { _bytesReceived += msg.getTotalLength(); } } catch(NullPointerException npe) { LOG.logSp("Caught NPE in readAndUpdateStatistics, throwing IO."); throw CONNECTION_CLOSED; } return msg; } /** * 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); // in order to analyze the savings of compression, // we must add the 'new' data to a stat. long priorCompressed = 0, priorUncompressed = 0; // 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 { if ( isWriteDeflated() && _deflater != null ) { priorUncompressed = _deflater.getTotalIn(); priorCompressed = _deflater.getTotalOut(); } if (this._socket.isClosed()) { //LOG.logSp("closed before write" + this.getAddress()); } m.write(_out, OUT_HEADER_BUF); if (this._socket.isClosed()) { //LOG.logSp("closed after write" + this.getAddress()); } updateWriteStatistics(m, priorUncompressed, priorCompressed); } catch(NullPointerException e) { //LOG.logSp("closed in send with NullPointerException" + this.getAddress()); throw CONNECTION_CLOSED; } catch (Exception e) { // LOG.logSp("closed in send " + this.getAddress() + e.getMessage()); } } /** * Flushes any buffered messages sent through the send method. */ public void flush() throws IOException { // in order to analyze the savings of compression, // we must add the 'new' data to a stat. long priorCompressed = 0, priorUncompressed = 0; // 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 { if ( isWriteDeflated() && _deflater != null) { priorUncompressed = _deflater.getTotalIn(); priorCompressed = _deflater.getTotalOut(); } _out.flush(); // we must update the write statistics again, // because flushing forces the deflater to deflate. updateWriteStatistics(null, priorUncompressed, priorCompressed); } catch(NullPointerException npe) { throw CONNECTION_CLOSED; } } /** * Updates the write statistics. * @param m the possibly null message to add to the bytes sent * @param pUn the prior uncompressed traffic, used for adding to stats * @param pComp the prior compressed traffic, used for adding to stats */ private void updateWriteStatistics(Message m, long pUn, long pComp) { if( m != null ) _bytesSent += m.getTotalLength(); if(isWriteDeflated() && _deflater != null) { _compressedBytesSent = _deflater.getTotalOut(); } } /** * 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 -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; } /** * 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 HEADERS_WRITTEN.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() { //LOG.logSp("close connecton in close" + this.getAddress()); if(_closed) return; //LOG.logSp("close connecton in close and call stack" + this.getAddress()); //LOG.callStack(); // 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 if (_in != null) { try { _in.close(); } catch (IOException e) {} } if (_out != null) { try { _out.close(); } catch (IOException e) {} } } /** 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 _headers.getUserAgent(); } /** * Returns whether or not the remote host is a LimeWire (or derivative) */ public boolean isLimeWire() { return _headers.isLimeWire(); } public boolean isOldLimeWire() { return _headers.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 _headers.isDeflateEnabled(); } // inherit doc comment public boolean isGoodUltrapeer() { return _headers.isGoodUltrapeer(); } // inherit doc comment public boolean isGoodLeaf() { return _headers.isGoodLeaf(); } // inherit doc comment public boolean supportsPongCaching() { return _headers.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 _headers.getNumIntraUltrapeerConnections(); } // implements ReplyHandler interface -- inherit doc comment public boolean isHighDegreeConnection() { return _headers.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 _headers.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 _headers.supportsProbeQueries(); } /** * Returns the authenticated domains listed in the connection headers * for this connection. * * @return the string of authenticated domains for this connection */ public String getDomainsAuthenticated() { return _headers.getDomainsAuthenticated(); } /** * 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 _headers != null; } /** * 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 _headers; } /** * Accessor for the LimeWire version reported in the connection headers * for this node. */ public String getVersion() { return _headers.getVersion(); } /** Returns true iff this connection wrote "Ultrapeer: false". * This does NOT necessarily mean the connection is shielded. */ public boolean isLeafConnection() { return _headers.isLeaf(); } /** Returns true iff this connection wrote "Supernode: true". */ public boolean isSupernodeConnection() { return _headers.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 _headers.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 _headers.isGUESSUltrapeer(); } /** Returns true iff this connection is a temporary connection as per the headers. */ public boolean isTempConnection() { return _headers.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 _headers.supportsGGEP(); } /** True if the remote host supports query routing (QRP). This is only * meaningful in the context of leaf-ultrapeer relationships. */ boolean isQueryRoutingEnabled() { return _headers.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 _headers.getLocalePref(); } // 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(); // } /////////////////////////// Unit Tests /////////////////////////////////// // /** Unit test */ // public static void main(String args[]) { // Assert.that(! notLessThan06("CONNECT")); // Assert.that(! notLessThan06("CONNECT/0.4")); // Assert.that(! notLessThan06("CONNECT/0.599")); // Assert.that(! notLessThan06("CONNECT/XP")); // Assert.that(notLessThan06("CONNECT/0.6")); // Assert.that(notLessThan06("CONNECT/0.7")); // Assert.that(notLessThan06("GNUTELLA CONNECT/1.0")); // final Properties props=new Properties(); // props.setProperty("Query-Routing", "0.3"); // HandshakeResponder standardResponder=new HandshakeResponder() { // public HandshakeResponse respond(HandshakeResponse response, // boolean outgoing) { // return new HandshakeResponse(props); // } // }; // HandshakeResponder secretResponder=new HandshakeResponder() { // public HandshakeResponse respond(HandshakeResponse response, // boolean outgoing) { // Properties props2=new Properties(); // props2.setProperty("Secret", "abcdefg"); // return new HandshakeResponse(props2); // } // }; // ConnectionPair p=null; // //1. 0.4 => 0.4 // p=connect(null, null, null); // Assert.that(p!=null); // Assert.that(p.in.getProperty("Query-Routing")==null); // Assert.that(p.out.getProperty("Query-Routing")==null); // disconnect(p); // //2. 0.6 => 0.6 // p=connect(standardResponder, props, secretResponder); // Assert.that(p!=null); // Assert.that(p.in.getProperty("Query-Routing").equals("0.3")); // Assert.that(p.out.getProperty("Query-Routing").equals("0.3")); // Assert.that(p.out.getProperty("Secret")==null); // Assert.that(p.in.getProperty("Secret").equals("abcdefg")); // disconnect(p); // //3. 0.4 => 0.6 (Incoming doesn't send properties) // p=connect(standardResponder, null, null); // Assert.that(p!=null); // Assert.that(p.in.getProperty("Query-Routing")==null); // Assert.that(p.out.getProperty("Query-Routing")==null); // disconnect(p); // //4. 0.6 => 0.4 (If the receiving connection were Gnutella 0.4, this // //wouldn't work. But the new guy will automatically upgrade to 0.6.) // p=connect(null, props, standardResponder); // Assert.that(p!=null); // //Assert.that(p.in.getProperty("Query-Routing")==null); // Assert.that(p.out.getProperty("Query-Routing")==null); // disconnect(p); // //5. // LOG.info("-Testing IOException reading from closed socket"); // p=connect(null, null, null); // Assert.that(p!=null); // p.in.close(); // try { // p.out.receive(); // Assert.that(false); // } catch (BadPacketException failed) { // Assert.that(false); // } catch (IOException pass) { // } // //6. // LOG.info("-Testing IOException writing to closed socket"); // p=connect(null, null, null); // Assert.that(p!=null); // p.in.close(); // try { Thread.sleep(2000); } catch (InterruptedException e) { } // try { // //You'd think that only one write is needed to get IOException. // //That doesn't seem to be the case, and I'm not 100% sure why. It // //has something to do with TCP half-close state. Anyway, this // //slightly weaker test is good enough. // p.out.send(new QueryRequest((byte)3, 0, "las")); // p.out.flush(); // p.out.send(new QueryRequest((byte)3, 0, "las")); // p.out.flush(); // Assert.that(false); // } catch (IOException pass) { // } // //7. // LOG.info("-Testing connect with timeout"); // Connection c=new Connection("this-host-does-not-exist.limewire.com", 6346); // int TIMEOUT=1000; // long start=System.currentTimeMillis(); // try { // c.initialize(TIMEOUT); // Assert.that(false); // } catch (IOException e) { // //Check that exception happened quickly. Note fudge factor below. // long elapsed=System.currentTimeMillis()-start; // Assert.that(elapsed<(3*TIMEOUT)/2, "Took too long to connect: "+elapsed); // } // } // private static class ConnectionPair { // Connection in; // Connection out; // } // private static ConnectionPair connect(HandshakeResponder inProperties, // Properties outRequestHeaders, // HandshakeResponder outProperties2) { // ConnectionPair ret=new ConnectionPair(); // com.limegroup.gnutella.tests.MiniAcceptor acceptor= // new com.limegroup.gnutella.tests.MiniAcceptor(inProperties); // try { // ret.out=new Connection("localhost", 6346, // outRequestHeaders, outProperties2, // true); // ret.out.initialize(); // } catch (IOException e) { } // ret.in=acceptor.accept(); // if (ret.in==null || ret.out==null) // return null; // else // return ret; // } // private static void disconnect(ConnectionPair cp) { // if (cp.in!=null) // cp.in.close(); // if (cp.out!=null) // cp.out.close(); // } }