package com.limegroup.gnutella.connection;
import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.Properties;
import java.util.concurrent.atomic.AtomicBoolean;
import org.limewire.core.settings.ConnectionSettings;
import org.limewire.core.settings.NetworkSettings;
import org.limewire.io.IOUtils;
import org.limewire.io.NetworkInstanceUtils;
import org.limewire.io.NetworkUtils;
import org.limewire.logging.Log;
import org.limewire.logging.LogFactory;
import org.limewire.net.SocketsManager.ConnectType;
import org.limewire.nio.ssl.SSLUtils;
import org.limewire.setting.StringSetting;
import com.limegroup.gnutella.Acceptor;
import com.limegroup.gnutella.NetworkManager;
import com.limegroup.gnutella.handshaking.HandshakeResponse;
import com.limegroup.gnutella.handshaking.Handshaker;
import com.limegroup.gnutella.handshaking.HeaderNames;
import com.limegroup.gnutella.messages.Message;
import com.limegroup.gnutella.messages.vendor.CapabilitiesVM;
import com.limegroup.gnutella.messages.vendor.CapabilitiesVMFactory;
import com.limegroup.gnutella.messages.vendor.HeaderUpdateVendorMessage;
import com.limegroup.gnutella.messages.vendor.MessagesSupportedVendorMessage;
import com.limegroup.gnutella.messages.vendor.VendorMessage;
/**
* A basic implementation of {@link Connection}. The only methods that
* subclasses must implement are <code>send</code> and <code>closeImpl</code>.
* However, in order to be useful in any manner, it is recommended that
* subclasses expose some way of starting the Connection. A blocking
* implementation might expose a <code>read</code> method that returns
* <code>Message</code>s as they're read. An asynchronous implementation (as
* used by implementations of {@link RoutedConnection} would expose a method to
* start receiving messages and would internally funnel them to a piece of code
* that would handle the incoming messages.
* <p>
*
* <code>AbstractConnection</code> will maintain all features and capabilities
* sent & received by headers and vendor messages. Additionally, it will keep
* track of the bandwidth used by compressed connections (incoming or outgoing),
* TLS-encoded connections, and the raw bandwidth used without the wrapping
* protocols.
* <p>
*
* An <code>AbstractConnection</code> can either be outgoing or incoming. If
* the class is constructed with a host/port, it is an outgoing connection and
* the socket must be set after the connection is finished with
* {@link #setSocket(Socket)}. Incoming connections must be constructed with a
* preconnected Socket.
* <p>
*
* Subclasses should do the following to ensure that
* <code>AbstractConnection</code> is setup correctly.
* <ul>
* <li> Call {@link #initializeHandshake()} after connecting, prior to
* performing a handshake, to validate the connection is allowed.</li>
* <li> Call {@link #handshakeInitialized(Handshaker)} after the handshake is
* finished, to ensure all headers are processed correctly.</li>
* <li> Call {@link #processReadMessage(Message)} when a new
* <code>Message</code> is read, and {@link #processWrittenMessage(Message)}
* when a new <code>Message</code> is written. These ensure that the bandwidth
* statistics are kept up-to-date.</li>
* </ul>
*/
public abstract class AbstractConnection implements Connection {
private static Log LOG = LogFactory.getLog(AbstractConnection.class);
/** Lock for maintaining accurate data for when to allow ping forwarding. */
private final Object pingLock = new Object();
/** Lock for maintaining accurate data for when to allow pong forwarding. */
private final Object pongLock = 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 ConnectType connectType;
/** The address of the remote host. */
private final String host;
/** The IP of the remote side in byte[] format */
private final byte []hostBytes;
/** The port the remote host is listening on. */
private volatile int port;
/** The socket connecting us to the remote host. */
protected volatile Socket socket;
/** True if this was an outgoing connection. */
private final boolean outgoing;
private volatile byte softMax;
/**
* 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. This also protects us from calling methods
* on the Inflater/Deflater objects after end() has been called on them.
*/
private final AtomicBoolean closed = new AtomicBoolean(false);
/**
* 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;
/** Factory for constructing new CapabilitiesVMs. */
private final CapabilitiesVMFactory capabilitiesVMFactory;
/** The sole MessagesSupportedVM this sends. */
private final MessagesSupportedVendorMessage supportedVendorMessage;
private final ConnectionCapabilities connectionCapabilities;
private final ConnectionBandwidthStatistics connectionBandwidthStatistics;
private volatile long connectionTime;
private final NetworkManager networkManager;
private final Acceptor acceptor;
private final SimpleProtocolBandwidthTracker simpleProtocolBandwidthTracker;
/** The IP of this connection if reported by the remote side */
protected volatile byte []myIp;
/**
* 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");
private final NetworkInstanceUtils networkInstanceUtils;
/**
* Creates an uninitialized outgoing Gnutella connection.
*
* @param host the name of the host to connect to
* @param port the port of the remote host
* @param connectType the type of connection that should be made
*/
AbstractConnection(String host, int port, ConnectType connectType,
CapabilitiesVMFactory capabilitiesVMFactory,
MessagesSupportedVendorMessage supportedVendorMessage, NetworkManager networkManager,
Acceptor acceptor, NetworkInstanceUtils networkInstanceUtils) {
this(host, port, connectType, null, capabilitiesVMFactory, supportedVendorMessage,
networkManager, acceptor, networkInstanceUtils);
}
/**
* Creates an uninitialized incoming Gnutella connection.
*
* @param socket the socket accepted by a ServerSocket. The word "GNUTELLA "
* and nothing else must have been read from the socket.
*/
AbstractConnection(Socket socket, CapabilitiesVMFactory capabilitiesVMFactory,
MessagesSupportedVendorMessage supportedVendorMessage, NetworkManager networkManager,
Acceptor acceptor, NetworkInstanceUtils networkInstanceUtils) {
this(socket.getInetAddress().getHostAddress(), socket.getPort(), SSLUtils
.isTLSEnabled(socket) ? ConnectType.TLS : ConnectType.PLAIN, socket,
capabilitiesVMFactory, supportedVendorMessage, networkManager, acceptor,
networkInstanceUtils);
}
private AbstractConnection(String host, int port, ConnectType connectType, Socket socket,
CapabilitiesVMFactory capabilitiesVMFactory,
MessagesSupportedVendorMessage supportedVendorMessage, NetworkManager networkManager,
Acceptor acceptor, NetworkInstanceUtils networkInstanceUtils) {
if (host == null)
throw new NullPointerException("null host");
if (!NetworkUtils.isValidPort(port))
throw new IllegalArgumentException("illegal port: " + port);
this.host = host;
this.port = port;
this.outgoing = socket == null;
this.connectType = connectType;
this.socket = socket;
this.capabilitiesVMFactory = capabilitiesVMFactory;
this.supportedVendorMessage = supportedVendorMessage;
this.connectionCapabilities = new ConnectionCapabilitiesImpl();
this.connectionBandwidthStatistics = new ConnectionBandwidthStatisticsImpl();
this.networkManager = networkManager;
this.acceptor = acceptor;
this.simpleProtocolBandwidthTracker = new SimpleProtocolBandwidthTracker();
this.networkInstanceUtils = networkInstanceUtils;
byte [] hostBytes = null;
try {
hostBytes = InetAddress.getByName(getAddress()).getAddress();
} catch (UnknownHostException bad) {
}
this.hostBytes = hostBytes;
if (!outgoing) {
connectionBandwidthStatistics.setTlsOption(SSLUtils.isTLSEnabled(socket), SSLUtils
.getSSLBandwidthTracker(socket));
}
connectionBandwidthStatistics.setRawBandwidthTracker(simpleProtocolBandwidthTracker);
}
/**
* Call this method when the Connection has been initialized and accepted as
* 'long-lived'.
*/
public void sendPostInitializeMessages() {
try {
if (getConnectionCapabilities().getHeadersRead().supportsVendorMessages() > 0) {
send(supportedVendorMessage);
send(capabilitiesVMFactory.getCapabilitiesVM());
}
} catch (IOException ioe) {
}
}
/**
* Call this method if you want to send your neighbours a message with your
* updated capabilities.
*/
public void sendUpdatedCapabilities() {
LOG.trace("Sending updated capabilities");
try {
if (getConnectionCapabilities().getHeadersRead().supportsVendorMessages() > 0)
send(capabilitiesVMFactory.getCapabilitiesVM());
} catch (IOException iox) {
}
}
/**
* Call this method when you want to handle us to handle a VM. We may....
*/
public void handleVendorMessage(VendorMessage vm) {
if (vm instanceof MessagesSupportedVendorMessage) {
getConnectionCapabilities().setMessagesSupportedVendorMessage(
(MessagesSupportedVendorMessage) vm);
} else if (vm instanceof CapabilitiesVM) {
getConnectionCapabilities().setCapabilitiesVendorMessage((CapabilitiesVM) vm);
} else if (vm instanceof HeaderUpdateVendorMessage) {
HeaderUpdateVendorMessage huvm = (HeaderUpdateVendorMessage) vm;
Properties props = getConnectionCapabilities().getHeadersRead().props();
props.putAll(huvm.getProperties());
setHeaders(HandshakeResponse.createResponse(props), null);
}
}
/** Sets the headers read & written. null headers are ignored. */
protected void setHeaders(HandshakeResponse headersRead, HandshakeResponse headersWritten) {
if (headersRead != null) {
getConnectionCapabilities().setHeadersRead(headersRead);
}
if (headersWritten != null) {
getConnectionCapabilities().setHeadersWritten(headersWritten);
}
}
/*
* (non-Javadoc)
*
* @see com.limegroup.gnutella.Connection#isOutgoing()
*/
public boolean isOutgoing() {
return outgoing;
}
/*
* (non-Javadoc)
*
* @see com.limegroup.gnutella.Connection#getAddress()
*/
public String getAddress() {
return host;
}
public byte [] getAddressBytes() {
return hostBytes;
}
/*
* (non-Javadoc)
*
* @see com.limegroup.gnutella.Connection#getPort()
*/
public int getPort() {
return port;
}
/*
* (non-Javadoc)
*
* @see com.limegroup.gnutella.Connection#getListeningPort()
*/
public int getListeningPort() {
if (isOutgoing()) {
if (socket == null) {
return -1;
} else {
return socket.getPort();
}
} else {
return getConnectionCapabilities().getHeadersRead().getListeningPort();
}
}
/**
* Sets the port where the connected node listens at, not the one got from
* socket.
*/
public void setListeningPort(int port) {
if (!NetworkUtils.isValidPort(port))
throw new IllegalArgumentException("invalid port: " + port);
this.port = port;
}
@Override
public String getAddressDescription() {
return getInetSocketAddress().toString();
}
public InetSocketAddress getInetSocketAddress() throws IllegalStateException {
return new InetSocketAddress(getInetAddress(), getPort());
}
public InetAddress getInetAddress() throws IllegalStateException {
if (socket == null) {
throw new IllegalStateException("Not initialized");
}
return socket.getInetAddress();
}
/*
* (non-Javadoc)
*
* @see com.limegroup.gnutella.Connection#getSocket()
*/
public Socket getSocket() throws IllegalStateException {
if (socket == null) {
throw new IllegalStateException("Not initialized");
}
return socket;
}
/** Sets the socket this is using. */
protected void setSocket(Socket socket) {
this.socket = socket;
getConnectionBandwidthStatistics().setTlsOption(SSLUtils.isTLSEnabled(socket),
SSLUtils.getSSLBandwidthTracker(socket));
}
/*
* (non-Javadoc)
*
* @see com.limegroup.gnutella.Connection#isStable()
*/
public boolean isStable() {
return isStable(System.currentTimeMillis());
}
/*
* (non-Javadoc)
*
* @see com.limegroup.gnutella.Connection#isStable(long)
*/
public boolean isStable(long millis) {
return (millis - getConnectionTime()) / 1000 > 5;
}
/*
* (non-Javadoc)
*
* @see com.limegroup.gnutella.Connection#getPropertyWritten(java.lang.String)
*/
public String getPropertyWritten(String name) {
return getConnectionCapabilities().getHeadersWritten().props().getProperty(name);
}
/*
* (non-Javadoc)
*
* @see com.limegroup.gnutella.Connection#isOpen()
*/
public boolean isOpen() {
return !closed.get();
}
/**
* 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;
}
/** Returns the ConnectType this was created with. */
protected ConnectType getConnectType() {
return connectType;
}
/*
* (non-Javadoc)
*
* @see com.limegroup.gnutella.Connection#close()
*/
public final void close() {
// return if it was already closed.
if (closed.getAndSet(true))
return;
IOUtils.close(socket);
closeImpl();
}
/**
* This should be implemented by subclasses to close any resources they
* acquired during the lifetime of the connection.
*/
protected abstract void closeImpl();
/*
* (non-Javadoc)
*
* @see com.limegroup.gnutella.Connection#isWriteDeflated()
*/
public boolean isWriteDeflated() {
return getConnectionCapabilities().getHeadersWritten().isDeflateEnabled();
}
/*
* (non-Javadoc)
*
* @see com.limegroup.gnutella.Connection#isReadDeflated()
*/
public boolean isReadDeflated() {
return getConnectionCapabilities().getHeadersRead().isDeflateEnabled();
}
/*
* (non-Javadoc)
*
* @see com.limegroup.gnutella.Connection#isTLSCapable()
*/
public boolean isTLSCapable() {
if (!getConnectionCapabilities().isCapabilitiesVmSet() && isTLSEncoded())
return true;
else if (getConnectionCapabilities().getCapability(ConnectionCapabilities.Capability.TLS) >= 1)
return true;
else
return false;
}
/*
* (non-Javadoc)
*
* @see com.limegroup.gnutella.Connection#isTLSEncoded()
*/
public boolean isTLSEncoded() {
return connectType == ConnectType.TLS;
}
/*
* (non-Javadoc)
*
* @see com.limegroup.gnutella.Connection#allowNewPings()
*/
public boolean allowNewPings() {
synchronized (pingLock) {
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;
}
}
/*
* (non-Javadoc)
*
* @see com.limegroup.gnutella.Connection#allowNewPongs()
*/
public boolean allowNewPongs() {
synchronized (pongLock) {
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;
}
}
protected void processReadMessage(Message m) {
simpleProtocolBandwidthTracker.addRead(m.getTotalLength());
}
protected void processWrittenMessage(Message m) {
simpleProtocolBandwidthTracker.addWritten(m.getTotalLength());
}
protected void initializeHandshake() throws IOException {
// Check to see if close() was called while the socket was initializing
if (!isOpen()) {
IOUtils.close(getSocket()); // TODO: why?
throw CONNECTION_CLOSED;
}
// Check to see if this is an attempt to connect to ourselves
InetAddress localAddress = getSocket().getLocalAddress();
if (ConnectionSettings.LOCAL_IS_PRIVATE.getValue()
&& getSocket().getInetAddress().equals(localAddress)
&& getPort() == NetworkSettings.PORT.getValue()) {
throw new IOException("Connection to self");
}
// Notify the acceptor of our address.
// TODO: move out of here!
// TODO store address in one place
acceptor.setAddress(localAddress);
}
protected byte getSoftMax() {
return softMax;
}
protected void handshakeInitialized(Handshaker handshaker) {
setHeaders(handshaker.getReadHeaders(), handshaker.getWrittenHeaders());
connectionTime = System.currentTimeMillis();
if(LOG.isInfoEnabled()) {
HandshakeResponse response = handshaker.getReadHeaders();
String ip = response.getProperty(HeaderNames.LISTEN_IP);
String agent = response.getProperty(HeaderNames.USER_AGENT);
LOG.info("Listen-ip " + ip + ", user agent " + agent);
}
// Now set the soft max TTL that should be used on this connection.
// The +1 on the soft max for "good" connections is because the message
// may come from a leaf, and therefore can have an extra hop.
// "Good" connections are connections with features such as
// intra-Ultrapeer QRP passing.
softMax = ConnectionSettings.SOFT_MAX.getValue();
if (getConnectionCapabilities().isGoodUltrapeer()
|| getConnectionCapabilities().isGoodLeaf()) {
// we give these an extra hop because they might be sending
// us traffic from their leaves
softMax++;
}
updateAddress(handshaker.getReadHeaders());
}
/**
* Determines if the address should be changed and changes it if necessary.
*/
// TODO: this really shouldn't be here -- use a listener pattern instead
// package private for testing
void updateAddress(HandshakeResponse readHeaders) {
String ipStringFromHeader = readHeaders.getProperty(HeaderNames.REMOTE_IP);
if (ipStringFromHeader == null) {
return;
}
InetAddress ipAddressFromHeader = null;
try {
ipAddressFromHeader = InetAddress.getByName(ipStringFromHeader);
} catch (UnknownHostException uhe) {
return; // invalid.
}
// invalid or private, exit
if (!NetworkUtils.isValidAddress(ipAddressFromHeader) || networkInstanceUtils.isPrivateAddress(ipAddressFromHeader))
return;
// TODO store address in one place
myIp = ipAddressFromHeader.getAddress();
// If we're forcing, change that if necessary.
if (ConnectionSettings.FORCE_IP_ADDRESS.getValue()) {
StringSetting addr = ConnectionSettings.FORCED_IP_ADDRESS_STRING;
if (!ipStringFromHeader.equals(addr.get())) {
// TODO store address in one place
addr.set(ipStringFromHeader);
networkManager.addressChanged();
}
}
// Otherwise, if our current address is invalid, change.
else if (!NetworkUtils.isValidAddress(networkManager.getAddress())) {
if(LOG.isInfoEnabled())
LOG.info("Updating address to " + ipAddressFromHeader);
// will auto-call addressChanged.
// TODO store address in one place
acceptor.setAddress(ipAddressFromHeader);
}
// TODO store address in one place
acceptor.setExternalAddress(ipAddressFromHeader);
}
// overrides Object.toString
@Override
public String toString() {
return "CONNECTION: host=" + host + " port=" + port;
}
/*
* (non-Javadoc)
*
* @see com.limegroup.gnutella.Connection#getLocalePref()
*/
public String getLocalePref() {
return getConnectionCapabilities().getHeadersRead().getLocalePref();
}
public ConnectionCapabilities getConnectionCapabilities() {
return connectionCapabilities;
}
public ConnectionBandwidthStatistics getConnectionBandwidthStatistics() {
return connectionBandwidthStatistics;
}
}