package com.limegroup.gnutella;
import java.io.IOException;
import java.net.Socket;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Properties;
import java.util.Set;
import java.util.StringTokenizer;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import com.limegroup.gnutella.connection.ConnectionChecker;
import com.limegroup.gnutella.connection.GnetConnectObserver;
import com.limegroup.gnutella.filters.IPFilter;
import com.limegroup.gnutella.handshaking.HandshakeResponse;
import com.limegroup.gnutella.handshaking.HeaderNames;
import com.limegroup.gnutella.messages.Message;
import com.limegroup.gnutella.messages.PingRequest;
import com.limegroup.gnutella.messages.vendor.QueryStatusResponse;
import com.limegroup.gnutella.messages.vendor.TCPConnectBackVendorMessage;
import com.limegroup.gnutella.messages.vendor.UDPConnectBackVendorMessage;
import com.limegroup.gnutella.settings.ApplicationSettings;
import com.limegroup.gnutella.settings.ConnectionSettings;
import com.limegroup.gnutella.settings.UltrapeerSettings;
import com.limegroup.gnutella.statistics.HTTPStat;
import com.limegroup.gnutella.util.IpPortSet;
import com.limegroup.gnutella.util.NetworkUtils;
import com.limegroup.gnutella.util.Sockets;
import com.limegroup.gnutella.util.SystemUtils;
import com.limegroup.gnutella.util.ThreadFactory;
/**
* The list of all ManagedConnection's. Provides a factory method for creating
* user-requested outgoing connections, accepts incoming connections, and
* fetches "automatic" outgoing connections as needed. Creates threads for
* handling these connections when appropriate.
*
* Because this is the only list of all connections, it plays an important role
* in message broadcasting. For this reason, the code is highly tuned to avoid
* locking in the getInitializedConnections() methods. Adding and removing
* connections is a slower operation.<p>
*
* LimeWire follows the following connection strategy:<br>
* As a leaf, LimeWire will ONLY connect to 'good' Ultrapeers. The definition
* of good is constantly changing. For a current view of 'good', review
* HandshakeResponse.isGoodUltrapeer(). LimeWire leaves will NOT deny
* a connection to an ultrapeer even if they've reached their maximum
* desired number of connections (currently 4). This means that if 5
* connections resolve simultaneously, the leaf will remain connected to all 5.
* <br>
* As an Ultrapeer, LimeWire will seek outgoing connections for 5 less than
* the number of it's desired peer slots. This is done so that newcomers
* on the network have a better chance of finding an ultrapeer with a slot
* open. LimeWire ultrapeers will allow ANY other ultrapeer to connect to it,
* and to ensure that the network does not become too LimeWire-centric, it
* reserves 3 slots for non-LimeWire peers. LimeWire ultrapeers will allow
* ANY leaf to connect, so long as there are atleast 15 slots open. Beyond
* that number, LimeWire will only allow 'good' leaves. To see what consitutes
* a good leave, view HandshakeResponse.isGoodLeaf(). To ensure that the
* network does not remain too LimeWire-centric, it reserves 3 slots for
* non-LimeWire leaves.<p>
*
* ConnectionManager has methods to get up and downstream bandwidth, but it
* doesn't quite fit the BandwidthTracker interface.
*/
public class ConnectionManager implements ConnectionAcceptor {
/**
* Timestamp for the last time the user selected to disconnect.
*/
private volatile long _disconnectTime = -1;
/**
* Timestamp for the last time we started trying to connect
*/
private volatile long _connectTime = Long.MAX_VALUE;
/**
* Timestamp for the time we began automatically connecting. We stop
* trying to automatically connect if the user has disconnected since that
* time.
*/
private volatile long _automaticConnectTime = 0;
/**
* Flag for whether or not the auto-connection process is in effect.
*/
private volatile boolean _automaticallyConnecting;
/**
* Timestamp of our last successful connection.
*/
private volatile long _lastSuccessfulConnect = 0;
/**
* Timestamp of the last time we checked to verify that the user has a live
* Internet connection.
*/
private volatile long _lastConnectionCheck = 0;
/**
* Counter for the number of connection attempts we've made.
*/
private volatile static int _connectionAttempts;
private static final Log LOG = LogFactory.getLog(ConnectionManager.class);
/**
* The number of connections leaves should maintain to Ultrapeers.
*/
public static final int PREFERRED_CONNECTIONS_FOR_LEAF = 3;
/**
* How many connect back requests to send if we have a single connection
*/
public static final int CONNECT_BACK_REDUNDANT_REQUESTS = 3;
/**
* The minimum amount of idle time before we switch to using 1 connection.
*/
private static final int MINIMUM_IDLE_TIME = 30 * 60 * 1000; // 30 minutes
/**
* The number of leaf connections reserved for non LimeWire clients.
* This is done to ensure that the network is not solely LimeWire centric.
*/
public static final int RESERVED_NON_LIMEWIRE_LEAVES = 2;
/**
* The current number of connections we want to maintain.
*/
private volatile int _preferredConnections = -1;
/**
* Reference to the <tt>HostCatcher</tt> for retrieving host data as well
* as adding host data.
*/
private HostCatcher _catcher;
/** Threads trying to maintain the NUM_CONNECTIONS.
* LOCKING: obtain this. */
private final List /* of ConnectionFetcher */ _fetchers =
new ArrayList();
/** Connections that have been fetched but not initialized. I don't
* know the relation between _initializingFetchedConnections and
* _connections (see below). LOCKING: obtain this. */
private final List /* of ManagedConnection */ _initializingFetchedConnections =
new ArrayList();
/**
* dedicated ConnectionFetcher used by leafs to fetch a
* locale matching connection
* NOTE: currently this is only used by leafs which will try
* to connect to one connection which matches the locale of the
* client.
*/
private ConnectionFetcher _dedicatedPrefFetcher;
/**
* boolean to check if a locale matching connection is needed.
*/
private volatile boolean _needPref = true;
/**
* boolean of whether or not the interruption of the prefFetcher thread
* has been scheduled.
*/
private boolean _needPrefInterrupterScheduled = false;
/**
* List of all connections. The core data structures are lists, which allow
* fast iteration for message broadcast purposes. Actually we keep a couple
* of lists: the list of all initialized and uninitialized connections
* (_connections), the list of all initialized non-leaf connections
* (_initializedConnections), and the list of all initialized leaf connections
* (_initializedClientConnections).
*
* INVARIANT: neither _connections, _initializedConnections, nor
* _initializedClientConnections contains any duplicates.
* INVARIANT: for all c in _initializedConnections,
* c.isSupernodeClientConnection()==false
* INVARIANT: for all c in _initializedClientConnections,
* c.isSupernodeClientConnection()==true
* COROLLARY: the intersection of _initializedClientConnections
* and _initializedConnections is the empty set
* INVARIANT: _initializedConnections is a subset of _connections
* INVARIANT: _initializedClientConnections is a subset of _connections
* INVARIANT: _shieldedConnections is the number of connections
* in _initializedConnections for which isClientSupernodeConnection()
* is true.
* INVARIANT: _nonLimeWireLeaves is the number of connections
* in _initializedClientConnections for which isLimeWire is false
* INVARIANT: _nonLimeWirePeers is the number of connections
* in _initializedConnections for which isLimeWire is false
*
* LOCKING: _connections, _initializedConnections and
* _initializedClientConnections MUST NOT BE MUTATED. Instead they should
* be replaced as necessary with new copies. Before replacing the
* structures, obtain this' monitor. This avoids lock overhead when
* message broadcasting, though it makes adding/removing connections
* much slower.
*/
//TODO:: why not use sets here??
private volatile List /* of ManagedConnection */
_connections = Collections.EMPTY_LIST;
private volatile List /* of ManagedConnection */
_initializedConnections = Collections.EMPTY_LIST;
private volatile List /* of ManagedConnection */
_initializedClientConnections = Collections.EMPTY_LIST;
private volatile int _shieldedConnections = 0;
private volatile int _nonLimeWireLeaves = 0;
private volatile int _nonLimeWirePeers = 0;
/** number of peers that matches the local locale pref. */
private volatile int _localeMatchingPeers = 0;
/**
* Variable for the number of times since we attempted to force ourselves
* to become an Ultrapeer that we were told to become leaves. If this
* number is too great, we give up and become a leaf.
*/
private volatile int _leafTries;
/**
* The number of demotions to ignore before allowing ourselves to become
* a leaf -- this number depends on how good this potential Ultrapeer seems
* to be.
*/
private volatile int _demotionLimit = 0;
/**
* The current measured upstream bandwidth.
*/
private volatile float _measuredUpstreamBandwidth = 0.f;
/**
* The current measured downstream bandwidth.
*/
private volatile float _measuredDownstreamBandwidth = 0.f;
/**
* Constructs a ConnectionManager. Must call initialize before using
* other methods of this class.
*/
public ConnectionManager() { }
/**
* Links the ConnectionManager up with the other back end pieces and
* launches the ConnectionWatchdog and the initial ConnectionFetchers.
*/
public void initialize() {
_catcher = RouterService.getHostCatcher();
RouterService.getConnectionDispatcher().
addConnectionAcceptor(this,
new String[]{ConnectionSettings.CONNECT_STRING_FIRST_WORD,"LIMEWIRE"},
false,
false);
// schedule the Runnable that will allow us to change
// the number of connections we're shooting for if
// we're idle.
if(SystemUtils.supportsIdleTime()) {
RouterService.schedule(new Runnable() {
public void run() {
setPreferredConnections();
}
}, 1000, 1000);
}
}
/**
* Create a new connection, blocking until it's initialized, but launching
* a new thread to do the message loop.
*/
public ManagedConnection createConnectionBlocking(String hostname, int portnum) throws IOException {
ManagedConnection c = new ManagedConnection(hostname, portnum);
// Initialize synchronously
initializeExternallyGeneratedConnection(c, null);
// Kick off a thread for the message loop.
ThreadFactory.startThread(new OutgoingConnector(c, false), "OutgoingConnector");
return c;
}
/**
* Create a new connection, allowing it to initialize and loop for messages on a new thread.
*/
public void createConnectionAsynchronously(
String hostname, int portnum) {
Runnable outgoingRunner =
new OutgoingConnector(new ManagedConnection(hostname, portnum),
true);
// Initialize and loop for messages on another thread.
ThreadFactory.startThread(outgoingRunner, "OutgoingConnectionThread");
}
public void acceptConnection(String word, Socket socket) {
if (word.equals(ConnectionSettings.CONNECT_STRING_FIRST_WORD))
HTTPStat.GNUTELLA_REQUESTS.incrementStat();
else if (ConnectionSettings.CONNECT_STRING.isDefault() &&
word.equals("LIMEWIRE") )
HTTPStat.GNUTELLA_LIMEWIRE_REQUESTS.incrementStat();
else
return; //drop it.
acceptConnection(socket);
}
/**
* Create an incoming connection.
*
* If the connection can support asynchronous messaging, this method will return
* immediately. Otherwise, this will block forever while the connection handshakes
* and then loops for messages (it will return when the connection dies).
*/
void acceptConnection(Socket socket) {
ManagedConnection connection = new ManagedConnection(socket);
GnetConnectObserver listener = null;
if(connection.isAsynchronous())
listener = new IncomingGNetObserver(connection);
else
Thread.currentThread().setName("IncomingConnectionThread");
try {
initializeExternallyGeneratedConnection(connection, listener);
} catch (IOException e) {
connection.close();
return;
}
// Otherwise, the listener will be notified when to start.
if(listener == null) {
try {
startConnection(connection);
} catch(IOException e) {
// the blocking connection died.
}
}
}
/**
* Removes the specified connection from currently active connections, also
* removing this connection from routing tables and modifying active
* connection fetchers accordingly.
*
* @param mc the <tt>ManagedConnection</tt> instance to remove
*/
public synchronized void remove(ManagedConnection mc) {
// removal may be disabled for tests
if(!ConnectionSettings.REMOVE_ENABLED.getValue()) return;
removeInternal(mc);
adjustConnectionFetchers();
}
/**
* True if this is currently or wants to be a supernode,
* otherwise false.
*/
public boolean isSupernode() {
return isActiveSupernode() || isSupernodeCapable();
}
/** Return true if we are not a private address, have been ultrapeer capable
* in the past, and are not being shielded by anybody, AND we don't have UP
* mode disabled.
*/
public boolean isSupernodeCapable() {
return !NetworkUtils.isPrivate() &&
UltrapeerSettings.EVER_ULTRAPEER_CAPABLE.getValue() &&
!isShieldedLeaf() &&
!UltrapeerSettings.DISABLE_ULTRAPEER_MODE.getValue() &&
!isBehindProxy() &&
minConnectTimePassed();
}
/**
* @return whether the minimum time since we started trying to connect has passed
*/
private boolean minConnectTimePassed() {
if (!UltrapeerSettings.NEED_MIN_CONNECT_TIME.getValue()) {
return true;
}
return Math.max(0,(System.currentTimeMillis() - _connectTime)) / 1000
>= UltrapeerSettings.MIN_CONNECT_TIME.getValue();
}
/**
* @return if we are currently using a http or socks4/5 proxy to connect.
*/
public boolean isBehindProxy() {
return ConnectionSettings.CONNECTION_METHOD.getValue() !=
ConnectionSettings.C_NO_PROXY;
}
/**
* Tells whether or not we're actively being a supernode to anyone.
*/
public boolean isActiveSupernode() {
return !isShieldedLeaf() &&
(_initializedClientConnections.size() > 0 ||
_initializedConnections.size() > 0);
}
/**
* Returns true if this is a leaf node with a connection to a ultrapeer. It
* is not required that the ultrapeer support query routing, though that is
* generally the case.
*/
public boolean isShieldedLeaf() {
return _shieldedConnections != 0;
}
/**
* Returns true if this is a super node with a connection to a leaf.
*/
public boolean hasSupernodeClientConnection() {
return getNumInitializedClientConnections() > 0;
}
/**
* Returns whether or not this node has any available connection
* slots. This is only relevant for Ultrapeers -- leaves will
* always return <tt>false</tt> to this call since they do not
* accept any incoming connections, at least for now.
*
* @return <tt>true</tt> if this node is an Ultrapeer with free
* leaf or Ultrapeer connections slots, otherwise <tt>false</tt>
*/
public boolean hasFreeSlots() {
return isSupernode() &&
(hasFreeUltrapeerSlots() || hasFreeLeafSlots());
}
/**
* Utility method for determing whether or not we have any available
* Ultrapeer connection slots. If this node is a leaf, it will
* always return <tt>false</tt>.
*
* @return <tt>true</tt> if there are available Ultrapeer connection
* slots, otherwise <tt>false</tt>
*/
private boolean hasFreeUltrapeerSlots() {
return getNumFreeNonLeafSlots() > 0;
}
/**
* Utility method for determing whether or not we have any available
* leaf connection slots. If this node is a leaf, it will
* always return <tt>false</tt>.
*
* @return <tt>true</tt> if there are available leaf connection
* slots, otherwise <tt>false</tt>
*/
private boolean hasFreeLeafSlots() {
return getNumFreeLeafSlots() > 0;
}
/**
* Returns whether this (probably) has a connection to the given host. This
* method is currently implemented by iterating through all connections and
* comparing addresses but not ports. (Incoming connections use ephemeral
* ports.) As a result, this test may conservatively return true even if
* this is not connected to <tt>host</tt>. Likewise, it may it mistakenly
* return false if <tt>host</tt> is a multihomed system. In the future,
* additional connection headers may make the test more precise.
*
* @return true if this is probably connected to <tt>host</tt>
*/
boolean isConnectedTo(String hostName) {
//A clone of the list of all connections, both initialized and
//uninitialized, leaves and unrouted. If Java could be prevented from
//making certain code transformations, it would be safe to replace the
//call to "getConnections()" with "_connections", thus avoiding a clone.
//(Remember that _connections is never mutated.)
List connections=getConnections();
for (Iterator iter=connections.iterator(); iter.hasNext(); ) {
ManagedConnection mc = (ManagedConnection)iter.next();
if (mc.getAddress().equals(hostName))
return true;
}
return false;
}
/**
* @return the number of connections, which is greater than or equal
* to the number of initialized connections.
*/
public int getNumConnections() {
return _connections.size();
}
/**
* @return the number of initialized connections, which is less than or
* equals to the number of connections.
*/
public int getNumInitializedConnections() {
return _initializedConnections.size();
}
/**
* @return the number of initializedclient connections, which is less than
* or equals to the number of connections.
*/
public int getNumInitializedClientConnections() {
return _initializedClientConnections.size();
}
/**
*@return the number of initialized connections for which
* isClientSupernodeConnection is true.
*/
public int getNumClientSupernodeConnections() {
return _shieldedConnections;
}
/**
*@return the number of ultrapeer -> ultrapeer connections.
*/
public synchronized int getNumUltrapeerConnections() {
return ultrapeerToUltrapeerConnections();
}
/**
*@return the number of old unrouted connections.
*/
public synchronized int getNumOldConnections() {
return oldConnections();
}
/**
* @return the number of free leaf slots.
*/
public int getNumFreeLeafSlots() {
if (isSupernode())
return UltrapeerSettings.MAX_LEAVES.getValue() -
getNumInitializedClientConnections();
else
return 0;
}
/**
* @return the number of free leaf slots that LimeWires can connect to.
*/
public int getNumFreeLimeWireLeafSlots() {
return Math.max(0,
getNumFreeLeafSlots() -
Math.max(0, RESERVED_NON_LIMEWIRE_LEAVES - _nonLimeWireLeaves)
);
}
/**
* @return the number of free non-leaf slots.
*/
public int getNumFreeNonLeafSlots() {
return _preferredConnections - getNumInitializedConnections();
}
/**
* @return the number of free non-leaf slots that LimeWires can connect to.
*/
public int getNumFreeLimeWireNonLeafSlots() {
return Math.max(0,
getNumFreeNonLeafSlots()
- Math.max(0, (int)
(ConnectionSettings.MIN_NON_LIME_PEERS.getValue() * _preferredConnections)
- _nonLimeWirePeers)
- getNumLimeWireLocalePrefSlots()
);
}
/**
* Returns true if we've made a locale-matching connection (or don't
* want any at all).
*/
public boolean isLocaleMatched() {
return !ConnectionSettings.USE_LOCALE_PREF.getValue() ||
_localeMatchingPeers != 0;
}
/**
* @return the number of locale reserved slots to be filled
*
* An ultrapeer may not have Free LimeWire Non Leaf Slots but may still
* have free slots that are reserved for locales
*/
public int getNumLimeWireLocalePrefSlots() {
return Math.max(0, ConnectionSettings.NUM_LOCALE_PREF.getValue()
- _localeMatchingPeers);
}
/**
* Determines if we've reached our maximum number of preferred connections.
*/
public boolean isFullyConnected() {
return _initializedConnections.size() >= _preferredConnections;
}
/**
* Returns whether or not the client has an established connection with
* another Gnutella client.
*
* @return <tt>true</tt> if the client is currently connected to
* another Gnutella client, <tt>false</tt> otherwise
*/
public boolean isConnected() {
return ((_initializedClientConnections.size() > 0) ||
(_initializedConnections.size() > 0));
}
/**
* Returns whether or not we are currently attempting to connect to the
* network.
*/
public boolean isConnecting() {
if(_disconnectTime != 0)
return false;
if(isConnected())
return false;
synchronized(this) {
return _fetchers.size() != 0 ||
_initializingFetchedConnections.size() != 0;
}
}
/**
* Takes a snapshot of the upstream and downstream bandwidth since the last
* call to measureBandwidth.
* @see BandwidthTracker#measureBandwidth
*/
public void measureBandwidth() {
float upstream=0.f;
float downstream=0.f;
List connections = getInitializedConnections();
for (Iterator iter=connections.iterator(); iter.hasNext(); ) {
ManagedConnection mc=(ManagedConnection)iter.next();
mc.measureBandwidth();
upstream+=mc.getMeasuredUpstreamBandwidth();
downstream+=mc.getMeasuredDownstreamBandwidth();
}
_measuredUpstreamBandwidth=upstream;
_measuredDownstreamBandwidth=downstream;
}
/**
* Returns the upstream bandwidth between the last two calls to
* measureBandwidth.
* @see BandwidthTracker#measureBandwidth
*/
public float getMeasuredUpstreamBandwidth() {
return _measuredUpstreamBandwidth;
}
/**
* Returns the downstream bandwidth between the last two calls to
* measureBandwidth.
* @see BandwidthTracker#measureBandwidth
*/
public float getMeasuredDownstreamBandwidth() {
return _measuredDownstreamBandwidth;
}
/**
* Checks if the connection received can be accepted,
* based upon the type of connection (e.g. client, ultrapeer,
* temporary etc).
* @param c The connection we received, for which to
* test if we have incoming slot.
* @return true, if we have incoming slot for the connection received,
* false otherwise
*/
private boolean allowConnection(ManagedConnection c) {
if(!c.receivedHeaders()) return false;
return allowConnection(c.headers(), false);
}
/**
* Checks if the connection received can be accepted,
* based upon the type of connection (e.g. client, ultrapeer,
* temporary etc).
* @param c The connection we received, for which to
* test if we have incoming slot.
* @return true, if we have incoming slot for the connection received,
* false otherwise
*/
public boolean allowConnectionAsLeaf(HandshakeResponse hr) {
return allowConnection(hr, true);
}
/**
* Checks if the connection received can be accepted,
* based upon the type of connection (e.g. client, ultrapeer,
* temporary etc).
* @param c The connection we received, for which to
* test if we have incoming slot.
* @return true, if we have incoming slot for the connection received,
* false otherwise
*/
public boolean allowConnection(HandshakeResponse hr) {
return allowConnection(hr, !hr.isUltrapeer());
}
/**
* Checks if there is any available slot of any kind.
* @return true, if we have incoming slot of some kind,
* false otherwise
*/
public boolean allowAnyConnection() {
//Stricter than necessary.
//See allowAnyConnection(boolean,String,String).
if (isShieldedLeaf())
return false;
//Do we have normal or leaf slots?
return getNumInitializedConnections() < _preferredConnections
|| (isSupernode()
&& getNumInitializedClientConnections() <
UltrapeerSettings.MAX_LEAVES.getValue());
}
/**
* Returns true if this has slots for an incoming connection, <b>without
* accounting for this' ultrapeer capabilities</b>. More specifically:
* <ul>
* <li>if ultrapeerHeader==null, returns true if this has space for an
* unrouted old-style connection.
* <li>if ultrapeerHeader.equals("true"), returns true if this has slots
* for a leaf connection.
* <li>if ultrapeerHeader.equals("false"), returns true if this has slots
* for an ultrapeer connection.
* </ul>
*
* <tt>useragentHeader</tt> is used to prefer LimeWire and certain trusted
* vendors. <tt>outgoing</tt> is currently unused, but may be used to
* prefer incoming or outgoing connections in the forward.
*
* @param outgoing true if this is an outgoing connection; true if incoming
* @param ultrapeerHeader the value of the X-Ultrapeer header, or null
* if it was not written
* @param useragentHeader the value of the User-Agent header, or null if
* it was not written
* @return true if a connection of the given type is allowed
*/
public boolean allowConnection(HandshakeResponse hr, boolean leaf) {
// preferencing may not be active for testing purposes --
// just return if it's not
if(!ConnectionSettings.PREFERENCING_ACTIVE.getValue()) return true;
// If it has not said whether or not it's an Ultrapeer or a Leaf
// (meaning it's an old-style connection), don't allow it.
if(!hr.isLeaf() && !hr.isUltrapeer())
return false;
//Old versions of LimeWire used to prefer incoming connections over
//outgoing. The rationale was that a large number of hosts were
//firewalled, so those who weren't had to make extra space for them.
//With the introduction of ultrapeers, this is not an issue; all
//firewalled hosts become leaf nodes. Hence we make no distinction
//between incoming and outgoing.
//
//At one point we would actively kill old-fashioned unrouted connections
//for ultrapeers. Later, we preferred ultrapeers to old-fashioned
//connections as follows: if the HostCatcher had marked ultrapeer pongs,
//we never allowed more than DESIRED_OLD_CONNECTIONS old
//connections--incoming or outgoing.
//
//Now we simply prefer connections by vendor, which has some of the same
//effect. We use BearShare's clumping algorithm. Let N be the
//keep-alive and K be RESERVED_GOOD_CONNECTIONS. (In BearShare's
//implementation, K=1.) Allow any connections in for the first N-K
//slots. But only allow good vendors for the last K slots. In other
//words, accept a connection C if there are fewer than N connections and
//one of the following is true: C is a good vendor or there are fewer
//than N-K connections. With time, this converges on all good
//connections.
int limeAttempts = ConnectionSettings.LIME_ATTEMPTS.getValue();
// Don't allow anything if disconnected.
if (!ConnectionSettings.ALLOW_WHILE_DISCONNECTED.getValue() && _preferredConnections <= 0) {
return false;
// If a leaf (shielded or not), check rules as such.
} else if (isShieldedLeaf() || !isSupernode()) {
// require ultrapeer.
if(!hr.isUltrapeer()) {
return false;
}
// If it's not good, or it's the first few attempts & not a LimeWire,
// never allow it.
if(!hr.isGoodUltrapeer() || (_connectionAttempts < limeAttempts && !hr.isLimeWire())) {
return false;
// if we have slots, allow it.
} else if (_shieldedConnections < _preferredConnections) {
// if it matched our preference, we don't need to preference
// anymore.
if(checkLocale(hr.getLocalePref()))
_needPref = false;
// while idle, only allow LimeWire connections.
if (isIdle()) {
return hr.isLimeWire();
}
return true;
} else {
// if we were still trying to get a locale connection
// and this one matches, allow it, 'cause no one else matches.
// (we would have turned _needPref off if someone matched.)
if(_needPref && checkLocale(hr.getLocalePref())) {
return true;
}
// don't allow it.
return false;
}
} else if (hr.isLeaf() || leaf) {
// no leaf connections if we're a leaf.
if(isShieldedLeaf() || !isSupernode()) {
return false;
}
if(!allowUltrapeer2LeafConnection(hr)) {
return false;
}
int leaves = getNumInitializedClientConnections();
int nonLimeWireLeaves = _nonLimeWireLeaves;
// Reserve RESERVED_NON_LIMEWIRE_LEAVES slots
// for non-limewire leaves to ensure that the network
// is well connected.
if(!hr.isLimeWire()) {
if( leaves < UltrapeerSettings.MAX_LEAVES.getValue() &&
nonLimeWireLeaves < RESERVED_NON_LIMEWIRE_LEAVES ) {
return true;
}
}
// Only allow good guys.
if(!hr.isGoodLeaf()) {
return false;
}
// if it's good, allow it.
return (leaves + Math.max(0, RESERVED_NON_LIMEWIRE_LEAVES -
nonLimeWireLeaves)) <
UltrapeerSettings.MAX_LEAVES.getValue();
} else if (hr.isGoodUltrapeer()) {
// Note that this code is NEVER CALLED when we are a leaf.
// As a leaf, we will allow however many ultrapeers we happen
// to connect to.
// Thus, we only worry about the case we're connecting to
// another ultrapeer (internally or externally generated)
int peers = getNumInitializedConnections();
int nonLimeWirePeers = _nonLimeWirePeers;
int locale_num = 0;
if(!allowUltrapeer2UltrapeerConnection(hr)) {
return false;
}
if(ConnectionSettings.USE_LOCALE_PREF.getValue()) {
//if locale matches and we haven't satisfied the
//locale reservation then we force return a true
if(checkLocale(hr.getLocalePref()) &&
_localeMatchingPeers
< ConnectionSettings.NUM_LOCALE_PREF.getValue()) {
return true;
}
//this number will be used at the end to figure out
//if the connection should be allowed
//(the reserved slots is to make sure we have at least
// NUM_LOCALE_PREF locale connections but we could have more so
// we get the max)
locale_num =
getNumLimeWireLocalePrefSlots();
}
// Reserve RESERVED_NON_LIMEWIRE_PEERS slots
// for non-limewire peers to ensure that the network
// is well connected.
if(!hr.isLimeWire()) {
double nonLimeRatio = ((double)nonLimeWirePeers) / _preferredConnections;
if (nonLimeRatio < ConnectionSettings.MIN_NON_LIME_PEERS.getValue())
return true;
return (nonLimeRatio < ConnectionSettings.MAX_NON_LIME_PEERS.getValue());
} else {
int minNonLime = (int)
(ConnectionSettings.MIN_NON_LIME_PEERS.getValue() * _preferredConnections);
return (peers +
Math.max(0,minNonLime - nonLimeWirePeers) +
locale_num) < _preferredConnections;
}
}
return false;
}
/**
* Utility method for determining whether or not the connection should be
* allowed as an Ultrapeer<->Ultrapeer connection. We may not allow the
* connection for a variety of reasons, including lack of support for
* specific features that are vital for good performance, or clients of
* specific vendors that are leechers or have serious bugs that make them
* detrimental to the network.
*
* @param hr the <tt>HandshakeResponse</tt> instance containing the
* connections headers of the remote host
* @return <tt>true</tt> if the connection should be allowed, otherwise
* <tt>false</tt>
*/
private static boolean allowUltrapeer2UltrapeerConnection(HandshakeResponse hr) {
if(hr.isLimeWire())
return true;
String userAgent = hr.getUserAgent();
if(userAgent == null)
return false;
userAgent = userAgent.toLowerCase();
String[] bad = ConnectionSettings.EVIL_HOSTS.getValue();
for(int i = 0; i < bad.length; i++)
if(userAgent.indexOf(bad[i]) != -1)
return false;
return true;
}
/**
* Utility method for determining whether or not the connection should be
* allowed as a leaf when we're an Ultrapeer.
*
* @param hr the <tt>HandshakeResponse</tt> containing their connection
* headers
* @return <tt>true</tt> if the connection should be allowed, otherwise
* <tt>false</tt>
*/
private static boolean allowUltrapeer2LeafConnection(HandshakeResponse hr) {
if(hr.isLimeWire())
return true;
String userAgent = hr.getUserAgent();
if(userAgent == null)
return false;
userAgent = userAgent.toLowerCase();
String[] bad = ConnectionSettings.EVIL_HOSTS.getValue();
for(int i = 0; i < bad.length; i++)
if(userAgent.indexOf(bad[i]) != -1)
return false;
return true;
}
/**
* Returns the number of connections that are ultrapeer -> ultrapeer.
* Caller MUST hold this' monitor.
*/
private int ultrapeerToUltrapeerConnections() {
//TODO3: augment state of this if needed to avoid loop
int ret=0;
for (Iterator iter=_initializedConnections.iterator(); iter.hasNext();){
ManagedConnection mc=(ManagedConnection)iter.next();
if (mc.isSupernodeSupernodeConnection())
ret++;
}
return ret;
}
/** Returns the number of old-fashioned unrouted connections. Caller MUST
* hold this' monitor. */
private int oldConnections() {
// technically, we can allow old connections.
int ret = 0;
for (Iterator iter=_initializedConnections.iterator(); iter.hasNext();){
ManagedConnection mc=(ManagedConnection)iter.next();
if (!mc.isSupernodeConnection())
ret++;
}
return ret;
}
/**
* Tells if this node thinks that more ultrapeers are needed on the
* network. This method should be invoked on a ultrapeer only, as
* only ultrapeer may have required information to make informed
* decision.
* @return true, if more ultrapeers needed, false otherwise
*/
public boolean supernodeNeeded() {
//if more than 90% slots are full, return true
if(getNumInitializedClientConnections() >=
(UltrapeerSettings.MAX_LEAVES.getValue() * 0.9)){
return true;
} else {
//else return false
return false;
}
}
/**
* @requires returned value not modified
* @effects returns a list of this' initialized connections. <b>This
* exposes the representation of this, but is needed in some cases
* as an optimization.</b> All lookup values in the returned value
* are guaranteed to run in linear time.
*/
public List getInitializedConnections() {
return _initializedConnections;
}
/**
* return a list of initialized connection that matches the parameter
* String loc.
* create a new linkedlist to return.
*/
public List getInitializedConnectionsMatchLocale(String loc) {
List matches = new LinkedList();
for(Iterator itr= _initializedConnections.iterator();
itr.hasNext();) {
Connection conn = (Connection)itr.next();
if(loc.equals(conn.getLocalePref()))
matches.add(conn);
}
return matches;
}
/**
* @requires returned value not modified
* @effects returns a list of this' initialized connections. <b>This
* exposes the representation of this, but is needed in some cases
* as an optimization.</b> All lookup values in the returned value
* are guaranteed to run in linear time.
*/
public List getInitializedClientConnections() {
return _initializedClientConnections;
}
/**
* return a list of initialized client connection that matches the parameter
* String loc.
* create a new linkedlist to return.
*/
public List getInitializedClientConnectionsMatchLocale(String loc) {
List matches = new LinkedList();
for(Iterator itr= _initializedClientConnections.iterator();
itr.hasNext();) {
Connection conn = (Connection)itr.next();
if(loc.equals(conn.getLocalePref()))
matches.add(conn);
}
return matches;
}
/**
* @return all of this' connections.
*/
public List getConnections() {
return _connections;
}
/**
* Accessor for the <tt>Set</tt> of push proxies for this node. If
* there are no push proxies available, or if this node is an Ultrapeer,
* this will return an empty <tt>Set</tt>.
*
* @return a <tt>Set</tt> of push proxies with a maximum size of 4
*
* TODO: should the set of pushproxy UPs be cached and updated as
* connections are killed and created?
*/
public Set getPushProxies() {
if (isShieldedLeaf()) {
// this should be fast since leaves don't maintain a lot of
// connections and the test for proxy support is cached boolean
// value
Iterator ultrapeers = getInitializedConnections().iterator();
Set proxies = new IpPortSet();
while (ultrapeers.hasNext() && (proxies.size() < 4)) {
ManagedConnection currMC = (ManagedConnection)ultrapeers.next();
if (currMC.isPushProxy())
proxies.add(currMC);
}
return proxies;
}
return Collections.EMPTY_SET;
}
/**
* Sends a TCPConnectBack request to (up to) 2 connected Ultrapeers.
* @returns false if no requests were sent, otherwise true.
*/
public boolean sendTCPConnectBackRequests() {
int sent = 0;
List peers = new ArrayList(getInitializedConnections());
Collections.shuffle(peers);
for (Iterator iter = peers.iterator(); iter.hasNext();) {
ManagedConnection currMC = (ManagedConnection) iter.next();
if (currMC.remoteHostSupportsTCPRedirect() < 0)
iter.remove();
}
if (peers.size() == 1) {
ManagedConnection myConn = (ManagedConnection) peers.get(0);
for (int i = 0; i < CONNECT_BACK_REDUNDANT_REQUESTS; i++) {
Message cb = new TCPConnectBackVendorMessage(RouterService.getPort());
myConn.send(cb);
sent++;
}
} else {
final Message cb = new TCPConnectBackVendorMessage(RouterService.getPort());
for(Iterator i = peers.iterator(); i.hasNext() && sent < 5; ) {
ManagedConnection currMC = (ManagedConnection)i.next();
currMC.send(cb);
sent++;
}
}
return (sent > 0);
}
/**
* Sends a UDPConnectBack request to (up to) 4 (and at least 2)
* connected Ultrapeers.
* @returns false if no requests were sent, otherwise true.
*/
public boolean sendUDPConnectBackRequests(GUID cbGuid) {
int sent = 0;
final Message cb =
new UDPConnectBackVendorMessage(RouterService.getPort(), cbGuid);
List peers = new ArrayList(getInitializedConnections());
Collections.shuffle(peers);
for(Iterator i = peers.iterator(); i.hasNext() && sent < 5; ) {
ManagedConnection currMC = (ManagedConnection)i.next();
if (currMC.remoteHostSupportsUDPConnectBack() >= 0) {
currMC.send(cb);
sent++;
}
}
return (sent > 0);
}
/**
* Sends a QueryStatusResponse message to as many Ultrapeers as possible.
*
* @param
*/
public void updateQueryStatus(QueryStatusResponse stat) {
if (isShieldedLeaf()) {
// this should be fast since leaves don't maintain a lot of
// connections and the test for query status response is a cached
// value
Iterator ultrapeers = getInitializedConnections().iterator();
while (ultrapeers.hasNext()) {
ManagedConnection currMC = (ManagedConnection)ultrapeers.next();
if (currMC.remoteHostSupportsLeafGuidance() >= 0)
currMC.send(stat);
}
}
}
/**
* Returns the <tt>Endpoint</tt> for an Ultrapeer connected via TCP,
* if available.
*
* @return the <tt>Endpoint</tt> for an Ultrapeer connected via TCP if
* there is one, otherwise returns <tt>null</tt>
*/
public Endpoint getConnectedGUESSUltrapeer() {
for(Iterator iter=_initializedConnections.iterator(); iter.hasNext();) {
ManagedConnection connection = (ManagedConnection)iter.next();
if(connection.isSupernodeConnection() &&
connection.isGUESSUltrapeer()) {
return new Endpoint(connection.getInetAddress().getAddress(),
connection.getPort());
}
}
return null;
}
/** Returns a <tt>List<tt> of Ultrapeers connected via TCP that are GUESS
* enabled.
*
* @return A non-null List of GUESS enabled, TCP connected Ultrapeers. The
* are represented as ManagedConnections.
*/
public List getConnectedGUESSUltrapeers() {
List retList = new ArrayList();
for(Iterator iter=_initializedConnections.iterator(); iter.hasNext();) {
ManagedConnection connection = (ManagedConnection)iter.next();
if(connection.isSupernodeConnection() &&
connection.isGUESSUltrapeer())
retList.add(connection);
}
return retList;
}
/**
* Adds an initializing connection.
* Should only be called from a thread that has this' monitor.
* This is called from initializeExternallyGeneratedConnection
* and initializeFetchedConnection, both times from within a
* synchronized(this) block.
*/
private void connectionInitializing(Connection c) {
//REPLACE _connections with the list _connections+[c]
List newConnections=new ArrayList(_connections);
newConnections.add(c);
_connections = Collections.unmodifiableList(newConnections);
}
/**
* Adds an incoming connection to the list of connections. Note that
* the incoming connection has already been initialized before
* this method is invoked.
* Should only be called from a thread that has this' monitor.
* This is called from initializeExternallyGeneratedConnection, for
* incoming connections
*/
private void connectionInitializingIncoming(ManagedConnection c) {
connectionInitializing(c);
}
/**
* Marks a connection fully initialized, but only if that connection wasn't
* removed from the list of open connections during its initialization.
* Should only be called from a thread that has this' monitor.
*/
private boolean connectionInitialized(ManagedConnection c) {
if(_connections.contains(c)) {
// Double-check that we haven't improperly allowed
// this connection. It is possible that, because of race-conditions,
// we may have allowed both a 'Peer' and an 'Ultrapeer', or an 'Ultrapeer'
// and a leaf. That'd 'cause undefined results if we allowed it.
if(!allowInitializedConnection(c)) {
removeInternal(c);
return false;
}
//update the appropriate list of connections
if(!c.isSupernodeClientConnection()){
//REPLACE _initializedConnections with the list
//_initializedConnections+[c]
List newConnections=new ArrayList(_initializedConnections);
newConnections.add(c);
_initializedConnections =
Collections.unmodifiableList(newConnections);
if(c.isClientSupernodeConnection()) {
killPeerConnections(); // clean up any extraneus peer conns.
_shieldedConnections++;
}
if(!c.isLimeWire())
_nonLimeWirePeers++;
if(checkLocale(c.getLocalePref()))
_localeMatchingPeers++;
} else {
//REPLACE _initializedClientConnections with the list
//_initializedClientConnections+[c]
List newConnections
=new ArrayList(_initializedClientConnections);
newConnections.add(c);
_initializedClientConnections =
Collections.unmodifiableList(newConnections);
if(!c.isLimeWire())
_nonLimeWireLeaves++;
}
// do any post-connection initialization that may involve sending.
c.postInit();
// sending the ping request.
sendInitialPingRequest(c);
return true;
}
return false;
}
/**
* like allowConnection, except more strict - if this is a leaf,
* only allow connections whom we have told we're leafs.
* @return whether the connection should be allowed
*/
private boolean allowInitializedConnection(Connection c) {
if ((isShieldedLeaf() || !isSupernode()) &&
!c.isClientSupernodeConnection())
return false;
return allowConnection(c.headers());
}
/**
* removes any supernode->supernode connections
*/
private void killPeerConnections() {
List conns = _initializedConnections;
for (Iterator iter = conns.iterator(); iter.hasNext();) {
ManagedConnection con = (ManagedConnection) iter.next();
if (con.isSupernodeSupernodeConnection())
removeInternal(con);
}
}
/**
* Iterates over all the connections and sends the updated CapabilitiesVM
* down every one of them.
*/
public void sendUpdatedCapabilities() {
for(Iterator iter = getInitializedConnections().iterator(); iter.hasNext(); ) {
Connection c = (Connection)iter.next();
c.sendUpdatedCapabilities();
}
for(Iterator iter = getInitializedClientConnections().iterator(); iter.hasNext(); ) {
Connection c = (Connection)iter.next();
c.sendUpdatedCapabilities();
}
}
/**
* Disconnects from the network. Closes all connections and sets
* the number of connections to zero.
*/
public synchronized void disconnect() {
_disconnectTime = System.currentTimeMillis();
_connectTime = Long.MAX_VALUE;
_preferredConnections = 0;
adjustConnectionFetchers(); // kill them all
//2. Remove all connections.
for (Iterator iter=getConnections().iterator();
iter.hasNext(); ) {
ManagedConnection c=(ManagedConnection)iter.next();
remove(c);
//add the endpoint to hostcatcher
if (c.isSupernodeConnection()) {
//add to catcher with the locale info.
_catcher.add(new Endpoint(c.getInetAddress().getHostAddress(),
c.getPort()), true, c.getLocalePref());
}
}
}
/**
* Connects to the network. Ensures the number of messaging connections
* is non-zero and recontacts the pong server as needed.
*/
public synchronized void connect() {
// Reset the disconnect time to be a long time ago.
_disconnectTime = 0;
_connectTime = System.currentTimeMillis();
// Ignore this call if we're already connected
// or not initialized yet.
if(isConnected() || _catcher == null) {
return;
}
_connectionAttempts = 0;
_lastConnectionCheck = 0;
_lastSuccessfulConnect = 0;
// Notify HostCatcher that we've connected.
_catcher.expire();
// Set the number of connections we want to maintain
setPreferredConnections();
// tell the catcher to start pinging people.
_catcher.sendUDPPings();
}
/**
* Sends the initial ping request to a newly initialized connection. The
* ttl of the PingRequest will be 1 if we don't need any connections.
* Otherwise, the ttl = max ttl.
*/
private void sendInitialPingRequest(ManagedConnection connection) {
if(connection.supportsPongCaching()) return;
//We need to compare how many connections we have to the keep alive to
//determine whether to send a broadcast ping or a handshake ping,
//initially. However, in this case, we can't check the number of
//connection fetchers currently operating, as that would always then
//send a handshake ping, since we're always adjusting the connection
//fetchers to have the difference between keep alive and num of
//connections.
PingRequest pr;
if (getNumInitializedConnections() >= _preferredConnections)
pr = new PingRequest((byte)1);
else
pr = new PingRequest((byte)4);
connection.send(pr);
//Ensure that the initial ping request is written in a timely fashion.
try {
connection.flush();
} catch (IOException e) { /* close it later */ }
}
/**
* An unsynchronized version of remove, meant to be used when the monitor
* is already held. This version does not kick off ConnectionFetchers;
* only the externally exposed version of remove does that.
*/
private void removeInternal(ManagedConnection c) {
// 1a) Remove from the initialized connections list and clean up the
// stuff associated with initialized connections. For efficiency
// reasons, this must be done before (2) so packets are not forwarded
// to dead connections (which results in lots of thrown exceptions).
if(!c.isSupernodeClientConnection()){
int i=_initializedConnections.indexOf(c);
if (i != -1) {
//REPLACE _initializedConnections with the list
//_initializedConnections-[c]
List newConnections=new ArrayList();
newConnections.addAll(_initializedConnections);
newConnections.remove(c);
_initializedConnections =
Collections.unmodifiableList(newConnections);
//maintain invariant
if(c.isClientSupernodeConnection())
_shieldedConnections--;
if(!c.isLimeWire())
_nonLimeWirePeers--;
if(checkLocale(c.getLocalePref()))
_localeMatchingPeers--;
}
}else{
//check in _initializedClientConnections
int i=_initializedClientConnections.indexOf(c);
if (i != -1) {
//REPLACE _initializedClientConnections with the list
//_initializedClientConnections-[c]
List newConnections=new ArrayList();
newConnections.addAll(_initializedClientConnections);
newConnections.remove(c);
_initializedClientConnections =
Collections.unmodifiableList(newConnections);
if(!c.isLimeWire())
_nonLimeWireLeaves--;
}
}
// 1b) Remove from the all connections list and clean up the
// stuff associated all connections
int i=_connections.indexOf(c);
if (i != -1) {
//REPLACE _connections with the list _connections-[c]
List newConnections=new ArrayList(_connections);
newConnections.remove(c);
_connections = Collections.unmodifiableList(newConnections);
}
// 2) Ensure that the connection is closed. This must be done before
// step (3) to ensure that dead connections are not added to the route
// table, resulting in dangling references.
c.close();
// 3) Clean up route tables.
RouterService.getMessageRouter().removeConnection(c);
// 4) Notify the listener
RouterService.getCallback().connectionClosed(c);
// 5) Clean up Unicaster
QueryUnicaster.instance().purgeQuery(c);
}
/**
* Stabilizes connections by removing extraneous ones.
*
* This will remove the connections that we've been connected to
* for the shortest amount of time.
*/
private synchronized void stabilizeConnections() {
while(getNumInitializedConnections() > _preferredConnections) {
ManagedConnection newest = null;
for(Iterator i = _initializedConnections.iterator(); i.hasNext();){
ManagedConnection c = (ManagedConnection)i.next();
// first see if this is a non-limewire connection and cut it off
// unless it is our only connection left
if (!c.isLimeWire()) {
newest = c;
break;
}
if(newest == null ||
c.getConnectionTime() > newest.getConnectionTime())
newest = c;
}
if(newest != null)
remove(newest);
}
adjustConnectionFetchers();
}
/**
* Starts or stops connection fetchers to maintain the invariant that numConnections + numFetchers >=
* _preferredConnections
*
* _preferredConnections - numConnections - numFetchers is called the need.
* This method is called whenever the need changes:
* 1. setPreferredConnections() -- _preferredConnections changes
* 2. remove(Connection) -- numConnections drops.
* 3. initializeExternallyGeneratedConnection() -- numConnections rises.
* 4. initialization error in initializeFetchedConnection() -- numConnections drops when removeInternal is called.
* Note that adjustConnectionFetchers is not called when a connection is successfully fetched from the host catcher.
* numConnections rises, but numFetchers drops, so need is unchanged.
*
* Only call this method when the monitor is held.
*/
private void adjustConnectionFetchers() {
if(ConnectionSettings.USE_LOCALE_PREF.getValue()) {
startDedicatedLocaleFetcher();
}
int goodConnections = getNumInitializedConnections();
int neededConnections = _preferredConnections - goodConnections;
//Now how many fetchers do we need? To increase parallelism, we
//allocate 3 fetchers per connection, but no more than 10 fetchers.
//(Too much parallelism increases chance of simultaneous connects,
//resulting in too many connections.) Note that we assume that all
//connections being fetched right now will become ultrapeers.
int multiple;
// The end result of the following logic, assuming _preferredConnections
// is 32 for Ultrapeers, is:
// When we have 22 active peer connections, we fetch
// (27-current)*1 connections.
// All other times, for Ultrapeers, we will fetch
// (32-current)*3, up to a maximum of 20.
// For leaves, assuming they maintin 4 Ultrapeers,
// we will fetch (4-current)*2 connections.
// If we have not accepted incoming, fetch 3 times
// as many connections as we need.
// We must also check if we're actively being a Ultrapeer because
// it's possible we may have turned off acceptedIncoming while
// being an Ultrapeer.
if( !RouterService.acceptedIncomingConnection() && !isActiveSupernode() ) {
multiple = 3;
}
// Otherwise, if we're not ultrapeer capable,
// or have not become an Ultrapeer to anyone,
// also fetch 3 times as many connections as we need.
// It is critical that active ultrapeers do not use a multiple of 3
// without reducing neededConnections, otherwise LimeWire would
// continue connecting and rejecting connections forever.
else if( !isSupernode() || getNumUltrapeerConnections() == 0 ) {
multiple = 3;
}
// Otherwise (we are actively Ultrapeering to someone)
// If we needed more than connections, still fetch
// 2 times as many connections as we need.
// It is critical that 10 is greater than RESERVED_NON_LIMEWIRE_PEERS,
// else LimeWire would try connecting and rejecting connections forever.
else if( neededConnections > 10 ) {
multiple = 2;
}
// Otherwise, if we need less than 10 connections (and we're an Ultrapeer),
// decrement the amount of connections we need by 5,
// leaving 5 slots open for newcomers to use,
// and decrease the rate at which we fetch to
// 1 times the amount of connections.
else {
multiple = 1;
neededConnections -= 5 +
ConnectionSettings.MIN_NON_LIME_PEERS.getValue() * _preferredConnections;
}
int need = Math.min(10, multiple*neededConnections)
- _fetchers.size()
- _initializingFetchedConnections.size();
// do not open more sockets than we can
need = Math.min(need, Sockets.getNumAllowedSockets());
// Build up lists of what we need to connect to & remove from connecting.
// Start connection fetchers as necessary
List fetchers = Collections.EMPTY_LIST;
if (need > 0) {
fetchers = new ArrayList(need);
while (need > 0) {
// This kicks off the thread for the fetcher
ConnectionFetcher fetcher = new ConnectionFetcher();
fetchers.add(fetcher);
need--;
}
_fetchers.addAll(fetchers);
}
// Stop ConnectionFetchers as necessary, but it's possible there
// aren't enough fetchers to stop. In this case, close some of the
// connections started by ConnectionFetchers.
int lastFetcherIndex = _fetchers.size();
List extras = new ArrayList();
while((need < 0) && (lastFetcherIndex > 0)) {
ConnectionFetcher fetcher = (ConnectionFetcher)_fetchers.remove(--lastFetcherIndex);
need++;
extras.add(fetcher);
}
int lastInitializingConnectionIndex = _initializingFetchedConnections.size();
while((need < 0) && (lastInitializingConnectionIndex > 0)) {
ManagedConnection connection =
(ManagedConnection)_initializingFetchedConnections.remove(--lastInitializingConnectionIndex);
need++;
extras.add(connection);
}
// Now connect'm.
for(int i = fetchers.size() - 1; i >= 0; i--) {
ConnectionFetcher fetcher = (ConnectionFetcher)fetchers.remove(i);
fetcher.connect();
}
// And delete extras.
for(int i = extras.size() - 1; i >= 0; i--) {
Object next = extras.remove(i);
if(next instanceof ConnectionFetcher) {
((ConnectionFetcher)next).stopConnecting();
} else {
removeInternal((ManagedConnection)next);
}
}
}
/** Starts the dedicated locale ConnectionFetcher, if necessary */
private void startDedicatedLocaleFetcher() {
// if it's a leaf and locale preferencing is on
// we will create a dedicated preference fetcher
// that tries to fetch a connection that matches the
// clients locale
if (RouterService.isShieldedLeaf() && _needPref && !_needPrefInterrupterScheduled
&& _dedicatedPrefFetcher == null) {
_dedicatedPrefFetcher = new ConnectionFetcher(true);
_dedicatedPrefFetcher.connect();
Runnable interrupted = new Runnable() {
public void run() {
synchronized (ConnectionManager.this) {
// always finish once this runs.
_needPref = false;
if (_dedicatedPrefFetcher == null)
return;
_dedicatedPrefFetcher.stopConnecting();
_dedicatedPrefFetcher = null;
}
}
};
_needPrefInterrupterScheduled = true;
// shut off this guy if he didn't have any luck
RouterService.schedule(interrupted, 15 * 1000, 0);
}
}
/**
* Begins initializing fetched connections.
* This will maintain all class invariants and then wait to hear
* back from the ConnectionFetcher after connecting succeeds or fails.
*/
private void initializeFetchedConnection(ManagedConnection mc,
ConnectionFetcher fetcher) {
synchronized(this) {
if(fetcher.isPrematurelyStopped()) {
fetcher.finish();
return;
}
_initializingFetchedConnections.add(mc);
if(fetcher == _dedicatedPrefFetcher)
_dedicatedPrefFetcher = null;
else
_fetchers.remove(fetcher);
connectionInitializing(mc);
// No need to adjust connection fetchers here. We haven't changed
// the need for connections; we've just replaced a ConnectionFetcher
// with a Connection.
}
RouterService.getCallback().connectionInitializing(mc);
try {
mc.initialize(fetcher);
} catch(IOException e) {
cleanupBrokenFetchedConnection(mc);
}
}
/**
* Cleans up references to the given connection.
*
* This removes the now-dead connection from the list of initializing fetched
* connections, ensures that the connection is closed and nothing can
* be routed to it, adjusts connection fetchers so as to spark a new fetcher
* if needed, and processes the headers of the connection for addition to the local
* hostcache.
* @param mc
*/
private void cleanupBrokenFetchedConnection(ManagedConnection mc) {
synchronized (this) {
_initializingFetchedConnections.remove(mc);
removeInternal(mc);
// We've removed a connection, so the need for connections went
// up. We may need to launch a fetcher.
adjustConnectionFetchers();
}
processConnectionHeaders(mc);
}
/**
* Processes the headers received during connection handshake and updates
* itself with any useful information contained in those headers.
* Also may change its state based upon the headers.
* @param headers The headers to be processed
* @param connection The connection on which we received the headers
*/
private void processConnectionHeaders(Connection connection){
if(!connection.receivedHeaders()) {
return;
}
//get the connection headers
Properties headers = connection.headers().props();
//return if no headers to process
if(headers == null)
return;
//update the addresses in the host cache (in case we received some
//in the headers)
updateHostCache(connection.headers());
//get remote address. If the more modern "Listen-IP" header is
//not included, try the old-fashioned "X-My-Address".
String remoteAddress = headers.getProperty(HeaderNames.LISTEN_IP);
if (remoteAddress==null)
remoteAddress = headers.getProperty(HeaderNames.X_MY_ADDRESS);
//set the remote port if not outgoing connection (as for the outgoing
//connection, we already know the port at which remote host listens)
if((remoteAddress != null) && (!connection.isOutgoing())) {
int colonIndex = remoteAddress.indexOf(':');
if(colonIndex == -1)
return;
colonIndex++;
if(colonIndex > remoteAddress.length())
return;
try {
int port = Integer.parseInt(remoteAddress.substring(colonIndex).trim());
if(NetworkUtils.isValidPort(port)) {
// for incoming connections, set the port based on what it's
// connection headers say the listening port is
connection.setListeningPort(port);
}
} catch(NumberFormatException e){
// should nothappen though if the other client is well-coded
}
}
}
/**
* Returns true if this can safely switch from Ultrapeer to leaf mode.
* Typically this means that we are an Ultrapeer and have no leaf
* connections.
*
* @return <tt>true</tt> if we will allow ourselves to become a leaf,
* otherwise <tt>false</tt>
*/
public boolean allowLeafDemotion() {
_leafTries++;
if (UltrapeerSettings.FORCE_ULTRAPEER_MODE.getValue() || isActiveSupernode())
return false;
else if(SupernodeAssigner.isTooGoodToPassUp() && _leafTries < _demotionLimit)
return false;
else
return true;
}
/**
* Notifies the connection manager that it should attempt to become an
* Ultrapeer. If we already are an Ultrapeer, this will be ignored.
*
* @param demotionLimit the number of attempts by other Ultrapeers to
* demote us to a leaf that we should allow before giving up in the
* attempt to become an Ultrapeer
*/
public void tryToBecomeAnUltrapeer(int demotionLimit) {
if(isSupernode()) return;
_demotionLimit = demotionLimit;
_leafTries = 0;
disconnect();
connect();
}
/**
* Adds the X-Try-Ultrapeer hosts from the connection headers to the
* host cache.
*
* @param headers the connection headers received
*/
private void updateHostCache(HandshakeResponse headers) {
if(!headers.hasXTryUltrapeers()) return;
//get the ultrapeers, and add those to the host cache
String hostAddresses = headers.getXTryUltrapeers();
//tokenize to retrieve individual addresses
StringTokenizer st = new StringTokenizer(hostAddresses,
Constants.ENTRY_SEPARATOR);
List hosts = new ArrayList(st.countTokens());
while(st.hasMoreTokens()){
String address = st.nextToken().trim();
try {
Endpoint e = new Endpoint(address);
hosts.add(e);
} catch(IllegalArgumentException iae){
continue;
}
}
_catcher.add(hosts);
}
/**
* Initializes an outgoing connection created by createConnection or any
* incomingConnection. If this is an incoming connection and there are no
* slots available, rejects it and throws IOException.
*
* @throws IOException on failure. No cleanup is necessary if this happens.
*/
private void initializeExternallyGeneratedConnection(ManagedConnection c, GnetConnectObserver observer)
throws IOException {
//For outgoing connections add it to the GUI and the fetcher lists now.
//For incoming, we'll do this below after checking incoming connection
//slots. This keeps reject connections from appearing in the GUI, as
//well as improving performance slightly.
if (c.isOutgoing()) {
synchronized(this) {
connectionInitializing(c);
// We've added a connection, so the need for connections went
// down.
adjustConnectionFetchers();
}
RouterService.getCallback().connectionInitializing(c);
}
try {
c.initialize(observer);
} catch(IOException e) {
cleanupBrokenExternallyGeneratedConnection(c);
throw e;
}
// If observer is null, we blocked while initializing.
// Otherwise, the observer will be notified on completion and do their own thing.
if(observer == null)
completeInitializeExternallyGeneratedConnection(c);
}
/**
* Cleans up a connection that couldn't be initialized.
* @param c
*/
private void cleanupBrokenExternallyGeneratedConnection(ManagedConnection c) {
remove(c);
processConnectionHeaders(c);
}
/**
* Completes the process of initializing an externally generated connection.
*
* @param c
* @throws IOException
*/
private void completeInitializeExternallyGeneratedConnection(ManagedConnection c) throws IOException {
processConnectionHeaders(c);
// If there's not space for the connection, destroy it.
// It really should have been destroyed earlier, but this is just in case.
if (!c.isOutgoing() && !allowConnection(c)) {
// No need to remove, since it hasn't been added to any lists.
throw new IOException("No space for connection");
}
// For incoming connections, add it to the GUI. For outgoing connections
// this was done at the top of the method. See note there.
if (!c.isOutgoing()) {
synchronized (this) {
connectionInitializingIncoming(c);
// We've added a connection, so the need for connections went
// down.
adjustConnectionFetchers();
}
RouterService.getCallback().connectionInitializing(c);
}
completeConnectionInitialization(c, false);
}
/**
* Performs the steps necessary to complete connection initialization.
*
* @param mc the <tt>ManagedConnection</tt> to finish initializing
* @param fetched Specifies whether or not this connection is was fetched by a connection fetcher. If so, this
* removes that connection from the list of fetched connections being initialized, keeping the connection
* fetcher data in sync
*/
private void completeConnectionInitialization(ManagedConnection mc,
boolean fetched) {
synchronized(this) {
if(fetched) {
_initializingFetchedConnections.remove(mc);
}
// If the connection was killed while initializing, we shouldn't
// announce its initialization
boolean connectionOpen = connectionInitialized(mc);
if(connectionOpen) {
RouterService.getCallback().connectionInitialized(mc);
setPreferredConnections();
}
}
}
/**
* Gets the number of preferred connections to maintain.
*/
public int getPreferredConnectionCount() {
return _preferredConnections;
}
/**
* Determines if we're attempting to maintain the idle connection count.
*/
public boolean isConnectionIdle() {
return
_preferredConnections == ConnectionSettings.IDLE_CONNECTIONS.getValue();
}
/**
* Sets the maximum number of connections we'll maintain.
*/
private void setPreferredConnections() {
// if we're disconnected, do nothing.
if(!ConnectionSettings.ALLOW_WHILE_DISCONNECTED.getValue() &&
_disconnectTime != 0)
return;
int oldPreferred = _preferredConnections;
if(isSupernode())
_preferredConnections = ConnectionSettings.NUM_CONNECTIONS.getValue();
else if(isIdle())
_preferredConnections = ConnectionSettings.IDLE_CONNECTIONS.getValue();
else
_preferredConnections = PREFERRED_CONNECTIONS_FOR_LEAF;
if(oldPreferred != _preferredConnections)
stabilizeConnections();
}
/**
* Determines if we're idle long enough to change the number of connections.
*/
private boolean isIdle() {
return SystemUtils.getIdleTime() >= MINIMUM_IDLE_TIME;
}
/**
* This thread does the initialization and the message loop for
* ManagedConnections created through createConnectionAsynchronously and
* createConnectionBlocking
*/
private class OutgoingConnector implements Runnable {
private final ManagedConnection _connection;
private final boolean _doInitialization;
/**
* Creates a new <tt>OutgoingConnector</tt> instance that will
* attempt to create a connection to the specified host.
*
* @param connection the host to connect to
*/
public OutgoingConnector(ManagedConnection connection,
boolean initialize) {
_connection = connection;
_doInitialization = initialize;
}
public void run() {
try {
if(_doInitialization)
initializeExternallyGeneratedConnection(_connection, null);
startConnection(_connection);
} catch(IOException ignored) {}
}
}
/**
* Runs standard calls that should be made whenever a connection is fully
* established and should wait for messages.
*
* @param conn the <tt>ManagedConnection</tt> instance to start
* @throws <tt>IOException</tt> if there is an excpetion while looping
* for messages
*/
private void startConnection(ManagedConnection conn) throws IOException {
if(conn.isGUESSUltrapeer())
QueryUnicaster.instance().addUnicastEndpoint(conn.getInetAddress(), conn.getPort());
// this can throw IOException
conn.loopForMessages();
}
/**
* Asynchronous GnetConnectObserver for externally generated connections.
* Not as robust as ConnectionFetcher because less accounting is needed.
*/
private class IncomingGNetObserver implements GnetConnectObserver {
private ManagedConnection connection;
IncomingGNetObserver(ManagedConnection connection) {
this.connection = connection;
}
public void handleConnect() {
try {
completeInitializeExternallyGeneratedConnection(connection);
startConnection(connection);
} catch(IOException ignored) {
LOG.warn("Failed to complete initialization", ignored);
}
}
public void handleBadHandshake() {
shutdown();
}
public void handleNoGnutellaOk(int code, String msg) {
shutdown();
}
public void shutdown() {
cleanupBrokenExternallyGeneratedConnection(connection);
}
}
/**
* Asynchronously fetches a connection from hostcatcher, then does
* then initialization and message loop.
*
* The ConnectionFetcher is responsible for recording its instantiation
* by adding itself to the fetchers list. It is responsible for recording
* its death by removing itself from the fetchers list only if fetching proceeded
* beyond the 'connect' stage. That is, if a connect attempt is performed and failed
* for any reason, this must clean itself up.
*/
private class ConnectionFetcher implements GnetConnectObserver, HostCatcher.EndpointObserver {
// set if this connectionfetcher is a preferencing fetcher
private boolean _pref = false;
private ManagedConnection connection;
private Endpoint endpoint;
private volatile boolean stoppedEarly = false;
public ConnectionFetcher() {
this(false);
}
public ConnectionFetcher(boolean pref) {
_pref = pref;
}
/** Starts the process of connecting to an arbitary endpoint. */
public void connect() {
_catcher.getAnEndpoint(this);
}
/**
* Marks this fetcher as not wanting to connect.
* It is entirely possible that this fetcher has proceeded to connect
* already. If that's the case, this call essentially does nothing.
*/
public void stopConnecting() {
stoppedEarly = true;
_catcher.removeEndpointObserver(this);
}
/** Returns whether or not we were told to stop early. */
public boolean isPrematurelyStopped() {
return stoppedEarly;
}
/** Does nothing right now. */
public void finish() {
}
/** Callback that an endpoint is available for connecting. */
public void handleEndpoint(Endpoint endpoint) {
Assert.that(endpoint != null);
// If this was an invalid endpoint, try again.
if (!IPFilter.instance().allow(endpoint.getAddress()) || isConnectedTo(endpoint.getAddress())) {
_catcher.getAnEndpoint(this);
return;
}
this.endpoint = endpoint;
connection = new ManagedConnection(endpoint.getAddress(), endpoint.getPort());
connection.setLocalePreferencing(_pref);
doConnectionCheck();
_connectionAttempts++;
initializeFetchedConnection(connection, this);
}
/** Callback that Sockets.connect worked. */
public void handleConnect() {
completeConnectionInitialization(connection, true);
processConnectionHeaders(connection);
_lastSuccessfulConnect = System.currentTimeMillis();
_catcher.doneWithConnect(endpoint, true);
if(_pref)
_needPref = false;
try {
startConnection(connection);
} catch(IOException ignored) {}
}
/** Callback that a connect failed. */
public void shutdown() {
cleanupBrokenFetchedConnection(connection);
_catcher.doneWithConnect(endpoint, false);
_catcher.expireHost(endpoint);
}
/** Callback that handshaking failed. */
public void handleBadHandshake() {
shutdown();
}
/** Callback that connecting worked, but we got something other than a Gnutella OK */
public void handleNoGnutellaOk(int code, String msg) {
cleanupBrokenFetchedConnection(connection);
_lastSuccessfulConnect = System.currentTimeMillis();
if (code == HandshakeResponse.LOCALE_NO_MATCH) {
// Failures because of locale aren't really a failure.
_catcher.add(endpoint, true, connection.getLocalePref());
} else {
_catcher.doneWithConnect(endpoint, true);
_catcher.putHostOnProbation(endpoint);
}
}
/** Checks to see if we need to check for a live connection. */
private void doConnectionCheck() {
// If we've been trying to connect for awhile, check to make
// sure the user's internet connection is live. We only do
// this if we're not already connected, have not made any
// successful connections recently, and have not checked the
// user's connection in the last little while or have very
// few hosts left to try.
long curTime = System.currentTimeMillis();
if (!isConnected() && _connectionAttempts > 40
&& ((curTime - _lastSuccessfulConnect) > 4000)
&& ((curTime - _lastConnectionCheck) > 60 * 60 * 1000)) {
_connectionAttempts = 0;
_lastConnectionCheck = curTime;
LOG.debug("checking for live connection");
ConnectionChecker.checkForLiveConnection();
}
}
}
/**
* This method notifies the connection manager that the user does not have
* a live connection to the Internet to the best of our determination.
* In this case, we notify the user with a message and maintain any
* Gnutella hosts we have already tried instead of discarding them.
*/
public void noInternetConnection() {
// Notify the user that they have no internet connection and that
// we will automatically retry
RouterService.getCallback().disconnected();
if(_automaticallyConnecting) {
// We've already notified the user about their connection and we're
// alread retrying automatically, so just return.
return;
}
// Kill all of the ConnectionFetchers.
disconnect();
// Try to reconnect in 10 seconds, and then every minute after
// that.
RouterService.schedule(new Runnable() {
public void run() {
// If the last time the user disconnected is more recent
// than when we started automatically connecting, just
// return without trying to connect. Note that the
// disconnect time is reset if the user selects to connect.
if(_automaticConnectTime < _disconnectTime) {
return;
}
if(!RouterService.isConnected()) {
// Try to re-connect. Note this call resets the time
// for our last check for a live connection, so we may
// hit web servers again to check for a live connection.
connect();
}
}
}, 10*1000, 2*60*1000);
_automaticConnectTime = System.currentTimeMillis();
_automaticallyConnecting = true;
recoverHosts();
}
/**
* Utility method that tells the host catcher to recover hosts from disk
* if it doesn't have enough hosts.
*/
private synchronized void recoverHosts() {
// Notify the HostCatcher that it should keep any hosts it has already
// used instead of discarding them.
// The HostCatcher can be null in testing.
if(_catcher != null && _catcher.getNumHosts() < 100) {
_catcher.recoverHosts();
}
}
/**
* Utility method to see if the passed in locale matches
* that of the local client. As of now, we assume that
* those clients not advertising locale as english locale
*/
private boolean checkLocale(String loc) {
if(loc == null)
loc = /** assume english if locale is not given... */
ApplicationSettings.DEFAULT_LOCALE.getValue();
return ApplicationSettings.LANGUAGE.getValue().equals(loc);
}
}