package com.limegroup.gnutella.connection;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.zip.Deflater;
import java.util.zip.Inflater;
import org.limewire.core.api.connection.ConnectionLifecycleEventType;
import org.limewire.core.settings.ConnectionSettings;
import org.limewire.core.settings.MessageSettings;
import org.limewire.core.settings.SearchSettings;
import org.limewire.io.GUID;
import org.limewire.io.IOUtils;
import org.limewire.io.IpPortImpl;
import org.limewire.io.NetworkInstanceUtils;
import org.limewire.listener.EventListener;
import org.limewire.listener.EventListenerList;
import org.limewire.listener.ListenerSupport;
import org.limewire.logging.Log;
import org.limewire.logging.LogFactory;
import org.limewire.net.SocketsManager;
import org.limewire.net.SocketsManager.ConnectType;
import org.limewire.nio.NBThrottle;
import org.limewire.nio.Throttle;
import org.limewire.nio.channel.ChannelWriter;
import org.limewire.nio.channel.DeflaterWriter;
import org.limewire.nio.channel.DelayedBufferWriter;
import org.limewire.nio.channel.InflaterReader;
import org.limewire.nio.channel.InterestWritableByteChannel;
import org.limewire.nio.channel.NIOMultiplexor;
import org.limewire.nio.channel.StatisticGatheringWriter;
import org.limewire.nio.channel.ThrottleWriter;
import org.limewire.nio.observer.ConnectObserver;
import org.limewire.nio.observer.Shutdownable;
import org.limewire.nio.ssl.SSLUtils;
import org.limewire.security.SecureMessageVerifier;
import org.limewire.service.ErrorService;
import org.limewire.util.ByteUtils;
import com.google.inject.Provider;
import com.limegroup.gnutella.Acceptor;
import com.limegroup.gnutella.ApplicationServices;
import com.limegroup.gnutella.BandwidthTrackerImpl;
import com.limegroup.gnutella.ConnectionManager;
import com.limegroup.gnutella.ConnectionServices;
import com.limegroup.gnutella.GuidMap;
import com.limegroup.gnutella.GuidMapManager;
import com.limegroup.gnutella.InsufficientDataException;
import com.limegroup.gnutella.MessageDispatcher;
import com.limegroup.gnutella.NetworkManager;
import com.limegroup.gnutella.ReplyHandler;
import com.limegroup.gnutella.filters.SpamFilter;
import com.limegroup.gnutella.filters.SpamFilterFactory;
import com.limegroup.gnutella.handshaking.AsyncIncomingHandshaker;
import com.limegroup.gnutella.handshaking.AsyncOutgoingHandshaker;
import com.limegroup.gnutella.handshaking.BadHandshakeException;
import com.limegroup.gnutella.handshaking.HandshakeObserver;
import com.limegroup.gnutella.handshaking.HandshakeResponder;
import com.limegroup.gnutella.handshaking.HandshakeResponderFactory;
import com.limegroup.gnutella.handshaking.Handshaker;
import com.limegroup.gnutella.handshaking.HeadersFactory;
import com.limegroup.gnutella.handshaking.NoGnutellaOkException;
import com.limegroup.gnutella.messages.BadPacketException;
import com.limegroup.gnutella.messages.Message;
import com.limegroup.gnutella.messages.MessageFactory;
import com.limegroup.gnutella.messages.PingReply;
import com.limegroup.gnutella.messages.PushRequest;
import com.limegroup.gnutella.messages.QueryReply;
import com.limegroup.gnutella.messages.QueryReplyFactory;
import com.limegroup.gnutella.messages.QueryRequest;
import com.limegroup.gnutella.messages.QueryRequestFactory;
import com.limegroup.gnutella.messages.Message.Network;
import com.limegroup.gnutella.messages.vendor.CapabilitiesVM;
import com.limegroup.gnutella.messages.vendor.CapabilitiesVMFactory;
import com.limegroup.gnutella.messages.vendor.HopsFlowVendorMessage;
import com.limegroup.gnutella.messages.vendor.MessagesSupportedVendorMessage;
import com.limegroup.gnutella.messages.vendor.OOBProxyControlVendorMessage;
import com.limegroup.gnutella.messages.vendor.PushProxyAcknowledgement;
import com.limegroup.gnutella.messages.vendor.PushProxyRequest;
import com.limegroup.gnutella.messages.vendor.QueryStatusResponse;
import com.limegroup.gnutella.messages.vendor.ReplyNumberVendorMessage;
import com.limegroup.gnutella.messages.vendor.TCPConnectBackVendorMessage;
import com.limegroup.gnutella.messages.vendor.UDPConnectBackVendorMessage;
import com.limegroup.gnutella.messages.vendor.VendorMessage;
import com.limegroup.gnutella.routing.PatchTableMessage;
import com.limegroup.gnutella.routing.QueryRouteTable;
import com.limegroup.gnutella.routing.ResetTableMessage;
import com.limegroup.gnutella.search.SearchResultHandler;
import com.limegroup.gnutella.statistics.OutOfBandStatistics;
import com.limegroup.gnutella.util.DataUtils;
import com.limegroup.gnutella.util.LimeWireUtils;
/**
* A Connection managed by a ConnectionManager.
*
* GnutellaConnection provides a sophisticated message buffering mechanism. When
* you call send(Message), the message is not actually delivered to the socket;
* instead it buffered in an application-level buffer. Periodically, messages
* are read from the buffer and written to the network. Furthermore,
* GnutellaConnection provides a simple form of flow control. If messages are
* queued faster than they can be written to the network, they are dropped in
* the following order: PingRequest, PingReply, QueryRequest, QueryReply, and
* PushRequest. See the implementation notes below for more details.
* <p>
*
* All GnutellaConnection have two underlying spam filters: a personal filter
* (controls what I see) and a route filter (also controls what I pass along to
* others). See SpamFilter for a description. You can change them with
* setPersonalFilter and setRouteFilter.
* <p>
*
* GnutellaConnection maintain a large number of statistics, such as the current
* bandwidth for upstream & downstream.
* <p>
*
* GnutellaConnection also takes care of various VendorMessage handling, in
* particular Hops Flow, UDP ConnectBack, and TCP ConnectBack. See
* handleVendorMessage().
* <p>
*
* This class implements ReplyHandler to route pongs and query replies that
* originated from it.
* <p>
*/
public class GnutellaConnection extends AbstractConnection implements ReplyHandler,
MessageReceiver, SentMessageHandler, Shutdownable, RoutedConnection,
ConnectionRoutingStatistics, ConnectionMessageStatistics, ListenerSupport<GnutellaConnectionEvent> {
private static final Log LOG = LogFactory.getLog(GnutellaConnection.class);
public enum EventType {
IS_PUSH_PROXY
}
/**
* The time to wait between route table updates for leaves, in milliseconds.
*/
private long LEAF_QUERY_ROUTE_UPDATE_TIME = 1000 * 60 * 5; // 5 minutes
/**
* The time to wait between route table updates for Ultrapeers, in
* milliseconds.
*/
private long ULTRAPEER_QUERY_ROUTE_UPDATE_TIME = 1000 * 60; // 1 minute
/**
* The timeout to use when connecting, in milliseconds. This is NOT used for
* bootstrap servers.
*/
private static final int CONNECT_TIMEOUT = 6000; // 6 seconds
/**
* The total amount of upstream messaging bandwidth for ALL connections in
* BYTES (not bits) per second.
*/
private static final int TOTAL_OUTGOING_MESSAGING_BANDWIDTH = 8000;
/**
* Filter for filtering out messages that are considered spam.
*/
private volatile SpamFilter _routeFilter;
/*
* IMPLEMENTATION NOTE: this class uses the SACHRIFC algorithm described at
* http://www.limewire.com/developer/sachrifc.txt. The basic idea is to use
* one queue for each message type. Messages are removed from the queue in a
* biased round-robin fashion. This prioritizes some messages types while
* preventing any one message type from dominating traffic. Query replies
* are further prioritized by "GUID volume", i.e., the number of bytes
* already routed for that GUID. Other messages are sorted by time and
* removed in a LIFO [sic] policy. This, coupled with timeouts, reduces
* latency.
*/
/** A lock for QRP activity on this connection. */
private final Object QRP_LOCK = new Object();
/** Non-blocking throttle for outgoing messages. */
private final static Throttle _nbThrottle = new NBThrottle(true,
TOTAL_OUTGOING_MESSAGING_BANDWIDTH, ConnectionSettings.NUM_CONNECTIONS.getValue(),
CompositeQueue.QUEUE_TIME);
/** The OutputRunner */
private volatile OutputRunner _outputRunner;
/** Keeps track of sent/received [dropped] & bandwidth. */
private final ConnectionStats _connectionStats = new ConnectionStats();
/**
* The next time I should send a query route table to this connection.
*/
private long _nextQRPForwardTime;
/**
* The bandwidth trackers for the up/downstream. These are not synchronized
* and not guaranteed to be 100% accurate.
*/
private BandwidthTrackerImpl _upBandwidthTracker = new BandwidthTrackerImpl();
private BandwidthTrackerImpl _downBandwidthTracker = new BandwidthTrackerImpl();
/**
* True iff this should not be policed by the ConnectionWatchdog, e.g.,
* because this is a connection to a Clip2 reflector.
*/
private boolean _isKillable = true;
/**
* Use this if a HopsFlowVM instructs us to stop sending queries below this
* certain hops value....
*/
private volatile int hopsFlowMax = -1;
/**
* This member contains the time beyond which, if this host is still busy
* (hops flow==0), that we should consider him as "truly idle" and should
* then remove his contributions last-hop QRTs. A value of -1 means that
* either the leaf isn't busy, or he is busy, and his busy-ness was already
* noticed by the MessageRouter, so we shouldn't 're-notice' him on the next
* QRT update iteration.
*/
private volatile long _busyTime = -1;
/**
* Whether this connection is a push proxy for me.
*/
private volatile boolean myPushProxy;
/**
* Whether I am a push proxy for this connection.
*/
private volatile boolean pushProxyFor;
/**
* Variable for the <tt>QueryRouteTable</tt> received for this connection.
*/
private volatile QueryRouteTable _lastQRPTableReceived;
/**
* Variable for the <tt>QueryRouteTable</tt> sent for this connection.
*/
private volatile QueryRouteTable _lastQRPTableSent;
/**
* Whether or not this was a supernode <-> client connection when message
* looping started.
*/
private boolean supernodeClientAtLooping = false;
private volatile Deflater deflater;
private volatile Inflater inflater;
/**
* The last clientGUID a Hops=0 QueryReply had.
*/
private volatile byte[] clientGUID = DataUtils.EMPTY_GUID;
/**
* Whether or not the HandshakeResponder should use locale preferencing
* during handshaking.
*/
private volatile boolean _useLocalPreference;
/**
* The maximum protocol version for which OOB proxying has been turned off
* by leaf peer. Defaults to 0 to allow all OOB versions to be proxied.
*/
private int _maxDisabledOOBProtocolVersion = 0;
private final ConnectionManager connectionManager;
private final NetworkManager networkManager;
private final QueryRequestFactory queryRequestFactory;
private final HeadersFactory headersFactory;
private final HandshakeResponderFactory handshakeResponderFactory;
private final QueryReplyFactory queryReplyFactory;
private final MessageDispatcher messageDispatcher;
private final Provider<SearchResultHandler> searchResultHandler;
private final Provider<ConnectionServices> connectionServices;
private final GuidMapManager guidMapManager;
private final SocketsManager socketsManager;
private final GuidMap guidMap;
private final MessageReaderFactory messageReaderFactory;
private final ApplicationServices applicationServices;
@SuppressWarnings("unused")
private final SecureMessageVerifier secureMessageVerifier;
private final OutOfBandStatistics outOfBandStatistics;
private final NetworkInstanceUtils networkInstanceUtils;
/** writers of statistics if any */
private static enum StatsWriters {TOP, DEFLATER, DELAYER, THROTTLE }
private final Map<StatsWriters,StatisticGatheringWriter> statsWriters = new HashMap<StatsWriters,StatisticGatheringWriter>();
private volatile long droppedBadHops, droppedBadAddress,droppedFW;
private final EventListenerList<GnutellaConnectionEvent> listeners =
new EventListenerList<GnutellaConnectionEvent>();
/**
* Creates a new outgoing connection to the specified host on the specified
* port, using the specified kind of ConnectType.
*
* @param host the address of the host we're connecting to
* @param port the port the host is listening on
* @param type the type of outgoing connection we want to make (TLS, PLAIN,
* etc)
*/
public GnutellaConnection(String host, int port, ConnectType type,
ConnectionManager connectionManager, NetworkManager networkManager,
QueryRequestFactory queryRequestFactory, HeadersFactory headersFactory,
HandshakeResponderFactory handshakeResponderFactory,
QueryReplyFactory queryReplyFactory, MessageDispatcher messageDispatcher,
Provider<SearchResultHandler> searchResultHandler, CapabilitiesVMFactory capabilitiesVMFactory,
SocketsManager socketsManager, Acceptor acceptor,
MessagesSupportedVendorMessage supportedVendorMessage,
Provider<ConnectionServices> connectionServices, GuidMapManager guidMapManager,
SpamFilterFactory spamFilterFactory, MessageReaderFactory messageReaderFactory,
MessageFactory messageFactory, ApplicationServices applicationServices,
SecureMessageVerifier secureMessageVerifier, OutOfBandStatistics outOfBandStatistics,
NetworkInstanceUtils networkInstanceUtils) {
super(host, port, type, capabilitiesVMFactory, supportedVendorMessage, networkManager,
acceptor, networkInstanceUtils);
this.connectionManager = connectionManager;
this.networkManager = networkManager;
this.queryRequestFactory = queryRequestFactory;
this.headersFactory = headersFactory;
this.handshakeResponderFactory = handshakeResponderFactory;
this.queryReplyFactory = queryReplyFactory;
this.messageDispatcher = messageDispatcher;
this.searchResultHandler = searchResultHandler;
this.connectionServices = connectionServices;
this.guidMapManager = guidMapManager;
this.messageReaderFactory = messageReaderFactory;
this.applicationServices = applicationServices;
this.guidMap = guidMapManager.getMap();
this._routeFilter = spamFilterFactory.createRouteFilter();
this.secureMessageVerifier = secureMessageVerifier;
this.socketsManager = socketsManager;
this.outOfBandStatistics = outOfBandStatistics;
this.networkInstanceUtils = networkInstanceUtils;
}
/**
* Creates an incoming connection. ManagedConnections should only be
* constructed within ConnectionManager.
*
* @requires the word "GNUTELLA " and nothing else has just been read from
* socket
* @effects wraps a connection around socket and does the rest of the
* Gnutella handshake.
*/
public GnutellaConnection(Socket socket, ConnectionManager connectionManager,
NetworkManager networkManager, QueryRequestFactory queryRequestFactory,
HeadersFactory headersFactory, HandshakeResponderFactory handshakeResponderFactory,
QueryReplyFactory queryReplyFactory, MessageDispatcher messageDispatcher,
Provider<SearchResultHandler> searchResultHandler, CapabilitiesVMFactory capabilitiesVMFactory,
Acceptor acceptor, MessagesSupportedVendorMessage supportedVendorMessage,
Provider<ConnectionServices> connectionServices, GuidMapManager guidMapManager,
SpamFilterFactory spamFilterFactory, MessageReaderFactory messageReaderFactory,
MessageFactory messageFactory, ApplicationServices applicationServices,
SecureMessageVerifier secureMessageVerifier, OutOfBandStatistics outOfBandStatistics,
NetworkInstanceUtils networkInstanceUtils) {
super(socket, capabilitiesVMFactory, supportedVendorMessage, networkManager, acceptor,
networkInstanceUtils);
this.connectionManager = connectionManager;
this.networkManager = networkManager;
this.queryRequestFactory = queryRequestFactory;
this.headersFactory = headersFactory;
this.handshakeResponderFactory = handshakeResponderFactory;
this.queryReplyFactory = queryReplyFactory;
this.messageDispatcher = messageDispatcher;
this.searchResultHandler = searchResultHandler;
this.connectionServices = connectionServices;
this.guidMapManager = guidMapManager;
this.messageReaderFactory = messageReaderFactory;
this.applicationServices = applicationServices;
this.guidMap = guidMapManager.getMap();
this._routeFilter = spamFilterFactory.createRouteFilter();
this.secureMessageVerifier = secureMessageVerifier;
this.socketsManager = null;
this.outOfBandStatistics = outOfBandStatistics;
this.networkInstanceUtils = networkInstanceUtils;
}
/*
* (non-Javadoc)
*
* @see com.limegroup.gnutella.RoutedConnection#initialize(com.limegroup.gnutella.connection.GnetConnectObserver)
*/
public void initialize(GnetConnectObserver observer) throws IOException {
if (observer == null && isOutgoing())
throw new NullPointerException("must have an observer if outgoing!");
Properties requestHeaders;
HandshakeResponder responder;
addListener(connectionManager);
if (isOutgoing()) {
String host = getAddress();
if (connectionServices.get().isSupernode()) {
requestHeaders = headersFactory.createUltrapeerHeaders(host);
responder = handshakeResponderFactory.createUltrapeerHandshakeResponder(host);
} else {
requestHeaders = headersFactory.createLeafHeaders(host);
responder = handshakeResponderFactory.createLeafHandshakeResponder(host);
}
} else {
String host = getSocket().getInetAddress().getHostAddress();
requestHeaders = null;
if (connectionServices.get().isSupernode()) {
responder = handshakeResponderFactory.createUltrapeerHandshakeResponder(host);
} else {
responder = handshakeResponderFactory.createLeafHandshakeResponder(host);
}
}
// Establish the socket (if needed), handshake.
initialize(requestHeaders, responder, CONNECT_TIMEOUT, observer);
// Nothing else should be done here. All post-init-sequences
// should be triggered from finishInitialize, which will be called
// when the socket is connected (if it connects).
}
/**
* Initialize the connection by doing the handshake. Throws IOException if
* we were unable to establish a normal messaging connection for any reason.
* Do not call send or receive if this happens.
*
* @param timeout for outgoing connections, the timeout in milliseconds to
* use in establishing the socket, or 0 for no timeout. If the
* platform does not support native timeouts, it will be emulated
* with threads.
* @exception IOException we were unable to connect to the host
* @exception NoGnutellaOkException one of the participants responded with
* an error code other than 200 OK (possibly after several rounds
* of 401's)
* @exception BadHandshakeException some other problem establishing the
* connection, e.g., the server responded with HTTP, closed the
* the connection during handshaking, etc.
*/
protected void initialize(Properties requestHeaders, HandshakeResponder responder, int timeout,
GnetConnectObserver observer) throws IOException {
responder.setLocalePreferencing(_useLocalPreference);
if (isOutgoing()) {
if(LOG.isInfoEnabled()) {
LOG.info("Outgoing connection to " +
getAddress() + ":" + getPort() +
" with timeout " + timeout);
}
ConnectObserver connectObserver = new AsyncHandshakeConnecter(requestHeaders,
responder, observer);
InetSocketAddress host = new InetSocketAddress(getAddress(), getPort());
Socket socket = socketsManager
.connect(host, timeout, connectObserver, getConnectType());
setSocket(socket);
} else {
if(LOG.isInfoEnabled()) {
LOG.info("Incoming connection from " +
getAddress() + ":" + getPort());
}
startHandshake(requestHeaders, responder, observer);
}
}
/**
* Starts the handshake process.
*/
private void startHandshake(Properties requestHeaders, HandshakeResponder responder,
GnetConnectObserver observer) throws IOException {
if(LOG.isInfoEnabled())
LOG.info("Shaking hands with " + getAddress() + ":" + getPort());
initializeHandshake();
HandshakeWatcher shakeObserver = new HandshakeWatcher(observer);
Handshaker shaker;
if (isOutgoing())
shaker = new AsyncOutgoingHandshaker(requestHeaders, responder, getSocket(),
shakeObserver);
else
shaker = new AsyncIncomingHandshaker(responder, getSocket(), shakeObserver);
shakeObserver.setHandshaker(shaker);
try {
shaker.shake();
} catch (IOException iox) {
ErrorService.error(iox); // impossible.
}
}
/**
* Starts out OutputRunners & notifies UpdateManager that this connection
* may have an update on it.
*/
private void postHandshakeInitialize(Handshaker shaker) {
handshakeInitialized(shaker);
if (isWriteDeflated()) {
deflater = new Deflater();
}
if (isReadDeflated()) {
inflater = new Inflater();
}
getConnectionBandwidthStatistics().setCompressionOption(isWriteDeflated(),
isReadDeflated(), new CompressionBandwidthTrackerImpl(inflater, deflater));
startOutput();
}
/*
* (non-Javadoc)
*
* @see com.limegroup.gnutella.RoutedConnection#resetQueryRouteTable(com.limegroup.gnutella.routing.ResetTableMessage)
*/
public void resetQueryRouteTable(ResetTableMessage rtm) {
if (_lastQRPTableReceived == null) {
_lastQRPTableReceived = new QueryRouteTable(rtm.getTableSize(), rtm.getInfinity());
} else {
_lastQRPTableReceived.reset(rtm);
}
}
/*
* (non-Javadoc)
*
* @see com.limegroup.gnutella.RoutedConnection#patchQueryRouteTable(com.limegroup.gnutella.routing.PatchTableMessage)
*/
public void patchQueryRouteTable(PatchTableMessage ptm) {
// we should always get a reset before a patch, but
// allocate a table in case we don't
if (_lastQRPTableReceived == null) {
_lastQRPTableReceived = new QueryRouteTable();
}
try {
_lastQRPTableReceived.patch(ptm);
} catch (BadPacketException e) {
// not sure what to do here!!
}
}
/**
* Set's a leaf's busy timer to now, if bSet is true, else clears the flag.
*
* @param bSet whether to SET or CLEAR the busy timer for this host
*/
public void setBusy(boolean bSet) {
if (bSet) {
if (_busyTime == -1)
_busyTime = System.currentTimeMillis();
} else
_busyTime = -1;
}
private byte getHopsFlowMax() {
return (byte) hopsFlowMax;
}
/*
* (non-Javadoc)
*
* @see com.limegroup.gnutella.RoutedConnection#isBusyLeaf()
*/
public boolean isBusyLeaf() {
if (!isSupernodeClientConnection())
return false;
int hfm = getHopsFlowMax();
return hfm >= 0 && hfm < 3;
}
/*
* (non-Javadoc)
*
* @see com.limegroup.gnutella.RoutedConnection#shouldForwardQuery(com.limegroup.gnutella.messages.QueryRequest)
*/
public boolean shouldForwardQuery(QueryRequest query) {
// special what is queries have version numbers attached to them - make
// sure that the remote host can answer the query....
if (query.isFeatureQuery()) {
if (isSupernodeClientConnection())
return (getConnectionCapabilities().getRemoteHostFeatureQuerySelector() >= query
.getFeatureSelector());
else if (getConnectionCapabilities().isSupernodeSupernodeConnection())
return getConnectionCapabilities().getRemoteHostSupportsFeatureQueries();
else
return false;
}
return hitsQueryRouteTable(query);
}
/**
* Determines whether or not this query hits the QRT.
*/
protected boolean hitsQueryRouteTable(QueryRequest query) {
if (_lastQRPTableReceived == null)
return false;
return _lastQRPTableReceived.contains(query);
}
/*
* (non-Javadoc)
*
* @see com.limegroup.gnutella.ConnectionStatistics#getQueryRouteTableReceived()
*/
public QueryRouteTable getQueryRouteTableReceived() {
return _lastQRPTableReceived;
}
/*
* (non-Javadoc)
*
* @see com.limegroup.gnutella.ConnectionStatistics#getQueryRouteTablePercentFull()
*/
public double getQueryRouteTablePercentFull() {
return _lastQRPTableReceived == null ? 0 : _lastQRPTableReceived.getPercentFull();
}
/*
* (non-Javadoc)
*
* @see com.limegroup.gnutella.ConnectionStatistics#getQueryRouteTableSize()
*/
public int getQueryRouteTableSize() {
return _lastQRPTableReceived == null ? 0 : _lastQRPTableReceived.getSize();
}
/*
* (non-Javadoc)
*
* @see com.limegroup.gnutella.ConnectionStatistics#getQueryRouteTableEmptyUnits()
*/
public int getQueryRouteTableEmptyUnits() {
return _lastQRPTableReceived == null ? -1 : _lastQRPTableReceived.getEmptyUnits();
}
/*
* (non-Javadoc)
*
* @see com.limegroup.gnutella.ConnectionStatistics#getQueryRouteTableUnitsInUse()
*/
public int getQueryRouteTableUnitsInUse() {
return _lastQRPTableReceived == null ? -1 : _lastQRPTableReceived.getUnitsInUse();
}
// //////////////////// Sending, Outgoing Flow Control
// //////////////////////
/** Starts outgoing messages being sent. */
private void startOutput() {
if(LOG.isInfoEnabled())
LOG.info("Starting output to " + getAddress() + ":" + getPort());
// add some heavier stats code in betas
if (LimeWireUtils.isBetaRelease() || LimeWireUtils.isTestingVersion()) {
statsWriters.put(StatsWriters.TOP,new StatisticGatheringWriter());
statsWriters.put(StatsWriters.DEFLATER, new StatisticGatheringWriter());
statsWriters.put(StatsWriters.DELAYER, new StatisticGatheringWriter());
statsWriters.put(StatsWriters.THROTTLE, new StatisticGatheringWriter());
}
MessageQueue queue;
// Taking this change out until we can safely handle attacks and
// overflow
// TODO: make a cheaper Queue that still prevents flooding of ultrapeer
// and ensures that clogged leaf doesn't drop QRP messages.
// if(isSupernodeSupernodeConnection())
queue = new CompositeQueue();
// else
// queue = new BasicQueue();
// TODO: ensure socket is asynchronous!
MessageWriter messager = new MessageWriter(_connectionStats, queue, this);
_outputRunner = messager;
ChannelWriter writer = messager;
if (statsWriters.containsKey(StatsWriters.TOP))
writer = addWriter(writer, statsWriters.get(StatsWriters.TOP));
if (isWriteDeflated()) {
writer = addWriter(writer, new DeflaterWriter(deflater));
if (statsWriters.containsKey(StatsWriters.DEFLATER))
writer = addWriter(writer, statsWriters.get(StatsWriters.DEFLATER));
}
writer = addWriter(writer, new DelayedBufferWriter(1400));
if (statsWriters.containsKey(StatsWriters.DELAYER))
writer = addWriter(writer, statsWriters.get(StatsWriters.DELAYER));
writer = addWriter(writer, new ThrottleWriter(_nbThrottle));
if (statsWriters.containsKey(StatsWriters.THROTTLE))
writer = addWriter(writer, statsWriters.get(StatsWriters.THROTTLE));
((NIOMultiplexor) getSocket()).setWriteObserver(messager);
}
private <T extends InterestWritableByteChannel & ChannelWriter> ChannelWriter addWriter(ChannelWriter chain, T newWriter) {
chain.setWriteChannel(newWriter);
return newWriter;
}
/*
* (non-Javadoc)
*
* @see com.limegroup.gnutella.RoutedConnection#send(com.limegroup.gnutella.messages.Message)
*/
public void send(Message m) {
if(m instanceof QueryRequest && !shouldSendQuery((QueryRequest)m))
return;
_outputRunner.send(m);
}
/**
* Default access for testing.
* @return true if the query should be sent.
*/
boolean shouldSendQuery(QueryRequest query) {
// if Hops Flow is in effect, and this is a QueryRequest, and the
// hoppage is too biggage, discardage time...
int smh = hopsFlowMax;
if (smh > -1 && query.getHops() >= smh)
return false;
// if we are an ultrapeer sending a query to a leaf, check its firewall status
boolean send = true;
if (isSupernodeClientConnection() && MessageSettings.ULTRAPEER_FIREWALL_FILTERING.getValue()) {
boolean incomingTCP = getConnectionCapabilities().canAcceptIncomingTCP();
boolean fwt = getConnectionCapabilities().canDoFWT();
// if either party can accept tcp, send.
// otherwise send if both sides can do fwt.
if (!incomingTCP && query.isFirewalledSource())
send = fwt && query.canDoFirewalledTransfer();
if (send)
droppedFW++;
}
return send;
}
/*
* (non-Javadoc)
*
* @see com.limegroup.gnutella.RoutedConnection#originateQuery(com.limegroup.gnutella.messages.QueryRequest)
*/
public void originateQuery(QueryRequest query) {
query.originate();
if(LOG.isInfoEnabled()) {
boolean leaf = getConnectionCapabilities().isClientSupernodeConnection();
int control = getConnectionCapabilities().getSupportedOOBProxyControlVersion();
boolean disabled = SearchSettings.DISABLE_OOB_V2.getBoolean();
LOG.info("Originating query, leaf " + leaf +
", proxy control version " + control +
", OOBv2 disabled " + disabled);
}
if (getConnectionCapabilities().isClientSupernodeConnection()
&& getConnectionCapabilities().getSupportedOOBProxyControlVersion() == -1
&& SearchSettings.DISABLE_OOB_V2.getBoolean()) {
LOG.info("Creating do not proxy OOB query");
// don't proxy if we are a leaf and the ultrapeer
// does not know OOB v3 and they would proxy for us
query = queryRequestFactory.createDoNotProxyQuery(query);
query.originate();
}
send(query);
}
/*
* (non-Javadoc)
*
* @see com.limegroup.gnutella.RoutedConnection#shutdown()
*/
public void shutdown() {
close();
}
/*
* (non-Javadoc)
*
* @see com.limegroup.gnutella.RoutedConnection#close()
*/
@Override
protected void closeImpl() {
IOUtils.close(deflater);
IOUtils.close(inflater);
if (_outputRunner != null)
_outputRunner.shutdown();
// release pointer to our _guidMap so it can be gc()'ed
guidMapManager.removeMap(guidMap);
}
// ////////////////////////////////////////////////////////////////////////
/**
* Handles core Gnutella request/reply protocol. If asynchronous messaging
* is supported, this immediately returns and messages are processed
* asynchronously via processMessage calls. Otherwise, if reading blocks,
* this will run until the connection is closed.
*
* @requires this is initialized
* @modifies the network underlying this, manager
* @effects receives request and sends appropriate replies.
*
* @throws IOException passed on from the receive call; failures to forward
* or route messages are silently swallowed, allowing the message
* loop to continue.
*/
public void startMessaging() {
supernodeClientAtLooping = isSupernodeClientConnection();
LOG.info("Starting asynchronous connection");
try {
getSocket().setSoTimeout(0); // no timeout for reading.
} catch (IOException iox) {
// ignore for now...
}
MessageReader reader = messageReaderFactory.createMessageReader(this);
if (isReadDeflated())
reader.setReadChannel(new InflaterReader(inflater));
((NIOMultiplexor) getSocket()).setReadObserver(reader);
}
/*
* (non-Javadoc)
*
* @see com.limegroup.gnutella.RoutedConnection#messagingClosed()
*/
public void messagingClosed() {
// we must run this in another thread, as manager.remove
// obtains locks, but this can be called from the NIO thread
if (connectionManager != null) {
messageDispatcher.dispatch(new Runnable() {
public void run() {
connectionManager.remove(GnutellaConnection.this);
}
});
}
}
@Override
public void processReadMessage(Message m) {
super.processReadMessage(m);
_connectionStats.addReceived();
handleMessageInternal(m);
}
public void processSentMessage(Message m) {
processWrittenMessage(m);
messageDispatcher.dispatchTCPMessageSent(m, this);
}
/**
* Handles a message without updating appropriate statistics.
*/
private void handleMessageInternal(Message m) {
// Run through the route spam filter and drop accordingly.
if (isSpam(m)) {
_connectionStats.addReceivedDropped();
} else {
if (m instanceof QueryReply) {
QueryReply reply = (QueryReply)m;
_connectionStats.replyReceived(reply);
if (m.getHops() == 0)
clientGUID = reply.getClientGUID();
if (MessageSettings.RETURN_PATH_IN_REPLIES.getValue() &&
connectionManager.isActiveSupernode() &&
!reply.hasSecureData()) {
m = queryReplyFactory.createWithReturnPathInfo(reply,
myIp == null ? null : new IpPortImpl(myIp,networkManager.getPort()),
this);
}
} else if (m instanceof PushProxyRequest && m.getHops() == 0) {
PushProxyRequest pushProxyRequest = (PushProxyRequest)m;
clientGUID = pushProxyRequest.getClientGUID().bytes();
}
if (m instanceof QueryRequest)
_connectionStats.queryReceived();
// special handling for proxying.
if (supernodeClientAtLooping) {
if (m instanceof QueryRequest)
m = tryToProxy((QueryRequest) m);
else if (m instanceof QueryStatusResponse)
m = morphToStopQuery((QueryStatusResponse) m);
}
messageDispatcher.dispatchTCP(m, this);
}
}
/*
* (non-Javadoc)
*
* @see com.limegroup.gnutella.ConnectionStatistics#getNumQueryReplies()
*/
public long getNumQueryReplies() {
return _connectionStats.getRepliesReceived();
}
public long getNumQueriesReceived() {
return _connectionStats.getQueriesReceived();
}
/*
* (non-Javadoc)
*
* @see com.limegroup.gnutella.RoutedConnection#getNetwork()
*/
public Network getNetwork() {
return Network.TCP;
}
// raise access for MessageReceiver
@Override
public byte getSoftMax() {
return super.getSoftMax();
}
private QueryRequest tryToProxy(QueryRequest query) {
// we must have the following qualifications:
// 1) Leaf must be sending SuperNode a query
// 2) Leaf must support Leaf Guidance
// 3) Query must not be OOB.
// 3.5) The query originator should not disallow proxying.
// 4) We must be able to OOB and have great success rate.
if (getConnectionCapabilities().remoteHostSupportsLeafGuidance() < 1) {
LOG.info("Not OOB proxying: remote host doesn't support leaf guidance");
return query;
}
if (query.desiresOutOfBandRepliesV3()) {
LOG.info("Not OOB proxying: query already requests OOBv3");
return query;
}
if (query.doNotProxy()) {
LOG.info("Not OOB proxying: query says no");
return query;
}
if (_maxDisabledOOBProtocolVersion >= ReplyNumberVendorMessage.VERSION) {
if (LOG.isInfoEnabled()) {
LOG.info("Not OOB proxying: disabled version is "
+ _maxDisabledOOBProtocolVersion);
}
return query;
} else {
if (LOG.isInfoEnabled()) {
LOG.info("query might be proxied for max disabled version "
+ _maxDisabledOOBProtocolVersion + " " + Arrays.toString(query.getGUID()));
}
}
if(!networkManager.isOOBCapable()) {
LOG.info("Not OOB proxying: not OOB capable");
return query;
}
if(!outOfBandStatistics.isSuccessRateGreat()) {
LOG.info("Not OOB proxying: success rate isn't great");
return query;
}
if(!outOfBandStatistics.isOOBEffectiveForProxy()) {
LOG.info("Not OOB proxying: OOB isn't effective enough");
return query;
}
// everything is a go - we need to do the following:
// 1) mutate the GUID of the query - you should maintain every param of
// the query except the new GUID and the OOB minspeed flag
// 2) set up mappings between the old guid and the new guid.
// after that, everything is set. all you need to do is map the guids
// of the replies back to the original guid. also, see if a you get a
// QueryStatusResponse message and morph it...
// THIS IS SOME MAJOR HOKERY-POKERY!!!
LOG.info("OOB proxying query");
// 1) mutate the GUID of the query
byte[] origGUID = query.getGUID();
byte[] oobGUID = new byte[origGUID.length];
System.arraycopy(origGUID, 0, oobGUID, 0, origGUID.length);
GUID.addressEncodeGuid(oobGUID, networkManager.getAddress(), networkManager.getPort());
if (MessageSettings.STAMP_QUERIES.getValue())
GUID.timeStampGuid(oobGUID);
query = queryRequestFactory.createProxyQuery(query, oobGUID);
// 2) set up mappings between the guids
guidMap.addMapping(origGUID, oobGUID);
outOfBandStatistics.addSentQuery();
return query;
}
private QueryStatusResponse morphToStopQuery(QueryStatusResponse resp) {
GUID oobGUID = guidMap.getNewGUID(resp.getQueryGUID());
// if we had a match, then just construct a new one....
if (oobGUID != null)
return new QueryStatusResponse(oobGUID, resp.getNumResults());
else
return resp;
}
boolean isSpam(Message m) {
if (!ConnectionSettings.LOCAL_IS_PRIVATE.getValue())
return !_routeFilter.allow(m);
// leafs can only send hops == 0
if (isSupernodeClientConnection() && m.getHops() != 0) {
droppedBadHops++;
return true;
}
// replies with hops 0 must match the address.
if (m instanceof QueryReply && m.getHops() == 0) {
QueryReply reply = (QueryReply)m;
byte [] ip = reply.getIPBytes();
if (!networkInstanceUtils.isPrivateAddress(ip) &&
!reply.hasSecureData() &&
!Arrays.equals(ip, getAddressBytes())) {
droppedBadAddress++;
return true;
}
}
return !_routeFilter.allow(m);
}
public void countDroppedMessage() {
LOG.trace("Dropped a message");
_connectionStats.addReceivedDropped();
}
public void setRouteFilter(SpamFilter filter) {
_routeFilter = filter;
}
public void handlePingReply(PingReply pingReply, ReplyHandler receivingConnection) {
send(pingReply);
}
public void handleQueryReply(QueryReply queryReply, ReplyHandler receivingConnection) {
boolean checkOOB = true;
if (guidMap != null) {
byte[] origGUID = guidMap.getOriginalGUID(queryReply.getGUID());
if (origGUID != null) {
checkOOB = false;
byte prevHops = queryReply.getHops();
queryReply = queryReplyFactory.createQueryReply(origGUID, queryReply);
queryReply.setTTL((byte) 2); // we ttl 1 more than necessary
queryReply.setHops(prevHops);
}
// ---------------------
}
// drop UDP replies that are not being proxied.
if (checkOOB && queryReply.isUDP() && !queryReply.isReplyToMulticastQuery())
return;
// if the remote side reported an ip and this reply is from us, make sure
// its source ip matches.
if (myIp != null &&
queryReply.isLocal() &&
!networkInstanceUtils.isPrivateAddress(queryReply.getIPBytes()) &&
!queryReply.hasSecureData()) // don't mess with signed results
queryReply = queryReplyFactory.createWithNewAddress(myIp, queryReply);
send(queryReply);
}
/*
* (non-Javadoc)
*
* @see com.limegroup.gnutella.RoutedConnection#getClientGUID()
*/
public byte[] getClientGUID() {
return clientGUID;
}
/*
* (non-Javadoc)
*
* @see com.limegroup.gnutella.RoutedConnection#handlePushRequest(com.limegroup.gnutella.messages.PushRequest,
* com.limegroup.gnutella.ReplyHandler)
*/
public void handlePushRequest(PushRequest pushRequest, ReplyHandler receivingConnection) {
send(pushRequest);
}
@Override
public void handleVendorMessage(VendorMessage vm) {
// let Connection do as needed....
super.handleVendorMessage(vm);
// now i can process
if (vm instanceof HopsFlowVendorMessage) {
// update the softMaxHops value so it can take effect....
HopsFlowVendorMessage hops = (HopsFlowVendorMessage) vm;
if (isSupernodeClientConnection())
// If the connection is to a leaf, and it is busy (HF == 0)
// then set the global busy leaf flag appropriately
setBusy(hops.getHopValue() == 0);
hopsFlowMax = hops.getHopValue();
} else if (vm instanceof PushProxyAcknowledgement) {
// this connection can serve as a PushProxy, so note this....
PushProxyAcknowledgement ack = (PushProxyAcknowledgement) vm;
if (Arrays.equals(ack.getGUID(), applicationServices.getMyGUID())) {
myPushProxy = true;
fireEvent(new GnutellaConnectionEvent(this, EventType.IS_PUSH_PROXY));
}
// else mistake on the server side - the guid should be my client
// guid - not really necessary but whatever
} else if (vm instanceof CapabilitiesVM) {
// fire a vendor event
connectionManager.dispatchEvent(new ConnectionLifecycleEvent(this,
ConnectionLifecycleEventType.CONNECTION_CAPABILITIES, this));
} else if (vm instanceof MessagesSupportedVendorMessage) {
// If this is a ClientSupernodeConnection and the host supports
// leaf guidance (because we have to tell them when to stop)
// then see if there are any old queries that we can re-originate
// on this connection.
if (getConnectionCapabilities().isClientSupernodeConnection()
&& (getConnectionCapabilities().remoteHostSupportsLeafGuidance() >= 0)) {
List<QueryRequest> queries = searchResultHandler.get().getQueriesToReSend();
for (QueryRequest qr : queries) {
send(qr);
}
}
// see if you need a PushProxy - the remoteHostSupportsPushProxy
// test incorporates my leaf status in it.....
if (getConnectionCapabilities().remoteHostSupportsPushProxy() > -1) {
// get the client GUID and send off a PushProxyRequest
GUID clientGUID = new GUID(applicationServices.getMyGUID());
PushProxyRequest req = new PushProxyRequest(clientGUID);
send(req);
}
// do i need to send any ConnectBack messages????
if (!networkManager.canReceiveUnsolicited()
&& connectionManager.canSendConnectBack(Network.UDP)
&& (getConnectionCapabilities().remoteHostSupportsUDPRedirect() > -1)) {
GUID connectBackGUID = networkManager.getUDPConnectBackGUID();
Message udp = new UDPConnectBackVendorMessage(networkManager.getPort(),
connectBackGUID);
send(udp);
connectionManager.connectBackSent(Network.UDP);
}
if (!networkManager.acceptedIncomingConnection()
&& connectionManager.canSendConnectBack(Network.TCP)
&& (getConnectionCapabilities().remoteHostSupportsTCPRedirect() > -1)) {
Message tcp = new TCPConnectBackVendorMessage(networkManager.getPort());
send(tcp);
connectionManager.connectBackSent(Network.TCP);
}
// disable oobv2 explicitly.
if (getConnectionCapabilities().isClientSupernodeConnection()
&& SearchSettings.DISABLE_OOB_V2.getBoolean()
&& getConnectionCapabilities().getSupportedOOBProxyControlVersion() != -1) {
Message stopv2 = new OOBProxyControlVendorMessage(
OOBProxyControlVendorMessage.Control.DISABLE_VERSION_2);
send(stopv2);
}
} else if (vm instanceof OOBProxyControlVendorMessage) {
_maxDisabledOOBProtocolVersion = ((OOBProxyControlVendorMessage) vm)
.getMaximumDisabledVersion();
if(LOG.isTraceEnabled()) {
LOG.trace("Max disabled OOB version set to " +
_maxDisabledOOBProtocolVersion);
}
}
}
public void addListener(EventListener<GnutellaConnectionEvent> listener) {
listeners.addListener(listener);
}
public boolean removeListener(EventListener<GnutellaConnectionEvent> listener) {
return listeners.removeListener(listener);
}
private void fireEvent(GnutellaConnectionEvent event) {
listeners.broadcast(event);
}
public int getNumMessagesSent() {
return _connectionStats.getSent();
}
public int getNumMessagesReceived() {
return _connectionStats.getReceived();
}
public int getNumSentMessagesDropped() {
return _connectionStats.getSentDropped();
}
public long getNumReceivedMessagesDropped() {
return _connectionStats.getReceivedDropped();
}
public void measureBandwidth() {
_upBandwidthTracker.measureBandwidth(ByteUtils.long2int(getConnectionBandwidthStatistics()
.getBytesSent()));
_downBandwidthTracker.measureBandwidth(ByteUtils
.long2int(getConnectionBandwidthStatistics().getBytesReceived()));
}
public float getMeasuredUpstreamBandwidth() {
float retValue = 0; // initialize to default
try {
retValue = _upBandwidthTracker.getMeasuredBandwidth();
} catch (InsufficientDataException ide) {
return 0;
}
return retValue;
}
public float getMeasuredDownstreamBandwidth() {
float retValue = 0;
try {
retValue = _downBandwidthTracker.getMeasuredBandwidth();
} catch (InsufficientDataException ide) {
return 0;
}
return retValue;
}
public long getNextQRPForwardTime() {
return _nextQRPForwardTime;
}
public void incrementNextQRPForwardTime(long curTime) {
if (isLeafConnection()) {
_nextQRPForwardTime = curTime + LEAF_QUERY_ROUTE_UPDATE_TIME;
} else {
// otherwise, it's an Ultrapeer
_nextQRPForwardTime = curTime + ULTRAPEER_QUERY_ROUTE_UPDATE_TIME;
}
}
public boolean isKillable() {
return _isKillable;
}
public QueryRouteTable getQueryRouteTableSent() {
return _lastQRPTableSent;
}
public void setQueryRouteTableSent(QueryRouteTable qrt) {
_lastQRPTableSent = qrt;
}
public boolean isMyPushProxy() {
return myPushProxy;
}
public boolean isPushProxyFor() {
return pushProxyFor;
}
public void setPushProxyFor(boolean pushProxyFor) {
this.pushProxyFor = pushProxyFor;
}
public Object getQRPLock() {
return QRP_LOCK;
}
public void setLocalePreferencing(boolean b) {
_useLocalPreference = b;
}
public void reply(Message m) {
send(m);
}
/**
* A ConnectObserver that continues the handshaking process in the same
* thread, expecting that performHandshake(...) callback to the observer.
*/
private class AsyncHandshakeConnecter implements ConnectObserver {
private Properties requestHeaders;
private HandshakeResponder responder;
private GnetConnectObserver observer;
AsyncHandshakeConnecter(Properties requestHeaders, HandshakeResponder responder,
GnetConnectObserver observer) {
this.requestHeaders = requestHeaders;
this.responder = responder;
this.observer = observer;
}
public void handleConnect(Socket s) throws IOException {
// _socket may not really have been set yet, this ensures it is.
setSocket(s);
if(LOG.isInfoEnabled()) {
LOG.info("Connected to " +
getAddress() + ":" + getPort() +
", TLS " + SSLUtils.isTLSEnabled(socket));
}
startHandshake(requestHeaders, responder, observer);
}
public void shutdown() {
if(LOG.isInfoEnabled()) {
LOG.info("Shutting down connection to " +
getAddress() + ":" + getPort() +
", TLS " + SSLUtils.isTLSEnabled(socket));
}
observer.shutdown();
}
// ignored.
public void handleIOException(IOException iox) {
if(LOG.isInfoEnabled()) {
LOG.info(iox + ", " +
getAddress() + ":" + getPort() +
", TLS " + SSLUtils.isTLSEnabled(socket));
}
}
}
/**
* A HandshakeObserver that notifies the GnetConnectObserver when
* handshaking finishes.
*/
private class HandshakeWatcher implements HandshakeObserver {
private Handshaker shaker;
private GnetConnectObserver observer;
HandshakeWatcher(GnetConnectObserver observer) {
this.observer = observer;
}
void setHandshaker(Handshaker shaker) {
this.shaker = shaker;
}
public void shutdown() {
if(LOG.isInfoEnabled()) {
LOG.info("Shutting down connection to " +
getAddress() + ":" + getPort() +
", TLS " + SSLUtils.isTLSEnabled(socket));
}
setHeaders(shaker.getReadHeaders(), shaker.getWrittenHeaders());
close();
observer.shutdown();
}
public void handleHandshakeFinished(Handshaker shaker) {
if(LOG.isInfoEnabled()) {
LOG.info("Finished handshake with " +
getAddress() + ":" + getPort() +
", TLS " + SSLUtils.isTLSEnabled(socket));
}
postHandshakeInitialize(shaker);
observer.handleConnect();
}
public void handleBadHandshake() {
if(LOG.isInfoEnabled()) {
LOG.info("Bad handshake with " +
getAddress() + ":" + getPort() +
", TLS " + SSLUtils.isTLSEnabled(socket));
}
setHeaders(shaker.getReadHeaders(), shaker.getWrittenHeaders());
close();
observer.handleBadHandshake();
}
public void handleNoGnutellaOk(int code, String msg) {
if(LOG.isInfoEnabled()) {
LOG.info("No Gnutella OK in handshake with " +
getAddress() + ":" + getPort() +
", TLS " + SSLUtils.isTLSEnabled(socket));
}
setHeaders(shaker.getReadHeaders(), shaker.getWrittenHeaders());
close();
observer.handleNoGnutellaOk(code, msg);
}
}
public ConnectionRoutingStatistics getRoutedConnectionStatistics() {
return this;
}
// all ReplyHandler things, to override, override from
// ConnectionCapabilities...
final public boolean isGoodLeaf() {
return getConnectionCapabilities().isGoodLeaf();
}
final public boolean isGoodUltrapeer() {
return getConnectionCapabilities().isGoodUltrapeer();
}
final public boolean isHighDegreeConnection() {
return getConnectionCapabilities().isHighDegreeConnection();
}
final public boolean isLeafConnection() {
return getConnectionCapabilities().isLeafConnection();
}
final public boolean isSupernodeClientConnection() {
return getConnectionCapabilities().isSupernodeClientConnection();
}
final public boolean isUltrapeerQueryRoutingConnection() {
return getConnectionCapabilities().isUltrapeerQueryRoutingConnection();
}
final public boolean supportsPongCaching() {
return getConnectionCapabilities().supportsPongCaching();
}
final public ConnectionMessageStatistics getConnectionMessageStatistics() {
return this;
}
}