package com.limegroup.gnutella.messagehandlers; import java.net.InetSocketAddress; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.limewire.core.settings.MessageSettings; import org.limewire.core.settings.SearchSettings; import org.limewire.inspection.Inspectable; import org.limewire.inspection.InspectableContainer; import org.limewire.inspection.InspectionPoint; import org.limewire.io.GUID; import org.limewire.io.NetworkInstanceUtils; import org.limewire.io.NetworkUtils; import org.limewire.security.InvalidSecurityTokenException; import org.limewire.security.MACCalculatorRepositoryManager; import org.limewire.security.SecurityToken; import org.limewire.util.ByteUtils; import com.google.inject.Inject; import com.google.inject.Singleton; import com.google.inject.name.Named; import com.limegroup.gnutella.BypassedResultsCache; import com.limegroup.gnutella.MessageRouter; import com.limegroup.gnutella.ReplyHandler; import com.limegroup.gnutella.messages.BadPacketException; import com.limegroup.gnutella.messages.Message; import com.limegroup.gnutella.messages.QueryReply; import com.limegroup.gnutella.messages.vendor.LimeACKVendorMessage; import com.limegroup.gnutella.messages.vendor.ReplyNumberVendorMessage; import com.limegroup.gnutella.statistics.OutOfBandStatistics; /** * Handles {@link ReplyNumberVendorMessage} and {@link QueryReply} for * out-of-band search results and manages a cache of session objects * to keep track of the results that have alreay been received. */ @Singleton public class OOBHandler implements MessageHandler, Runnable { private static final Log LOG = LogFactory.getLog(OOBHandler.class); /** How long to remember the port associated with each address */ private static final int RESPONDER_PORT_LIFETIME = 60 * 1000; /** How long to remember ignored addresses */ private static final int IGNORED_ADDRESS_LIFETIME = 10 * 60 * 1000; /** Magic port number that means an address should be ignored */ private static final int IGNORE = -1; /** Don't ignore localhost (used for testing) */ private static final int LOCALHOST = ByteUtils.leb2int(new byte[]{127, 0, 0, 1}, 0); private final MessageRouter router; private final MACCalculatorRepositoryManager MACCalculatorRepositoryManager; private final ScheduledExecutorService executor; private final OutOfBandStatistics outOfBandStatistics; private final NetworkInstanceUtils networkInstanceUtils; private final Map<Integer,OOBSession> sessions = Collections.synchronizedMap(new HashMap<Integer,OOBSession>()); /** * The port associated with each responding address and the time at which * it was recorded. This is used to detect addresses that respond from * multiple ports and also to record misbehaving addresses; if the port * is IGNORE, RNVMs from the address should be ignored. */ private final Map<Integer,ResponderPort> responderPorts = Collections.synchronizedMap(new HashMap<Integer,ResponderPort>()); @Inject public OOBHandler(MessageRouter router, MACCalculatorRepositoryManager MACCalculatorRepositoryManager, @Named("backgroundExecutor") ScheduledExecutorService executor, OutOfBandStatistics outOfBandStatistics, NetworkInstanceUtils networkInstanceUtils) { this.router = router; this.MACCalculatorRepositoryManager = MACCalculatorRepositoryManager; this.executor = executor; this.outOfBandStatistics = outOfBandStatistics; this.networkInstanceUtils = networkInstanceUtils; } public void handleMessage(Message msg, InetSocketAddress addr, ReplyHandler handler) { if (msg instanceof ReplyNumberVendorMessage) handleRNVM((ReplyNumberVendorMessage)msg, handler); else if (msg instanceof QueryReply) handleOOBReply((QueryReply)msg, handler); else throw new IllegalArgumentException("can't handle this type of message"); } /** * Handles the reply number message, verifying the query for it is still alive * and more results are wanted and sending a {@link LimeACKVendorMessage} in * that case. Otherwise the source of the <code>msg</code> is added to the * {@link BypassedResultsCache}. */ private void handleRNVM(ReplyNumberVendorMessage msg, final ReplyHandler handler) { GUID g = new GUID(msg.getGUID()); if(LOG.isDebugEnabled()) { LOG.debug("Received RNVM from " + handler.getAddress() + ":" + handler.getPort() + " with " + msg.getNumResults() + " results"); } // Only allow responses from one port per address byte[] handlerAddress = handler.getInetAddress().getAddress(); if(shouldIgnore (handlerAddress, handler.getPort())) return; int toRequest; if(!router.isQueryAlive(g) || (toRequest = router.getNumOOBToRequest(msg)) <= 0) { // remember as possible GUESS source though LOG.debug("Bypassing source"); router.addBypassedSource(msg, handler); outOfBandStatistics.addBypassedResponse(msg.getNumResults()); return; } LimeACKVendorMessage ack = null; if (msg.isOOBv3()) { SecurityToken t = new OOBSecurityToken(new OOBSecurityToken.OOBTokenData(handler, msg.getGUID(), toRequest), MACCalculatorRepositoryManager); int hash = Arrays.hashCode(t.getBytes()); synchronized(sessions) { if(!sessions.containsKey(hash)) { sessions.put(hash, new OOBSession(t, toRequest, new GUID(msg.getGUID()))); ack = new LimeACKVendorMessage(g, toRequest, t); if(LOG.isDebugEnabled()) { LOG.debug("Sending OOBv3 LimeACK to " + handler.getAddress() + ":" + handler.getPort()); } } else { LOG.debug("RNVM has already been acked"); } } } else { ack = new LimeACKVendorMessage(g, toRequest); if(LOG.isDebugEnabled()) { LOG.debug("Sending OOBv2 LimeACK to " + handler.getAddress() + ":" + handler.getPort()); } } if (ack != null) { outOfBandStatistics.addRequestedResponse(toRequest); handler.reply(ack); if (MessageSettings.OOB_REDUNDANCY.getValue()) { LOG.debug("Sending redundant LimeACK"); final LimeACKVendorMessage ackf = ack; executor.schedule(new Runnable() { public void run() { handler.reply(ackf); } }, 100, TimeUnit.MILLISECONDS); } } } /** * Handles an out-of-band query reply verifying if its security token is valid * and creating a session object that keeps track of the number of results * received for that security token. * * Invalid messages with invalid security token or without token or duplicate * messages are ignored. * * If the query is not alive messages are discarded and added to the * {@link BypassedResultsCache}. */ private void handleOOBReply(QueryReply reply, ReplyHandler handler) { if(LOG.isDebugEnabled()) { LOG.debug("Handling OOB reply from " + handler.getAddress() + ":" + handler.getPort() + " with " + reply.getResultCount() + " results"); } // check if ip address of reply and sender of reply match // and update address of reply if necessary byte[] handlerAddress = handler.getInetAddress().getAddress(); if (!Arrays.equals(handlerAddress, reply.getIPBytes())) { if(LOG.isDebugEnabled()) { LOG.debug("Reply has wrong address " + reply.getIP() + ":" + reply.getPort()); } // override address in packet try { // needs a push, we can update: works for fw-fw case and classic push // or not private, we can update if (reply.getNeedsPush() || !networkInstanceUtils.isPrivateAddress(reply.getIPBytes())) { reply.setOOBAddress(handler.getInetAddress(), handler.getPort()); } else { // messed up case: doesn't want a push, but has a private address } } catch (BadPacketException bpe) { // invalid packet, don't handle it LOG.debug("Error overriding address"); return; } } SecurityToken token = null; try { token = getVerifiedSecurityToken(reply, handler); } catch(InvalidSecurityTokenException e) { LOG.debug("Invalid security token"); return; } if(token == null) { LOG.debug("No security token"); if (!SearchSettings.DISABLE_OOB_V2.getBoolean()) { LOG.debug("Handling as an OOBv2 reply"); router.handleQueryReply(reply, handler); } return; } int numResps = reply.getResultCount(); outOfBandStatistics.addReceivedResponse(numResps); /* * Router will handle the reply if it * it has a route && we still expect results for this OOB session */ // if query is not of interest anymore return GUID queryGUID = new GUID(reply.getGUID()); if (!router.isQueryAlive(queryGUID)) { LOG.debug("Query is dead - bypassing source"); router.addBypassedSource(reply, handler); } else { synchronized(sessions) { int hashKey = Arrays.hashCode(token.getBytes()); OOBSession session = sessions.get(hashKey); if(session == null) { LOG.debug("Query is alive but OOB session has expired"); return; } int remaining = session.getRemainingResultsCount() - numResps; if(LOG.isDebugEnabled()) { LOG.debug("Reply has " + numResps + " results, " + remaining + " remaining"); } if(remaining >= 0) { // parsing of query reply already done here in message dispatcher thread try { int added = session.countAddedResponses(reply.getResultsArray()); if(LOG.isDebugEnabled()) LOG.debug("Reply has " + added + " new results"); if(added > 0) { LOG.debug("Handling as an OOBv3 reply"); router.handleQueryReply(reply, handler); } } catch (BadPacketException e) { LOG.debug("Error getting results"); // ignore packet } } else { tooManyResults(handlerAddress); } } } } /** * Returns true if a message from the given address and port * should be ignored because the address is responding from * multiple ports. */ private boolean shouldIgnore(byte[] addr, int port) { if(!SearchSettings.OOB_IGNORE_MULTIPLE_PORTS.getValue()) return false; Integer address = ByteUtils.leb2int(addr, 0); if(address == LOCALHOST) return false; long now = System.currentTimeMillis(); synchronized(responderPorts) { ResponderPort rp = responderPorts.get(address); if(rp == null || rp.hasExpired(now)) { // No port is known for this address rp = new ResponderPort(port, now); responderPorts.put(address, rp); return false; } else if(rp.port == IGNORE) { // Continue ignoring the address rp.timestamp = now; return true; } else if(rp.port != port) { if(LOG.isInfoEnabled()) { String ip = NetworkUtils.ip2string(addr); LOG.info("Ignoring " + ip + " - too many ports"); } // Too many ports - ignore the address for a while rp = new ResponderPort(IGNORE, now); responderPorts.put(address, rp); return true; } else { // Same port as before return false; } } } /** * Ignores an address that sent more results than it offered. */ private void tooManyResults(byte[] addr) { if(!SearchSettings.OOB_IGNORE_EXCESS_RESULTS.getValue()) return; Integer address = ByteUtils.leb2int(addr, 0); long now = System.currentTimeMillis(); synchronized(responderPorts) { ResponderPort rp = responderPorts.get(address); if(rp == null || rp.port != IGNORE) { if(LOG.isInfoEnabled()) { String ip = NetworkUtils.ip2string(addr); LOG.info("Ignoring " + ip + " - too many results"); } // Too many results - ignore the address for a while rp = new ResponderPort(IGNORE, now); responderPorts.put(address, rp); } else { // Continue ignoring the address rp.timestamp = now; } } } /** * Reconstructs the security token from the query reply and verifies it * against the handler, the number of results requested and the GUID of * the reply. * * @return the security token, or null if there is no security token * @throws InvalidSecurityTokenException if the security token is invalid */ private SecurityToken getVerifiedSecurityToken(QueryReply reply, ReplyHandler handler) throws InvalidSecurityTokenException { byte[] securityBytes = reply.getSecurityToken(); if(securityBytes == null) return null; OOBSecurityToken oobKey = new OOBSecurityToken(securityBytes, MACCalculatorRepositoryManager); OOBSecurityToken.OOBTokenData data = new OOBSecurityToken.OOBTokenData(handler, reply.getGUID(), securityBytes[0] & 0xFF); if(oobKey.isFor(data)) return oobKey; else throw new InvalidSecurityTokenException("invalid token"); } private void expire() { synchronized(sessions) { if(LOG.isDebugEnabled()) LOG.debug(sessions.size() + " OOB sessions"); Iterator<Map.Entry<Integer,OOBSession>> iter = sessions.entrySet().iterator(); while(iter.hasNext()) { if(!router.isQueryAlive(iter.next().getValue().getGUID())) iter.remove(); } } long now = System.currentTimeMillis(); synchronized(responderPorts) { if(LOG.isDebugEnabled()) LOG.debug(responderPorts.size() + " responder ports"); Iterator<Map.Entry<Integer,ResponderPort>> iter = responderPorts.entrySet().iterator(); while(iter.hasNext()) { if(iter.next().getValue().hasExpired(now)) iter.remove(); } } } public void run() { expire(); } private static class ResponderPort { final int port; long timestamp; ResponderPort(int port, long timestamp) { this.port = port; this.timestamp = timestamp; } boolean hasExpired(long now) { if(port == IGNORE) return now - timestamp > IGNORED_ADDRESS_LIFETIME; else return now - timestamp > RESPONDER_PORT_LIFETIME; } } @InspectableContainer @SuppressWarnings("unused") private class OOBInspectable { @InspectionPoint("oob sessions") public final Inspectable oobSessions = new Inspectable() { @Override public Object inspect() { List<Object> list; synchronized(sessions) { list = new ArrayList<Object>(sessions.size()); for(OOBSession o : sessions.values()) { list.add(o.inspect()); } } return list; } }; } }