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();
// }
}