package com.limegroup.gnutella; import java.io.IOException; import java.io.InputStream; import java.net.DatagramSocket; import java.net.InetAddress; import java.net.MulticastSocket; import java.net.ServerSocket; import java.net.Socket; import java.net.SocketException; import java.net.UnknownHostException; import java.util.Arrays; import java.util.Random; import java.util.List; import java.util.ArrayList; import java.util.Collection; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import com.limegroup.gnutella.filters.IPFilter; import com.limegroup.gnutella.io.AcceptObserver; import com.limegroup.gnutella.io.ChannelInterestReadAdapter; import com.limegroup.gnutella.io.NIOMultiplexor; import com.limegroup.gnutella.io.SocketFactory; import com.limegroup.gnutella.settings.ConnectionSettings; import com.limegroup.gnutella.settings.SettingsHandler; import com.limegroup.gnutella.statistics.HTTPStat; import com.limegroup.gnutella.util.IOUtils; import com.limegroup.gnutella.util.NetworkUtils; import com.limegroup.gnutella.util.ThreadFactory; /** * Listens on ports, accepts incoming connections, and dispatches threads to * handle those connections. Currently supports Gnutella messaging, HTTP, and * chat connections over TCP; more may be supported in the future.<p> * This class has a special relationship with UDPService and should really be * the only class that intializes it. See setListeningPort() for more * info. */ public class Acceptor implements ConnectionAcceptor { private static final Log LOG = LogFactory.getLog(Acceptor.class); // various time delays for checking of firewalled status. static long INCOMING_EXPIRE_TIME = 30 * 60 * 1000; // 30 minutes static long WAIT_TIME_AFTER_REQUESTS = 30 * 1000; // 30 seconds static long TIME_BETWEEN_VALIDATES = 10 * 60 * 1000; // 10 minutes /** the UPnPManager to use */ private static final UPnPManager UPNP_MANAGER = (!ConnectionSettings.DISABLE_UPNP.getValue()) ? UPnPManager.instance() : null; /** * The socket that listens for incoming connections. Can be changed to * listen to new ports. * * LOCKING: obtain _socketLock before modifying either. Notify _socketLock * when done. */ private volatile ServerSocket _socket=null; /** * The port of the server socket. */ private volatile int _port = 6346; /** * The real address of this host--assuming there's only one--used for pongs * and query replies. This value is ignored if FORCE_IP_ADDRESS is * true. This is initialized in three stages: * 1. Statically initialized to all zeroes. * 2. Initialized in the Acceptor thread to getLocalHost(). * 3. Initialized each time a connection is initialized to the local * address of that connection's socket. * * Why are all three needed? Step (3) is needed because (2) can often fail * due to a JDK bug #4073539, or if your address changes via DHCP. Step (2) * is needed because (3) ignores local addresses of 127.x.x.x. Step (1) is * needed because (2) can't occur in the main thread, as it may block * because the update checker is trying to resolve addresses. (See JDK bug * #4147517.) Note this may delay the time to create a listening socket by * a few seconds; big deal! * * LOCKING: obtain Acceptor.class' lock */ private static byte[] _address = new byte[4]; /** * The external address. This is the address as visible from other peers. * * LOCKING: obtain Acceptor.class' lock */ private static byte[] _externalAddress = new byte[4]; /** * Variable for whether or not we have accepted an incoming connection -- * used to determine firewall status. */ private volatile boolean _acceptedIncoming = false; /** * Keep track of the last time _acceptedIncoming was set - we want to * revalidate it every so often. */ private volatile long _lastIncomingTime = 0; /** * The last time you did a connect back check. It is set to the time * we start up since we try once when we start up. */ private volatile long _lastConnectBackTime = System.currentTimeMillis(); /** * Whether or not this Acceptor was started. All connections accepted prior * to starting are dropped. */ private volatile boolean _started; /** * @modifes this * @effects sets the IP address to use in pongs and query replies. * If addr is invalid or a local address, this is not modified. * This method must be to get around JDK bug #4073539, as well * as to try to handle the case of a computer whose IP address * keeps changing. */ public void setAddress(InetAddress address) { byte[] byteAddr = address.getAddress(); if( !NetworkUtils.isValidAddress(byteAddr) ) return; if( byteAddr[0] == 127 && ConnectionSettings.LOCAL_IS_PRIVATE.getValue()) { return; } boolean addrChanged = false; synchronized(Acceptor.class) { if( !Arrays.equals(_address, byteAddr) ) { _address = byteAddr; addrChanged = true; } } if( addrChanged ) RouterService.addressChanged(); } /** * Sets the external address. */ public void setExternalAddress(InetAddress address) { byte[] byteAddr = address.getAddress(); if( byteAddr[0] == 127 && ConnectionSettings.LOCAL_IS_PRIVATE.getValue()) { return; } synchronized(Acceptor.class) { _externalAddress = byteAddr; } } /** * tries to bind the serversocket and create UPnPMappings. * call before running. */ public void init() { int tempPort; // try a random port if we have not received an incoming connection // and have been running on the default port (6346) // and the user has not changed the settings boolean tryingRandom = ConnectionSettings.PORT.isDefault() && !ConnectionSettings.EVER_ACCEPTED_INCOMING.getValue() && !ConnectionSettings.FORCE_IP_ADDRESS.getValue(); Random gen = null; if (tryingRandom) { gen = new Random(); tempPort = gen.nextInt(50000)+2000; } else tempPort = ConnectionSettings.PORT.getValue(); //0. Get local address. This must be done here because it can // block under certain conditions. // See the notes for _address. try { setAddress(UPNP_MANAGER != null ? NetworkUtils.getLocalAddress() : InetAddress.getLocalHost()); } catch (UnknownHostException e) { } catch (SecurityException e) { } // Create the server socket, bind it to a port, and listen for // incoming connections. If there are problems, we can continue // onward. //1. Try suggested port. int oldPort = tempPort; try { setListeningPort(tempPort); _port = tempPort; } catch (IOException e) { LOG.warn("can't set initial port", e); // 2. Try 20 different ports. int numToTry = 20; for (int i=0; i<numToTry; i++) { if(gen == null) gen = new Random(); tempPort = gen.nextInt(50000); tempPort += 2000;//avoid the first 2000 ports // do not try to bind to the multicast port. if (tempPort == ConnectionSettings.MULTICAST_PORT.getValue()) { numToTry++; continue; } try { setListeningPort(tempPort); _port = tempPort; break; } catch (IOException e2) { LOG.warn("can't set port", e2); } } // If we still don't have a socket, there's an error if(_socket == null) { MessageService.showError("ERROR_NO_PORTS_AVAILABLE"); } } if (_port != oldPort || tryingRandom) { ConnectionSettings.PORT.setValue(_port); SettingsHandler.save(); RouterService.addressChanged(); } // if we created a socket and have a NAT, and the user is not // explicitly forcing a port, create the mappings if (_socket != null && UPNP_MANAGER != null) { // wait a bit for the device. UPNP_MANAGER.waitForDevice(); // if we haven't discovered the router by now, its not there UPNP_MANAGER.stop(); boolean natted = UPNP_MANAGER.isNATPresent(); boolean validPort = NetworkUtils.isValidPort(_port); boolean forcedIP = ConnectionSettings.FORCE_IP_ADDRESS.getValue() && !ConnectionSettings.UPNP_IN_USE.getValue(); if(LOG.isDebugEnabled()) LOG.debug("Natted: " + natted + ", validPort: " + validPort + ", forcedIP: " + forcedIP); if(natted && validPort && !forcedIP) { int mappedPort = UPNP_MANAGER.mapPort(_port); if(LOG.isDebugEnabled()) LOG.debug("UPNP port mapped: " + mappedPort); //if we created a mapping successfully, update the forced port if (mappedPort != 0 ) { UPNP_MANAGER.clearMappingsOnShutdown(); // mark UPNP as being on so that if LimeWire shuts // down prematurely, we know the FORCE_IP was from UPnP // and that we can continue trying to use UPnP ConnectionSettings.FORCE_IP_ADDRESS.setValue(true); ConnectionSettings.FORCED_PORT.setValue(mappedPort); ConnectionSettings.UPNP_IN_USE.setValue(true); if (mappedPort != _port) RouterService.addressChanged(); // we could get our external address from the NAT but its too slow // so we clear the last connect back times. // This will not help with already established connections, but if // we establish new ones in the near future resetLastConnectBackTime(); UDPService.instance().resetLastConnectBackTime(); } } } } /** * Launches the port monitoring thread, MulticastService, and UDPService. */ public void start() { MulticastService.instance().start(); UDPService.instance().start(); RouterService.schedule(new IncomingValidator(), TIME_BETWEEN_VALIDATES, TIME_BETWEEN_VALIDATES); RouterService.getConnectionDispatcher(). addConnectionAcceptor(this, new String[]{"CONNECT","\n\n"}, false, false); _started = true; } /** * Returns whether or not our advertised IP address is the same as what remote peers believe it is. */ public boolean isAddressExternal() { if (!ConnectionSettings.LOCAL_IS_PRIVATE.getValue()) return true; synchronized(Acceptor.class) { return Arrays.equals(getAddress(true), _externalAddress); } } /** * Returns this' external address. */ public byte[] getExternalAddress() { synchronized(Acceptor.class) { return _externalAddress; } } /** * Returns this' address to use for ping replies, query replies, * and pushes. * * @param checkForce whether or not to check if the IP address is forced. * If false, the forced IP address will never be used. * If true, the forced IP address will only be used if one is set. */ public byte[] getAddress(boolean checkForce) { if(checkForce && ConnectionSettings.FORCE_IP_ADDRESS.getValue()) { String address = ConnectionSettings.FORCED_IP_ADDRESS_STRING.getValue(); try { InetAddress ia = InetAddress.getByName(address); return ia.getAddress(); } catch (UnknownHostException err) { // ignore and return _address } } synchronized (Acceptor.class) { return _address; } } /** * Returns the port at which the Connection Manager listens for incoming * connections * * @param checkForce whether or not to check if the port is forced. * @return the listening port */ public int getPort(boolean checkForce) { if(checkForce && ConnectionSettings.FORCE_IP_ADDRESS.getValue()) return ConnectionSettings.FORCED_PORT.getValue(); return _port; } /** * @requires only one thread is calling this method at a time * @modifies this * @effects sets the port on which the ConnectionManager AND the UDPService * is listening. If either service CANNOT bind TCP/UDP to the port, * <i>neither<i> service is modified and a IOException is throw. * If port==0, tells this to stop listening for incoming GNUTELLA TCP AND * UDP connections/messages. This is properly synchronized and can be * called even while run() is being called. */ public void setListeningPort(int port) throws IOException { //1. Special case: if unchanged, do nothing. if (_socket!=null && _port==port) return; //2. Special case if port==0. This ALWAYS works. //Note that we must close the socket BEFORE grabbing //the lock. Otherwise deadlock will occur since //the acceptor thread is listening to the socket //while holding the lock. Also note that port //will not have changed before we grab the lock. else if (port==0) { LOG.trace("shutting off service."); IOUtils.close(_socket); _socket=null; _port=0; //Shut off UDPService also! UDPService.instance().setListeningSocket(null); //Shut off MulticastServier too! MulticastService.instance().setListeningSocket(null); LOG.trace("service OFF."); return; } //3. Normal case. See note about locking above. /* Since we want the UDPService to bind to the same port as the * Acceptor, we need to be careful about this case. Essentially, we * need to confirm that the port can be bound by BOTH UDP and TCP * before actually acceping the port as valid. To effect this change, * we first attempt to bind the port for UDP traffic. If that fails, a * IOException will be thrown. If we successfully UDP bind the port * we keep that bound DatagramSocket around and try to bind the port to * TCP. If that fails, a IOException is thrown and the valid * DatagramSocket is closed. If that succeeds, we then 'commit' the * operation, setting our new TCP socket and UDP sockets. */ else { if(LOG.isDebugEnabled()) LOG.debug("changing port to " + port); DatagramSocket udpServiceSocket = UDPService.instance().newListeningSocket(port); LOG.trace("UDP Service is ready."); MulticastSocket mcastServiceSocket = null; try { InetAddress mgroup = InetAddress.getByName( ConnectionSettings.MULTICAST_ADDRESS.getValue() ); mcastServiceSocket = MulticastService.instance().newListeningSocket( ConnectionSettings.MULTICAST_PORT.getValue(), mgroup ); LOG.trace("multicast service setup"); } catch(IOException e) { LOG.warn("can't create multicast socket", e); mcastServiceSocket = null; } //a) Try new port. ServerSocket newSocket=null; try { newSocket = SocketFactory.newServerSocket(port, new SocketListener()); } catch (IOException e) { LOG.warn("can't create ServerSocket", e); udpServiceSocket.close(); throw e; } catch (IllegalArgumentException e) { LOG.warn("can't create ServerSocket", e); udpServiceSocket.close(); throw new IOException("could not create a listening socket"); } //b) Close old socket IOUtils.close(_socket); //c) Replace with new sock. _socket=newSocket; _port=port; LOG.trace("Acceptor ready.."); // Commit UDPService's new socket UDPService.instance().setListeningSocket(udpServiceSocket); // Commit the MulticastService's new socket // if we were able to get it if (mcastServiceSocket != null) { MulticastService.instance().setListeningSocket(mcastServiceSocket); } if(LOG.isDebugEnabled()) LOG.debug("listening UDP/TCP on " + _port); } } /** * Determines whether or not LimeWire has detected it is firewalled or not. */ public boolean acceptedIncoming() { return _acceptedIncoming; } /** * Sets the new incoming status. * Returns whether or not the status changed. */ private boolean setIncoming(boolean status) { if (_acceptedIncoming == status) return false; _acceptedIncoming = status; RouterService.getCallback().acceptedIncomingChanged(status); return true; } public void acceptConnection(String word, Socket s) { if (ConnectionSettings.UNSET_FIREWALLED_FROM_CONNECTBACK.getValue()) checkFirewall(s.getInetAddress()); IOUtils.close(s); } /** * Updates the firewalled status with info from the given incoming address. */ public void checkFirewall(InetAddress address) { // we have accepted an incoming socket -- only record // that we've accepted incoming if it's definitely // not from our local subnet and we aren't connected to // the host already. boolean changed = false; if(isOutsideConnection(address)) { synchronized (Acceptor.class) { changed = setIncoming(true); ConnectionSettings.EVER_ACCEPTED_INCOMING.setValue(true); _lastIncomingTime = System.currentTimeMillis(); } } if(changed) RouterService.incomingStatusChanged(); } /** * Listens for new incoming sockets & starts a thread to * process them if necessary. */ private class SocketListener implements AcceptObserver { public void handleIOException(IOException iox) { LOG.warn("IOX while accepting", iox); } public void shutdown() { LOG.debug("shutdown one SocketListener"); } public void handleAccept(Socket client) { if(!_started) { IOUtils.close(client); return; } // If the client was closed before we were able to get the address, // then getInetAddress will return null. InetAddress address = client.getInetAddress(); if (address == null) { IOUtils.close(client); LOG.warn("connection closed while accepting"); } else if(isBannedIP(address.getAddress())) { if (LOG.isWarnEnabled()) LOG.warn("Ignoring banned host: " + address); HTTPStat.BANNED_REQUESTS.incrementStat(); IOUtils.close(client); } else { if (LOG.isDebugEnabled()) LOG.debug("Dispatching new client connecton: " + address); // if we want to unset firewalled from any connection, // do it here. if (!ConnectionSettings.UNSET_FIREWALLED_FROM_CONNECTBACK.getValue()) checkFirewall(client.getInetAddress()); // Set our IP address of the local address of this socket. InetAddress localAddress = client.getLocalAddress(); setAddress(localAddress); try { _socket.setSoTimeout(Constants.TIMEOUT); } catch(SocketException se) { IOUtils.close(_socket); return; } // Dispatch asynchronously if possible. if(client.getChannel() != null) // supports non-blocking reads ((NIOMultiplexor)client).setReadObserver(new AsyncConnectionDispatcher(client)); else ThreadFactory.startThread(new BlockingConnectionDispatcher(client), "ConnectionDispatchRunner"); } } } /** * Determines whether or not this INetAddress is found an outside * source, so as to correctly set "acceptedIncoming" to true. * * This ignores connections from private or local addresses, * ignores those who may be on the same subnet, and ignores those * who we are already connected to. */ private boolean isOutsideConnection(InetAddress addr) { // short-circuit for tests. if(!ConnectionSettings.LOCAL_IS_PRIVATE.getValue()) return true; return !RouterService.isConnectedTo(addr) && !NetworkUtils.isLocalAddress(addr); } /** * Returns whether <tt>ip</tt> is a banned address. * * @param ip an address in resolved dotted-quad format, e.g., 18.239.0.144 * @return true iff ip is a banned address. */ public boolean isBannedIP(byte[] addr) { return !IPFilter.instance().allow(addr); } /** * Resets the last connectback time. */ void resetLastConnectBackTime() { _lastConnectBackTime = System.currentTimeMillis() - INCOMING_EXPIRE_TIME - 1; } /** * If we used UPnP Mappings this session, clean them up and revert * any relevant settings. */ public void shutdown() { if(UPNP_MANAGER != null && UPNP_MANAGER.isNATPresent() && UPNP_MANAGER.mappingsExist() && ConnectionSettings.UPNP_IN_USE.getValue()) { // reset the forced port values - must happen before we save them to disk ConnectionSettings.FORCE_IP_ADDRESS.revertToDefault(); ConnectionSettings.FORCED_PORT.revertToDefault(); ConnectionSettings.UPNP_IN_USE.revertToDefault(); } } /** * (Re)validates acceptedIncoming. */ private class IncomingValidator implements Runnable { public IncomingValidator() {} public void run() { // clear and revalidate if 1) we haven't had in incoming // or 2) we've never had incoming and we haven't checked final long currTime = System.currentTimeMillis(); final ConnectionManager cm = RouterService.getConnectionManager(); if ( (_acceptedIncoming && //1) ((currTime - _lastIncomingTime) > INCOMING_EXPIRE_TIME)) || (!_acceptedIncoming && //2) ((currTime - _lastConnectBackTime) > INCOMING_EXPIRE_TIME)) ) { // send a connectback request to a few peers and clear // _acceptedIncoming IF some requests were sent. if(cm.sendTCPConnectBackRequests()) { _lastConnectBackTime = System.currentTimeMillis(); Runnable resetter = new Runnable() { public void run() { boolean changed = false; synchronized (Acceptor.class) { if (_lastIncomingTime < currTime) { changed = setIncoming(false); } } if(changed) RouterService.incomingStatusChanged(); } }; RouterService.schedule(resetter, WAIT_TIME_AFTER_REQUESTS, 0); } } } } /** * A ConnectionDispatcher that reads asynchronously from the socket. */ private static class AsyncConnectionDispatcher extends ChannelInterestReadAdapter { private final Socket client; AsyncConnectionDispatcher(Socket client) { super(); this.client = client; } protected int getBufferSize() { // + 1 for whitespace return RouterService.getConnectionDispatcher().getMaximumWordSize() + 1; } public void shutdown() { super.shutdown(); HTTPStat.CLOSED_REQUESTS.incrementStat(); } public void handleRead() throws IOException { // Fill up our buffer as much we can. int read = 0; while(buffer.hasRemaining() && (read = source.read(buffer)) > 0); // See if we have a full word. for(int i = 0; i < buffer.position(); i++) { if(buffer.get(i) == ' ') { String word = new String(buffer.array(), 0, i); buffer.limit(buffer.position()).position(i+1); buffer.compact(); source.interest(false); RouterService.getConnectionDispatcher().dispatch(word, client, true); return; } } // If there's no room to read more or there's nothing left to read, // we aren't going to read our word. if(!buffer.hasRemaining() || read == -1) close(); } } /** * A ConnectionDispatcher that blocks while reading. */ private static class BlockingConnectionDispatcher implements Runnable { private final Socket client; public BlockingConnectionDispatcher(Socket socket) { client = socket; } /** Reads a word and sends it off to the ConnectionDispatcher for dispatching. */ public void run() { try { //The try-catch below is a work-around for JDK bug 4091706. InputStream in=null; try { in=client.getInputStream(); } catch (IOException e) { HTTPStat.CLOSED_REQUESTS.incrementStat(); throw e; } catch(NullPointerException e) { // This should only happen extremely rarely. // JDK bug 4091706 throw new IOException(e.getMessage()); } ConnectionDispatcher dispatcher = RouterService.getConnectionDispatcher(); String word = IOUtils.readLargestWord(in, dispatcher.getMaximumWordSize()); dispatcher.dispatch(word, client, false); } catch (IOException iox) { HTTPStat.CLOSED_REQUESTS.incrementStat(); IOUtils.close(client); } } } }